From 1db25e8a1eb1fe07bf8e0e35b575dd7355d80877 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 3 Jul 2023 16:46:12 +0100 Subject: [PATCH 001/479] chore: prepare Release 51 for development --- meteor/package.json | 2 +- meteor/scripts/libs-sync-version.js | 2 +- packages/blueprints-integration/package.json | 4 +- packages/corelib/package.json | 6 +-- packages/documentation/package.json | 2 +- packages/job-worker/package.json | 8 ++-- packages/lerna.json | 2 +- packages/live-status-gateway/package.json | 10 ++--- packages/mos-gateway/package.json | 6 +-- packages/openapi/package.json | 2 +- packages/playout-gateway/package.json | 6 +-- packages/server-core-integration/package.json | 4 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 38 +++++++++---------- 14 files changed, 47 insertions(+), 47 deletions(-) diff --git a/meteor/package.json b/meteor/package.json index 1dbd53a1b3..eddae96abb 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -1,6 +1,6 @@ { "name": "automation-core", - "version": "1.50.0-in-testing.0", + "version": "1.51.0-in-development", "private": true, "engines": { "node": ">=14.19.1" diff --git a/meteor/scripts/libs-sync-version.js b/meteor/scripts/libs-sync-version.js index dfb59ce8c3..e24f415984 100644 --- a/meteor/scripts/libs-sync-version.js +++ b/meteor/scripts/libs-sync-version.js @@ -2,7 +2,7 @@ const { exec } = require('child_process') const PACKAGE_VERSION = require('../package.json').version -const cmd = 'cd ../packages && yarn set-version ' + PACKAGE_VERSION +const cmd = `cd ../packages && yarn set-version ${PACKAGE_VERSION} --force-publish` console.log(cmd) const child = exec(cmd, (error, stdout, stderr) => { if (error) { diff --git a/packages/blueprints-integration/package.json b/packages/blueprints-integration/package.json index 6a6e1df0d7..139d5ffdc6 100644 --- a/packages/blueprints-integration/package.json +++ b/packages/blueprints-integration/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/blueprints-integration", - "version": "1.50.0-in-testing.0", + "version": "1.51.0-in-development", "description": "Library to define the interaction between core and the blueprints.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -38,7 +38,7 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/shared-lib": "1.50.0-in-testing.0", + "@sofie-automation/shared-lib": "1.51.0-in-development", "tslib": "^2.4.0", "type-fest": "^2.19.0" }, diff --git a/packages/corelib/package.json b/packages/corelib/package.json index 98a6748d40..740cab7472 100644 --- a/packages/corelib/package.json +++ b/packages/corelib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/corelib", - "version": "1.50.0-in-testing.0", + "version": "1.51.0-in-development", "private": true, "description": "Internal library for some types shared by core and workers", "main": "dist/index.js", @@ -39,8 +39,8 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/blueprints-integration": "1.50.0-in-testing.0", - "@sofie-automation/shared-lib": "1.50.0-in-testing.0", + "@sofie-automation/blueprints-integration": "1.51.0-in-development", + "@sofie-automation/shared-lib": "1.51.0-in-development", "fast-clone": "^1.5.13", "i18next": "^21.9.1", "influx": "^5.9.3", diff --git a/packages/documentation/package.json b/packages/documentation/package.json index 1056570319..47f17c3b20 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -1,6 +1,6 @@ { "name": "sofie-documentation", - "version": "1.50.0-in-testing.0", + "version": "1.51.0-in-development", "private": true, "scripts": { "docusaurus": "docusaurus", diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index a041cca2a2..21ae16e868 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/job-worker", - "version": "1.50.0-in-testing.0", + "version": "1.51.0-in-development", "description": "Worker for things", "main": "dist/index.js", "license": "MIT", @@ -41,9 +41,9 @@ ], "dependencies": { "@slack/webhook": "^6.1.0", - "@sofie-automation/blueprints-integration": "1.50.0-in-testing.0", - "@sofie-automation/corelib": "1.50.0-in-testing.0", - "@sofie-automation/shared-lib": "1.50.0-in-testing.0", + "@sofie-automation/blueprints-integration": "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", "deepmerge": "^4.3.1", "elastic-apm-node": "^3.43.0", diff --git a/packages/lerna.json b/packages/lerna.json index 7ce15eea13..038f5515c8 100644 --- a/packages/lerna.json +++ b/packages/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.50.0-in-testing.0", + "version": "1.51.0-in-development", "npmClient": "yarn", "useWorkspaces": true } diff --git a/packages/live-status-gateway/package.json b/packages/live-status-gateway/package.json index d13e910dd8..b1e171be42 100644 --- a/packages/live-status-gateway/package.json +++ b/packages/live-status-gateway/package.json @@ -1,6 +1,6 @@ { "name": "live-status-gateway", - "version": "1.50.0-in-testing.0", + "version": "1.51.0-in-development", "private": true, "description": "Provides state from Sofie over sockets", "license": "MIT", @@ -52,10 +52,10 @@ "production" ], "dependencies": { - "@sofie-automation/blueprints-integration": "1.50.0-in-testing.0", - "@sofie-automation/corelib": "1.50.0-in-testing.0", - "@sofie-automation/server-core-integration": "1.50.0-in-testing.0", - "@sofie-automation/shared-lib": "1.50.0-in-testing.0", + "@sofie-automation/blueprints-integration": "1.51.0-in-development", + "@sofie-automation/corelib": "1.51.0-in-development", + "@sofie-automation/server-core-integration": "1.51.0-in-development", + "@sofie-automation/shared-lib": "1.51.0-in-development", "debug": "^4.3.2", "fast-clone": "^1.5.13", "influx": "^5.9.2", diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index 041a76f057..0787f0beca 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -1,6 +1,6 @@ { "name": "mos-gateway", - "version": "1.50.0-in-testing.0", + "version": "1.51.0-in-development", "private": true, "description": "MOS-Gateway for the Sofie project", "license": "MIT", @@ -66,8 +66,8 @@ ], "dependencies": { "@mos-connection/connector": "^3.0.4", - "@sofie-automation/server-core-integration": "1.50.0-in-testing.0", - "@sofie-automation/shared-lib": "1.50.0-in-testing.0", + "@sofie-automation/server-core-integration": "1.51.0-in-development", + "@sofie-automation/shared-lib": "1.51.0-in-development", "tslib": "^2.4.0", "type-fest": "^2.19.0", "underscore": "^1.13.4", diff --git a/packages/openapi/package.json b/packages/openapi/package.json index 18a9af97e0..3cf340d853 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/openapi", - "version": "1.50.0-in-testing.0", + "version": "1.51.0-in-development", "private": true, "license": "MIT", "repository": { diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index 2e5e343a4f..689805530f 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -1,6 +1,6 @@ { "name": "playout-gateway", - "version": "1.50.0-in-testing.0", + "version": "1.51.0-in-development", "private": true, "description": "Connect to Core, play stuff", "license": "MIT", @@ -56,8 +56,8 @@ "production" ], "dependencies": { - "@sofie-automation/server-core-integration": "1.50.0-in-testing.0", - "@sofie-automation/shared-lib": "1.50.0-in-testing.0", + "@sofie-automation/server-core-integration": "1.51.0-in-development", + "@sofie-automation/shared-lib": "1.51.0-in-development", "debug": "^4.3.3", "influx": "^5.9.3", "timeline-state-resolver": "9.0.0-release50.5", diff --git a/packages/server-core-integration/package.json b/packages/server-core-integration/package.json index e742fd0bbe..cbdffe468d 100644 --- a/packages/server-core-integration/package.json +++ b/packages/server-core-integration/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/server-core-integration", - "version": "1.50.0-in-testing.0", + "version": "1.51.0-in-development", "description": "Library for connecting to Core", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -70,7 +70,7 @@ "production" ], "dependencies": { - "@sofie-automation/shared-lib": "1.50.0-in-testing.0", + "@sofie-automation/shared-lib": "1.51.0-in-development", "ejson": "^2.2.3", "eventemitter3": "^4.0.7", "faye-websocket": "^0.11.4", diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 7bc7e12293..b09fe5a6a1 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/shared-lib", - "version": "1.50.0-in-testing.0", + "version": "1.51.0-in-development", "description": "Library for types & values shared by core, workers and gateways", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/packages/yarn.lock b/packages/yarn.lock index 4aeab9c832..f2311696d5 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -5341,11 +5341,11 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/blueprints-integration@1.50.0-in-testing.0, @sofie-automation/blueprints-integration@workspace:blueprints-integration": +"@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" dependencies: - "@sofie-automation/shared-lib": 1.50.0-in-testing.0 + "@sofie-automation/shared-lib": 1.51.0-in-development tslib: ^2.4.0 type-fest: ^2.19.0 languageName: unknown @@ -5382,12 +5382,12 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/corelib@1.50.0-in-testing.0, @sofie-automation/corelib@workspace:corelib": +"@sofie-automation/corelib@1.51.0-in-development, @sofie-automation/corelib@workspace:corelib": version: 0.0.0-use.local resolution: "@sofie-automation/corelib@workspace:corelib" dependencies: - "@sofie-automation/blueprints-integration": 1.50.0-in-testing.0 - "@sofie-automation/shared-lib": 1.50.0-in-testing.0 + "@sofie-automation/blueprints-integration": 1.51.0-in-development + "@sofie-automation/shared-lib": 1.51.0-in-development fast-clone: ^1.5.13 i18next: ^21.9.1 influx: ^5.9.3 @@ -5418,9 +5418,9 @@ __metadata: resolution: "@sofie-automation/job-worker@workspace:job-worker" dependencies: "@slack/webhook": ^6.1.0 - "@sofie-automation/blueprints-integration": 1.50.0-in-testing.0 - "@sofie-automation/corelib": 1.50.0-in-testing.0 - "@sofie-automation/shared-lib": 1.50.0-in-testing.0 + "@sofie-automation/blueprints-integration": 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 deepmerge: ^4.3.1 elastic-apm-node: ^3.43.0 @@ -5449,11 +5449,11 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/server-core-integration@1.50.0-in-testing.0, @sofie-automation/server-core-integration@workspace:server-core-integration": +"@sofie-automation/server-core-integration@1.51.0-in-development, @sofie-automation/server-core-integration@workspace:server-core-integration": version: 0.0.0-use.local resolution: "@sofie-automation/server-core-integration@workspace:server-core-integration" dependencies: - "@sofie-automation/shared-lib": 1.50.0-in-testing.0 + "@sofie-automation/shared-lib": 1.51.0-in-development ejson: ^2.2.3 eventemitter3: ^4.0.7 faye-websocket: ^0.11.4 @@ -5463,7 +5463,7 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/shared-lib@1.50.0-in-testing.0, @sofie-automation/shared-lib@workspace:shared-lib": +"@sofie-automation/shared-lib@1.51.0-in-development, @sofie-automation/shared-lib@workspace:shared-lib": version: 0.0.0-use.local resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: @@ -15360,10 +15360,10 @@ asn1@evs-broadcast/node-asn1: "@asyncapi/generator": 1.9.13 "@asyncapi/html-template": 0.26.0 "@asyncapi/nodejs-ws-template": 0.9.25 - "@sofie-automation/blueprints-integration": 1.50.0-in-testing.0 - "@sofie-automation/corelib": 1.50.0-in-testing.0 - "@sofie-automation/server-core-integration": 1.50.0-in-testing.0 - "@sofie-automation/shared-lib": 1.50.0-in-testing.0 + "@sofie-automation/blueprints-integration": 1.51.0-in-development + "@sofie-automation/corelib": 1.51.0-in-development + "@sofie-automation/server-core-integration": 1.51.0-in-development + "@sofie-automation/shared-lib": 1.51.0-in-development debug: ^4.3.2 fast-clone: ^1.5.13 influx: ^5.9.2 @@ -16441,8 +16441,8 @@ asn1@evs-broadcast/node-asn1: resolution: "mos-gateway@workspace:mos-gateway" dependencies: "@mos-connection/connector": ^3.0.4 - "@sofie-automation/server-core-integration": 1.50.0-in-testing.0 - "@sofie-automation/shared-lib": 1.50.0-in-testing.0 + "@sofie-automation/server-core-integration": 1.51.0-in-development + "@sofie-automation/shared-lib": 1.51.0-in-development tslib: ^2.4.0 type-fest: ^2.19.0 underscore: ^1.13.4 @@ -18417,8 +18417,8 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "playout-gateway@workspace:playout-gateway" dependencies: - "@sofie-automation/server-core-integration": 1.50.0-in-testing.0 - "@sofie-automation/shared-lib": 1.50.0-in-testing.0 + "@sofie-automation/server-core-integration": 1.51.0-in-development + "@sofie-automation/shared-lib": 1.51.0-in-development debug: ^4.3.3 influx: ^5.9.3 timeline-state-resolver: 9.0.0-release50.5 From fcd5053d3cb5ad2027ec5d9a8bea0777b62aadd7 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 3 Jul 2023 16:56:17 +0100 Subject: [PATCH 002/479] chore: update dependencies --- meteor/package.json | 96 +- meteor/yarn.lock | 1304 +++++++++++--- package.json | 8 +- packages/blueprints-integration/package.json | 2 +- packages/corelib/package.json | 8 +- packages/documentation/package.json | 10 +- packages/job-worker/package.json | 12 +- packages/live-status-gateway/package.json | 14 +- packages/mos-gateway/package.json | 6 +- packages/openapi/package.json | 8 +- packages/package.json | 24 +- packages/playout-gateway/package.json | 8 +- packages/server-core-integration/package.json | 4 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 1504 ++++++++++++----- yarn.lock | 104 +- 16 files changed, 2375 insertions(+), 739 deletions(-) diff --git a/meteor/package.json b/meteor/package.json index eddae96abb..140932e00d 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -39,7 +39,7 @@ "watch-types": "run check-types --watch" }, "dependencies": { - "@babel/runtime": "^7.21.0", + "@babel/runtime": "^7.22.5", "@crello/react-lottie": "0.0.9", "@fortawesome/fontawesome-free": "^6.4.0", "@fortawesome/fontawesome-svg-core": "~6.4.0", @@ -50,7 +50,7 @@ "@koa/router": "^12.0.0", "@mos-connection/helper": "^3.0.4", "@nrk/core-icons": "^9.6.0", - "@popperjs/core": "^2.11.7", + "@popperjs/core": "^2.11.8", "@slack/webhook": "^6.1.0", "@sofie-automation/blueprints-integration": "portal:../packages/blueprints-integration", "@sofie-automation/corelib": "portal:../packages/corelib", @@ -58,100 +58,100 @@ "@sofie-automation/shared-lib": "portal:../packages/shared-lib", "@sofie-automation/sorensen": "^1.4.2", "app-root-path": "^3.1.0", - "bcrypt": "^5.0.1", + "bcrypt": "^5.1.0", "body-parser": "^1.20.2", "classnames": "^2.3.2", - "core-js": "^3.29.1", + "core-js": "^3.31.0", "cubic-spline": "^3.0.3", "deep-extend": "0.6.0", "deepmerge": "^4.3.1", - "i18next": "^21.9.1", - "i18next-browser-languagedetector": "^6.1.5", - "i18next-http-backend": "^1.4.1", + "i18next": "^21.10.0", + "i18next-browser-languagedetector": "^6.1.8", + "i18next-http-backend": "^1.4.5", "immutability-helper": "^3.1.1", "indexof": "0.0.1", - "koa": "^2.13.4", - "koa-bodyparser": "^4.3.0", - "lottie-web": "^5.10.2", + "koa": "^2.14.2", + "koa-bodyparser": "^4.4.1", + "lottie-web": "^5.12.2", "meteor-node-stubs": "^1.2.5", - "moment": "^2.29.2", - "nanoid": "^3.3.4", - "node-gyp": "^9.3.1", + "moment": "^2.29.4", + "nanoid": "^3.3.6", + "node-gyp": "^9.4.0", "ntp-client": "^0.5.3", "object-path": "^0.11.8", "p-lazy": "^3.1.0", "p-queue": "^7.3.4", - "promise.allsettled": "^1.0.5", + "promise.allsettled": "^1.0.6", "prop-types": "^15.8.1", "query-string": "^6.14.1", "rc-tooltip": "^6.0.1", "react": "^18.2.0", "react-circular-progressbar": "^2.1.0", "react-datepicker": "^3.8.0", - "react-dnd": "^14.0.3", - "react-dnd-html5-backend": "^14.0.1", + "react-dnd": "^14.0.5", + "react-dnd-html5-backend": "^14.1.0", "react-dom": "^18.2.0", "react-hotkeys": "^2.0.0", "react-i18next": "^11.18.6", - "react-intersection-observer": "^9.4.3", + "react-intersection-observer": "^9.5.2", "react-moment": "^0.9.7", - "react-popper": "^2.2.5", - "react-router-dom": "^5.3.3", + "react-popper": "^2.3.0", + "react-router-dom": "^5.3.4", "react-timer-hoc": "^2.3.0", - "semver": "^7.3.7", + "semver": "^7.5.3", "superfly-timeline": "^8.3.1", - "threadedclass": "^1.1.2", + "threadedclass": "^1.2.1", "timecode": "0.0.4", "type-fest": "^2.19.0", - "underscore": "^1.13.4", + "underscore": "^1.13.6", "velocity-animate": "^1.5.2", "velocity-react": "^1.4.3", - "vm2": "^3.9.14", + "vm2": "^3.9.19", "webmidi": "^2.5.3", - "winston": "^3.8.2", + "winston": "^3.9.0", "xmlbuilder": "^15.1.1" }, "devDependencies": { - "@babel/core": "^7.21.3", - "@babel/plugin-transform-modules-commonjs": "^7.21.2", + "@babel/core": "^7.22.5", + "@babel/plugin-transform-modules-commonjs": "^7.22.5", "@shopify/jest-koa-mocks": "^5.1.1", - "@sofie-automation/code-standard-preset": "~2.4.6", + "@sofie-automation/code-standard-preset": "~2.4.7", "@sofie-automation/eslint-plugin": "^0.1.1", - "@types/app-root-path": "^1.2.4", + "@types/app-root-path": "^1.2.5", "@types/body-parser": "^1.19.2", "@types/classnames": "^2.3.1", "@types/deep-extend": "^0.6.0", "@types/fibers": "^3.1.1", - "@types/jest": "^29.5.0", - "@types/koa": "^2.13.5", - "@types/koa-bodyparser": "^4.3.8", - "@types/koa__cors": "^3.3.0", + "@types/jest": "^29.5.2", + "@types/koa": "^2.13.6", + "@types/koa-bodyparser": "^4.3.10", + "@types/koa__cors": "^3.3.1", "@types/koa__router": "^12.0.0", "@types/meteor": "^2.9.2", - "@types/node": "^14", + "@types/node": "^14.18.53", "@types/prop-types": "^15.7.5", - "@types/react": "^18.0.35", + "@types/react": "^18.2.14", "@types/react-circular-progressbar": "^1.1.0", "@types/react-datepicker": "^3.1.8", - "@types/react-dom": "^18.0.11", + "@types/react-dom": "^18.2.6", "@types/react-router": "^5.1.20", "@types/react-router-dom": "^5.3.3", "@types/request": "^2.48.8", - "@types/semver": "^7.3.12", - "@types/sinon": "^10.0.13", - "@types/underscore": "1.11.4", + "@types/semver": "^7.5.0", + "@types/sinon": "^10.0.15", + "@types/underscore": "1.11.5", "@types/xml2js": "^0.4.11", - "@typescript-eslint/eslint-plugin": "^5.59.1", - "@typescript-eslint/parser": "^5.59.1", - "@typescript-eslint/utils": "^5.59.1", - "@welldone-software/why-did-you-render": "^4.3.1", - "@xmldom/xmldom": "^0.8.0", + "@typescript-eslint/eslint-plugin": "^5.60.1", + "@typescript-eslint/parser": "^5.60.1", + "@typescript-eslint/utils": "^5.60.1", + "@welldone-software/why-did-you-render": "^4.3.2", + "@xmldom/xmldom": "^0.8.8", "babel-jest": "^29.5.0", "ejson": "^2.2.3", - "eslint": "^8.39.0", + "eslint": "^8.44.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-custom-rules": "link:eslint-rules", - "eslint-plugin-jest": "^27.2.1", + "eslint-plugin-jest": "^27.2.2", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.32.2", @@ -166,12 +166,12 @@ "meteor-promise": "0.9.0", "open-cli": "^7.2.0", "prettier": "^2.8.8", - "sinon": "^14.0.0", + "sinon": "^14.0.2", "standard-version": "^9.5.0", - "ts-jest": "^29.0.5", + "ts-jest": "^29.1.1", "typescript": "~4.5", "xml2js": "^0.4.23", - "yargs": "^17.7.1" + "yargs": "^17.7.2" }, "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", "lint-staged": { diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 5191d75383..6dca08b175 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -5,6 +5,13 @@ __metadata: version: 6 cacheKey: 8 +"@aashutoshrathi/word-wrap@npm:^1.2.3": + version: 1.2.6 + resolution: "@aashutoshrathi/word-wrap@npm:1.2.6" + checksum: ada901b9e7c680d190f1d012c84217ce0063d8f5c5a7725bb91ec3c5ed99bb7572680eb2d2938a531ccbaec39a95422fcd8a6b4a13110c7d98dd75402f66a0cd + languageName: node + linkType: hard + "@acuminous/bitsyntax@npm:^0.1.2": version: 0.1.2 resolution: "@acuminous/bitsyntax@npm:0.1.2" @@ -898,6 +905,15 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/code-frame@npm:7.22.5" + dependencies: + "@babel/highlight": ^7.22.5 + checksum: cfe804f518f53faaf9a1d3e0f9f74127ab9a004912c3a16fda07fb6a633393ecb9918a053cb71804204c1b7ec3d49e1699604715e2cfb0c9f7bc4933d324ebb6 + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.20.5": version: 7.21.0 resolution: "@babel/compat-data@npm:7.21.0" @@ -905,7 +921,14 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.21.3": +"@babel/compat-data@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/compat-data@npm:7.22.5" + checksum: eb1a47ebf79ae268b4a16903e977be52629339806e248455eb9973897c503a04b701f36a9de64e19750d6e081d0561e77a514c8dc470babbeba59ae94298ed18 + languageName: node + linkType: hard + +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3": version: 7.21.3 resolution: "@babel/core@npm:7.21.3" dependencies: @@ -928,6 +951,29 @@ __metadata: languageName: node linkType: hard +"@babel/core@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/core@npm:7.22.5" + dependencies: + "@ampproject/remapping": ^2.2.0 + "@babel/code-frame": ^7.22.5 + "@babel/generator": ^7.22.5 + "@babel/helper-compilation-targets": ^7.22.5 + "@babel/helper-module-transforms": ^7.22.5 + "@babel/helpers": ^7.22.5 + "@babel/parser": ^7.22.5 + "@babel/template": ^7.22.5 + "@babel/traverse": ^7.22.5 + "@babel/types": ^7.22.5 + convert-source-map: ^1.7.0 + debug: ^4.1.0 + gensync: ^1.0.0-beta.2 + json5: ^2.2.2 + semver: ^6.3.0 + checksum: 173ae426958c90c7bbd7de622c6f13fcab8aef0fac3f138e2d47bc466d1cd1f86f71ca82ae0acb9032fd8794abed8efb56fea55c031396337eaec0d673b69d56 + languageName: node + linkType: hard + "@babel/generator@npm:^7.21.3, @babel/generator@npm:^7.7.2": version: 7.21.3 resolution: "@babel/generator@npm:7.21.3" @@ -940,6 +986,18 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/generator@npm:7.22.5" + dependencies: + "@babel/types": ^7.22.5 + "@jridgewell/gen-mapping": ^0.3.2 + "@jridgewell/trace-mapping": ^0.3.17 + jsesc: ^2.5.1 + checksum: efa64da70ca88fe69f05520cf5feed6eba6d30a85d32237671488cc355fdc379fe2c3246382a861d49574c4c2f82a317584f8811e95eb024e365faff3232b49d + languageName: node + linkType: hard + "@babel/helper-compilation-targets@npm:^7.20.7": version: 7.20.7 resolution: "@babel/helper-compilation-targets@npm:7.20.7" @@ -955,6 +1013,21 @@ __metadata: languageName: node linkType: hard +"@babel/helper-compilation-targets@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-compilation-targets@npm:7.22.5" + dependencies: + "@babel/compat-data": ^7.22.5 + "@babel/helper-validator-option": ^7.22.5 + browserslist: ^4.21.3 + lru-cache: ^5.1.1 + semver: ^6.3.0 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: a479460615acffa0f4fd0a29b740eafb53a93694265207d23a6038ccd18d183a382cacca515e77b7c9b042c3ba80b0aca0da5f1f62215140e81660d2cf721b68 + languageName: node + linkType: hard + "@babel/helper-environment-visitor@npm:^7.18.9": version: 7.18.9 resolution: "@babel/helper-environment-visitor@npm:7.18.9" @@ -962,6 +1035,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-environment-visitor@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-environment-visitor@npm:7.22.5" + checksum: 248532077d732a34cd0844eb7b078ff917c3a8ec81a7f133593f71a860a582f05b60f818dc5049c2212e5baa12289c27889a4b81d56ef409b4863db49646c4b1 + languageName: node + linkType: hard + "@babel/helper-function-name@npm:^7.21.0": version: 7.21.0 resolution: "@babel/helper-function-name@npm:7.21.0" @@ -972,6 +1052,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-function-name@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-function-name@npm:7.22.5" + dependencies: + "@babel/template": ^7.22.5 + "@babel/types": ^7.22.5 + checksum: 6b1f6ce1b1f4e513bf2c8385a557ea0dd7fa37971b9002ad19268ca4384bbe90c09681fe4c076013f33deabc63a53b341ed91e792de741b4b35e01c00238177a + languageName: node + linkType: hard + "@babel/helper-hoist-variables@npm:^7.18.6": version: 7.18.6 resolution: "@babel/helper-hoist-variables@npm:7.18.6" @@ -981,6 +1071,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-hoist-variables@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-hoist-variables@npm:7.22.5" + dependencies: + "@babel/types": ^7.22.5 + checksum: 394ca191b4ac908a76e7c50ab52102669efe3a1c277033e49467913c7ed6f7c64d7eacbeabf3bed39ea1f41731e22993f763b1edce0f74ff8563fd1f380d92cc + languageName: node + linkType: hard + "@babel/helper-module-imports@npm:^7.18.6": version: 7.18.6 resolution: "@babel/helper-module-imports@npm:7.18.6" @@ -990,6 +1089,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-imports@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-module-imports@npm:7.22.5" + dependencies: + "@babel/types": ^7.22.5 + checksum: 9ac2b0404fa38b80bdf2653fbeaf8e8a43ccb41bd505f9741d820ed95d3c4e037c62a1bcdcb6c9527d7798d2e595924c4d025daed73283badc180ada2c9c49ad + languageName: node + linkType: hard + "@babel/helper-module-transforms@npm:^7.21.2": version: 7.21.2 resolution: "@babel/helper-module-transforms@npm:7.21.2" @@ -1006,13 +1114,36 @@ __metadata: languageName: node linkType: hard -"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.19.0, @babel/helper-plugin-utils@npm:^7.20.2, @babel/helper-plugin-utils@npm:^7.8.0": +"@babel/helper-module-transforms@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-module-transforms@npm:7.22.5" + dependencies: + "@babel/helper-environment-visitor": ^7.22.5 + "@babel/helper-module-imports": ^7.22.5 + "@babel/helper-simple-access": ^7.22.5 + "@babel/helper-split-export-declaration": ^7.22.5 + "@babel/helper-validator-identifier": ^7.22.5 + "@babel/template": ^7.22.5 + "@babel/traverse": ^7.22.5 + "@babel/types": ^7.22.5 + checksum: 8985dc0d971fd17c467e8b84fe0f50f3dd8610e33b6c86e5b3ca8e8859f9448bcc5c84e08a2a14285ef388351c0484797081c8f05a03770bf44fc27bf4900e68 + languageName: node + linkType: hard + +"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.19.0, @babel/helper-plugin-utils@npm:^7.8.0": version: 7.20.2 resolution: "@babel/helper-plugin-utils@npm:7.20.2" checksum: f6cae53b7fdb1bf3abd50fa61b10b4470985b400cc794d92635da1e7077bb19729f626adc0741b69403d9b6e411cddddb9c0157a709cc7c4eeb41e663be5d74b languageName: node linkType: hard +"@babel/helper-plugin-utils@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-plugin-utils@npm:7.22.5" + checksum: c0fc7227076b6041acd2f0e818145d2e8c41968cc52fb5ca70eed48e21b8fe6dd88a0a91cbddf4951e33647336eb5ae184747ca706817ca3bef5e9e905151ff5 + languageName: node + linkType: hard + "@babel/helper-simple-access@npm:^7.20.2": version: 7.20.2 resolution: "@babel/helper-simple-access@npm:7.20.2" @@ -1022,6 +1153,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-simple-access@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-simple-access@npm:7.22.5" + dependencies: + "@babel/types": ^7.22.5 + checksum: fe9686714caf7d70aedb46c3cce090f8b915b206e09225f1e4dbc416786c2fdbbee40b38b23c268b7ccef749dd2db35f255338fb4f2444429874d900dede5ad2 + languageName: node + linkType: hard + "@babel/helper-split-export-declaration@npm:^7.18.6": version: 7.18.6 resolution: "@babel/helper-split-export-declaration@npm:7.18.6" @@ -1031,6 +1171,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-split-export-declaration@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-split-export-declaration@npm:7.22.5" + dependencies: + "@babel/types": ^7.22.5 + checksum: d10e05a02f49c1f7c578cea63d2ac55356501bbf58856d97ac9bfde4957faee21ae97c7f566aa309e38a256eef58b58e5b670a7f568b362c00e93dfffe072650 + languageName: node + linkType: hard + "@babel/helper-string-parser@npm:^7.19.4": version: 7.19.4 resolution: "@babel/helper-string-parser@npm:7.19.4" @@ -1038,6 +1187,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-string-parser@npm:7.22.5" + checksum: 836851ca5ec813077bbb303acc992d75a360267aa3b5de7134d220411c852a6f17de7c0d0b8c8dcc0f567f67874c00f4528672b2a4f1bc978a3ada64c8c78467 + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.18.6, @babel/helper-validator-identifier@npm:^7.19.1": version: 7.19.1 resolution: "@babel/helper-validator-identifier@npm:7.19.1" @@ -1045,6 +1201,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-identifier@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-validator-identifier@npm:7.22.5" + checksum: 7f0f30113474a28298c12161763b49de5018732290ca4de13cdaefd4fd0d635a6fe3f6686c37a02905fb1e64f21a5ee2b55140cf7b070e729f1bd66866506aea + languageName: node + linkType: hard + "@babel/helper-validator-option@npm:^7.18.6": version: 7.21.0 resolution: "@babel/helper-validator-option@npm:7.21.0" @@ -1052,6 +1215,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-option@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-validator-option@npm:7.22.5" + checksum: bbeca8a85ee86990215c0424997438b388b8d642d69b9f86c375a174d3cdeb270efafd1ff128bc7a1d370923d13b6e45829ba8581c027620e83e3a80c5c414b3 + languageName: node + linkType: hard + "@babel/helpers@npm:^7.21.0": version: 7.21.0 resolution: "@babel/helpers@npm:7.21.0" @@ -1063,6 +1233,17 @@ __metadata: languageName: node linkType: hard +"@babel/helpers@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helpers@npm:7.22.5" + dependencies: + "@babel/template": ^7.22.5 + "@babel/traverse": ^7.22.5 + "@babel/types": ^7.22.5 + checksum: a96e785029dff72f171190943df895ab0f76e17bf3881efd630bc5fae91215042d1c2e9ed730e8e4adf4da6f28b24bd1f54ed93b90ffbca34c197351872a084e + languageName: node + linkType: hard + "@babel/highlight@npm:^7.18.6": version: 7.18.6 resolution: "@babel/highlight@npm:7.18.6" @@ -1074,6 +1255,17 @@ __metadata: languageName: node linkType: hard +"@babel/highlight@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/highlight@npm:7.22.5" + dependencies: + "@babel/helper-validator-identifier": ^7.22.5 + chalk: ^2.0.0 + js-tokens: ^4.0.0 + checksum: f61ae6de6ee0ea8d9b5bcf2a532faec5ab0a1dc0f7c640e5047fc61630a0edb88b18d8c92eb06566d30da7a27db841aca11820ecd3ebe9ce514c9350fbed39c4 + languageName: node + linkType: hard + "@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.21.3": version: 7.21.3 resolution: "@babel/parser@npm:7.21.3" @@ -1083,6 +1275,15 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/parser@npm:7.22.5" + bin: + parser: ./bin/babel-parser.js + checksum: 470ebba516417ce8683b36e2eddd56dcfecb32c54b9bb507e28eb76b30d1c3e618fd0cfeee1f64d8357c2254514e1a19e32885cfb4e73149f4ae875436a6d59c + languageName: node + linkType: hard + "@babel/plugin-syntax-async-generators@npm:^7.8.4": version: 7.8.4 resolution: "@babel/plugin-syntax-async-generators@npm:7.8.4" @@ -1237,20 +1438,20 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-commonjs@npm:^7.21.2": - version: 7.21.2 - resolution: "@babel/plugin-transform-modules-commonjs@npm:7.21.2" +"@babel/plugin-transform-modules-commonjs@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.22.5" dependencies: - "@babel/helper-module-transforms": ^7.21.2 - "@babel/helper-plugin-utils": ^7.20.2 - "@babel/helper-simple-access": ^7.20.2 + "@babel/helper-module-transforms": ^7.22.5 + "@babel/helper-plugin-utils": ^7.22.5 + "@babel/helper-simple-access": ^7.22.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 65aa06e3e3792f39b99eb5f807034693ff0ecf80438580f7ae504f4c4448ef04147b1889ea5e6f60f3ad4a12ebbb57c6f1f979a249dadbd8d11fe22f4441918b + checksum: 2067aca8f6454d54ffcce69b02c457cfa61428e11372f6a1d99ff4fcfbb55c396ed2ca6ca886bf06c852e38c1a205b8095921b2364fd0243f3e66bc1dda61caa languageName: node linkType: hard -"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.19.0, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.9.2": +"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.19.0, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.9.2": version: 7.21.0 resolution: "@babel/runtime@npm:7.21.0" dependencies: @@ -1259,6 +1460,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/runtime@npm:7.22.5" + dependencies: + regenerator-runtime: ^0.13.11 + checksum: 12a50b7de2531beef38840d17af50c55a094253697600cee255311222390c68eed704829308d4fd305e1b3dfbce113272e428e9d9d45b1730e0fede997eaceb1 + languageName: node + linkType: hard + "@babel/template@npm:^7.20.7, @babel/template@npm:^7.3.3": version: 7.20.7 resolution: "@babel/template@npm:7.20.7" @@ -1270,6 +1480,17 @@ __metadata: languageName: node linkType: hard +"@babel/template@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/template@npm:7.22.5" + dependencies: + "@babel/code-frame": ^7.22.5 + "@babel/parser": ^7.22.5 + "@babel/types": ^7.22.5 + checksum: c5746410164039aca61829cdb42e9a55410f43cace6f51ca443313f3d0bdfa9a5a330d0b0df73dc17ef885c72104234ae05efede37c1cc8a72dc9f93425977a3 + languageName: node + linkType: hard + "@babel/traverse@npm:^7.21.0, @babel/traverse@npm:^7.21.2, @babel/traverse@npm:^7.21.3, @babel/traverse@npm:^7.7.2": version: 7.21.3 resolution: "@babel/traverse@npm:7.21.3" @@ -1288,6 +1509,24 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/traverse@npm:7.22.5" + dependencies: + "@babel/code-frame": ^7.22.5 + "@babel/generator": ^7.22.5 + "@babel/helper-environment-visitor": ^7.22.5 + "@babel/helper-function-name": ^7.22.5 + "@babel/helper-hoist-variables": ^7.22.5 + "@babel/helper-split-export-declaration": ^7.22.5 + "@babel/parser": ^7.22.5 + "@babel/types": ^7.22.5 + debug: ^4.1.0 + globals: ^11.1.0 + checksum: 560931422dc1761f2df723778dcb4e51ce0d02e560cf2caa49822921578f49189a5a7d053b78a32dca33e59be886a6b2200a6e24d4ae9b5086ca0ba803815694 + languageName: node + linkType: hard + "@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.6, @babel/types@npm:^7.20.2, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.0, @babel/types@npm:^7.21.2, @babel/types@npm:^7.21.3, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3": version: 7.21.3 resolution: "@babel/types@npm:7.21.3" @@ -1299,6 +1538,17 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/types@npm:7.22.5" + dependencies: + "@babel/helper-string-parser": ^7.22.5 + "@babel/helper-validator-identifier": ^7.22.5 + to-fast-properties: ^2.0.0 + checksum: c13a9c1dc7d2d1a241a2f8363540cb9af1d66e978e8984b400a20c4f38ba38ca29f06e26a0f2d49a70bad9e57615dac09c35accfddf1bb90d23cd3e0a0bab892 + languageName: node + linkType: hard + "@babel/types@npm:^7.8.3": version: 7.21.4 resolution: "@babel/types@npm:7.21.4" @@ -1400,6 +1650,23 @@ __metadata: languageName: node linkType: hard +"@eslint/eslintrc@npm:^2.1.0": + version: 2.1.0 + resolution: "@eslint/eslintrc@npm:2.1.0" + dependencies: + ajv: ^6.12.4 + debug: ^4.3.2 + espree: ^9.6.0 + globals: ^13.19.0 + ignore: ^5.2.0 + import-fresh: ^3.2.1 + js-yaml: ^4.1.0 + minimatch: ^3.1.2 + strip-json-comments: ^3.1.1 + checksum: d5ed0adbe23f6571d8c9bb0ca6edf7618dc6aed4046aa56df7139f65ae7b578874e0d9c796df784c25bda648ceb754b6320277d828c8b004876d7443b8dc018c + languageName: node + linkType: hard + "@eslint/js@npm:8.39.0": version: 8.39.0 resolution: "@eslint/js@npm:8.39.0" @@ -1407,6 +1674,13 @@ __metadata: languageName: node linkType: hard +"@eslint/js@npm:8.44.0": + version: 8.44.0 + resolution: "@eslint/js@npm:8.44.0" + checksum: fc539583226a28f5677356e9f00d2789c34253f076643d2e32888250e509a4e13aafe0880cb2425139051de0f3a48d25bfc5afa96b7304f203b706c17340e3cf + languageName: node + linkType: hard + "@fortawesome/fontawesome-common-types@npm:6.4.0": version: 6.4.0 resolution: "@fortawesome/fontawesome-common-types@npm:6.4.0" @@ -1458,6 +1732,17 @@ __metadata: languageName: node linkType: hard +"@humanwhocodes/config-array@npm:^0.11.10": + version: 0.11.10 + resolution: "@humanwhocodes/config-array@npm:0.11.10" + dependencies: + "@humanwhocodes/object-schema": ^1.2.1 + debug: ^4.1.1 + minimatch: ^3.0.5 + checksum: 1b1302e2403d0e35bc43e66d67a2b36b0ad1119efc704b5faff68c41f791a052355b010fb2d27ef022670f550de24cd6d08d5ecf0821c16326b7dcd0ee5d5d8a + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.11.8": version: 0.11.8 resolution: "@humanwhocodes/config-array@npm:0.11.8" @@ -1503,6 +1788,20 @@ __metadata: languageName: node linkType: hard +"@isaacs/cliui@npm:^8.0.2": + version: 8.0.2 + resolution: "@isaacs/cliui@npm:8.0.2" + dependencies: + string-width: ^5.1.2 + string-width-cjs: "npm:string-width@^4.2.0" + strip-ansi: ^7.0.1 + strip-ansi-cjs: "npm:strip-ansi@^6.0.1" + wrap-ansi: ^8.1.0 + wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" + checksum: 4a473b9b32a7d4d3cfb7a614226e555091ff0c5a29a1734c28c72a182c2f6699b26fc6b5c2131dfd841e86b185aea714c72201d7c98c2fba5f17709333a67aeb + languageName: node + linkType: hard + "@istanbuljs/load-nyc-config@npm:^1.0.0": version: 1.1.0 resolution: "@istanbuljs/load-nyc-config@npm:1.1.0" @@ -1916,6 +2215,15 @@ __metadata: languageName: node linkType: hard +"@npmcli/fs@npm:^3.1.0": + version: 3.1.0 + resolution: "@npmcli/fs@npm:3.1.0" + dependencies: + semver: ^7.3.5 + checksum: a50a6818de5fc557d0b0e6f50ec780a7a02ab8ad07e5ac8b16bf519e0ad60a144ac64f97d05c443c3367235d337182e1d012bbac0eb8dbae8dc7b40b193efd0e + languageName: node + linkType: hard + "@npmcli/move-file@npm:^2.0.0": version: 2.0.1 resolution: "@npmcli/move-file@npm:2.0.1" @@ -1940,10 +2248,60 @@ __metadata: languageName: node linkType: hard -"@popperjs/core@npm:^2.11.7": - version: 2.11.7 - resolution: "@popperjs/core@npm:2.11.7" - checksum: 5b6553747899683452a1d28898c1b39173a4efd780e74360bfcda8eb42f1c5e819602769c81a10920fc68c881d07fb40429604517d499567eac079cfa6470f19 +"@opentelemetry/core@npm:1.14.0, @opentelemetry/core@npm:^1.11.0": + version: 1.14.0 + resolution: "@opentelemetry/core@npm:1.14.0" + dependencies: + "@opentelemetry/semantic-conventions": 1.14.0 + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.5.0" + checksum: 061e6f3bee492e020915427da94da902846ed6ee922bdb21a70563e5fbd3ebb39e8e3ae6eb1071eb775d27ca31093929ab65fe8abc81228042be8d9a3c96bac8 + languageName: node + linkType: hard + +"@opentelemetry/resources@npm:1.14.0": + version: 1.14.0 + resolution: "@opentelemetry/resources@npm:1.14.0" + dependencies: + "@opentelemetry/core": 1.14.0 + "@opentelemetry/semantic-conventions": 1.14.0 + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.5.0" + checksum: 4494fcad2a9d8de215149675ce4f16885f6d7d0b3318c44a1f8d2e50009de07853c94b428462f70d1dd3d3681a5738d6b625806b7939839524db49a81eb90a2c + languageName: node + linkType: hard + +"@opentelemetry/sdk-metrics@npm:^1.12.0": + version: 1.14.0 + resolution: "@opentelemetry/sdk-metrics@npm:1.14.0" + dependencies: + "@opentelemetry/core": 1.14.0 + "@opentelemetry/resources": 1.14.0 + lodash.merge: 4.6.2 + peerDependencies: + "@opentelemetry/api": ">=1.3.0 <1.5.0" + checksum: 633f58b02cae9d16827e3fc6e7a37329ee0ebc63e4b0193cf3c80afa3553b0d737019df889ba73644254fc3134108b965c93e6f097aacee396a671bed1b2188d + languageName: node + linkType: hard + +"@opentelemetry/semantic-conventions@npm:1.14.0": + version: 1.14.0 + resolution: "@opentelemetry/semantic-conventions@npm:1.14.0" + checksum: 0d4f752035ac25d10cfcaf0e16f29bff8fa47877baff61dc4d047f9fe1e70f1459d02f5b70982c579db48584038e4fe263755b35ffa7254385604917337469f3 + languageName: node + linkType: hard + +"@pkgjs/parseargs@npm:^0.11.0": + version: 0.11.0 + resolution: "@pkgjs/parseargs@npm:0.11.0" + checksum: 6ad6a00fc4f2f2cfc6bff76fb1d88b8ee20bc0601e18ebb01b6d4be583733a860239a521a7fbca73b612e66705078809483549d2b18f370eb346c5155c8e4a0f + languageName: node + linkType: hard + +"@popperjs/core@npm:^2.11.8": + version: 2.11.8 + resolution: "@popperjs/core@npm:2.11.8" + checksum: e5c69fdebf52a4012f6a1f14817ca8e9599cb1be73dd1387e1785e2ed5e5f0862ff817f420a87c7fc532add1f88a12e25aeb010ffcbdc98eace3d55ce2139cf0 languageName: node linkType: hard @@ -2093,15 +2451,15 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/blueprints-integration@portal:../packages/blueprints-integration::locator=automation-core%40workspace%3A." dependencies: - "@sofie-automation/shared-lib": 1.50.0-in-testing - tslib: ^2.4.0 + "@sofie-automation/shared-lib": 1.51.0-in-development + tslib: ^2.6.0 type-fest: ^2.19.0 languageName: node linkType: soft -"@sofie-automation/code-standard-preset@npm:~2.4.6": - version: 2.4.6 - resolution: "@sofie-automation/code-standard-preset@npm:2.4.6" +"@sofie-automation/code-standard-preset@npm:~2.4.7": + version: 2.4.7 + resolution: "@sofie-automation/code-standard-preset@npm:2.4.7" dependencies: "@sofie-automation/eslint-plugin": ^0.1.1 "@typescript-eslint/eslint-plugin": ^5.59.1 @@ -2126,7 +2484,7 @@ __metadata: bin: sofie-licensecheck: ./bin/checkLicenses.mjs sofie-version: ./bin/updateVersion.mjs - checksum: 3fe36dd192d4b3a774988c3fe60bd8f2c1130a07d77c335ae1abf731e9584bb8a8741d18f38ed58b9a5f90998570f3fccc048e32b7f77da7afd22a5e09adc337 + checksum: 399ec1db25a07da1b19533403fb86ad67f930bf3196df71562b41800636a1f0008038bf7ae4c720f9ec478e5330d31d335d0690e1e2a281fd9c33a66e25ed740 languageName: node linkType: hard @@ -2134,18 +2492,18 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/corelib@portal:../packages/corelib::locator=automation-core%40workspace%3A." dependencies: - "@sofie-automation/blueprints-integration": 1.50.0-in-testing - "@sofie-automation/shared-lib": 1.50.0-in-testing + "@sofie-automation/blueprints-integration": 1.51.0-in-development + "@sofie-automation/shared-lib": 1.51.0-in-development fast-clone: ^1.5.13 - i18next: ^21.9.1 + i18next: ^21.10.0 influx: ^5.9.3 - nanoid: ^3.3.4 + nanoid: ^3.3.6 object-path: ^0.11.8 prom-client: ^14.2.0 timecode: 0.0.4 - tslib: ^2.4.0 + tslib: ^2.6.0 type-fest: ^2.19.0 - underscore: ^1.13.4 + underscore: ^1.13.6 peerDependencies: mongodb: ^4.13.0 languageName: node @@ -2166,22 +2524,22 @@ __metadata: resolution: "@sofie-automation/job-worker@portal:../packages/job-worker::locator=automation-core%40workspace%3A." dependencies: "@slack/webhook": ^6.1.0 - "@sofie-automation/blueprints-integration": 1.50.0-in-testing - "@sofie-automation/corelib": 1.50.0-in-testing - "@sofie-automation/shared-lib": 1.50.0-in-testing + "@sofie-automation/blueprints-integration": 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 deepmerge: ^4.3.1 - elastic-apm-node: ^3.43.0 + elastic-apm-node: ^3.47.0 eventemitter3: ^4.0.7 - mongodb: ^4.13.0 + mongodb: ^4.16.0 p-lazy: ^3.1.0 p-timeout: ^4.1.0 superfly-timeline: ^8.3.1 - threadedclass: ^1.1.2 - tslib: ^2.4.0 + threadedclass: ^1.2.1 + tslib: ^2.6.0 type-fest: ^2.19.0 - underscore: ^1.13.4 - vm2: ^3.9.14 + underscore: ^1.13.6 + vm2: ^3.9.19 languageName: node linkType: soft @@ -2191,7 +2549,7 @@ __metadata: dependencies: "@mos-connection/model": ^3.0.4 timeline-state-resolver-types: 9.0.0-release50.5 - tslib: ^2.4.0 + tslib: ^2.6.0 type-fest: ^2.19.0 languageName: node linkType: soft @@ -2226,10 +2584,10 @@ __metadata: languageName: node linkType: hard -"@types/app-root-path@npm:^1.2.4": - version: 1.2.4 - resolution: "@types/app-root-path@npm:1.2.4" - checksum: 3807abaf9006bb3fb72e8feb4cca5b252de035ee5992118158a0c519f0756fd22242bd9f3d46349558ae079899746f7501f7558206ba2e90f077b4a8fa64072f +"@types/app-root-path@npm:^1.2.5": + version: 1.2.5 + resolution: "@types/app-root-path@npm:1.2.5" + checksum: d6a08494dadae4a751ddbf28c821665b01e51e20fe35cb04a122ffd56305d4d0a04107f4d9d2003ee20336a2949050b93bddf8645905e3ecc8b5b95f1d084936 languageName: node linkType: hard @@ -2421,13 +2779,13 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:^29.5.0": - version: 29.5.0 - resolution: "@types/jest@npm:29.5.0" +"@types/jest@npm:^29.5.2": + version: 29.5.2 + resolution: "@types/jest@npm:29.5.2" dependencies: expect: ^29.0.0 pretty-format: ^29.0.0 - checksum: cd877e5c56d299cceb8bfdcbb1a77723c706750dd3c3bc47403bc3599b8faff590a3b009c68bb5b11bf7a8c77d1fb01de5e124329b4a08e65f1cdda28b0ecdb8 + checksum: 7d205599ea3cccc262bad5cc173d3242d6bf8138c99458509230e4ecef07a52d6ddcde5a1dbd49ace655c0af51d2dbadef3748697292ea4d86da19d9e03e19c0 languageName: node linkType: hard @@ -2465,7 +2823,7 @@ __metadata: languageName: node linkType: hard -"@types/koa-bodyparser@npm:^4.3.8": +"@types/koa-bodyparser@npm:^4.3.10": version: 4.3.10 resolution: "@types/koa-bodyparser@npm:4.3.10" dependencies: @@ -2483,7 +2841,7 @@ __metadata: languageName: node linkType: hard -"@types/koa@npm:*, @types/koa@npm:^2.13.5": +"@types/koa@npm:*, @types/koa@npm:^2.13.6": version: 2.13.6 resolution: "@types/koa@npm:2.13.6" dependencies: @@ -2499,7 +2857,7 @@ __metadata: languageName: node linkType: hard -"@types/koa__cors@npm:^3.3.0": +"@types/koa__cors@npm:^3.3.1": version: 3.3.1 resolution: "@types/koa__cors@npm:3.3.1" dependencies: @@ -2559,10 +2917,10 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^14": - version: 14.18.42 - resolution: "@types/node@npm:14.18.42" - checksum: 1c92f04a482ab54a21342b3911fc6f0093f04d3314197bc0e2f20012e9efc929c44e2ea41990b9b3cde420d7859c9ed716733f3e65c0cd6c2910a55799465f6b +"@types/node@npm:^14.18.53": + version: 14.18.53 + resolution: "@types/node@npm:14.18.53" + checksum: 3acbcf4e38cdc5999c824db5c6689ca8f4a185562aad5c0263d9859381d8590a16e6a72343aeb30d48ff9a7ac9b6ae259bcb8c2d594703a30d12277904524704 languageName: node linkType: hard @@ -2630,12 +2988,12 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:^18.0.11": - version: 18.0.11 - resolution: "@types/react-dom@npm:18.0.11" +"@types/react-dom@npm:^18.2.6": + version: 18.2.6 + resolution: "@types/react-dom@npm:18.2.6" dependencies: "@types/react": "*" - checksum: 579691e4d5ec09688087568037c35edf8cfb1ab3e07f6c60029280733ee7b5c06d66df6fcc90786702c93ac8cb13bc7ff16c79ddfc75d082938fbaa36e1cdbf4 + checksum: b56e42efab121a3a8013d2eb8c1688e6028a25ea6d33c4362d2846f0af3760b164b4d7c34846614024cfb8956cca70dd1743487f152e32ff89a00fe6fbd2be54 languageName: node linkType: hard @@ -2660,7 +3018,7 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:*, @types/react@npm:^18.0.35": +"@types/react@npm:*": version: 18.0.35 resolution: "@types/react@npm:18.0.35" dependencies: @@ -2671,6 +3029,17 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:^18.2.14": + version: 18.2.14 + resolution: "@types/react@npm:18.2.14" + dependencies: + "@types/prop-types": "*" + "@types/scheduler": "*" + csstype: ^3.0.2 + checksum: a6a5e8cc78f486b9020d1ad009aa6c56943c68c7c6376e0f8399e9cbcd950b7b8f5d73f00200f5379f5e58d31d57d8aed24357f301d8e86108cd438ce6c8b3dd + languageName: node + linkType: hard + "@types/request@npm:^2.48.8": version: 2.48.8 resolution: "@types/request@npm:2.48.8" @@ -2697,6 +3066,13 @@ __metadata: languageName: node linkType: hard +"@types/semver@npm:^7.5.0": + version: 7.5.0 + resolution: "@types/semver@npm:7.5.0" + checksum: 0a64b9b9c7424d9a467658b18dd70d1d781c2d6f033096a6e05762d20ebbad23c1b69b0083b0484722aabf35640b78ccc3de26368bcae1129c87e9df028a22e2 + languageName: node + linkType: hard + "@types/send@npm:*": version: 0.17.1 resolution: "@types/send@npm:0.17.1" @@ -2717,12 +3093,12 @@ __metadata: languageName: node linkType: hard -"@types/sinon@npm:^10.0.13": - version: 10.0.13 - resolution: "@types/sinon@npm:10.0.13" +"@types/sinon@npm:^10.0.15": + version: 10.0.15 + resolution: "@types/sinon@npm:10.0.15" dependencies: "@types/sinonjs__fake-timers": "*" - checksum: 46a14c888db50f0098ec53d451877e0111d878ec4a653b9e9ed7f8e54de386d6beb0e528ddc3e95cd3361a8ab9ad54e4cca33cd88d45b9227b83e9fc8fb6688a + checksum: cec6d7d9d5582ca3ac851b029d5d90451bfe6d376164253792a6eb6ddcd609a0411a7fac9ed92e1879e7d3ec091d2ea2e8dbb4f6140a1065439b81dc20cafa7c languageName: node linkType: hard @@ -2761,13 +3137,20 @@ __metadata: languageName: node linkType: hard -"@types/underscore@npm:*, @types/underscore@npm:1.11.4": +"@types/underscore@npm:*": version: 1.11.4 resolution: "@types/underscore@npm:1.11.4" checksum: db9f8486bc851b732259e51f42d62aad1ae2158be5724612dc125ece5f5d61c51447f9dea28284c2a0f79cb95e788d01cb5ce97709880019213e69fab0dd1696 languageName: node linkType: hard +"@types/underscore@npm:1.11.5": + version: 1.11.5 + resolution: "@types/underscore@npm:1.11.5" + checksum: 6cd928c436bd65a7b544c979e0958762816dc3fd4f0d430d055faa1e914336568c3a8dd52350760c16ecc37be71d40b0792012bae455d8c5d63e50f02986c9e2 + languageName: node + linkType: hard + "@types/webidl-conversions@npm:*": version: 7.0.0 resolution: "@types/webidl-conversions@npm:7.0.0" @@ -2834,6 +3217,30 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/eslint-plugin@npm:^5.60.1": + version: 5.60.1 + resolution: "@typescript-eslint/eslint-plugin@npm:5.60.1" + dependencies: + "@eslint-community/regexpp": ^4.4.0 + "@typescript-eslint/scope-manager": 5.60.1 + "@typescript-eslint/type-utils": 5.60.1 + "@typescript-eslint/utils": 5.60.1 + debug: ^4.3.4 + grapheme-splitter: ^1.0.4 + ignore: ^5.2.0 + natural-compare-lite: ^1.4.0 + semver: ^7.3.7 + tsutils: ^3.21.0 + peerDependencies: + "@typescript-eslint/parser": ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 6ea3fdc64b216ee709318bfce1573cd8d90836150f0075aaa8755c347541af9ec026043e538a3264d28d1b32ff49b1fd7c6163826b8513f19f0957fefccf7752 + languageName: node + linkType: hard + "@typescript-eslint/parser@npm:^5.59.1": version: 5.59.1 resolution: "@typescript-eslint/parser@npm:5.59.1" @@ -2851,6 +3258,23 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/parser@npm:^5.60.1": + version: 5.60.1 + resolution: "@typescript-eslint/parser@npm:5.60.1" + dependencies: + "@typescript-eslint/scope-manager": 5.60.1 + "@typescript-eslint/types": 5.60.1 + "@typescript-eslint/typescript-estree": 5.60.1 + debug: ^4.3.4 + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 08f1552ab0da178524a8de3654d2fb7c8ecb9efdad8e771c9cbf4af555c42e77d17b2c182d139a531cc76c3cabd091d1d25024c2c215cb809dca8b147c8a493c + languageName: node + linkType: hard + "@typescript-eslint/scope-manager@npm:5.57.0": version: 5.57.0 resolution: "@typescript-eslint/scope-manager@npm:5.57.0" @@ -2871,6 +3295,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:5.60.1": + version: 5.60.1 + resolution: "@typescript-eslint/scope-manager@npm:5.60.1" + dependencies: + "@typescript-eslint/types": 5.60.1 + "@typescript-eslint/visitor-keys": 5.60.1 + checksum: 32c0786123f12fbb861aba3527471134a2e9978c7f712e0d7650080651870903482aed72a55f81deba9493118c1ca3c57edaaaa75d7acd9892818e3e9cc341ef + languageName: node + linkType: hard + "@typescript-eslint/type-utils@npm:5.59.1": version: 5.59.1 resolution: "@typescript-eslint/type-utils@npm:5.59.1" @@ -2888,6 +3322,23 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/type-utils@npm:5.60.1": + version: 5.60.1 + resolution: "@typescript-eslint/type-utils@npm:5.60.1" + dependencies: + "@typescript-eslint/typescript-estree": 5.60.1 + "@typescript-eslint/utils": 5.60.1 + debug: ^4.3.4 + tsutils: ^3.21.0 + peerDependencies: + eslint: "*" + peerDependenciesMeta: + typescript: + optional: true + checksum: f8d5f87b5441d5c671f69631efd103f5f45e0cb7dbe0131a5b4234a5208ac845041219e8baaa3adc341e82a602165dd6fabf4fd06964d0109d0875425c8ac918 + languageName: node + linkType: hard + "@typescript-eslint/types@npm:5.57.0": version: 5.57.0 resolution: "@typescript-eslint/types@npm:5.57.0" @@ -2902,6 +3353,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:5.60.1": + version: 5.60.1 + resolution: "@typescript-eslint/types@npm:5.60.1" + checksum: 766b6c857493b72a8f515e6a8e409476a317b7a7f0401fbcdf18f417839fca004dcaf06f58eb5ba00777e3ca9c68cd2f56fda79f3a8eb8a418095b5b1f625712 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:5.57.0": version: 5.57.0 resolution: "@typescript-eslint/typescript-estree@npm:5.57.0" @@ -2938,6 +3396,24 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:5.60.1": + version: 5.60.1 + resolution: "@typescript-eslint/typescript-estree@npm:5.60.1" + dependencies: + "@typescript-eslint/types": 5.60.1 + "@typescript-eslint/visitor-keys": 5.60.1 + debug: ^4.3.4 + globby: ^11.1.0 + is-glob: ^4.0.3 + semver: ^7.3.7 + tsutils: ^3.21.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 5bb9d08c3cbc303fc64647878cae37283c4cfa9e3ed00da02ee25dc2e46798a1ad6964c9f04086f0134716671357e6569a65ea0ae75f0f3ff94ae67666385c6f + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:5.59.1, @typescript-eslint/utils@npm:^5.59.1": version: 5.59.1 resolution: "@typescript-eslint/utils@npm:5.59.1" @@ -2956,6 +3432,24 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:5.60.1, @typescript-eslint/utils@npm:^5.60.1": + version: 5.60.1 + resolution: "@typescript-eslint/utils@npm:5.60.1" + dependencies: + "@eslint-community/eslint-utils": ^4.2.0 + "@types/json-schema": ^7.0.9 + "@types/semver": ^7.3.12 + "@typescript-eslint/scope-manager": 5.60.1 + "@typescript-eslint/types": 5.60.1 + "@typescript-eslint/typescript-estree": 5.60.1 + eslint-scope: ^5.1.1 + semver: ^7.3.7 + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 00c1adaa09d5d5be947e98962a78c21ed08c3ac46dd5ddd7b78f6102537d50afd4578a42a3e09a24dd51f5bc493f0b968627b4423647540164b2d2380afa9246 + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:^5.10.0": version: 5.57.0 resolution: "@typescript-eslint/utils@npm:5.57.0" @@ -2994,7 +3488,17 @@ __metadata: languageName: node linkType: hard -"@welldone-software/why-did-you-render@npm:^4.3.1": +"@typescript-eslint/visitor-keys@npm:5.60.1": + version: 5.60.1 + resolution: "@typescript-eslint/visitor-keys@npm:5.60.1" + dependencies: + "@typescript-eslint/types": 5.60.1 + eslint-visitor-keys: ^3.3.0 + checksum: 137f6a6f8efb398969087147b59f99f7d0deed044d89d7efce3631bb90bc32e3a13a5cee6a65e1c9830862c5c4402ac1a9b2c9e31fe46d1716602af2813bffae + languageName: node + linkType: hard + +"@welldone-software/why-did-you-render@npm:^4.3.2": version: 4.3.2 resolution: "@welldone-software/why-did-you-render@npm:4.3.2" dependencies: @@ -3005,10 +3509,10 @@ __metadata: languageName: node linkType: hard -"@xmldom/xmldom@npm:^0.8.0": - version: 0.8.6 - resolution: "@xmldom/xmldom@npm:0.8.6" - checksum: f17ac6d99a971a6aeb831fcfc5cfa86f367664e45815046548814b2deb17ccc421fef4e0d5ba29e66179d112b552f6caa5680064f8e7bd8a389b788a60404c8e +"@xmldom/xmldom@npm:^0.8.8": + version: 0.8.8 + resolution: "@xmldom/xmldom@npm:0.8.8" + checksum: 5f5fc0482fcc599f62e3009516932a265e00f1bb2093fe2c76f3f8d9bfebdd13246f48d4132c9b301c7a573f0fa8712e56aa747dce75b179c2b73f1dde7b5f42 languageName: node linkType: hard @@ -3147,6 +3651,15 @@ __metadata: languageName: node linkType: hard +"acorn@npm:^8.9.0": + version: 8.9.0 + resolution: "acorn@npm:8.9.0" + bin: + acorn: bin/acorn + checksum: 25dfb94952386ecfb847e61934de04a4e7c2dc21c2e700fc4e2ef27ce78cb717700c4c4f279cd630bb4774948633c3859fc16063ec8573bda4568e0a312e6744 + languageName: node + linkType: hard + "add-stream@npm:^1.0.0": version: 1.0.0 resolution: "add-stream@npm:1.0.0" @@ -3263,7 +3776,7 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^6.0.0": +"ansi-styles@npm:^6.0.0, ansi-styles@npm:^6.1.0": version: 6.2.1 resolution: "ansi-styles@npm:6.2.1" checksum: ef940f2f0ced1a6347398da88a91da7930c33ecac3c77b72c5905f8b8fe402c52e6fde304ff5347f616e27a742da3f1dc76de98f6866c69251ad0b07a66776d9 @@ -3523,9 +4036,9 @@ __metadata: version: 0.0.0-use.local resolution: "automation-core@workspace:." dependencies: - "@babel/core": ^7.21.3 - "@babel/plugin-transform-modules-commonjs": ^7.21.2 - "@babel/runtime": ^7.21.0 + "@babel/core": ^7.22.5 + "@babel/plugin-transform-modules-commonjs": ^7.22.5 + "@babel/runtime": ^7.22.5 "@crello/react-lottie": 0.0.9 "@fortawesome/fontawesome-free": ^6.4.0 "@fortawesome/fontawesome-svg-core": ~6.4.0 @@ -3536,124 +4049,124 @@ __metadata: "@koa/router": ^12.0.0 "@mos-connection/helper": ^3.0.4 "@nrk/core-icons": ^9.6.0 - "@popperjs/core": ^2.11.7 + "@popperjs/core": ^2.11.8 "@shopify/jest-koa-mocks": ^5.1.1 "@slack/webhook": ^6.1.0 "@sofie-automation/blueprints-integration": "portal:../packages/blueprints-integration" - "@sofie-automation/code-standard-preset": ~2.4.6 + "@sofie-automation/code-standard-preset": ~2.4.7 "@sofie-automation/corelib": "portal:../packages/corelib" "@sofie-automation/eslint-plugin": ^0.1.1 "@sofie-automation/job-worker": "portal:../packages/job-worker" "@sofie-automation/shared-lib": "portal:../packages/shared-lib" "@sofie-automation/sorensen": ^1.4.2 - "@types/app-root-path": ^1.2.4 + "@types/app-root-path": ^1.2.5 "@types/body-parser": ^1.19.2 "@types/classnames": ^2.3.1 "@types/deep-extend": ^0.6.0 "@types/fibers": ^3.1.1 - "@types/jest": ^29.5.0 - "@types/koa": ^2.13.5 - "@types/koa-bodyparser": ^4.3.8 - "@types/koa__cors": ^3.3.0 + "@types/jest": ^29.5.2 + "@types/koa": ^2.13.6 + "@types/koa-bodyparser": ^4.3.10 + "@types/koa__cors": ^3.3.1 "@types/koa__router": ^12.0.0 "@types/meteor": ^2.9.2 - "@types/node": ^14 + "@types/node": ^14.18.53 "@types/prop-types": ^15.7.5 - "@types/react": ^18.0.35 + "@types/react": ^18.2.14 "@types/react-circular-progressbar": ^1.1.0 "@types/react-datepicker": ^3.1.8 - "@types/react-dom": ^18.0.11 + "@types/react-dom": ^18.2.6 "@types/react-router": ^5.1.20 "@types/react-router-dom": ^5.3.3 "@types/request": ^2.48.8 - "@types/semver": ^7.3.12 - "@types/sinon": ^10.0.13 - "@types/underscore": 1.11.4 + "@types/semver": ^7.5.0 + "@types/sinon": ^10.0.15 + "@types/underscore": 1.11.5 "@types/xml2js": ^0.4.11 - "@typescript-eslint/eslint-plugin": ^5.59.1 - "@typescript-eslint/parser": ^5.59.1 - "@typescript-eslint/utils": ^5.59.1 - "@welldone-software/why-did-you-render": ^4.3.1 - "@xmldom/xmldom": ^0.8.0 + "@typescript-eslint/eslint-plugin": ^5.60.1 + "@typescript-eslint/parser": ^5.60.1 + "@typescript-eslint/utils": ^5.60.1 + "@welldone-software/why-did-you-render": ^4.3.2 + "@xmldom/xmldom": ^0.8.8 app-root-path: ^3.1.0 babel-jest: ^29.5.0 - bcrypt: ^5.0.1 + bcrypt: ^5.1.0 body-parser: ^1.20.2 classnames: ^2.3.2 - core-js: ^3.29.1 + core-js: ^3.31.0 cubic-spline: ^3.0.3 deep-extend: 0.6.0 deepmerge: ^4.3.1 ejson: ^2.2.3 - eslint: ^8.39.0 + eslint: ^8.44.0 eslint-config-prettier: ^8.8.0 eslint-plugin-custom-rules: "link:eslint-rules" - eslint-plugin-jest: ^27.2.1 + eslint-plugin-jest: ^27.2.2 eslint-plugin-node: ^11.1.0 eslint-plugin-prettier: ^4.2.1 eslint-plugin-react: ^7.32.2 fast-clone: ^1.5.13 fibers-npm: "npm:fibers@4.0.3" glob: ^8.1.0 - i18next: ^21.9.1 - i18next-browser-languagedetector: ^6.1.5 + i18next: ^21.10.0 + i18next-browser-languagedetector: ^6.1.8 i18next-conv: ^10.2.0 - i18next-http-backend: ^1.4.1 + i18next-http-backend: ^1.4.5 i18next-scanner: ^4.2.0 immutability-helper: ^3.1.1 indexof: 0.0.1 jest: ^29.5.0 jest-environment-jsdom: ^29.5.0 - koa: ^2.13.4 - koa-bodyparser: ^4.3.0 + koa: ^2.14.2 + koa-bodyparser: ^4.4.1 legally: ^3.5.10 - lottie-web: ^5.10.2 + lottie-web: ^5.12.2 meteor-node-stubs: ^1.2.5 meteor-promise: 0.9.0 - moment: ^2.29.2 - nanoid: ^3.3.4 - node-gyp: ^9.3.1 + moment: ^2.29.4 + nanoid: ^3.3.6 + node-gyp: ^9.4.0 ntp-client: ^0.5.3 object-path: ^0.11.8 open-cli: ^7.2.0 p-lazy: ^3.1.0 p-queue: ^7.3.4 prettier: ^2.8.8 - promise.allsettled: ^1.0.5 + promise.allsettled: ^1.0.6 prop-types: ^15.8.1 query-string: ^6.14.1 rc-tooltip: ^6.0.1 react: ^18.2.0 react-circular-progressbar: ^2.1.0 react-datepicker: ^3.8.0 - react-dnd: ^14.0.3 - react-dnd-html5-backend: ^14.0.1 + react-dnd: ^14.0.5 + react-dnd-html5-backend: ^14.1.0 react-dom: ^18.2.0 react-hotkeys: ^2.0.0 react-i18next: ^11.18.6 - react-intersection-observer: ^9.4.3 + react-intersection-observer: ^9.5.2 react-moment: ^0.9.7 - react-popper: ^2.2.5 - react-router-dom: ^5.3.3 + react-popper: ^2.3.0 + react-router-dom: ^5.3.4 react-timer-hoc: ^2.3.0 - semver: ^7.3.7 - sinon: ^14.0.0 + semver: ^7.5.3 + sinon: ^14.0.2 standard-version: ^9.5.0 superfly-timeline: ^8.3.1 - threadedclass: ^1.1.2 + threadedclass: ^1.2.1 timecode: 0.0.4 - ts-jest: ^29.0.5 + ts-jest: ^29.1.1 type-fest: ^2.19.0 typescript: ~4.5 - underscore: ^1.13.4 + underscore: ^1.13.6 velocity-animate: ^1.5.2 velocity-react: ^1.4.3 - vm2: ^3.9.14 + vm2: ^3.9.19 webmidi: ^2.5.3 - winston: ^3.8.2 + winston: ^3.9.0 xml2js: ^0.4.23 xmlbuilder: ^15.1.1 - yargs: ^17.7.1 + yargs: ^17.7.2 languageName: unknown linkType: soft @@ -3772,7 +4285,7 @@ __metadata: languageName: node linkType: hard -"bcrypt@npm:^5.0.1": +"bcrypt@npm:^5.1.0": version: 5.1.0 resolution: "bcrypt@npm:5.1.0" dependencies: @@ -4002,7 +4515,7 @@ __metadata: languageName: node linkType: hard -"bson@npm:^4.7.0": +"bson@npm:^4.7.0, bson@npm:^4.7.2": version: 4.7.2 resolution: "bson@npm:4.7.2" dependencies: @@ -4098,6 +4611,26 @@ __metadata: languageName: node linkType: hard +"cacache@npm:^17.0.0": + version: 17.1.3 + resolution: "cacache@npm:17.1.3" + dependencies: + "@npmcli/fs": ^3.1.0 + fs-minipass: ^3.0.0 + glob: ^10.2.2 + lru-cache: ^7.7.1 + minipass: ^5.0.0 + minipass-collect: ^1.0.2 + minipass-flush: ^1.0.5 + minipass-pipeline: ^1.2.4 + p-map: ^4.0.0 + ssri: ^10.0.0 + tar: ^6.1.11 + unique-filename: ^3.0.0 + checksum: 385756781e1e21af089160d89d7462b7ed9883c978e848c7075b90b73cb823680e66092d61513050164588387d2ca87dd6d910e28d64bc13a9ac82cd8580c796 + languageName: node + linkType: hard + "cache-content-type@npm:^1.0.0": version: 1.0.1 resolution: "cache-content-type@npm:1.0.1" @@ -4798,10 +5331,10 @@ __metadata: languageName: node linkType: hard -"core-js@npm:^3.29.1": - version: 3.29.1 - resolution: "core-js@npm:3.29.1" - checksum: b38446dbfcfd3887b3d4922990da487e2c95044cb4c5717aaf95e786a4c6b218f05c056c7ed6c699169b9794a49fec890e402659d54661fc56965a0eb717e7bd +"core-js@npm:^3.31.0": + version: 3.31.0 + resolution: "core-js@npm:3.31.0" + checksum: f7cf9b3010f7ca99c026d95b61743baca1a85512742ed2b67e8f65a72ac4f4fe0b90b00057783e886bdd39d3a295f42f845d33e7cba3973ed263df978343ab79 languageName: node linkType: hard @@ -4858,7 +5391,7 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": version: 7.0.3 resolution: "cross-spawn@npm:7.0.3" dependencies: @@ -5361,9 +5894,9 @@ __metadata: languageName: node linkType: hard -"elastic-apm-http-client@npm:11.3.1": - version: 11.3.1 - resolution: "elastic-apm-http-client@npm:11.3.1" +"elastic-apm-http-client@npm:11.4.0": + version: 11.4.0 + resolution: "elastic-apm-http-client@npm:11.4.0" dependencies: agentkeepalive: ^4.2.1 breadth-filter: ^2.0.0 @@ -5374,24 +5907,25 @@ __metadata: readable-stream: ^3.4.0 semver: ^6.3.0 stream-chopper: ^3.0.1 - checksum: 07cadc9af24845658c0a3da6b332e320edf585be133e2cd1fda884918a03a3c9ec28f9b05cfc26f8ce4f991ce6876c020d5d0b17d1f5bd47e812c62c244b2abc + checksum: 1b82e55395084c362907e8ed456b9bd51285eebd66d2579b4d88f2d023199a16a3fb8aecffe6e777da129981cb2d9b705f21dab5af92de359e979576ee19fd93 languageName: node linkType: hard -"elastic-apm-node@npm:^3.43.0": - version: 3.44.1 - resolution: "elastic-apm-node@npm:3.44.1" +"elastic-apm-node@npm:^3.47.0": + version: 3.47.0 + resolution: "elastic-apm-node@npm:3.47.0" dependencies: "@elastic/ecs-pino-format": ^1.2.0 "@opentelemetry/api": ^1.4.1 + "@opentelemetry/core": ^1.11.0 + "@opentelemetry/sdk-metrics": ^1.12.0 after-all-results: ^2.0.0 async-cache: ^1.1.0 async-value-promise: ^1.1.1 basic-auth: ^2.0.1 cookie: ^0.5.0 core-util-is: ^1.0.2 - debug: ^4.1.1 - elastic-apm-http-client: 11.3.1 + elastic-apm-http-client: 11.4.0 end-of-stream: ^1.4.4 error-callsites: ^2.0.4 error-stack-parser: ^2.0.6 @@ -5408,15 +5942,13 @@ __metadata: original-url: ^1.2.3 pino: ^6.11.2 relative-microtime: ^2.0.0 - resolve: ^1.22.1 + require-in-the-middle: ^7.0.1 semver: ^6.3.0 - set-cookie-serde: ^1.0.0 shallow-clone-shim: ^2.0.0 source-map: ^0.8.0-beta.0 sql-summary: ^1.0.1 - traverse: ^0.6.6 unicode-byte-truncate: ^1.0.0 - checksum: 506a5727485f427b6c58e28fa6bef8982e0b283a68c51f26c3336e016de614899ba390caec521fcc6d7611ba00b08a483ccd097ff0c8b378c42e2e33ae559f19 + checksum: 1fe255467edc0c1098d0d407caf6a616aedb68a7e1f593aee4d0c5d2e2e149c9d15f03eb3591c14a940fc9b7bdd0ee5c77dfaea62b965dbec13c00324e8d3560 languageName: node linkType: hard @@ -5742,20 +6274,38 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-jest@npm:^27.2.1": - version: 27.2.1 - resolution: "eslint-plugin-jest@npm:27.2.1" +"eslint-plugin-jest@npm:^27.2.1": + version: 27.2.1 + resolution: "eslint-plugin-jest@npm:27.2.1" + dependencies: + "@typescript-eslint/utils": ^5.10.0 + peerDependencies: + "@typescript-eslint/eslint-plugin": ^5.0.0 + eslint: ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + "@typescript-eslint/eslint-plugin": + optional: true + jest: + optional: true + checksum: 579a4d26304cc6748b2e6dff6c965ea7a21b618d8b051eb02727d25cf5c7767f6db8ef5237531635ff77e242b983b973e7cb8c820a4d20d5bda73358c452a8ab + languageName: node + linkType: hard + +"eslint-plugin-jest@npm:^27.2.2": + version: 27.2.2 + resolution: "eslint-plugin-jest@npm:27.2.2" dependencies: "@typescript-eslint/utils": ^5.10.0 peerDependencies: "@typescript-eslint/eslint-plugin": ^5.0.0 eslint: ^7.0.0 || ^8.0.0 + jest: "*" peerDependenciesMeta: "@typescript-eslint/eslint-plugin": optional: true jest: optional: true - checksum: 579a4d26304cc6748b2e6dff6c965ea7a21b618d8b051eb02727d25cf5c7767f6db8ef5237531635ff77e242b983b973e7cb8c820a4d20d5bda73358c452a8ab + checksum: 98b63252d985f5dedf36ce9587dd4a0d24daf71ca8a997258343402c0d33ddd5070502378dafd9ac7fc0ef2e0d557b5c77f18e09ad73c71a52de8061db88293f languageName: node linkType: hard @@ -5858,6 +6408,13 @@ __metadata: languageName: node linkType: hard +"eslint-visitor-keys@npm:^3.4.1": + version: 3.4.1 + resolution: "eslint-visitor-keys@npm:3.4.1" + checksum: f05121d868202736b97de7d750847a328fcfa8593b031c95ea89425333db59676ac087fa905eba438d0a3c5769632f828187e0c1a0d271832a2153c1d3661c2c + languageName: node + linkType: hard + "eslint@npm:^8.39.0": version: 8.39.0 resolution: "eslint@npm:8.39.0" @@ -5908,6 +6465,55 @@ __metadata: languageName: node linkType: hard +"eslint@npm:^8.44.0": + version: 8.44.0 + resolution: "eslint@npm:8.44.0" + dependencies: + "@eslint-community/eslint-utils": ^4.2.0 + "@eslint-community/regexpp": ^4.4.0 + "@eslint/eslintrc": ^2.1.0 + "@eslint/js": 8.44.0 + "@humanwhocodes/config-array": ^0.11.10 + "@humanwhocodes/module-importer": ^1.0.1 + "@nodelib/fs.walk": ^1.2.8 + ajv: ^6.10.0 + chalk: ^4.0.0 + cross-spawn: ^7.0.2 + debug: ^4.3.2 + doctrine: ^3.0.0 + escape-string-regexp: ^4.0.0 + eslint-scope: ^7.2.0 + eslint-visitor-keys: ^3.4.1 + espree: ^9.6.0 + esquery: ^1.4.2 + esutils: ^2.0.2 + fast-deep-equal: ^3.1.3 + file-entry-cache: ^6.0.1 + find-up: ^5.0.0 + glob-parent: ^6.0.2 + globals: ^13.19.0 + graphemer: ^1.4.0 + ignore: ^5.2.0 + import-fresh: ^3.0.0 + imurmurhash: ^0.1.4 + is-glob: ^4.0.0 + is-path-inside: ^3.0.3 + js-yaml: ^4.1.0 + json-stable-stringify-without-jsonify: ^1.0.1 + levn: ^0.4.1 + lodash.merge: ^4.6.2 + minimatch: ^3.1.2 + natural-compare: ^1.4.0 + optionator: ^0.9.3 + strip-ansi: ^6.0.1 + strip-json-comments: ^3.1.0 + text-table: ^0.2.0 + bin: + eslint: bin/eslint.js + checksum: d06309ce4aafb9d27d558c8e5e5aa5cba3bbec3ce8ceccbc7d4b7a35f2b67fd40189159155553270e2e6febeb69bd8a3b60d6241c8f5ddc2ef1702ccbd328501 + languageName: node + linkType: hard + "espree@npm:^9.5.1": version: 9.5.1 resolution: "espree@npm:9.5.1" @@ -5919,6 +6525,17 @@ __metadata: languageName: node linkType: hard +"espree@npm:^9.6.0": + version: 9.6.0 + resolution: "espree@npm:9.6.0" + dependencies: + acorn: ^8.9.0 + acorn-jsx: ^5.3.2 + eslint-visitor-keys: ^3.4.1 + checksum: 1287979510efb052a6a97c73067ea5d0a40701b29adde87bbe2d3eb1667e39ca55e8129e20e2517fed3da570150e7ef470585228459a8f3e3755f45007a1c662 + languageName: node + linkType: hard + "esprima-next@npm:^5.7.0": version: 5.8.4 resolution: "esprima-next@npm:5.8.4" @@ -6057,6 +6674,13 @@ __metadata: languageName: node linkType: hard +"exponential-backoff@npm:^3.1.1": + version: 3.1.1 + resolution: "exponential-backoff@npm:3.1.1" + checksum: 3d21519a4f8207c99f7457287291316306255a328770d320b401114ec8481986e4e467e854cb9914dd965e0a1ca810a23ccb559c642c88f4c7f55c55778a9b48 + languageName: node + linkType: hard + "extend@npm:^3.0.0": version: 3.0.2 resolution: "extend@npm:3.0.2" @@ -6345,6 +6969,16 @@ __metadata: languageName: node linkType: hard +"foreground-child@npm:^3.1.0": + version: 3.1.1 + resolution: "foreground-child@npm:3.1.1" + dependencies: + cross-spawn: ^7.0.0 + signal-exit: ^4.0.1 + checksum: 139d270bc82dc9e6f8bc045fe2aae4001dc2472157044fdfad376d0a3457f77857fa883c1c8b21b491c6caade9a926a4bed3d3d2e8d3c9202b151a4cbbd0bcd5 + languageName: node + linkType: hard + "form-data@npm:^2.5.0": version: 2.5.1 resolution: "form-data@npm:2.5.1" @@ -6390,6 +7024,15 @@ __metadata: languageName: node linkType: hard +"fs-minipass@npm:^3.0.0": + version: 3.0.2 + resolution: "fs-minipass@npm:3.0.2" + dependencies: + minipass: ^5.0.0 + checksum: e9cc0e1f2d01c6f6f62f567aee59530aba65c6c7b2ae88c5027bc34c711ebcfcfaefd0caf254afa6adfe7d1fba16bc2537508a6235196bac7276747d078aef0a + languageName: node + linkType: hard + "fs-mkdirp-stream@npm:^1.0.0": version: 1.0.0 resolution: "fs-mkdirp-stream@npm:1.0.0" @@ -6659,6 +7302,21 @@ __metadata: languageName: node linkType: hard +"glob@npm:^10.2.2": + version: 10.3.1 + resolution: "glob@npm:10.3.1" + dependencies: + foreground-child: ^3.1.0 + jackspeak: ^2.0.3 + minimatch: ^9.0.1 + minipass: ^5.0.0 || ^6.0.2 + path-scurry: ^1.10.0 + bin: + glob: dist/cjs/src/bin.js + checksum: 19c8c2805658b1002fecf0722cd609a33153d756a0d5260676bd0e9c5e6ef889ec9cce6d3dac0411aa90bce8de3d14f25b6f5589a3292582cccbfeddd0e98cc4 + languageName: node + linkType: hard + "glob@npm:^7.0.0, glob@npm:^7.1.1, glob@npm:^7.1.3, glob@npm:^7.1.4": version: 7.2.3 resolution: "glob@npm:7.2.3" @@ -6748,6 +7406,13 @@ __metadata: languageName: node linkType: hard +"graphemer@npm:^1.4.0": + version: 1.4.0 + resolution: "graphemer@npm:1.4.0" + checksum: bab8f0be9b568857c7bec9fda95a89f87b783546d02951c40c33f84d05bb7da3fd10f863a9beb901463669b6583173a8c8cc6d6b306ea2b9b9d5d3d943c3a673 + languageName: node + linkType: hard + "gud@npm:^1.0.0": version: 1.0.0 resolution: "gud@npm:1.0.0" @@ -6973,7 +7638,7 @@ __metadata: languageName: node linkType: hard -"http-cache-semantics@npm:^4.1.0": +"http-cache-semantics@npm:^4.1.0, http-cache-semantics@npm:^4.1.1": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" checksum: 83ac0bc60b17a3a36f9953e7be55e5c8f41acc61b22583060e8dedc9dd5e3607c823a88d0926f9150e571f90946835c7fe150732801010845c72cd8bbff1a236 @@ -7075,7 +7740,7 @@ __metadata: languageName: node linkType: hard -"i18next-browser-languagedetector@npm:^6.1.5": +"i18next-browser-languagedetector@npm:^6.1.8": version: 6.1.8 resolution: "i18next-browser-languagedetector@npm:6.1.8" dependencies: @@ -7100,7 +7765,7 @@ __metadata: languageName: node linkType: hard -"i18next-http-backend@npm:^1.4.1": +"i18next-http-backend@npm:^1.4.5": version: 1.4.5 resolution: "i18next-http-backend@npm:1.4.5" dependencies: @@ -7148,7 +7813,7 @@ __metadata: languageName: node linkType: hard -"i18next@npm:^21.9.1": +"i18next@npm:^21.10.0": version: 21.10.0 resolution: "i18next@npm:21.10.0" dependencies: @@ -7886,6 +8551,19 @@ __metadata: languageName: node linkType: hard +"jackspeak@npm:^2.0.3": + version: 2.2.1 + resolution: "jackspeak@npm:2.2.1" + dependencies: + "@isaacs/cliui": ^8.0.2 + "@pkgjs/parseargs": ^0.11.0 + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: e29291c0d0f280a063fa18fbd1e891ab8c2d7519fd34052c0ebde38538a15c603140d60c2c7f432375ff7ee4c5f1c10daa8b2ae19a97c3d4affe308c8360c1df + languageName: node + linkType: hard + "jest-changed-files@npm:^29.5.0": version: 29.5.0 resolution: "jest-changed-files@npm:29.5.0" @@ -8525,13 +9203,14 @@ __metadata: languageName: node linkType: hard -"koa-bodyparser@npm:^4.3.0": - version: 4.4.0 - resolution: "koa-bodyparser@npm:4.4.0" +"koa-bodyparser@npm:^4.4.1": + version: 4.4.1 + resolution: "koa-bodyparser@npm:4.4.1" dependencies: co-body: ^6.0.0 copy-to: ^2.0.1 - checksum: 9bdc6b1b95eca8cdff1124ef14d570cafff049bc29e5fa72185d6174b6f3284f5faea82c06d14670cb3994bfcb0bdd5550661b1d91e35a6f1c483286b8e58223 + type-is: ^1.6.18 + checksum: 2b839acc53d6c86558a5faf750ea42527ab0a9f90228abfd559fdae388be1c4789a9c36b178b33883d83701d4d9a5c3e3b55fbadd950a2d9fab32ec4d1e67a75 languageName: node linkType: hard @@ -8552,7 +9231,7 @@ __metadata: languageName: node linkType: hard -"koa@npm:^2.13.4": +"koa@npm:^2.13.4, koa@npm:^2.14.2": version: 2.14.2 resolution: "koa@npm:2.14.2" dependencies: @@ -8802,7 +9481,7 @@ __metadata: languageName: node linkType: hard -"lodash.merge@npm:^4.6.2": +"lodash.merge@npm:4.6.2, lodash.merge@npm:^4.6.2": version: 4.6.2 resolution: "lodash.merge@npm:4.6.2" checksum: ad580b4bdbb7ca1f7abf7e1bce63a9a0b98e370cf40194b03380a46b4ed799c9573029599caebc1b14e3f24b111aef72b96674a56cfa105e0f5ac70546cdc005 @@ -8867,10 +9546,10 @@ __metadata: languageName: node linkType: hard -"lottie-web@npm:^5.10.2": - version: 5.10.2 - resolution: "lottie-web@npm:5.10.2" - checksum: 9ec80b479d8d03c079bd4f246d301f0c4aac2c4d6f8daaa79f6f3fc285aaba377c64879f6748d49c1db52b9854aea81c3203d858e9c686f7a47cd8b771d112d0 +"lottie-web@npm:^5.12.2": + version: 5.12.2 + resolution: "lottie-web@npm:5.12.2" + checksum: af5bc3bc405fd760de8b17a36158d5a8c3e8c586c711d0c63681ddf056b65bc6b54ea36b1fcad782fb02dbe12e696a40e0ba72daf41b8f10ab5b5d1113793636 languageName: node linkType: hard @@ -8909,6 +9588,13 @@ __metadata: languageName: node linkType: hard +"lru-cache@npm:^9.1.1 || ^10.0.0": + version: 10.0.0 + resolution: "lru-cache@npm:10.0.0" + checksum: 18f101675fe283bc09cda0ef1e3cc83781aeb8373b439f086f758d1d91b28730950db785999cd060d3c825a8571c03073e8c14512b6655af2188d623031baf50 + languageName: node + linkType: hard + "make-dir@npm:^3.0.0, make-dir@npm:^3.1.0": version: 3.1.0 resolution: "make-dir@npm:3.1.0" @@ -8949,6 +9635,29 @@ __metadata: languageName: node linkType: hard +"make-fetch-happen@npm:^11.0.3": + version: 11.1.1 + resolution: "make-fetch-happen@npm:11.1.1" + dependencies: + agentkeepalive: ^4.2.1 + cacache: ^17.0.0 + http-cache-semantics: ^4.1.1 + http-proxy-agent: ^5.0.0 + https-proxy-agent: ^5.0.0 + is-lambda: ^1.0.1 + lru-cache: ^7.7.1 + minipass: ^5.0.0 + minipass-fetch: ^3.0.0 + minipass-flush: ^1.0.5 + minipass-pipeline: ^1.2.4 + negotiator: ^0.6.3 + promise-retry: ^2.0.1 + socks-proxy-agent: ^7.0.0 + ssri: ^10.0.0 + checksum: 7268bf274a0f6dcf0343829489a4506603ff34bd0649c12058753900b0eb29191dce5dba12680719a5d0a983d3e57810f594a12f3c18494e93a1fbc6348a4540 + languageName: node + linkType: hard + "makeerror@npm:1.0.12": version: 1.0.12 resolution: "makeerror@npm:1.0.12" @@ -9232,6 +9941,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^9.0.1": + version: 9.0.2 + resolution: "minimatch@npm:9.0.2" + dependencies: + brace-expansion: ^2.0.1 + checksum: 2eb12e2047a062fdb973fb51b9803f2455e3a00977858c038d66646d303a5a15bbcbc6ed5a2fc403bc869b1309f829ed3acd881d3246faf044ea7a494974b924 + languageName: node + linkType: hard + "minimist-options@npm:4.1.0": version: 4.1.0 resolution: "minimist-options@npm:4.1.0" @@ -9274,6 +9992,21 @@ __metadata: languageName: node linkType: hard +"minipass-fetch@npm:^3.0.0": + version: 3.0.3 + resolution: "minipass-fetch@npm:3.0.3" + dependencies: + encoding: ^0.1.13 + minipass: ^5.0.0 + minipass-sized: ^1.0.3 + minizlib: ^2.1.2 + dependenciesMeta: + encoding: + optional: true + checksum: af5ab2552a16fcf505d35fd7ffb84b57f4a0eeb269e6e1d9a2a75824dda48b36e527083250b7cca4a4def21d9544e2ade441e4730e233c0bc2133f6abda31e18 + languageName: node + linkType: hard + "minipass-flush@npm:^1.0.5": version: 1.0.5 resolution: "minipass-flush@npm:1.0.5" @@ -9317,6 +10050,20 @@ __metadata: languageName: node linkType: hard +"minipass@npm:^5.0.0": + version: 5.0.0 + resolution: "minipass@npm:5.0.0" + checksum: 425dab288738853fded43da3314a0b5c035844d6f3097a8e3b5b29b328da8f3c1af6fc70618b32c29ff906284cf6406b6841376f21caaadd0793c1d5a6a620ea + languageName: node + linkType: hard + +"minipass@npm:^5.0.0 || ^6.0.2": + version: 6.0.2 + resolution: "minipass@npm:6.0.2" + checksum: d140b91f4ab2e5ce5a9b6c468c0e82223504acc89114c1a120d4495188b81fedf8cade72a9f4793642b4e66672f990f1e0d902dd858485216a07cd3c8a62fac9 + languageName: node + linkType: hard + "minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": version: 2.1.2 resolution: "minizlib@npm:2.1.2" @@ -9361,7 +10108,7 @@ __metadata: languageName: node linkType: hard -"moment@npm:^2.29.2": +"moment@npm:^2.29.4": version: 2.29.4 resolution: "moment@npm:2.29.4" checksum: 0ec3f9c2bcba38dc2451b1daed5daded747f17610b92427bebe1d08d48d8b7bdd8d9197500b072d14e326dd0ccf3e326b9e3d07c5895d3d49e39b6803b76e80e @@ -9378,7 +10125,25 @@ __metadata: languageName: node linkType: hard -"mongodb@npm:^4.13.0, mongodb@npm:^4.3.1": +"mongodb@npm:^4.16.0": + version: 4.16.0 + resolution: "mongodb@npm:4.16.0" + dependencies: + "@aws-sdk/credential-providers": ^3.186.0 + bson: ^4.7.2 + mongodb-connection-string-url: ^2.5.4 + saslprep: ^1.0.3 + socks: ^2.7.1 + dependenciesMeta: + "@aws-sdk/credential-providers": + optional: true + saslprep: + optional: true + checksum: f0b1347739cc362b82b3aabc7e7d4d74bc7a344ed1bbafd6f92681bcab440f6cc618ffa0438d41d2789cb34818f3b09d4c78f517b42160ebae55bf2c96f13953 + languageName: node + linkType: hard + +"mongodb@npm:^4.3.1": version: 4.14.0 resolution: "mongodb@npm:4.14.0" dependencies: @@ -9424,7 +10189,7 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^3.3.4": +"nanoid@npm:^3.3.6": version: 3.3.6 resolution: "nanoid@npm:3.3.6" bin: @@ -9527,7 +10292,28 @@ __metadata: languageName: node linkType: hard -"node-gyp@npm:^9.3.1, node-gyp@npm:latest": +"node-gyp@npm:^9.4.0": + version: 9.4.0 + resolution: "node-gyp@npm:9.4.0" + dependencies: + env-paths: ^2.2.0 + exponential-backoff: ^3.1.1 + glob: ^7.1.4 + graceful-fs: ^4.2.6 + make-fetch-happen: ^11.0.3 + nopt: ^6.0.0 + npmlog: ^6.0.0 + rimraf: ^3.0.2 + semver: ^7.3.5 + tar: ^6.1.2 + which: ^2.0.2 + bin: + node-gyp: bin/node-gyp.js + checksum: 78b404e2e0639d64e145845f7f5a3cb20c0520cdaf6dda2f6e025e9b644077202ea7de1232396ba5bde3fee84cdc79604feebe6ba3ec84d464c85d407bb5da99 + languageName: node + linkType: hard + +"node-gyp@npm:latest": version: 9.3.1 resolution: "node-gyp@npm:9.3.1" dependencies: @@ -9962,6 +10748,20 @@ __metadata: languageName: node linkType: hard +"optionator@npm:^0.9.3": + version: 0.9.3 + resolution: "optionator@npm:0.9.3" + dependencies: + "@aashutoshrathi/word-wrap": ^1.2.3 + deep-is: ^0.1.3 + fast-levenshtein: ^2.0.6 + levn: ^0.4.1 + prelude-ls: ^1.2.1 + type-check: ^0.4.0 + checksum: 09281999441f2fe9c33a5eeab76700795365a061563d66b098923eb719251a42bdbe432790d35064d0816ead9296dbeb1ad51a733edf4167c96bd5d0882e428a + languageName: node + linkType: hard + "ordered-read-streams@npm:^1.0.0": version: 1.0.1 resolution: "ordered-read-streams@npm:1.0.1" @@ -10283,6 +11083,16 @@ __metadata: languageName: node linkType: hard +"path-scurry@npm:^1.10.0": + version: 1.10.0 + resolution: "path-scurry@npm:1.10.0" + dependencies: + lru-cache: ^9.1.1 || ^10.0.0 + minipass: ^5.0.0 || ^6.0.2 + checksum: 3b66a4a6ab66e45755b577c966ecf0da92d3e068b3c992d8f69aa2cc908ef4eda9358253e9b4f86cad43d3ad810ec445be164105975f5cb3fdab68459c59dc6e + languageName: node + linkType: hard + "path-to-regexp@npm:^1.7.0": version: 1.8.0 resolution: "path-to-regexp@npm:1.8.0" @@ -10509,7 +11319,7 @@ __metadata: languageName: node linkType: hard -"promise.allsettled@npm:^1.0.5": +"promise.allsettled@npm:^1.0.6": version: 1.0.6 resolution: "promise.allsettled@npm:1.0.6" dependencies: @@ -10855,7 +11665,7 @@ __metadata: languageName: node linkType: hard -"react-dnd-html5-backend@npm:^14.0.1": +"react-dnd-html5-backend@npm:^14.1.0": version: 14.1.0 resolution: "react-dnd-html5-backend@npm:14.1.0" dependencies: @@ -10864,7 +11674,7 @@ __metadata: languageName: node linkType: hard -"react-dnd@npm:^14.0.3": +"react-dnd@npm:^14.0.5": version: 14.0.5 resolution: "react-dnd@npm:14.0.5" dependencies: @@ -10937,12 +11747,12 @@ __metadata: languageName: node linkType: hard -"react-intersection-observer@npm:^9.4.3": - version: 9.4.3 - resolution: "react-intersection-observer@npm:9.4.3" +"react-intersection-observer@npm:^9.5.2": + version: 9.5.2 + resolution: "react-intersection-observer@npm:9.5.2" peerDependencies: react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 - checksum: ac31c6c76ce72019a1fb50fe6b53ef6429d98b9e9b937f92d16635ef8586392c7058bb61526a8fe6bea6ce36c006015fe2d8893bac43eda6157b9e6b17ad9b68 + checksum: cdbe40544930d59fc3820bce017fb688910cbecd7baaf382387a3d60d98a36e1d1e4bc27a876775e99b2a9f86762286a29d7bca21f3345f653cff29850c2641d languageName: node linkType: hard @@ -11005,7 +11815,7 @@ __metadata: languageName: node linkType: hard -"react-popper@npm:^2.2.5": +"react-popper@npm:^2.3.0": version: 2.3.0 resolution: "react-popper@npm:2.3.0" dependencies: @@ -11019,7 +11829,7 @@ __metadata: languageName: node linkType: hard -"react-router-dom@npm:^5.3.3": +"react-router-dom@npm:^5.3.4": version: 5.3.4 resolution: "react-router-dom@npm:5.3.4" dependencies: @@ -11359,6 +12169,17 @@ __metadata: languageName: node linkType: hard +"require-in-the-middle@npm:^7.0.1": + version: 7.1.1 + resolution: "require-in-the-middle@npm:7.1.1" + dependencies: + debug: ^4.1.1 + module-details-from-path: ^1.0.3 + resolve: ^1.22.1 + checksum: 00c7e28c271cefc219962b18c1803f6124c761238c1edbf588c68a7143bfeb5779df455d2f0791c2c1f6e841580c50080d4435156e72841fa0bc50c3f383d032 + languageName: node + linkType: hard + "requires-port@npm:^1.0.0": version: 1.0.0 resolution: "requires-port@npm:1.0.0" @@ -11658,7 +12479,16 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.x, semver@npm:^7.1.1, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7": +"semver@npm:^6.0.0, semver@npm:^6.1.0, semver@npm:^6.3.0": + version: 6.3.0 + resolution: "semver@npm:6.3.0" + bin: + semver: ./bin/semver.js + checksum: 1b26ecf6db9e8292dd90df4e781d91875c0dcc1b1909e70f5d12959a23c7eebb8f01ea581c00783bbee72ceeaad9505797c381756326073850dc36ed284b21b9 + languageName: node + linkType: hard + +"semver@npm:^7.1.1, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.3": version: 7.5.3 resolution: "semver@npm:7.5.3" dependencies: @@ -11669,15 +12499,6 @@ __metadata: languageName: node linkType: hard -"semver@npm:^6.0.0, semver@npm:^6.1.0, semver@npm:^6.3.0": - version: 6.3.0 - resolution: "semver@npm:6.3.0" - bin: - semver: ./bin/semver.js - checksum: 1b26ecf6db9e8292dd90df4e781d91875c0dcc1b1909e70f5d12959a23c7eebb8f01ea581c00783bbee72ceeaad9505797c381756326073850dc36ed284b21b9 - languageName: node - linkType: hard - "semver@npm:^7.5.0": version: 7.5.0 resolution: "semver@npm:7.5.0" @@ -11696,13 +12517,6 @@ __metadata: languageName: node linkType: hard -"set-cookie-serde@npm:^1.0.0": - version: 1.0.0 - resolution: "set-cookie-serde@npm:1.0.0" - checksum: 0ef01f2f54ecd72532f6c5ac131b7ed531d568b845ba73bf27a1017704acad96abc46e1bb1a5a5616bf3273acf4e735e09803738e0e1fa7e539ff9e5cc0fce40 - languageName: node - linkType: hard - "setimmediate@npm:^1.0.4": version: 1.0.5 resolution: "setimmediate@npm:1.0.5" @@ -11792,6 +12606,13 @@ __metadata: languageName: node linkType: hard +"signal-exit@npm:^4.0.1": + version: 4.0.2 + resolution: "signal-exit@npm:4.0.2" + checksum: 41f5928431cc6e91087bf0343db786a6313dd7c6fd7e551dbc141c95bb5fb26663444fd9df8ea47c5d7fc202f60aa7468c3162a9365cbb0615fc5e1b1328fe31 + languageName: node + linkType: hard + "simple-swizzle@npm:^0.2.2": version: 0.2.2 resolution: "simple-swizzle@npm:0.2.2" @@ -11801,7 +12622,7 @@ __metadata: languageName: node linkType: hard -"sinon@npm:^14.0.0": +"sinon@npm:^14.0.2": version: 14.0.2 resolution: "sinon@npm:14.0.2" dependencies: @@ -12050,6 +12871,15 @@ __metadata: languageName: node linkType: hard +"ssri@npm:^10.0.0": + version: 10.0.4 + resolution: "ssri@npm:10.0.4" + dependencies: + minipass: ^5.0.0 + checksum: fb14da9f8a72b04eab163eb13a9dda11d5962cd2317f85457c4e0b575e9a6e0e3a6a87b5bf122c75cb36565830cd5f263fb457571bf6f1587eb5f95d095d6165 + languageName: node + linkType: hard + "ssri@npm:^9.0.0": version: 9.0.1 resolution: "ssri@npm:9.0.1" @@ -12198,7 +13028,7 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -12209,7 +13039,7 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^5.0.0": +"string-width@npm:^5.0.0, string-width@npm:^5.0.1, string-width@npm:^5.1.2": version: 5.1.2 resolution: "string-width@npm:5.1.2" dependencies: @@ -12301,7 +13131,7 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" dependencies: @@ -12514,7 +13344,7 @@ __metadata: languageName: node linkType: hard -"threadedclass@npm:^1.1.2": +"threadedclass@npm:^1.2.1": version: 1.2.1 resolution: "threadedclass@npm:1.2.1" dependencies: @@ -12713,13 +13543,6 @@ __metadata: languageName: node linkType: hard -"traverse@npm:^0.6.6": - version: 0.6.7 - resolution: "traverse@npm:0.6.7" - checksum: 21018085ab72f717991597e12e2b52446962ed59df591502e4d7e1a709bc0a989f7c3d451aa7d882666ad0634f1546d696c5edecda1f2fc228777df7bb529a1e - languageName: node - linkType: hard - "treeify@npm:^1.1.0": version: 1.1.0 resolution: "treeify@npm:1.1.0" @@ -12748,9 +13571,9 @@ __metadata: languageName: node linkType: hard -"ts-jest@npm:^29.0.5": - version: 29.0.5 - resolution: "ts-jest@npm:29.0.5" +"ts-jest@npm:^29.1.1": + version: 29.1.1 + resolution: "ts-jest@npm:29.1.1" dependencies: bs-logger: 0.x fast-json-stable-stringify: 2.x @@ -12758,14 +13581,14 @@ __metadata: json5: ^2.2.3 lodash.memoize: 4.x make-error: 1.x - semver: 7.x + semver: ^7.5.3 yargs-parser: ^21.0.1 peerDependencies: "@babel/core": ">=7.0.0-beta.0 <8" "@jest/types": ^29.0.0 babel-jest: ^29.0.0 jest: ^29.0.0 - typescript: ">=4.3" + typescript: ">=4.3 <6" peerDependenciesMeta: "@babel/core": optional: true @@ -12777,7 +13600,7 @@ __metadata: optional: true bin: ts-jest: cli.js - checksum: f60f129c2287f4c963d9ee2677132496c5c5a5d39c27ad234199a1140c26318a7d5bda34890ab0e30636ec42a8de28f84487c09e9dcec639c9c67812b3a38373 + checksum: a8c9e284ed4f819526749f6e4dc6421ec666f20ab44d31b0f02b4ed979975f7580b18aea4813172d43e39b29464a71899f8893dd29b06b4a351a3af8ba47b402 languageName: node linkType: hard @@ -12802,6 +13625,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^2.6.0": + version: 2.6.0 + resolution: "tslib@npm:2.6.0" + checksum: c01066038f950016a18106ddeca4649b4d76caa76ec5a31e2a26e10586a59fceb4ee45e96719bf6c715648e7c14085a81fee5c62f7e9ebee68e77a5396e5538f + languageName: node + linkType: hard + "tsscmp@npm:1.0.6": version: 1.0.6 resolution: "tsscmp@npm:1.0.6" @@ -12991,7 +13821,7 @@ __metadata: languageName: node linkType: hard -"underscore@npm:^1.13.4, underscore@npm:^1.13.6": +"underscore@npm:^1.13.6": version: 1.13.6 resolution: "underscore@npm:1.13.6" checksum: d5cedd14a9d0d91dd38c1ce6169e4455bb931f0aaf354108e47bd46d3f2da7464d49b2171a5cf786d61963204a42d01ea1332a903b7342ad428deaafaf70ec36 @@ -13024,6 +13854,15 @@ __metadata: languageName: node linkType: hard +"unique-filename@npm:^3.0.0": + version: 3.0.0 + resolution: "unique-filename@npm:3.0.0" + dependencies: + unique-slug: ^4.0.0 + checksum: 8e2f59b356cb2e54aab14ff98a51ac6c45781d15ceaab6d4f1c2228b780193dc70fae4463ce9e1df4479cb9d3304d7c2043a3fb905bdeca71cc7e8ce27e063df + languageName: node + linkType: hard + "unique-slug@npm:^3.0.0": version: 3.0.0 resolution: "unique-slug@npm:3.0.0" @@ -13033,6 +13872,15 @@ __metadata: languageName: node linkType: hard +"unique-slug@npm:^4.0.0": + version: 4.0.0 + resolution: "unique-slug@npm:4.0.0" + dependencies: + imurmurhash: ^0.1.4 + checksum: 0884b58365af59f89739e6f71e3feacb5b1b41f2df2d842d0757933620e6de08eff347d27e9d499b43c40476cbaf7988638d3acb2ffbcb9d35fd035591adfd15 + languageName: node + linkType: hard + "unique-stream@npm:^2.0.2": version: 2.3.1 resolution: "unique-stream@npm:2.3.1" @@ -13277,7 +14125,7 @@ __metadata: languageName: node linkType: hard -"vm2@npm:^3.9.14": +"vm2@npm:^3.9.19": version: 3.9.19 resolution: "vm2@npm:3.9.19" dependencies: @@ -13456,9 +14304,9 @@ __metadata: languageName: node linkType: hard -"winston@npm:^3.8.2": - version: 3.8.2 - resolution: "winston@npm:3.8.2" +"winston@npm:^3.9.0": + version: 3.9.0 + resolution: "winston@npm:3.9.0" dependencies: "@colors/colors": 1.5.0 "@dabh/diagnostics": ^2.0.2 @@ -13471,7 +14319,7 @@ __metadata: stack-trace: 0.0.x triple-beam: ^1.3.0 winston-transport: ^4.5.0 - checksum: f7b901798b92ab9e93c850110bf6e98500e9a0e762b62dab410cf928b2a4145533dfa6d3d2b24f7bf0dc94b53808d5bd28aaaeff9a4b43b89ea4c798cce308ea + checksum: 410f82b7a502106e7d93e62cd21d7e9bcfd37884d0d95921b12526d2fe163e654ec9cd39e18f9884fad5cf6506a45d07bd2519c1dc9c88e82f0f12b2ce9fa510 languageName: node linkType: hard @@ -13489,6 +14337,17 @@ __metadata: languageName: node linkType: hard +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": + version: 7.0.0 + resolution: "wrap-ansi@npm:7.0.0" + dependencies: + ansi-styles: ^4.0.0 + string-width: ^4.1.0 + strip-ansi: ^6.0.0 + checksum: a790b846fd4505de962ba728a21aaeda189b8ee1c7568ca5e817d85930e06ef8d1689d49dbf0e881e8ef84436af3a88bc49115c2e2788d841ff1b8b5b51a608b + languageName: node + linkType: hard + "wrap-ansi@npm:^6.2.0": version: 6.2.0 resolution: "wrap-ansi@npm:6.2.0" @@ -13500,14 +14359,14 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^7.0.0": - version: 7.0.0 - resolution: "wrap-ansi@npm:7.0.0" +"wrap-ansi@npm:^8.1.0": + version: 8.1.0 + resolution: "wrap-ansi@npm:8.1.0" dependencies: - ansi-styles: ^4.0.0 - string-width: ^4.1.0 - strip-ansi: ^6.0.0 - checksum: a790b846fd4505de962ba728a21aaeda189b8ee1c7568ca5e817d85930e06ef8d1689d49dbf0e881e8ef84436af3a88bc49115c2e2788d841ff1b8b5b51a608b + ansi-styles: ^6.1.0 + string-width: ^5.0.1 + strip-ansi: ^7.0.1 + checksum: 371733296dc2d616900ce15a0049dca0ef67597d6394c57347ba334393599e800bab03c41d4d45221b6bc967b8c453ec3ae4749eff3894202d16800fdfe0e238 languageName: node linkType: hard @@ -13663,7 +14522,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^17.3.1, yargs@npm:^17.7.1": +"yargs@npm:^17.3.1": version: 17.7.1 resolution: "yargs@npm:17.7.1" dependencies: @@ -13678,6 +14537,21 @@ __metadata: languageName: node linkType: hard +"yargs@npm:^17.7.2": + version: 17.7.2 + resolution: "yargs@npm:17.7.2" + dependencies: + cliui: ^8.0.1 + escalade: ^3.1.1 + get-caller-file: ^2.0.5 + require-directory: ^2.1.1 + string-width: ^4.2.3 + y18n: ^5.0.5 + yargs-parser: ^21.1.1 + checksum: 73b572e863aa4a8cbef323dd911d79d193b772defd5a51aab0aca2d446655216f5002c42c5306033968193bdbf892a7a4c110b0d77954a7fdf563e653967b56a + languageName: node + linkType: hard + "ylru@npm:^1.2.0": version: 1.3.2 resolution: "ylru@npm:1.3.2" diff --git a/package.json b/package.json index a094d6dff2..24135e1304 100644 --- a/package.json +++ b/package.json @@ -34,11 +34,11 @@ "test-all": "yarn install && run install-and-build && run check-types:meteor && run lint:packages && run lint:meteor && run test:packages && run test:meteor" }, "devDependencies": { - "concurrently": "^8.0.1", - "lint-staged": "^13.2.2", + "concurrently": "^8.2.0", + "lint-staged": "^13.2.3", "rimraf": "^4.4.1", - "semver": "^7.3.8", - "snyk-nodejs-lockfile-parser": "^1.48.2" + "semver": "^7.5.3", + "snyk-nodejs-lockfile-parser": "^1.52.1" }, "packageManager": "yarn@3.5.0" } diff --git a/packages/blueprints-integration/package.json b/packages/blueprints-integration/package.json index 139d5ffdc6..5468de7efe 100644 --- a/packages/blueprints-integration/package.json +++ b/packages/blueprints-integration/package.json @@ -39,7 +39,7 @@ ], "dependencies": { "@sofie-automation/shared-lib": "1.51.0-in-development", - "tslib": "^2.4.0", + "tslib": "^2.6.0", "type-fest": "^2.19.0" }, "lint-staged": { diff --git a/packages/corelib/package.json b/packages/corelib/package.json index 740cab7472..ec7d3fe3d4 100644 --- a/packages/corelib/package.json +++ b/packages/corelib/package.json @@ -42,15 +42,15 @@ "@sofie-automation/blueprints-integration": "1.51.0-in-development", "@sofie-automation/shared-lib": "1.51.0-in-development", "fast-clone": "^1.5.13", - "i18next": "^21.9.1", + "i18next": "^21.10.0", "influx": "^5.9.3", - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "object-path": "^0.11.8", "prom-client": "^14.2.0", "timecode": "0.0.4", - "tslib": "^2.4.0", + "tslib": "^2.6.0", "type-fest": "^2.19.0", - "underscore": "^1.13.4" + "underscore": "^1.13.6" }, "peerDependencies": { "mongodb": "^4.13.0" diff --git a/packages/documentation/package.json b/packages/documentation/package.json index 47f17c3b20..0664ee210b 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -17,13 +17,13 @@ "devDependencies": { "@docusaurus/core": "2.0.0-beta.20", "@docusaurus/preset-classic": "2.0.0-beta.20", - "@mdx-js/react": "^1.6.21", + "@mdx-js/react": "^1.6.22", "@svgr/webpack": "^5.5.0", - "clsx": "^1.1.1", + "clsx": "^1.2.1", "file-loader": "^6.2.0", - "prism-react-renderer": "^1.2.1", - "react": "^17.0.1", - "react-dom": "^17.0.1", + "prism-react-renderer": "^1.3.5", + "react": "^17.0.2", + "react-dom": "^17.0.2", "url-loader": "^4.1.1" }, "browserslist": { diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index 21ae16e868..dfe90e8492 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -46,17 +46,17 @@ "@sofie-automation/shared-lib": "1.51.0-in-development", "amqplib": "^0.10.3", "deepmerge": "^4.3.1", - "elastic-apm-node": "^3.43.0", + "elastic-apm-node": "^3.47.0", "eventemitter3": "^4.0.7", - "mongodb": "^4.13.0", + "mongodb": "^4.16.0", "p-lazy": "^3.1.0", "p-timeout": "^4.1.0", "superfly-timeline": "^8.3.1", - "threadedclass": "^1.1.2", - "tslib": "^2.4.0", + "threadedclass": "^1.2.1", + "tslib": "^2.6.0", "type-fest": "^2.19.0", - "underscore": "^1.13.4", - "vm2": "^3.9.14" + "underscore": "^1.13.6", + "vm2": "^3.9.19" }, "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", "lint-staged": { diff --git a/packages/live-status-gateway/package.json b/packages/live-status-gateway/package.json index b1e171be42..9bb26856a6 100644 --- a/packages/live-status-gateway/package.json +++ b/packages/live-status-gateway/package.json @@ -56,17 +56,17 @@ "@sofie-automation/corelib": "1.51.0-in-development", "@sofie-automation/server-core-integration": "1.51.0-in-development", "@sofie-automation/shared-lib": "1.51.0-in-development", - "debug": "^4.3.2", + "debug": "^4.3.4", "fast-clone": "^1.5.13", - "influx": "^5.9.2", - "tslib": "^2.4.0", - "winston": "^3.8.1", - "ws": "^8.10.0" + "influx": "^5.9.3", + "tslib": "^2.6.0", + "winston": "^3.9.0", + "ws": "^8.13.0" }, "devDependencies": { - "@asyncapi/generator": "1.9.13", + "@asyncapi/generator": "1.10.9", "@asyncapi/html-template": "0.26.0", - "@asyncapi/nodejs-ws-template": "0.9.25" + "@asyncapi/nodejs-ws-template": "0.9.33" }, "lint-staged": { "*.{css,json,md,scss}": [ diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index 0787f0beca..5e5633a8bc 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -68,10 +68,10 @@ "@mos-connection/connector": "^3.0.4", "@sofie-automation/server-core-integration": "1.51.0-in-development", "@sofie-automation/shared-lib": "1.51.0-in-development", - "tslib": "^2.4.0", + "tslib": "^2.6.0", "type-fest": "^2.19.0", - "underscore": "^1.13.4", - "winston": "^3.8.2" + "underscore": "^1.13.6", + "winston": "^3.9.0" }, "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", "lint-staged": { diff --git a/packages/openapi/package.json b/packages/openapi/package.json index 3cf340d853..e2072b6d70 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -36,13 +36,13 @@ "install_swagger.js" ], "dependencies": { - "tslib": "^2.4.0" + "tslib": "^2.6.0" }, "devDependencies": { - "@openapitools/openapi-generator-cli": "^2.5.2", - "eslint-plugin-yml": "^1.2.0", + "@openapitools/openapi-generator-cli": "^2.6.0", + "eslint-plugin-yml": "^1.8.0", "js-yaml": "^4.1.0", - "wget-improved": "^3.3.1" + "wget-improved": "^3.4.0" }, "lint-staged": { "*.{css,json,md,scss}": [ diff --git a/packages/package.json b/packages/package.json index 61fbd9c2ce..c28a69ef40 100644 --- a/packages/package.json +++ b/packages/package.json @@ -34,29 +34,29 @@ "eslint": "cd $INIT_CWD && \"$PROJECT_CWD/node_modules/.bin/eslint\"" }, "devDependencies": { - "@babel/core": "^7.21.3", - "@babel/plugin-transform-modules-commonjs": "^7.21.2", - "@sofie-automation/code-standard-preset": "~2.4.6", + "@babel/core": "^7.22.5", + "@babel/plugin-transform-modules-commonjs": "^7.22.5", + "@sofie-automation/code-standard-preset": "~2.4.7", "@types/amqplib": "^0.10.1", - "@types/debug": "^4.1.7", + "@types/debug": "^4.1.8", "@types/ejson": "^2.2.0", "@types/got": "^9.6.12", - "@types/jest": "^29.5.0", - "@types/node": "^14.18.12", + "@types/jest": "^29.5.2", + "@types/node": "^14.18.53", "@types/object-path": "^0.11.1", - "@types/underscore": "^1.11.4", + "@types/underscore": "^1.11.5", "babel-jest": "^29.5.0", "copyfiles": "^2.4.1", "jest": "^29.5.0", - "json-schema-to-typescript": "^10.0.0", - "lerna": "^6.6.1", + "json-schema-to-typescript": "^10.1.5", + "lerna": "^6.6.2", "nodemon": "^2.0.22", "open-cli": "^7.2.0", "pinst": "^3.0.0", "rimraf": "^4.4.1", - "semver": "^7.3.7", - "ts-jest": "^29.0.5", - "ts-node": "^10.8.0", + "semver": "^7.5.3", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", "typedoc": "^0.23.28", "typescript": "~4.9" }, diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index 689805530f..09568af009 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -58,12 +58,12 @@ "dependencies": { "@sofie-automation/server-core-integration": "1.51.0-in-development", "@sofie-automation/shared-lib": "1.51.0-in-development", - "debug": "^4.3.3", + "debug": "^4.3.4", "influx": "^5.9.3", "timeline-state-resolver": "9.0.0-release50.5", - "tslib": "^2.4.0", - "underscore": "^1.13.4", - "winston": "^3.8.2" + "tslib": "^2.6.0", + "underscore": "^1.13.6", + "winston": "^3.9.0" }, "lint-staged": { "*.{css,json,md,scss}": [ diff --git a/packages/server-core-integration/package.json b/packages/server-core-integration/package.json index cbdffe468d..7e2c135dc0 100644 --- a/packages/server-core-integration/package.json +++ b/packages/server-core-integration/package.json @@ -75,8 +75,8 @@ "eventemitter3": "^4.0.7", "faye-websocket": "^0.11.4", "got": "^11.8.6", - "tslib": "^2.4.0", - "underscore": "^1.13.4" + "tslib": "^2.6.0", + "underscore": "^1.13.6" }, "lint-staged": { "*.{js,css,json,md,scss}": [ diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index b09fe5a6a1..9ea0a4884c 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -40,7 +40,7 @@ "dependencies": { "@mos-connection/model": "^3.0.4", "timeline-state-resolver-types": "9.0.0-release50.5", - "tslib": "^2.4.0", + "tslib": "^2.6.0", "type-fest": "^2.19.0" }, "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", diff --git a/packages/yarn.lock b/packages/yarn.lock index f2311696d5..3b3431ad4e 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -226,6 +226,17 @@ __metadata: languageName: node linkType: hard +"@asyncapi/avro-schema-parser@npm:^3.0.2": + version: 3.0.2 + resolution: "@asyncapi/avro-schema-parser@npm:3.0.2" + dependencies: + "@asyncapi/parser": ^2.0.3 + "@types/json-schema": ^7.0.11 + avsc: ^5.7.6 + checksum: e53edd5bb43b8682623a442e9174b1f39052fd241027fc384c2eb687256526474c26629df8cfcec9b72928ae6914ba7b59a5f1e010343619f2234db0dc8727f1 + languageName: node + linkType: hard + "@asyncapi/generator-filters@npm:^2.1.0": version: 2.1.0 resolution: "@asyncapi/generator-filters@npm:2.1.0" @@ -264,20 +275,19 @@ __metadata: languageName: node linkType: hard -"@asyncapi/generator@npm:1.9.13": - version: 1.9.13 - resolution: "@asyncapi/generator@npm:1.9.13" +"@asyncapi/generator@npm:1.10.9": + version: 1.10.9 + resolution: "@asyncapi/generator@npm:1.10.9" dependencies: - "@asyncapi/avro-schema-parser": ^1.1.0 + "@asyncapi/avro-schema-parser": ^3.0.2 "@asyncapi/generator-react-sdk": ^0.2.23 - "@asyncapi/openapi-schema-parser": ^2.0.1 - "@asyncapi/parser": ^1.17.1 - "@asyncapi/raml-dt-schema-parser": ^2.0.1 + "@asyncapi/openapi-schema-parser": ^3.0.3 + "@asyncapi/parser": ^2.0.3 + "@asyncapi/raml-dt-schema-parser": ^4.0.3 "@npmcli/arborist": ^2.2.4 - ajv: ^6.10.2 + ajv: ^8.12.0 chokidar: ^3.4.0 commander: ^6.1.0 - conventional-changelog-conventionalcommits: ^5.0.0 filenamify: ^4.1.0 fs.extra: ^1.3.2 global-dirs: ^3.0.0 @@ -294,12 +304,12 @@ __metadata: semver: ^7.3.2 simple-git: ^3.3.0 source-map-support: ^0.5.19 - ts-node: ^9.1.1 - typescript: ^4.2.2 + ts-node: ^10.9.1 + typescript: ^4.9.3 bin: ag: cli.js asyncapi-generator: cli.js - checksum: e9cd7c873564d81641e2e62caad78004a06d60aad619e3f15c38671e087df1c2188818026c07c864e066b0b6d04f6a02609f37af64a39c02d5e3d84922387a30 + checksum: f086bd583ae54c09999f3ce10434a2dafbc2103135e00c3d8dabb8cfee538d744fa39921a8a44c14500529a82f6976e6fafb7e730ed013d64d2b124e1eca03e9 languageName: node linkType: hard @@ -318,13 +328,13 @@ __metadata: languageName: node linkType: hard -"@asyncapi/nodejs-ws-template@npm:0.9.25": - version: 0.9.25 - resolution: "@asyncapi/nodejs-ws-template@npm:0.9.25" +"@asyncapi/nodejs-ws-template@npm:0.9.33": + version: 0.9.33 + resolution: "@asyncapi/nodejs-ws-template@npm:0.9.33" dependencies: "@asyncapi/generator-filters": ^2.1.0 "@asyncapi/generator-hooks": ^0.1.0 - checksum: cd50dbe1416b0b22aa03563b479634240280683f2d26d8ab77ee097441ed5825ae4e280b6a017882b58fe56683d24798b0d0bbf6cdd01face149e693bc3871a7 + checksum: 847b7249223ce7733191b0ec3cd1f462e69ed55badcf32a1c6cc3a0a343864ee7f311529e4e4ce7dd0b783772ea5ab83f5e6c1a90c7c67ac413187a40703ba6c languageName: node linkType: hard @@ -338,7 +348,20 @@ __metadata: languageName: node linkType: hard -"@asyncapi/parser@npm:^1.15.1, @asyncapi/parser@npm:^1.17.0, @asyncapi/parser@npm:^1.17.1, @asyncapi/parser@npm:^1.18.0": +"@asyncapi/openapi-schema-parser@npm:^3.0.3": + version: 3.0.3 + resolution: "@asyncapi/openapi-schema-parser@npm:3.0.3" + dependencies: + "@asyncapi/parser": ^2.0.3 + "@openapi-contrib/openapi-schema-to-json-schema": ~3.2.0 + ajv: ^8.11.0 + ajv-errors: ^3.0.0 + ajv-formats: ^2.1.1 + checksum: b532ef013bc7fa742e72d007104bcf6c5d813a06ec3adf4936a8b9fb6c337c9c2ed633627c2c6ef07b36a5fddee1eb94f2fdfc90ad4e59771a0c49521499dcc9 + languageName: node + linkType: hard + +"@asyncapi/parser@npm:^1.15.1, @asyncapi/parser@npm:^1.17.0, @asyncapi/parser@npm:^1.18.0": version: 1.18.1 resolution: "@asyncapi/parser@npm:1.18.1" dependencies: @@ -355,13 +378,40 @@ __metadata: languageName: node linkType: hard -"@asyncapi/raml-dt-schema-parser@npm:^2.0.1": - version: 2.0.1 - resolution: "@asyncapi/raml-dt-schema-parser@npm:2.0.1" +"@asyncapi/parser@npm:^2.0.3": + version: 2.0.3 + resolution: "@asyncapi/parser@npm:2.0.3" dependencies: - js-yaml: ^3.13.1 - ramldt2jsonschema: ^1.1.0 - checksum: e15db97e5378505a5abd10e3c47b26fd2b73db97387076bec7407e1b6dee4b3b53e2c54e1079c5e1c7da01777af9ae76796cb6c756b5dcda5bd1059b1d78c18f + "@asyncapi/specs": ^5.1.0 + "@openapi-contrib/openapi-schema-to-json-schema": ~3.2.0 + "@stoplight/json-ref-resolver": ^3.1.5 + "@stoplight/spectral-core": ^1.16.1 + "@stoplight/spectral-functions": ^1.7.2 + "@stoplight/spectral-parsers": ^1.0.2 + "@types/json-schema": ^7.0.11 + "@types/urijs": ^1.19.19 + ajv: ^8.11.0 + ajv-errors: ^3.0.0 + ajv-formats: ^2.1.1 + avsc: ^5.7.5 + js-yaml: ^4.1.0 + jsonpath-plus: ^7.2.0 + node-fetch: 2.6.7 + ramldt2jsonschema: ^1.2.3 + webapi-parser: ^0.5.0 + checksum: 0241fe3c3f859ee78d6093417a88d111f098f6c148a7749421f185ec374e20cd1d824e45075f47863c8fc1bf8107ef9cda21e415f72cc1bd35c5e6cfe7a77ff4 + languageName: node + linkType: hard + +"@asyncapi/raml-dt-schema-parser@npm:^4.0.3": + version: 4.0.3 + resolution: "@asyncapi/raml-dt-schema-parser@npm:4.0.3" + dependencies: + "@asyncapi/parser": ^2.0.3 + js-yaml: ^4.1.0 + ramldt2jsonschema: ^1.2.3 + webapi-parser: ^0.5.0 + checksum: 85bd0717aa584851cb5daa846cf96e6a47a1283e35cf12e6062ef7d0e572c78238fe8ed445c1bc8ca3f6c4668e52d0a7d9d31e02efe83f35590cf33c17a5c933 languageName: node linkType: hard @@ -393,6 +443,15 @@ __metadata: languageName: node linkType: hard +"@asyncapi/specs@npm:^5.1.0": + version: 5.1.0 + resolution: "@asyncapi/specs@npm:5.1.0" + dependencies: + "@types/json-schema": ^7.0.11 + checksum: a4ca804d028c8ecb4d6484de84633854e64ba32fb995caa413cb1388da1e4f2bce3b8c635cd3a9217f6f0c202b566498d09301cd825d6485014a7b510a5d3faf + languageName: node + linkType: hard + "@aws-crypto/ie11-detection@npm:^3.0.0": version: 3.0.0 resolution: "@aws-crypto/ie11-detection@npm:3.0.0" @@ -1274,6 +1333,15 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/code-frame@npm:7.22.5" + dependencies: + "@babel/highlight": ^7.22.5 + checksum: cfe804f518f53faaf9a1d3e0f9f74127ab9a004912c3a16fda07fb6a633393ecb9918a053cb71804204c1b7ec3d49e1699604715e2cfb0c9f7bc4933d324ebb6 + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.17.7, @babel/compat-data@npm:^7.20.1, @babel/compat-data@npm:^7.20.5": version: 7.21.0 resolution: "@babel/compat-data@npm:7.21.0" @@ -1288,6 +1356,13 @@ __metadata: languageName: node linkType: hard +"@babel/compat-data@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/compat-data@npm:7.22.5" + checksum: eb1a47ebf79ae268b4a16903e977be52629339806e248455eb9973897c503a04b701f36a9de64e19750d6e081d0561e77a514c8dc470babbeba59ae94298ed18 + languageName: node + linkType: hard + "@babel/core@npm:7.12.9": version: 7.12.9 resolution: "@babel/core@npm:7.12.9" @@ -1312,7 +1387,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.17.10, @babel/core@npm:^7.19.6, @babel/core@npm:^7.21.3": +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.17.10, @babel/core@npm:^7.19.6": version: 7.21.3 resolution: "@babel/core@npm:7.21.3" dependencies: @@ -1335,6 +1410,29 @@ __metadata: languageName: node linkType: hard +"@babel/core@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/core@npm:7.22.5" + dependencies: + "@ampproject/remapping": ^2.2.0 + "@babel/code-frame": ^7.22.5 + "@babel/generator": ^7.22.5 + "@babel/helper-compilation-targets": ^7.22.5 + "@babel/helper-module-transforms": ^7.22.5 + "@babel/helpers": ^7.22.5 + "@babel/parser": ^7.22.5 + "@babel/template": ^7.22.5 + "@babel/traverse": ^7.22.5 + "@babel/types": ^7.22.5 + convert-source-map: ^1.7.0 + debug: ^4.1.0 + gensync: ^1.0.0-beta.2 + json5: ^2.2.2 + semver: ^6.3.0 + checksum: 173ae426958c90c7bbd7de622c6f13fcab8aef0fac3f138e2d47bc466d1cd1f86f71ca82ae0acb9032fd8794abed8efb56fea55c031396337eaec0d673b69d56 + languageName: node + linkType: hard + "@babel/generator@npm:^7.12.5, @babel/generator@npm:^7.17.10, @babel/generator@npm:^7.21.3, @babel/generator@npm:^7.7.2": version: 7.21.3 resolution: "@babel/generator@npm:7.21.3" @@ -1359,6 +1457,18 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/generator@npm:7.22.5" + dependencies: + "@babel/types": ^7.22.5 + "@jridgewell/gen-mapping": ^0.3.2 + "@jridgewell/trace-mapping": ^0.3.17 + jsesc: ^2.5.1 + checksum: efa64da70ca88fe69f05520cf5feed6eba6d30a85d32237671488cc355fdc379fe2c3246382a861d49574c4c2f82a317584f8811e95eb024e365faff3232b49d + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.18.6": version: 7.18.6 resolution: "@babel/helper-annotate-as-pure@npm:7.18.6" @@ -1408,6 +1518,21 @@ __metadata: languageName: node linkType: hard +"@babel/helper-compilation-targets@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-compilation-targets@npm:7.22.5" + dependencies: + "@babel/compat-data": ^7.22.5 + "@babel/helper-validator-option": ^7.22.5 + browserslist: ^4.21.3 + lru-cache: ^5.1.1 + semver: ^6.3.0 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: a479460615acffa0f4fd0a29b740eafb53a93694265207d23a6038ccd18d183a382cacca515e77b7c9b042c3ba80b0aca0da5f1f62215140e81660d2cf721b68 + languageName: node + linkType: hard + "@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.21.0": version: 7.21.0 resolution: "@babel/helper-create-class-features-plugin@npm:7.21.0" @@ -1468,6 +1593,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-environment-visitor@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-environment-visitor@npm:7.22.5" + checksum: 248532077d732a34cd0844eb7b078ff917c3a8ec81a7f133593f71a860a582f05b60f818dc5049c2212e5baa12289c27889a4b81d56ef409b4863db49646c4b1 + languageName: node + linkType: hard + "@babel/helper-explode-assignable-expression@npm:^7.18.6": version: 7.18.6 resolution: "@babel/helper-explode-assignable-expression@npm:7.18.6" @@ -1487,6 +1619,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-function-name@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-function-name@npm:7.22.5" + dependencies: + "@babel/template": ^7.22.5 + "@babel/types": ^7.22.5 + checksum: 6b1f6ce1b1f4e513bf2c8385a557ea0dd7fa37971b9002ad19268ca4384bbe90c09681fe4c076013f33deabc63a53b341ed91e792de741b4b35e01c00238177a + languageName: node + linkType: hard + "@babel/helper-hoist-variables@npm:^7.18.6": version: 7.18.6 resolution: "@babel/helper-hoist-variables@npm:7.18.6" @@ -1496,6 +1638,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-hoist-variables@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-hoist-variables@npm:7.22.5" + dependencies: + "@babel/types": ^7.22.5 + checksum: 394ca191b4ac908a76e7c50ab52102669efe3a1c277033e49467913c7ed6f7c64d7eacbeabf3bed39ea1f41731e22993f763b1edce0f74ff8563fd1f380d92cc + languageName: node + linkType: hard + "@babel/helper-member-expression-to-functions@npm:^7.20.7, @babel/helper-member-expression-to-functions@npm:^7.21.0": version: 7.21.0 resolution: "@babel/helper-member-expression-to-functions@npm:7.21.0" @@ -1523,6 +1674,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-imports@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-module-imports@npm:7.22.5" + dependencies: + "@babel/types": ^7.22.5 + checksum: 9ac2b0404fa38b80bdf2653fbeaf8e8a43ccb41bd505f9741d820ed95d3c4e037c62a1bcdcb6c9527d7798d2e595924c4d025daed73283badc180ada2c9c49ad + languageName: node + linkType: hard + "@babel/helper-module-transforms@npm:^7.12.1, @babel/helper-module-transforms@npm:^7.18.6, @babel/helper-module-transforms@npm:^7.20.11, @babel/helper-module-transforms@npm:^7.21.2": version: 7.21.2 resolution: "@babel/helper-module-transforms@npm:7.21.2" @@ -1555,6 +1715,22 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-transforms@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-module-transforms@npm:7.22.5" + dependencies: + "@babel/helper-environment-visitor": ^7.22.5 + "@babel/helper-module-imports": ^7.22.5 + "@babel/helper-simple-access": ^7.22.5 + "@babel/helper-split-export-declaration": ^7.22.5 + "@babel/helper-validator-identifier": ^7.22.5 + "@babel/template": ^7.22.5 + "@babel/traverse": ^7.22.5 + "@babel/types": ^7.22.5 + checksum: 8985dc0d971fd17c467e8b84fe0f50f3dd8610e33b6c86e5b3ca8e8859f9448bcc5c84e08a2a14285ef388351c0484797081c8f05a03770bf44fc27bf4900e68 + languageName: node + linkType: hard + "@babel/helper-optimise-call-expression@npm:^7.18.6": version: 7.18.6 resolution: "@babel/helper-optimise-call-expression@npm:7.18.6" @@ -1585,6 +1761,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-plugin-utils@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-plugin-utils@npm:7.22.5" + checksum: c0fc7227076b6041acd2f0e818145d2e8c41968cc52fb5ca70eed48e21b8fe6dd88a0a91cbddf4951e33647336eb5ae184747ca706817ca3bef5e9e905151ff5 + languageName: node + linkType: hard + "@babel/helper-remap-async-to-generator@npm:^7.18.9": version: 7.18.9 resolution: "@babel/helper-remap-async-to-generator@npm:7.18.9" @@ -1631,6 +1814,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-simple-access@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-simple-access@npm:7.22.5" + dependencies: + "@babel/types": ^7.22.5 + checksum: fe9686714caf7d70aedb46c3cce090f8b915b206e09225f1e4dbc416786c2fdbbee40b38b23c268b7ccef749dd2db35f255338fb4f2444429874d900dede5ad2 + languageName: node + linkType: hard + "@babel/helper-skip-transparent-expression-wrappers@npm:^7.20.0": version: 7.20.0 resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.20.0" @@ -1649,6 +1841,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-split-export-declaration@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-split-export-declaration@npm:7.22.5" + dependencies: + "@babel/types": ^7.22.5 + checksum: d10e05a02f49c1f7c578cea63d2ac55356501bbf58856d97ac9bfde4957faee21ae97c7f566aa309e38a256eef58b58e5b670a7f568b362c00e93dfffe072650 + languageName: node + linkType: hard + "@babel/helper-string-parser@npm:^7.19.4": version: 7.19.4 resolution: "@babel/helper-string-parser@npm:7.19.4" @@ -1663,6 +1864,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-string-parser@npm:7.22.5" + checksum: 836851ca5ec813077bbb303acc992d75a360267aa3b5de7134d220411c852a6f17de7c0d0b8c8dcc0f567f67874c00f4528672b2a4f1bc978a3ada64c8c78467 + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.18.6, @babel/helper-validator-identifier@npm:^7.19.1": version: 7.19.1 resolution: "@babel/helper-validator-identifier@npm:7.19.1" @@ -1670,6 +1878,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-identifier@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-validator-identifier@npm:7.22.5" + checksum: 7f0f30113474a28298c12161763b49de5018732290ca4de13cdaefd4fd0d635a6fe3f6686c37a02905fb1e64f21a5ee2b55140cf7b070e729f1bd66866506aea + languageName: node + linkType: hard + "@babel/helper-validator-option@npm:^7.18.6, @babel/helper-validator-option@npm:^7.21.0": version: 7.21.0 resolution: "@babel/helper-validator-option@npm:7.21.0" @@ -1677,6 +1892,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-option@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-validator-option@npm:7.22.5" + checksum: bbeca8a85ee86990215c0424997438b388b8d642d69b9f86c375a174d3cdeb270efafd1ff128bc7a1d370923d13b6e45829ba8581c027620e83e3a80c5c414b3 + languageName: node + linkType: hard + "@babel/helper-wrap-function@npm:^7.18.9": version: 7.20.5 resolution: "@babel/helper-wrap-function@npm:7.20.5" @@ -1700,6 +1922,17 @@ __metadata: languageName: node linkType: hard +"@babel/helpers@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helpers@npm:7.22.5" + dependencies: + "@babel/template": ^7.22.5 + "@babel/traverse": ^7.22.5 + "@babel/types": ^7.22.5 + checksum: a96e785029dff72f171190943df895ab0f76e17bf3881efd630bc5fae91215042d1c2e9ed730e8e4adf4da6f28b24bd1f54ed93b90ffbca34c197351872a084e + languageName: node + linkType: hard + "@babel/highlight@npm:^7.18.6": version: 7.18.6 resolution: "@babel/highlight@npm:7.18.6" @@ -1711,6 +1944,17 @@ __metadata: languageName: node linkType: hard +"@babel/highlight@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/highlight@npm:7.22.5" + dependencies: + "@babel/helper-validator-identifier": ^7.22.5 + chalk: ^2.0.0 + js-tokens: ^4.0.0 + checksum: f61ae6de6ee0ea8d9b5bcf2a532faec5ab0a1dc0f7c640e5047fc61630a0edb88b18d8c92eb06566d30da7a27db841aca11820ecd3ebe9ce514c9350fbed39c4 + languageName: node + linkType: hard + "@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.12.7, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.17.10, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.21.3": version: 7.21.3 resolution: "@babel/parser@npm:7.21.3" @@ -1729,6 +1973,15 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/parser@npm:7.22.5" + bin: + parser: ./bin/babel-parser.js + checksum: 470ebba516417ce8683b36e2eddd56dcfecb32c54b9bb507e28eb76b30d1c3e618fd0cfeee1f64d8357c2254514e1a19e32885cfb4e73149f4ae875436a6d59c + languageName: node + linkType: hard + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.18.6" @@ -2390,7 +2643,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-commonjs@npm:^7.19.6, @babel/plugin-transform-modules-commonjs@npm:^7.21.2": +"@babel/plugin-transform-modules-commonjs@npm:^7.19.6": version: 7.21.2 resolution: "@babel/plugin-transform-modules-commonjs@npm:7.21.2" dependencies: @@ -2416,6 +2669,19 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-modules-commonjs@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.22.5" + dependencies: + "@babel/helper-module-transforms": ^7.22.5 + "@babel/helper-plugin-utils": ^7.22.5 + "@babel/helper-simple-access": ^7.22.5 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 2067aca8f6454d54ffcce69b02c457cfa61428e11372f6a1d99ff4fcfbb55c396ed2ca6ca886bf06c852e38c1a205b8095921b2364fd0243f3e66bc1dda61caa + languageName: node + linkType: hard + "@babel/plugin-transform-modules-systemjs@npm:^7.19.6, @babel/plugin-transform-modules-systemjs@npm:^7.20.11": version: 7.20.11 resolution: "@babel/plugin-transform-modules-systemjs@npm:7.20.11" @@ -2975,6 +3241,17 @@ __metadata: languageName: node linkType: hard +"@babel/template@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/template@npm:7.22.5" + dependencies: + "@babel/code-frame": ^7.22.5 + "@babel/parser": ^7.22.5 + "@babel/types": ^7.22.5 + checksum: c5746410164039aca61829cdb42e9a55410f43cace6f51ca443313f3d0bdfa9a5a330d0b0df73dc17ef885c72104234ae05efede37c1cc8a72dc9f93425977a3 + languageName: node + linkType: hard + "@babel/traverse@npm:^7.12.9, @babel/traverse@npm:^7.17.10, @babel/traverse@npm:^7.20.5, @babel/traverse@npm:^7.20.7, @babel/traverse@npm:^7.21.0, @babel/traverse@npm:^7.21.2, @babel/traverse@npm:^7.21.3, @babel/traverse@npm:^7.7.2": version: 7.21.3 resolution: "@babel/traverse@npm:7.21.3" @@ -3011,6 +3288,24 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/traverse@npm:7.22.5" + dependencies: + "@babel/code-frame": ^7.22.5 + "@babel/generator": ^7.22.5 + "@babel/helper-environment-visitor": ^7.22.5 + "@babel/helper-function-name": ^7.22.5 + "@babel/helper-hoist-variables": ^7.22.5 + "@babel/helper-split-export-declaration": ^7.22.5 + "@babel/parser": ^7.22.5 + "@babel/types": ^7.22.5 + debug: ^4.1.0 + globals: ^11.1.0 + checksum: 560931422dc1761f2df723778dcb4e51ce0d02e560cf2caa49822921578f49189a5a7d053b78a32dca33e59be886a6b2200a6e24d4ae9b5086ca0ba803815694 + languageName: node + linkType: hard + "@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.6, @babel/types@npm:^7.12.7, @babel/types@npm:^7.18.6, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.2, @babel/types@npm:^7.20.5, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.0, @babel/types@npm:^7.21.2, @babel/types@npm:^7.21.3, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": version: 7.21.3 resolution: "@babel/types@npm:7.21.3" @@ -3033,6 +3328,17 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/types@npm:7.22.5" + dependencies: + "@babel/helper-string-parser": ^7.22.5 + "@babel/helper-validator-identifier": ^7.22.5 + to-fast-properties: ^2.0.0 + checksum: c13a9c1dc7d2d1a241a2f8363540cb9af1d66e978e8984b400a20c4f38ba38ca29f06e26a0f2d49a70bad9e57615dac09c35accfddf1bb90d23cd3e0a0bab892 + languageName: node + linkType: hard + "@babel/types@npm:^7.8.3": version: 7.21.4 resolution: "@babel/types@npm:7.21.4" @@ -4045,6 +4351,24 @@ __metadata: languageName: node linkType: hard +"@jsep-plugin/regex@npm:^1.0.1": + version: 1.0.3 + resolution: "@jsep-plugin/regex@npm:1.0.3" + peerDependencies: + jsep: ^0.4.0||^1.0.0 + checksum: a57718ae5c86bd10ff5de51843a771b96a10a9c6b5c5f4e02aa5318257c3d5fdec96f8b389fcbe129c7a6ad6b0746d9a0fd934c949b80882230fbc14b548c922 + languageName: node + linkType: hard + +"@jsep-plugin/ternary@npm:^1.0.2": + version: 1.1.3 + resolution: "@jsep-plugin/ternary@npm:1.1.3" + peerDependencies: + jsep: ^0.4.0||^1.0.0 + checksum: c05408b0302844723f98b90787425beb4e8ad14029df3d98e88b9d61343d81201a7f0bf3db5806dcf0378c7be69f5b4c9fcd04f055bda282c73f4d1b425e502a + languageName: node + linkType: hard + "@juggle/resize-observer@npm:^3.3.1": version: 3.4.0 resolution: "@juggle/resize-observer@npm:3.4.0" @@ -4075,41 +4399,41 @@ __metadata: languageName: node linkType: hard -"@lerna/child-process@npm:6.6.1": - version: 6.6.1 - resolution: "@lerna/child-process@npm:6.6.1" +"@lerna/child-process@npm:6.6.2": + version: 6.6.2 + resolution: "@lerna/child-process@npm:6.6.2" dependencies: chalk: ^4.1.0 execa: ^5.0.0 strong-log-transformer: ^2.1.0 - checksum: 075f872e43b462e07fe3ab690384138f7bfc8313306981e4f2251f3bf8ecc7bae37b35b404cd8cd33e403a7d81975f92bf78c6a1fc1d3140d2b6d3cc38eae555 + checksum: f6c001043b2ab2756b56fca1bbac8b2e61ce8235c660d849e8abaf123fb0ac64cd4c8e0a01eef309850ab638a3903bf6c6910fbfbe3ce93d1cd4070a07269ab0 languageName: node linkType: hard -"@lerna/create@npm:6.6.1": - version: 6.6.1 - resolution: "@lerna/create@npm:6.6.1" +"@lerna/create@npm:6.6.2": + version: 6.6.2 + resolution: "@lerna/create@npm:6.6.2" dependencies: - "@lerna/child-process": 6.6.1 + "@lerna/child-process": 6.6.2 dedent: ^0.7.0 fs-extra: ^9.1.0 init-package-json: ^3.0.2 npm-package-arg: 8.1.1 p-reduce: ^2.1.0 - pacote: ^13.6.1 + pacote: 15.1.1 pify: ^5.0.0 semver: ^7.3.4 slash: ^3.0.0 validate-npm-package-license: ^3.0.4 validate-npm-package-name: ^4.0.0 yargs-parser: 20.2.4 - checksum: be8273644d6f156c3c81a89a370e13451ee92c7aeff5dba126be6a909479f2364031a327bd0f43a7beddc03c196c3effff9516201966cc903de3a2e3230dc4d1 + checksum: 83ff84bb27b2f244986c7c1e0d5c2258eb91da1dc2fb2e998d4b0e47373e1caef130888da19f33645199b5a04b9ba48a9eedba444998060b74f36949a674df62 languageName: node linkType: hard -"@lerna/legacy-package-management@npm:6.6.1": - version: 6.6.1 - resolution: "@lerna/legacy-package-management@npm:6.6.1" +"@lerna/legacy-package-management@npm:6.6.2": + version: 6.6.2 + resolution: "@lerna/legacy-package-management@npm:6.6.2" dependencies: "@npmcli/arborist": 6.2.3 "@npmcli/run-script": 4.1.7 @@ -4140,7 +4464,7 @@ __metadata: inquirer: 8.2.4 is-ci: 2.0.0 is-stream: 2.0.0 - libnpmpublish: 6.0.4 + libnpmpublish: 7.1.4 load-json-file: 6.2.0 make-dir: 3.1.0 minimatch: 3.0.5 @@ -4154,7 +4478,7 @@ __metadata: p-map-series: 2.1.0 p-queue: 6.6.2 p-waterfall: 2.1.1 - pacote: 13.6.2 + pacote: 15.1.1 pify: 5.0.0 pretty-format: 29.4.3 read-cmd-shim: 3.0.0 @@ -4173,7 +4497,7 @@ __metadata: write-file-atomic: 4.0.1 write-pkg: 4.0.0 yargs: 16.2.0 - checksum: 198cad91376e16edcb66b77cab58217f04ea97a27ccbd811842c9886330c66b2898fe3972ba91231f54c520f208a52207db7060651b2ff290d3702cfc1daaf77 + checksum: e5f8cf3a68b6b98c2a2546dec81927b99d1f58200892597943254ec2b5b027036ace7998c787662e95835e1c7d2b1f196204e1e62930362e06b04bb4b1045f4a languageName: node linkType: hard @@ -4211,7 +4535,7 @@ __metadata: languageName: node linkType: hard -"@mdx-js/react@npm:^1.6.21, @mdx-js/react@npm:^1.6.22": +"@mdx-js/react@npm:^1.6.22": version: 1.6.22 resolution: "@mdx-js/react@npm:1.6.22" peerDependencies: @@ -4483,23 +4807,6 @@ __metadata: languageName: node linkType: hard -"@npmcli/git@npm:^3.0.0": - version: 3.0.2 - resolution: "@npmcli/git@npm:3.0.2" - dependencies: - "@npmcli/promise-spawn": ^3.0.0 - lru-cache: ^7.4.4 - mkdirp: ^1.0.4 - npm-pick-manifest: ^7.0.0 - proc-log: ^2.0.0 - promise-inflight: ^1.0.1 - promise-retry: ^2.0.1 - semver: ^7.3.5 - which: ^2.0.2 - checksum: bdfd1229bb1113ad4883ef89b74b5dc442a2c96225d830491dd0dec4fa83d083b93cde92b6978d4956a8365521e61bc8dc1891fb905c7c693d5d6aa178f2ab44 - languageName: node - linkType: hard - "@npmcli/git@npm:^4.0.0": version: 4.0.4 resolution: "@npmcli/git@npm:4.0.4" @@ -4721,19 +5028,6 @@ __metadata: languageName: node linkType: hard -"@npmcli/run-script@npm:^4.1.0": - version: 4.2.1 - resolution: "@npmcli/run-script@npm:4.2.1" - dependencies: - "@npmcli/node-gyp": ^2.0.0 - "@npmcli/promise-spawn": ^3.0.0 - node-gyp: ^9.0.0 - read-package-json-fast: ^2.0.3 - which: ^2.0.2 - checksum: 7b8d6676353f157e68b26baf848e01e5d887bcf90ce81a52f23fc9a5d93e6ffb60057532d664cfd7aeeb76d464d0c8b0d314ee6cccb56943acb3b6c570b756c8 - languageName: node - linkType: hard - "@npmcli/run-script@npm:^6.0.0": version: 6.0.0 resolution: "@npmcli/run-script@npm:6.0.0" @@ -5038,7 +5332,7 @@ __metadata: languageName: node linkType: hard -"@openapitools/openapi-generator-cli@npm:^2.5.2": +"@openapitools/openapi-generator-cli@npm:^2.6.0": version: 2.6.0 resolution: "@openapitools/openapi-generator-cli@npm:2.6.0" dependencies: @@ -5064,13 +5358,56 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/api@npm:^1.1.0": +"@opentelemetry/api@npm:^1.4.1": version: 1.4.1 resolution: "@opentelemetry/api@npm:1.4.1" checksum: e783c40d1a518abf9c4c5d65223237c1392cd9a6c53ac6e2c3ef0c05ff7266e3dfc4fd9874316dae0dcb7a97950878deb513bcbadfaad653d48f0215f2a0911b languageName: node linkType: hard +"@opentelemetry/core@npm:1.14.0, @opentelemetry/core@npm:^1.11.0": + version: 1.14.0 + resolution: "@opentelemetry/core@npm:1.14.0" + dependencies: + "@opentelemetry/semantic-conventions": 1.14.0 + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.5.0" + checksum: 061e6f3bee492e020915427da94da902846ed6ee922bdb21a70563e5fbd3ebb39e8e3ae6eb1071eb775d27ca31093929ab65fe8abc81228042be8d9a3c96bac8 + languageName: node + linkType: hard + +"@opentelemetry/resources@npm:1.14.0": + version: 1.14.0 + resolution: "@opentelemetry/resources@npm:1.14.0" + dependencies: + "@opentelemetry/core": 1.14.0 + "@opentelemetry/semantic-conventions": 1.14.0 + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.5.0" + checksum: 4494fcad2a9d8de215149675ce4f16885f6d7d0b3318c44a1f8d2e50009de07853c94b428462f70d1dd3d3681a5738d6b625806b7939839524db49a81eb90a2c + languageName: node + linkType: hard + +"@opentelemetry/sdk-metrics@npm:^1.12.0": + version: 1.14.0 + resolution: "@opentelemetry/sdk-metrics@npm:1.14.0" + dependencies: + "@opentelemetry/core": 1.14.0 + "@opentelemetry/resources": 1.14.0 + lodash.merge: 4.6.2 + peerDependencies: + "@opentelemetry/api": ">=1.3.0 <1.5.0" + checksum: 633f58b02cae9d16827e3fc6e7a37329ee0ebc63e4b0193cf3c80afa3553b0d737019df889ba73644254fc3134108b965c93e6f097aacee396a671bed1b2188d + languageName: node + linkType: hard + +"@opentelemetry/semantic-conventions@npm:1.14.0": + version: 1.14.0 + resolution: "@opentelemetry/semantic-conventions@npm:1.14.0" + checksum: 0d4f752035ac25d10cfcaf0e16f29bff8fa47877baff61dc4d047f9fe1e70f1459d02f5b70982c579db48584038e4fe263755b35ffa7254385604917337469f3 + languageName: node + linkType: hard + "@parcel/watcher@npm:2.0.4": version: 2.0.4 resolution: "@parcel/watcher@npm:2.0.4" @@ -5273,6 +5610,16 @@ __metadata: languageName: node linkType: hard +"@sigstore/tuf@npm:^1.0.1": + version: 1.0.2 + resolution: "@sigstore/tuf@npm:1.0.2" + dependencies: + "@sigstore/protobuf-specs": ^0.1.0 + tuf-js: ^1.1.7 + checksum: 564d897d4d9028d2aaf1b26d56c7e7c874d097480ea093a94769ccea2a60507275665aba6216b32e3f31d3b3e1c6f2fd7225784d6d3472f9480bc7556543cee9 + languageName: node + linkType: hard + "@sinclair/typebox@npm:^0.25.16": version: 0.25.24 resolution: "@sinclair/typebox@npm:0.25.24" @@ -5346,14 +5693,14 @@ __metadata: resolution: "@sofie-automation/blueprints-integration@workspace:blueprints-integration" dependencies: "@sofie-automation/shared-lib": 1.51.0-in-development - tslib: ^2.4.0 + tslib: ^2.6.0 type-fest: ^2.19.0 languageName: unknown linkType: soft -"@sofie-automation/code-standard-preset@npm:~2.4.6": - version: 2.4.6 - resolution: "@sofie-automation/code-standard-preset@npm:2.4.6" +"@sofie-automation/code-standard-preset@npm:~2.4.7": + version: 2.4.7 + resolution: "@sofie-automation/code-standard-preset@npm:2.4.7" dependencies: "@sofie-automation/eslint-plugin": ^0.1.1 "@typescript-eslint/eslint-plugin": ^5.59.1 @@ -5378,7 +5725,7 @@ __metadata: bin: sofie-licensecheck: ./bin/checkLicenses.mjs sofie-version: ./bin/updateVersion.mjs - checksum: 3fe36dd192d4b3a774988c3fe60bd8f2c1130a07d77c335ae1abf731e9584bb8a8741d18f38ed58b9a5f90998570f3fccc048e32b7f77da7afd22a5e09adc337 + checksum: 399ec1db25a07da1b19533403fb86ad67f930bf3196df71562b41800636a1f0008038bf7ae4c720f9ec478e5330d31d335d0690e1e2a281fd9c33a66e25ed740 languageName: node linkType: hard @@ -5389,15 +5736,15 @@ __metadata: "@sofie-automation/blueprints-integration": 1.51.0-in-development "@sofie-automation/shared-lib": 1.51.0-in-development fast-clone: ^1.5.13 - i18next: ^21.9.1 + i18next: ^21.10.0 influx: ^5.9.3 - nanoid: ^3.3.4 + nanoid: ^3.3.6 object-path: ^0.11.8 prom-client: ^14.2.0 timecode: 0.0.4 - tslib: ^2.4.0 + tslib: ^2.6.0 type-fest: ^2.19.0 - underscore: ^1.13.4 + underscore: ^1.13.6 peerDependencies: mongodb: ^4.13.0 languageName: unknown @@ -5423,17 +5770,17 @@ __metadata: "@sofie-automation/shared-lib": 1.51.0-in-development amqplib: ^0.10.3 deepmerge: ^4.3.1 - elastic-apm-node: ^3.43.0 + elastic-apm-node: ^3.47.0 eventemitter3: ^4.0.7 - mongodb: ^4.13.0 + mongodb: ^4.16.0 p-lazy: ^3.1.0 p-timeout: ^4.1.0 superfly-timeline: ^8.3.1 - threadedclass: ^1.1.2 - tslib: ^2.4.0 + threadedclass: ^1.2.1 + tslib: ^2.6.0 type-fest: ^2.19.0 - underscore: ^1.13.4 - vm2: ^3.9.14 + underscore: ^1.13.6 + vm2: ^3.9.19 languageName: unknown linkType: soft @@ -5441,11 +5788,11 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/openapi@workspace:openapi" dependencies: - "@openapitools/openapi-generator-cli": ^2.5.2 - eslint-plugin-yml: ^1.2.0 + "@openapitools/openapi-generator-cli": ^2.6.0 + eslint-plugin-yml: ^1.8.0 js-yaml: ^4.1.0 - tslib: ^2.4.0 - wget-improved: ^3.3.1 + tslib: ^2.6.0 + wget-improved: ^3.4.0 languageName: unknown linkType: soft @@ -5458,8 +5805,8 @@ __metadata: eventemitter3: ^4.0.7 faye-websocket: ^0.11.4 got: ^11.8.6 - tslib: ^2.4.0 - underscore: ^1.13.4 + tslib: ^2.6.0 + underscore: ^1.13.6 languageName: unknown linkType: soft @@ -5469,11 +5816,242 @@ __metadata: dependencies: "@mos-connection/model": ^3.0.4 timeline-state-resolver-types: 9.0.0-release50.5 - tslib: ^2.4.0 + tslib: ^2.6.0 type-fest: ^2.19.0 languageName: unknown linkType: soft +"@stoplight/better-ajv-errors@npm:1.0.3": + version: 1.0.3 + resolution: "@stoplight/better-ajv-errors@npm:1.0.3" + dependencies: + jsonpointer: ^5.0.0 + leven: ^3.1.0 + peerDependencies: + ajv: ">=8" + checksum: 642fe5636a72a86de72e4ffc7bbf07499fc09d8446b386f31d3667b07dd1849d921c38a74c109a9e2554d405b6e90dc150728a0c455bf93f158ff139e0538ddd + languageName: node + linkType: hard + +"@stoplight/json-ref-readers@npm:1.2.2": + version: 1.2.2 + resolution: "@stoplight/json-ref-readers@npm:1.2.2" + dependencies: + node-fetch: ^2.6.0 + tslib: ^1.14.1 + checksum: 31b0e78b119f7afd7dd84a4fbb0c4aaceeb6e889179e785ddb9880ee548d4d161dce5743451ef6dad4b7a902d9f0711909c87b63ad794bede234a144bcf2b2b4 + languageName: node + linkType: hard + +"@stoplight/json-ref-resolver@npm:^3.1.5, @stoplight/json-ref-resolver@npm:~3.1.5": + version: 3.1.5 + resolution: "@stoplight/json-ref-resolver@npm:3.1.5" + dependencies: + "@stoplight/json": ^3.17.0 + "@stoplight/path": ^1.3.2 + "@stoplight/types": ^12.3.0 || ^13.0.0 + "@types/urijs": ^1.19.19 + dependency-graph: ~0.11.0 + fast-memoize: ^2.5.2 + immer: ^9.0.6 + lodash: ^4.17.21 + tslib: ^2.3.1 + urijs: ^1.19.11 + checksum: b2df82e899717ffa7bab9ec4c380f206511b3971d242724afd34e8b964e5aa7be2a52e32ba48d435548a9ac13a63a2271ca69182abec8506fc3bb3cc129f6380 + languageName: node + linkType: hard + +"@stoplight/json@npm:^3.17.0, @stoplight/json@npm:^3.17.1": + version: 3.21.0 + resolution: "@stoplight/json@npm:3.21.0" + dependencies: + "@stoplight/ordered-object-literal": ^1.0.3 + "@stoplight/path": ^1.3.2 + "@stoplight/types": ^13.6.0 + jsonc-parser: ~2.2.1 + lodash: ^4.17.21 + safe-stable-stringify: ^1.1 + checksum: 16fe56a6804cd47837bd82d85a8500c4226669558f3feda55d8fb0cd615ca2261622963700f04f049cf30a3a9764eb3c861516003d948743b6ae85dbbabf8a59 + languageName: node + linkType: hard + +"@stoplight/json@npm:~3.20.1": + version: 3.20.3 + resolution: "@stoplight/json@npm:3.20.3" + dependencies: + "@stoplight/ordered-object-literal": ^1.0.3 + "@stoplight/path": ^1.3.2 + "@stoplight/types": ^13.6.0 + jsonc-parser: ~2.2.1 + lodash: ^4.17.21 + safe-stable-stringify: ^1.1 + checksum: 52a4251deca0be91c98172287def5cb004ff2ab801c3b7bb24f02abe14ad0890ee453588f235401fa048c5fefe1a750d7f68857f681d57afb62dd5310f50d71a + languageName: node + linkType: hard + +"@stoplight/ordered-object-literal@npm:^1.0.1, @stoplight/ordered-object-literal@npm:^1.0.3": + version: 1.0.4 + resolution: "@stoplight/ordered-object-literal@npm:1.0.4" + checksum: 81afa24943880b0a213af3728a9fe0a28bd01d4840b9583d448f7823ced5b6e673628698b59d201cef50afebcbd89256e133714a174968d11b624d943e0c2c2f + languageName: node + linkType: hard + +"@stoplight/path@npm:1.3.2, @stoplight/path@npm:^1.3.2": + version: 1.3.2 + resolution: "@stoplight/path@npm:1.3.2" + checksum: 8a1143cef9edcf9fd8cb24ca3f250693d475ce1f635f0dc95e5b045aad303fbf4d702c939f0c4ed8d28a04208d1aa4471fb10912ef1e3a94a9e6810878a7cfbb + languageName: node + linkType: hard + +"@stoplight/spectral-core@npm:^1.16.1, @stoplight/spectral-core@npm:^1.7.0, @stoplight/spectral-core@npm:^1.8.0": + version: 1.18.0 + resolution: "@stoplight/spectral-core@npm:1.18.0" + dependencies: + "@stoplight/better-ajv-errors": 1.0.3 + "@stoplight/json": ~3.20.1 + "@stoplight/path": 1.3.2 + "@stoplight/spectral-parsers": ^1.0.0 + "@stoplight/spectral-ref-resolver": ^1.0.0 + "@stoplight/spectral-runtime": ^1.0.0 + "@stoplight/types": ~13.6.0 + "@types/es-aggregate-error": ^1.0.2 + "@types/json-schema": ^7.0.11 + ajv: ^8.6.0 + ajv-errors: ~3.0.0 + ajv-formats: ~2.1.0 + es-aggregate-error: ^1.0.7 + jsonpath-plus: 7.1.0 + lodash: ~4.17.21 + lodash.topath: ^4.5.2 + minimatch: 3.1.2 + nimma: 0.2.2 + pony-cause: ^1.0.0 + simple-eval: 1.0.0 + tslib: ^2.3.0 + checksum: 76ff9e2349c035fa2b98556c8bd67bb531f2b9c29e19e33484270d62f30900d36ceb51ef0075255479cc5071b45bcb7249088f568fff9d44cd159b5ee8f07cec + languageName: node + linkType: hard + +"@stoplight/spectral-formats@npm:^1.0.0": + version: 1.5.0 + resolution: "@stoplight/spectral-formats@npm:1.5.0" + dependencies: + "@stoplight/json": ^3.17.0 + "@stoplight/spectral-core": ^1.8.0 + "@types/json-schema": ^7.0.7 + tslib: ^2.3.1 + checksum: dc80c0ee37ff5708b2078aa588d3c8c895ba36b18e52f5813cd7f91d1a476629aa36f1ceaf62e8bd96987c0f7c51537bdb6780a08d32be982db230be5634c93e + languageName: node + linkType: hard + +"@stoplight/spectral-functions@npm:^1.7.2": + version: 1.7.2 + resolution: "@stoplight/spectral-functions@npm:1.7.2" + dependencies: + "@stoplight/better-ajv-errors": 1.0.3 + "@stoplight/json": ^3.17.1 + "@stoplight/spectral-core": ^1.7.0 + "@stoplight/spectral-formats": ^1.0.0 + "@stoplight/spectral-runtime": ^1.1.0 + ajv: ^8.6.3 + ajv-draft-04: ~1.0.0 + ajv-errors: ~3.0.0 + ajv-formats: ~2.1.0 + lodash: ~4.17.21 + tslib: ^2.3.0 + checksum: f89d966d33dd484e5ea63a7971478d176c94215b4ffd2ef24eb8e507a2b60ed3bcfa391b9137793e939f3a10443914db6da62d081055fb8ba49d2d397f0d5907 + languageName: node + linkType: hard + +"@stoplight/spectral-parsers@npm:^1.0.0, @stoplight/spectral-parsers@npm:^1.0.2": + version: 1.0.2 + resolution: "@stoplight/spectral-parsers@npm:1.0.2" + dependencies: + "@stoplight/json": ~3.20.1 + "@stoplight/types": ^13.6.0 + "@stoplight/yaml": ~4.2.3 + tslib: ^2.3.1 + checksum: 89bc5c77ebeedca0abf9ffa9e074beed57d47b71814f23a1d65c03469aa6ea923007b42e7cb18ca7bcc4ff9fd399d604d32a41aa4c4c57a9cc3bebb4b24c3e34 + languageName: node + linkType: hard + +"@stoplight/spectral-ref-resolver@npm:^1.0.0": + version: 1.0.3 + resolution: "@stoplight/spectral-ref-resolver@npm:1.0.3" + dependencies: + "@stoplight/json-ref-readers": 1.2.2 + "@stoplight/json-ref-resolver": ~3.1.5 + "@stoplight/spectral-runtime": ^1.1.2 + dependency-graph: 0.11.0 + tslib: ^2.3.1 + checksum: efe93b1d1d32647b675c221530ecb20c55303dfcd1009efd8e036f6415eb1255fff19781358a74046160516a7b7088b6329885bb507a444c14db32fa23392bc9 + languageName: node + linkType: hard + +"@stoplight/spectral-runtime@npm:^1.0.0, @stoplight/spectral-runtime@npm:^1.1.0, @stoplight/spectral-runtime@npm:^1.1.2": + version: 1.1.2 + resolution: "@stoplight/spectral-runtime@npm:1.1.2" + dependencies: + "@stoplight/json": ^3.17.0 + "@stoplight/path": ^1.3.2 + "@stoplight/types": ^12.3.0 + abort-controller: ^3.0.0 + lodash: ^4.17.21 + node-fetch: ^2.6.7 + tslib: ^2.3.1 + checksum: 35964a38f82384e6e0158988173a50ab7f473a2ed6e942073de023bd28fb696b5b913336a84d016b046346294be9cfa3a88c6a908c2622c0ceb36f16ca76e084 + languageName: node + linkType: hard + +"@stoplight/types@npm:^12.3.0": + version: 12.5.0 + resolution: "@stoplight/types@npm:12.5.0" + dependencies: + "@types/json-schema": ^7.0.4 + utility-types: ^3.10.0 + checksum: fe4a09df6e1c2f0cdb53f474b180cc7b8184e814e1ac4427d199642f10958335f597060530a908c0e5800ba2569d077afe124a51deaee466255ce942e1e03941 + languageName: node + linkType: hard + +"@stoplight/types@npm:^12.3.0 || ^13.0.0, @stoplight/types@npm:^13.0.0, @stoplight/types@npm:^13.6.0": + version: 13.15.0 + resolution: "@stoplight/types@npm:13.15.0" + dependencies: + "@types/json-schema": ^7.0.4 + utility-types: ^3.10.0 + checksum: 839f0bbedb791bd6792ef22b6a821ca504b14b705927f7c510c4cdcc591eddc8818c82b8857129501aa809d6f369b82e4487bfe18dfc5ce00e28317ecad2df9a + languageName: node + linkType: hard + +"@stoplight/types@npm:~13.6.0": + version: 13.6.0 + resolution: "@stoplight/types@npm:13.6.0" + dependencies: + "@types/json-schema": ^7.0.4 + utility-types: ^3.10.0 + checksum: 4cc81cf29decc0392f15c71b21fd11cd806bcf99168ae4509ed41c2b7dbcfbd5a83c7f9f320edb5a518cc483fd18dd8794c54b232fb6a6f2a7b6e9fb6ca20269 + languageName: node + linkType: hard + +"@stoplight/yaml-ast-parser@npm:0.0.48": + version: 0.0.48 + resolution: "@stoplight/yaml-ast-parser@npm:0.0.48" + checksum: 4e252a874636d4015ff78a638075c438ccf7b8b4b38e3df12f7b8381da2da0411dfff7a6de38354b8093a36a8911a9dd656264fb0d34453cb7bcf78a3627dfa0 + languageName: node + linkType: hard + +"@stoplight/yaml@npm:~4.2.3": + version: 4.2.3 + resolution: "@stoplight/yaml@npm:4.2.3" + dependencies: + "@stoplight/ordered-object-literal": ^1.0.1 + "@stoplight/types": ^13.0.0 + "@stoplight/yaml-ast-parser": 0.0.48 + tslib: ^2.2.0 + checksum: 8e61c4499c0849dafecf487e51e10d4d3e99192834dd87eba4b27c20c42d3fe1b5b022f9c3eb63b96249b46d7277ffb4ce37447d84d01d4768b88d142e3d0e15 + languageName: node + linkType: hard + "@svgr/babel-plugin-add-jsx-attribute@npm:^5.4.0": version: 5.4.0 resolution: "@svgr/babel-plugin-add-jsx-attribute@npm:5.4.0" @@ -5835,6 +6413,13 @@ __metadata: languageName: node linkType: hard +"@tufjs/canonical-json@npm:1.0.0": + version: 1.0.0 + resolution: "@tufjs/canonical-json@npm:1.0.0" + checksum: 9ff3bcd12988fb23643690da3e009f9130b7b10974f8e7af4bd8ad230a228119de8609aa76d75264fe80f152b50872dea6ea53def69534436a4c24b4fcf6a447 + languageName: node + linkType: hard + "@tufjs/models@npm:1.0.1": version: 1.0.1 resolution: "@tufjs/models@npm:1.0.1" @@ -5844,6 +6429,16 @@ __metadata: languageName: node linkType: hard +"@tufjs/models@npm:1.0.4": + version: 1.0.4 + resolution: "@tufjs/models@npm:1.0.4" + dependencies: + "@tufjs/canonical-json": 1.0.0 + minimatch: ^9.0.0 + checksum: b489baa854abce6865f360591c20d5eb7d8dde3fb150f42840c12bb7ee3e5e7a69eab9b2e44ea82ae1f8cd95b586963c5a5c5af8ba4ffa3614b3ddccbc306779 + languageName: node + linkType: hard + "@tv2media/v-connection@npm:^7.3.0": version: 7.3.0 resolution: "@tv2media/v-connection@npm:7.3.0" @@ -5957,12 +6552,12 @@ __metadata: languageName: node linkType: hard -"@types/debug@npm:^4.1.7": - version: 4.1.7 - resolution: "@types/debug@npm:4.1.7" +"@types/debug@npm:^4.1.8": + version: 4.1.8 + resolution: "@types/debug@npm:4.1.8" dependencies: "@types/ms": "*" - checksum: 0a7b89d8ed72526858f0b61c6fd81f477853e8c4415bb97f48b1b5545248d2ae389931680b94b393b993a7cfe893537a200647d93defe6d87159b96812305adc + checksum: a9a9bb40a199e9724aa944e139a7659173a9b274798ea7efbc277cb084bc37d32fc4c00877c3496fac4fed70a23243d284adb75c00b5fdabb38a22154d18e5df languageName: node linkType: hard @@ -5982,6 +6577,15 @@ __metadata: languageName: node linkType: hard +"@types/es-aggregate-error@npm:^1.0.2": + version: 1.0.2 + resolution: "@types/es-aggregate-error@npm:1.0.2" + dependencies: + "@types/node": "*" + checksum: 076fd59b595f33c8c7e7eb68ec55bd43cf8b2cf7bbc6778e25d7ae1a5fa0538a0a56f149015f403d7bbcefe59f1d8182351685b59c1fe719fd46d0dd8a9737fa + languageName: node + linkType: hard + "@types/eslint-scope@npm:^3.7.3": version: 3.7.4 resolution: "@types/eslint-scope@npm:3.7.4" @@ -6140,13 +6744,13 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:^29.5.0": - version: 29.5.0 - resolution: "@types/jest@npm:29.5.0" +"@types/jest@npm:^29.5.2": + version: 29.5.2 + resolution: "@types/jest@npm:29.5.2" dependencies: expect: ^29.0.0 pretty-format: ^29.0.0 - checksum: cd877e5c56d299cceb8bfdcbb1a77723c706750dd3c3bc47403bc3599b8faff590a3b009c68bb5b11bf7a8c77d1fb01de5e124329b4a08e65f1cdda28b0ecdb8 + checksum: 7d205599ea3cccc262bad5cc173d3242d6bf8138c99458509230e4ecef07a52d6ddcde5a1dbd49ace655c0af51d2dbadef3748697292ea4d86da19d9e03e19c0 languageName: node linkType: hard @@ -6224,10 +6828,10 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^14.18.12": - version: 14.18.42 - resolution: "@types/node@npm:14.18.42" - checksum: 1c92f04a482ab54a21342b3911fc6f0093f04d3314197bc0e2f20012e9efc929c44e2ea41990b9b3cde420d7859c9ed716733f3e65c0cd6c2910a55799465f6b +"@types/node@npm:^14.18.53": + version: 14.18.53 + resolution: "@types/node@npm:14.18.53" + checksum: 3acbcf4e38cdc5999c824db5c6689ca8f4a185562aad5c0263d9859381d8590a16e6a72343aeb30d48ff9a7ac9b6ae259bcb8c2d594703a30d12277904524704 languageName: node linkType: hard @@ -6455,10 +7059,10 @@ __metadata: languageName: node linkType: hard -"@types/underscore@npm:^1.11.4": - version: 1.11.4 - resolution: "@types/underscore@npm:1.11.4" - checksum: db9f8486bc851b732259e51f42d62aad1ae2158be5724612dc125ece5f5d61c51447f9dea28284c2a0f79cb95e788d01cb5ce97709880019213e69fab0dd1696 +"@types/underscore@npm:^1.11.5": + version: 1.11.5 + resolution: "@types/underscore@npm:1.11.5" + checksum: 6cd928c436bd65a7b544c979e0958762816dc3fd4f0d430d055faa1e914336568c3a8dd52350760c16ecc37be71d40b0792012bae455d8c5d63e50f02986c9e2 languageName: node linkType: hard @@ -6469,6 +7073,13 @@ __metadata: languageName: node linkType: hard +"@types/urijs@npm:^1.19.19": + version: 1.19.19 + resolution: "@types/urijs@npm:1.19.19" + checksum: 2c08d41782149a243b374b28be009ca461f541c440d8d47c9d75b1d3255ff7169b34bb721cf2dd6266c2c44be6b70fc6d67a1abad50c4dae369774042b1facd8 + languageName: node + linkType: hard + "@types/webidl-conversions@npm:*": version: 7.0.0 resolution: "@types/webidl-conversions@npm:7.0.0" @@ -7067,7 +7678,28 @@ __metadata: languageName: node linkType: hard -"ajv-formats@npm:^2.1.1": +"ajv-draft-04@npm:~1.0.0": + version: 1.0.0 + resolution: "ajv-draft-04@npm:1.0.0" + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + checksum: 3f11fa0e7f7359bef6608657f02ab78e9cc62b1fb7bdd860db0d00351b3863a1189c1a23b72466d2d82726cab4eb20725c76f5e7c134a89865e2bfd0e6828137 + languageName: node + linkType: hard + +"ajv-errors@npm:^3.0.0, ajv-errors@npm:~3.0.0": + version: 3.0.0 + resolution: "ajv-errors@npm:3.0.0" + peerDependencies: + ajv: ^8.0.1 + checksum: f3d1610a104fa776c2f90534acbe2113842a40d5ee446062da9e956ae6de6959afc997da1e3948c47316faa225255fc2d9d97aacd0803f47998fb38156d3d03c + languageName: node + linkType: hard + +"ajv-formats@npm:^2.1.1, ajv-formats@npm:~2.1.0": version: 2.1.1 resolution: "ajv-formats@npm:2.1.1" dependencies: @@ -7125,7 +7757,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.10.0, ajv@npm:^6.10.1, ajv@npm:^6.10.2, ajv@npm:^6.11.0, ajv@npm:^6.12.2, ajv@npm:^6.12.3, ajv@npm:^6.12.4, ajv@npm:^6.12.5": +"ajv@npm:^6.10.0, ajv@npm:^6.10.1, ajv@npm:^6.11.0, ajv@npm:^6.12.2, ajv@npm:^6.12.3, ajv@npm:^6.12.4, ajv@npm:^6.12.5": version: 6.12.6 resolution: "ajv@npm:6.12.6" dependencies: @@ -7137,7 +7769,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^8.0.0, ajv@npm:^8.8.0": +"ajv@npm:^8.0.0, ajv@npm:^8.11.0, ajv@npm:^8.12.0, ajv@npm:^8.6.0, ajv@npm:^8.6.3, ajv@npm:^8.8.0": version: 8.12.0 resolution: "ajv@npm:8.12.0" dependencies: @@ -7512,6 +8144,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"astring@npm:^1.8.1": + version: 1.8.6 + resolution: "astring@npm:1.8.6" + bin: + astring: bin/astring + checksum: 6f034d2acef1dac8bb231e7cc26c573d3c14e1975ea6e04f20312b43d4f462f963209bc64187d25d477a182dc3c33277959a0156ab7a3617aa79b1eac4d88e1f + languageName: node + linkType: hard + "async-cache@npm:^1.1.0": version: 1.1.0 resolution: "async-cache@npm:1.1.0" @@ -7618,7 +8259,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"avsc@npm:^5.7.3": +"avsc@npm:^5.7.3, avsc@npm:^5.7.5, avsc@npm:^5.7.6": version: 5.7.7 resolution: "avsc@npm:5.7.7" checksum: e3361aa88a61397b3345876263f79c8c8bfe013d849142202758205459a37e24cdbf02edc49ae019d6e82d93bbc7bc73e9e7fefca049aae91626bae28de4d1a9 @@ -8153,7 +8794,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"bson@npm:^4.7.0": +"bson@npm:^4.7.2": version: 4.7.2 resolution: "bson@npm:4.7.2" dependencies: @@ -8288,7 +8929,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"cacache@npm:^16.0.0, cacache@npm:^16.1.0": +"cacache@npm:^16.1.0": version: 16.1.3 resolution: "cacache@npm:16.1.3" dependencies: @@ -8672,7 +9313,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"ci-info@npm:^3.2.0": +"ci-info@npm:^3.2.0, ci-info@npm:^3.6.1": version: 3.8.0 resolution: "ci-info@npm:3.8.0" checksum: d0a4d3160497cae54294974a7246202244fff031b0a6ea20dd57b10ec510aa17399c41a1b0982142c105f3255aff2173e5c0dd7302ee1b2f28ba3debda375098 @@ -8841,7 +9482,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"clsx@npm:^1.1.1": +"clsx@npm:^1.1.1, clsx@npm:^1.2.1": version: 1.2.1 resolution: "clsx@npm:1.2.1" checksum: 30befca8019b2eb7dbad38cff6266cf543091dae2825c856a62a8ccf2c3ab9c2907c4d12b288b73101196767f66812365400a227581484a05f968b0307cfaf12 @@ -10108,6 +10749,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"dependency-graph@npm:0.11.0, dependency-graph@npm:~0.11.0": + version: 0.11.0 + resolution: "dependency-graph@npm:0.11.0" + checksum: 477204beaa9be69e642bc31ffe7a8c383d0cf48fa27acbc91c5df01431ab913e65c154213d2ef83d034c98d77280743ec85e5da018a97a18dd43d3c0b78b28cd + languageName: node + linkType: hard + "deprecation@npm:^2.0.0, deprecation@npm:^2.3.1": version: 2.3.1 resolution: "deprecation@npm:2.3.1" @@ -10467,9 +11115,9 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"elastic-apm-http-client@npm:11.2.0": - version: 11.2.0 - resolution: "elastic-apm-http-client@npm:11.2.0" +"elastic-apm-http-client@npm:11.4.0": + version: 11.4.0 + resolution: "elastic-apm-http-client@npm:11.4.0" dependencies: agentkeepalive: ^4.2.1 breadth-filter: ^2.0.0 @@ -10480,24 +11128,25 @@ asn1@evs-broadcast/node-asn1: readable-stream: ^3.4.0 semver: ^6.3.0 stream-chopper: ^3.0.1 - checksum: 6177e836202a6cfb2d68106aff68cdfa04eb75ad973a7b26a3d2cc985f92c1e747fb78aeb1937fece4f417901e4147848fb40be428ac9be8e909e86aece8e738 + checksum: 1b82e55395084c362907e8ed456b9bd51285eebd66d2579b4d88f2d023199a16a3fb8aecffe6e777da129981cb2d9b705f21dab5af92de359e979576ee19fd93 languageName: node linkType: hard -"elastic-apm-node@npm:^3.43.0": - version: 3.43.0 - resolution: "elastic-apm-node@npm:3.43.0" +"elastic-apm-node@npm:^3.47.0": + version: 3.47.0 + resolution: "elastic-apm-node@npm:3.47.0" dependencies: "@elastic/ecs-pino-format": ^1.2.0 - "@opentelemetry/api": ^1.1.0 + "@opentelemetry/api": ^1.4.1 + "@opentelemetry/core": ^1.11.0 + "@opentelemetry/sdk-metrics": ^1.12.0 after-all-results: ^2.0.0 async-cache: ^1.1.0 async-value-promise: ^1.1.1 basic-auth: ^2.0.1 cookie: ^0.5.0 core-util-is: ^1.0.2 - debug: ^4.1.1 - elastic-apm-http-client: 11.2.0 + elastic-apm-http-client: 11.4.0 end-of-stream: ^1.4.4 error-callsites: ^2.0.4 error-stack-parser: ^2.0.6 @@ -10514,15 +11163,13 @@ asn1@evs-broadcast/node-asn1: original-url: ^1.2.3 pino: ^6.11.2 relative-microtime: ^2.0.0 - resolve: ^1.22.1 + require-in-the-middle: ^7.0.1 semver: ^6.3.0 - set-cookie-serde: ^1.0.0 shallow-clone-shim: ^2.0.0 source-map: ^0.8.0-beta.0 sql-summary: ^1.0.1 - traverse: ^0.6.6 unicode-byte-truncate: ^1.0.0 - checksum: 709010fe7ca1a133025d64ca476e7471b7bef34327dfa8084fee853fe0bfd96c68354424ecd1d75c79edbcaa50426f739a46f3da612d9ddc173546b3ae6393b5 + checksum: 1fe255467edc0c1098d0d407caf6a616aedb68a7e1f593aee4d0c5d2e2e149c9d15f03eb3591c14a940fc9b7bdd0ee5c77dfaea62b965dbec13c00324e8d3560 languageName: node linkType: hard @@ -10767,6 +11414,21 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"es-aggregate-error@npm:^1.0.7": + version: 1.0.9 + resolution: "es-aggregate-error@npm:1.0.9" + dependencies: + define-properties: ^1.1.4 + es-abstract: ^1.20.4 + function-bind: ^1.1.1 + functions-have-names: ^1.2.3 + get-intrinsic: ^1.1.3 + globalthis: ^1.0.3 + has-property-descriptors: ^1.0.0 + checksum: bef86c4fcd3b924929e8d89d288dc0a577b31bb17d6a3f0a7676f22c5d7ffa4e1ff08bb393f06b15e1baad132ae725a8c8692728a5f73cd2b78a1c49ce664605 + languageName: node + linkType: hard + "es-array-method-boxes-properly@npm:^1.0.0": version: 1.0.0 resolution: "es-array-method-boxes-properly@npm:1.0.0" @@ -10979,17 +11641,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"eslint-plugin-yml@npm:^1.2.0": - version: 1.5.0 - resolution: "eslint-plugin-yml@npm:1.5.0" +"eslint-plugin-yml@npm:^1.8.0": + version: 1.8.0 + resolution: "eslint-plugin-yml@npm:1.8.0" dependencies: debug: ^4.3.2 lodash: ^4.17.21 natural-compare: ^1.4.0 - yaml-eslint-parser: ^1.1.0 + yaml-eslint-parser: ^1.2.1 peerDependencies: eslint: ">=6.0.0" - checksum: f8f61165b765c0b8db7867b5b51156c9341a0a9133398f59d9e91fe5f6d59c397c7b20ca3d640451d220812baf01957c9e12af7fdb2bc40d4382ab4a0461849f + checksum: f07dfe550b10797237cd4a8752813d9fbdad4c9feefef6b1b270af9290ed19f5d3f0379e6ca6945a8123ab95707e8ff9bb1cbe76824c21b3575ad44d6390df0d languageName: node linkType: hard @@ -11479,6 +12141,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"fast-memoize@npm:^2.5.2": + version: 2.5.2 + resolution: "fast-memoize@npm:2.5.2" + checksum: 79fa759719ba4eac7e8c22fb3b0eb3f18f4a31e218c00b1eb4a5b53c5781921133a6b84472d59ec5a6ea8f26ad57b43cd99a350c0547ccce51489bc9a5f0b28d + languageName: node + linkType: hard + "fast-redact@npm:^3.0.0": version: 3.1.2 resolution: "fast-redact@npm:3.1.2" @@ -12099,7 +12768,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"functions-have-names@npm:^1.2.2": +"functions-have-names@npm:^1.2.2, functions-have-names@npm:^1.2.3": version: 1.2.3 resolution: "functions-have-names@npm:1.2.3" checksum: c3f1f5ba20f4e962efb71344ce0a40722163e85bee2101ce25f88214e78182d2d2476aa85ef37950c579eb6cf6ee811c17b3101bb84004bb75655f3e33f3fdb5 @@ -13220,7 +13889,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"i18next@npm:^21.9.1": +"i18next@npm:^21.10.0": version: 21.10.0 resolution: "i18next@npm:21.10.0" dependencies: @@ -13329,7 +13998,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"immer@npm:^9.0.7": +"immer@npm:^9.0.6, immer@npm:^9.0.7": version: 9.0.21 resolution: "immer@npm:9.0.21" checksum: 70e3c274165995352f6936695f0ef4723c52c92c92dd0e9afdfe008175af39fa28e76aafb3a2ca9d57d1fb8f796efc4dd1e1cc36f18d33fa5b74f3dfb0375432 @@ -13410,7 +14079,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"influx@npm:^5.9.2, influx@npm:^5.9.3": +"influx@npm:^5.9.3": version: 5.9.3 resolution: "influx@npm:5.9.3" checksum: 93b174d4f6bd606f70af21fdda3f9d58fe6a6c193b6656897d6aab9494105eca116b976bffb80be7bec712c9bf413c40085767914f3c0dcc1155bb9b5efc1560 @@ -14791,6 +15460,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"jsep@npm:^1.1.2, jsep@npm:^1.2.0": + version: 1.3.8 + resolution: "jsep@npm:1.3.8" + checksum: d6de7f3bc3aa93e71b6a8fd5436db87efd11d7081230bf072c3359c5f9ff1e36dd01e4e09b09f10cacf35d5dbaf2f32ea5cf98ffe41717ea7bd489d580bbab83 + languageName: node + linkType: hard + "jsesc@npm:^2.5.1": version: 2.5.2 resolution: "jsesc@npm:2.5.2" @@ -14871,7 +15547,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"json-schema-to-typescript@npm:^10.0.0": +"json-schema-to-typescript@npm:^10.1.5": version: 10.1.5 resolution: "json-schema-to-typescript@npm:10.1.5" dependencies: @@ -14971,6 +15647,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"jsonc-parser@npm:~2.2.1": + version: 2.2.1 + resolution: "jsonc-parser@npm:2.2.1" + checksum: c113878b5edd4232ba0742c7e0ddefb22a2a8ef1aafa1674c0eb4c5df0be11ed02bc8288f52ebe44b1696de336e1bc06e7bbc1458d0f910540d72b57ee7c8084 + languageName: node + linkType: hard + "jsonfile@npm:^6.0.1": version: 6.1.0 resolution: "jsonfile@npm:6.1.0" @@ -14984,17 +15667,45 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"jsonfile@npm:~1.0.1": - version: 1.0.1 - resolution: "jsonfile@npm:1.0.1" - checksum: ea726b450794def0f9317dc56fbf00b0ed3e67abff73af119c263d2d4113e5d9ae686c8a9283c3baf244450077e670952082d6de36d360395961126b8c89cb96 +"jsonfile@npm:~1.0.1": + version: 1.0.1 + resolution: "jsonfile@npm:1.0.1" + checksum: ea726b450794def0f9317dc56fbf00b0ed3e67abff73af119c263d2d4113e5d9ae686c8a9283c3baf244450077e670952082d6de36d360395961126b8c89cb96 + languageName: node + linkType: hard + +"jsonparse@npm:^1.2.0, jsonparse@npm:^1.3.1": + version: 1.3.1 + resolution: "jsonparse@npm:1.3.1" + checksum: 6514a7be4674ebf407afca0eda3ba284b69b07f9958a8d3113ef1005f7ec610860c312be067e450c569aab8b89635e332cee3696789c750692bb60daba627f4d + languageName: node + linkType: hard + +"jsonpath-plus@npm:7.1.0": + version: 7.1.0 + resolution: "jsonpath-plus@npm:7.1.0" + checksum: a4005dc860c6b7e339229842537ceb6eb839d87a3447f989792b9c64f2564bbbd40663515f9481fb5a1b6cb0f988afba5b0b150e0285c463b794a45ed1aaf555 + languageName: node + linkType: hard + +"jsonpath-plus@npm:^6.0.1": + version: 6.0.1 + resolution: "jsonpath-plus@npm:6.0.1" + checksum: bddec34b742249c5b38077dfcd8eb479fab4e077943253017326503ce4f527ef66938288c728712fd923907493d6eaba69a43015dc3dd9fdf48d89028ae7f466 + languageName: node + linkType: hard + +"jsonpath-plus@npm:^7.2.0": + version: 7.2.0 + resolution: "jsonpath-plus@npm:7.2.0" + checksum: 05f447339d29be861e307d6e812aec1b9b88a3ba6bba286966a4e8bed3e752bee3d715eabfc21dce968be85ccb48bf79d2c1af78da7b9b74cd1b446d4d5d02f5 languageName: node linkType: hard -"jsonparse@npm:^1.2.0, jsonparse@npm:^1.3.1": - version: 1.3.1 - resolution: "jsonparse@npm:1.3.1" - checksum: 6514a7be4674ebf407afca0eda3ba284b69b07f9958a8d3113ef1005f7ec610860c312be067e450c569aab8b89635e332cee3696789c750692bb60daba627f4d +"jsonpointer@npm:^5.0.0": + version: 5.0.1 + resolution: "jsonpointer@npm:5.0.1" + checksum: 0b40f712900ad0c846681ea2db23b6684b9d5eedf55807b4708c656f5894b63507d0e28ae10aa1bddbea551241035afe62b6df0800fc94c2e2806a7f3adecd7c languageName: node linkType: hard @@ -15103,13 +15814,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"lerna@npm:^6.6.1": - version: 6.6.1 - resolution: "lerna@npm:6.6.1" +"lerna@npm:^6.6.2": + version: 6.6.2 + resolution: "lerna@npm:6.6.2" dependencies: - "@lerna/child-process": 6.6.1 - "@lerna/create": 6.6.1 - "@lerna/legacy-package-management": 6.6.1 + "@lerna/child-process": 6.6.2 + "@lerna/create": 6.6.2 + "@lerna/legacy-package-management": 6.6.2 "@npmcli/arborist": 6.2.3 "@npmcli/run-script": 4.1.7 "@nrwl/devkit": ">=15.5.2 < 16" @@ -15143,8 +15854,8 @@ asn1@evs-broadcast/node-asn1: is-ci: 2.0.0 is-stream: 2.0.0 js-yaml: ^4.1.0 - libnpmaccess: 6.0.3 - libnpmpublish: 6.0.4 + libnpmaccess: ^6.0.3 + libnpmpublish: 7.1.4 load-json-file: 6.2.0 make-dir: 3.1.0 minimatch: 3.0.5 @@ -15161,7 +15872,7 @@ asn1@evs-broadcast/node-asn1: p-queue: 6.6.2 p-reduce: 2.1.0 p-waterfall: 2.1.1 - pacote: 13.6.2 + pacote: 15.1.1 pify: 5.0.0 read-cmd-shim: 3.0.0 read-package-json: 5.0.1 @@ -15185,7 +15896,7 @@ asn1@evs-broadcast/node-asn1: yargs-parser: 20.2.4 bin: lerna: dist/cli.js - checksum: 67c7c0975f6dcc2cab8d2b7bd2ddb7c769f88ca55cae7f88153e03b3009c3f3eebc58fe8953b635e04c0cf807f1fa7020c7d272e9f84b1bf1eb8fde9ff701cca + checksum: ece77edd8ab1f1ddfe47095c9f812af6b65a58c8851b146ecba5d6a8ae1b316195e7968781cd15e57fa67895de5e60211600c6d8a987264f2f322b0f59ee6772 languageName: node linkType: hard @@ -15225,28 +15936,31 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"libnpmaccess@npm:6.0.3": - version: 6.0.3 - resolution: "libnpmaccess@npm:6.0.3" +"libnpmaccess@npm:^6.0.3": + version: 6.0.4 + resolution: "libnpmaccess@npm:6.0.4" dependencies: aproba: ^2.0.0 minipass: ^3.1.1 npm-package-arg: ^9.0.1 npm-registry-fetch: ^13.0.0 - checksum: 4a437390d52bd5e6145164210cfab4cdbc824c4f4a62e11cf186cad9c159a7c8f0c1b6e37346db1cc675bcdf1508e92ed64d47ac1a9bcf838a670bb4741a50c9 + checksum: 86130b435c67a03254489c3b3684d435260b609164f76bcc69adbee78652c36a64551228b2c5ddc2b16851e9e367ee0ba173a641406768397716faa006042322 languageName: node linkType: hard -"libnpmpublish@npm:6.0.4": - version: 6.0.4 - resolution: "libnpmpublish@npm:6.0.4" +"libnpmpublish@npm:7.1.4": + version: 7.1.4 + resolution: "libnpmpublish@npm:7.1.4" dependencies: - normalize-package-data: ^4.0.0 - npm-package-arg: ^9.0.1 - npm-registry-fetch: ^13.0.0 + ci-info: ^3.6.1 + normalize-package-data: ^5.0.0 + npm-package-arg: ^10.1.0 + npm-registry-fetch: ^14.0.3 + proc-log: ^3.0.0 semver: ^7.3.7 - ssri: ^9.0.0 - checksum: d653e0d9be0b01011c020f8252f480ca68105b56fde575a6c4fda650f6b5ff33a51fda43897ba817d2955579cc096910561e60e26628c59f5ac2d031157551d1 + sigstore: ^1.4.0 + ssri: ^10.0.1 + checksum: 334996850d0015b97e615f47cf13e4eb65c9d74b702da70031209a969a0cd99b6b8577dc153f6588843172f930fba71199bd9a71b4ac034ec94ede6d27acbbd6 languageName: node linkType: hard @@ -15357,19 +16071,19 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "live-status-gateway@workspace:live-status-gateway" dependencies: - "@asyncapi/generator": 1.9.13 + "@asyncapi/generator": 1.10.9 "@asyncapi/html-template": 0.26.0 - "@asyncapi/nodejs-ws-template": 0.9.25 + "@asyncapi/nodejs-ws-template": 0.9.33 "@sofie-automation/blueprints-integration": 1.51.0-in-development "@sofie-automation/corelib": 1.51.0-in-development "@sofie-automation/server-core-integration": 1.51.0-in-development "@sofie-automation/shared-lib": 1.51.0-in-development - debug: ^4.3.2 + debug: ^4.3.4 fast-clone: ^1.5.13 - influx: ^5.9.2 - tslib: ^2.4.0 - winston: ^3.8.1 - ws: ^8.10.0 + influx: ^5.9.3 + tslib: ^2.6.0 + winston: ^3.9.0 + ws: ^8.13.0 languageName: unknown linkType: soft @@ -15511,7 +16225,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"lodash.merge@npm:^4.6.2": +"lodash.merge@npm:4.6.2, lodash.merge@npm:^4.6.2": version: 4.6.2 resolution: "lodash.merge@npm:4.6.2" checksum: ad580b4bdbb7ca1f7abf7e1bce63a9a0b98e370cf40194b03380a46b4ed799c9573029599caebc1b14e3f24b111aef72b96674a56cfa105e0f5ac70546cdc005 @@ -15525,6 +16239,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"lodash.topath@npm:^4.5.2": + version: 4.5.2 + resolution: "lodash.topath@npm:4.5.2" + checksum: 04583e220f4bb1c4ac0008ff8f46d9cb4ddce0ea1090085790da30a41f4cb1b904d885cb73257fca619fa825cd96f9bb97c67d039635cb76056e18f5e08bfdee + languageName: node + linkType: hard + "lodash.uniq@npm:4.5.0, lodash.uniq@npm:^4.5.0": version: 4.5.0 resolution: "lodash.uniq@npm:4.5.0" @@ -15532,7 +16253,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"lodash@npm:4.17.21, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.7.0": +"lodash@npm:4.17.21, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.7.0, lodash@npm:~4.17.21": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7 @@ -15754,6 +16475,29 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"make-fetch-happen@npm:^11.1.1": + version: 11.1.1 + resolution: "make-fetch-happen@npm:11.1.1" + dependencies: + agentkeepalive: ^4.2.1 + cacache: ^17.0.0 + http-cache-semantics: ^4.1.1 + http-proxy-agent: ^5.0.0 + https-proxy-agent: ^5.0.0 + is-lambda: ^1.0.1 + lru-cache: ^7.7.1 + minipass: ^5.0.0 + minipass-fetch: ^3.0.0 + minipass-flush: ^1.0.5 + minipass-pipeline: ^1.2.4 + negotiator: ^0.6.3 + promise-retry: ^2.0.1 + socks-proxy-agent: ^7.0.0 + ssri: ^10.0.0 + checksum: 7268bf274a0f6dcf0343829489a4506603ff34bd0649c12058753900b0eb29191dce5dba12680719a5d0a983d3e57810f594a12f3c18494e93a1fbc6348a4540 + languageName: node + linkType: hard + "make-fetch-happen@npm:^9.0.1": version: 9.1.0 resolution: "make-fetch-happen@npm:9.1.0" @@ -16193,6 +16937,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"minimatch@npm:^9.0.0": + version: 9.0.2 + resolution: "minimatch@npm:9.0.2" + dependencies: + brace-expansion: ^2.0.1 + checksum: 2eb12e2047a062fdb973fb51b9803f2455e3a00977858c038d66646d303a5a15bbcbc6ed5a2fc403bc869b1309f829ed3acd881d3246faf044ea7a494974b924 + languageName: node + linkType: hard + "minimist-options@npm:4.1.0": version: 4.1.0 resolution: "minimist-options@npm:4.1.0" @@ -16411,12 +17164,12 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"mongodb@npm:^4.13.0": - version: 4.14.0 - resolution: "mongodb@npm:4.14.0" +"mongodb@npm:^4.16.0": + version: 4.16.0 + resolution: "mongodb@npm:4.16.0" dependencies: "@aws-sdk/credential-providers": ^3.186.0 - bson: ^4.7.0 + bson: ^4.7.2 mongodb-connection-string-url: ^2.5.4 saslprep: ^1.0.3 socks: ^2.7.1 @@ -16425,7 +17178,7 @@ asn1@evs-broadcast/node-asn1: optional: true saslprep: optional: true - checksum: ab3b8f27e99fab4ea0c163067158ad360a161edf9ebf74368b9b561da7b75d23c09282d530cb4e2fac32ea30eae29620e861d837d334ddbdfa50d57240c1bd8e + checksum: f0b1347739cc362b82b3aabc7e7d4d74bc7a344ed1bbafd6f92681bcab440f6cc618ffa0438d41d2789cb34818f3b09d4c78f517b42160ebae55bf2c96f13953 languageName: node linkType: hard @@ -16443,10 +17196,10 @@ asn1@evs-broadcast/node-asn1: "@mos-connection/connector": ^3.0.4 "@sofie-automation/server-core-integration": 1.51.0-in-development "@sofie-automation/shared-lib": 1.51.0-in-development - tslib: ^2.4.0 + tslib: ^2.6.0 type-fest: ^2.19.0 - underscore: ^1.13.4 - winston: ^3.8.2 + underscore: ^1.13.6 + winston: ^3.9.0 languageName: unknown linkType: soft @@ -16521,7 +17274,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"nanoid@npm:^3.3.4": +"nanoid@npm:^3.3.4, nanoid@npm:^3.3.6": version: 3.3.6 resolution: "nanoid@npm:3.3.6" bin: @@ -16588,6 +17341,25 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"nimma@npm:0.2.2": + version: 0.2.2 + resolution: "nimma@npm:0.2.2" + dependencies: + "@jsep-plugin/regex": ^1.0.1 + "@jsep-plugin/ternary": ^1.0.2 + astring: ^1.8.1 + jsep: ^1.2.0 + jsonpath-plus: ^6.0.1 + lodash.topath: ^4.5.2 + dependenciesMeta: + jsonpath-plus: + optional: true + lodash.topath: + optional: true + checksum: 09369253a962e6cdddd37c4994d414a5fa00abc955c4d91946140b45b57465749a9f05663a64812ad5ac70caacb7ca22a8fc7c8db002032d0768c83dbba7b3ad + languageName: node + linkType: hard + "no-case@npm:^3.0.4": version: 3.0.4 resolution: "no-case@npm:3.0.4" @@ -16910,15 +17682,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"npm-bundled@npm:^2.0.0": - version: 2.0.1 - resolution: "npm-bundled@npm:2.0.1" - dependencies: - npm-normalize-package-bin: ^2.0.0 - checksum: 7747293985c48c5268871efe691545b03731cb80029692000cbdb0b3344b9617be5187aa36281cabbe6b938e3651b4e87236d1c31f9e645eef391a1a779413e6 - languageName: node - linkType: hard - "npm-bundled@npm:^3.0.0": version: 3.0.0 resolution: "npm-bundled@npm:3.0.0" @@ -16937,15 +17700,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"npm-install-checks@npm:^5.0.0": - version: 5.0.0 - resolution: "npm-install-checks@npm:5.0.0" - dependencies: - semver: ^7.1.1 - checksum: 0e7d1aae52b1fe9d3a0fd4a008850c7047931722dd49ee908afd13fd0297ac5ddb10964d9c59afcdaaa2ca04b51d75af2788f668c729ae71fec0e4cdac590ffc - languageName: node - linkType: hard - "npm-install-checks@npm:^6.0.0": version: 6.1.0 resolution: "npm-install-checks@npm:6.1.0" @@ -17010,7 +17764,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"npm-package-arg@npm:^9.0.0, npm-package-arg@npm:^9.0.1": +"npm-package-arg@npm:^9.0.1": version: 9.1.2 resolution: "npm-package-arg@npm:9.1.2" dependencies: @@ -17050,20 +17804,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"npm-packlist@npm:^5.1.0": - version: 5.1.3 - resolution: "npm-packlist@npm:5.1.3" - dependencies: - glob: ^8.0.1 - ignore-walk: ^5.0.1 - npm-bundled: ^2.0.0 - npm-normalize-package-bin: ^2.0.0 - bin: - npm-packlist: bin/index.js - checksum: 94cc9c66740e8f80243301de85eb0a2cec5bbd570c3f26b6ad7af1a3eca155f7e810580dc7ea4448f12a8fd82f6db307e7132a5fe69e157eb45b325acadeb22a - languageName: node - linkType: hard - "npm-packlist@npm:^7.0.0": version: 7.0.4 resolution: "npm-packlist@npm:7.0.4" @@ -17085,18 +17825,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"npm-pick-manifest@npm:^7.0.0": - version: 7.0.2 - resolution: "npm-pick-manifest@npm:7.0.2" - dependencies: - npm-install-checks: ^5.0.0 - npm-normalize-package-bin: ^2.0.0 - npm-package-arg: ^9.0.0 - semver: ^7.3.5 - checksum: a93ec449c12219a2be8556837db9ac5332914f304a69469bb6f1f47717adc6e262aa318f79166f763512688abd9c4e4b6a2d83b2dd19753a7abe5f0360f2c8bc - languageName: node - linkType: hard - "npm-pick-manifest@npm:^8.0.0, npm-pick-manifest@npm:^8.0.1": version: 8.0.1 resolution: "npm-pick-manifest@npm:8.0.1" @@ -17138,7 +17866,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"npm-registry-fetch@npm:^13.0.0, npm-registry-fetch@npm:^13.0.1": +"npm-registry-fetch@npm:^13.0.0": version: 13.3.1 resolution: "npm-registry-fetch@npm:13.3.1" dependencies: @@ -17903,62 +18631,59 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "packages@workspace:." dependencies: - "@babel/core": ^7.21.3 - "@babel/plugin-transform-modules-commonjs": ^7.21.2 - "@sofie-automation/code-standard-preset": ~2.4.6 + "@babel/core": ^7.22.5 + "@babel/plugin-transform-modules-commonjs": ^7.22.5 + "@sofie-automation/code-standard-preset": ~2.4.7 "@types/amqplib": ^0.10.1 - "@types/debug": ^4.1.7 + "@types/debug": ^4.1.8 "@types/ejson": ^2.2.0 "@types/got": ^9.6.12 - "@types/jest": ^29.5.0 - "@types/node": ^14.18.12 + "@types/jest": ^29.5.2 + "@types/node": ^14.18.53 "@types/object-path": ^0.11.1 - "@types/underscore": ^1.11.4 + "@types/underscore": ^1.11.5 babel-jest: ^29.5.0 copyfiles: ^2.4.1 jest: ^29.5.0 - json-schema-to-typescript: ^10.0.0 - lerna: ^6.6.1 + json-schema-to-typescript: ^10.1.5 + lerna: ^6.6.2 nodemon: ^2.0.22 open-cli: ^7.2.0 pinst: ^3.0.0 rimraf: ^4.4.1 - semver: ^7.3.7 - ts-jest: ^29.0.5 - ts-node: ^10.8.0 + semver: ^7.5.3 + ts-jest: ^29.1.1 + ts-node: ^10.9.1 typedoc: ^0.23.28 typescript: ~4.9 languageName: unknown linkType: soft -"pacote@npm:13.6.2, pacote@npm:^13.6.1": - version: 13.6.2 - resolution: "pacote@npm:13.6.2" +"pacote@npm:15.1.1, pacote@npm:^15.0.0, pacote@npm:^15.0.8": + version: 15.1.1 + resolution: "pacote@npm:15.1.1" dependencies: - "@npmcli/git": ^3.0.0 - "@npmcli/installed-package-contents": ^1.0.7 - "@npmcli/promise-spawn": ^3.0.0 - "@npmcli/run-script": ^4.1.0 - cacache: ^16.0.0 - chownr: ^2.0.0 - fs-minipass: ^2.1.0 - infer-owner: ^1.0.4 - minipass: ^3.1.6 - mkdirp: ^1.0.4 - npm-package-arg: ^9.0.0 - npm-packlist: ^5.1.0 - npm-pick-manifest: ^7.0.0 - npm-registry-fetch: ^13.0.1 - proc-log: ^2.0.0 + "@npmcli/git": ^4.0.0 + "@npmcli/installed-package-contents": ^2.0.1 + "@npmcli/promise-spawn": ^6.0.1 + "@npmcli/run-script": ^6.0.0 + cacache: ^17.0.0 + fs-minipass: ^3.0.0 + minipass: ^4.0.0 + npm-package-arg: ^10.0.0 + npm-packlist: ^7.0.0 + npm-pick-manifest: ^8.0.0 + npm-registry-fetch: ^14.0.0 + proc-log: ^3.0.0 promise-retry: ^2.0.1 - read-package-json: ^5.0.0 - read-package-json-fast: ^2.0.3 - rimraf: ^3.0.2 - ssri: ^9.0.0 + read-package-json: ^6.0.0 + read-package-json-fast: ^3.0.0 + sigstore: ^1.0.0 + ssri: ^10.0.0 tar: ^6.1.11 bin: pacote: lib/bin.js - checksum: a7b7f97094ab570a23e1c174537e9953a4d53176cc4b18bac77d7728bd89e2b9fa331d0f78fa463add03df79668a918bbdaa2750819504ee39242063abf53c6e + checksum: 109388e873615cdad342f5dbd3639389c00aaac2c84b824dcb1a9460b4cf1c66264387b1d0200b1769abda7feca94165804d1308ca5e59904ae24d489d3bfb13 languageName: node linkType: hard @@ -17991,34 +18716,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"pacote@npm:^15.0.0, pacote@npm:^15.0.8": - version: 15.1.1 - resolution: "pacote@npm:15.1.1" - dependencies: - "@npmcli/git": ^4.0.0 - "@npmcli/installed-package-contents": ^2.0.1 - "@npmcli/promise-spawn": ^6.0.1 - "@npmcli/run-script": ^6.0.0 - cacache: ^17.0.0 - fs-minipass: ^3.0.0 - minipass: ^4.0.0 - npm-package-arg: ^10.0.0 - npm-packlist: ^7.0.0 - npm-pick-manifest: ^8.0.0 - npm-registry-fetch: ^14.0.0 - proc-log: ^3.0.0 - promise-retry: ^2.0.1 - read-package-json: ^6.0.0 - read-package-json-fast: ^3.0.0 - sigstore: ^1.0.0 - ssri: ^10.0.0 - tar: ^6.1.11 - bin: - pacote: lib/bin.js - checksum: 109388e873615cdad342f5dbd3639389c00aaac2c84b824dcb1a9460b4cf1c66264387b1d0200b1769abda7feca94165804d1308ca5e59904ae24d489d3bfb13 - languageName: node - linkType: hard - "param-case@npm:^3.0.4": version: 3.0.4 resolution: "param-case@npm:3.0.4" @@ -18419,15 +19116,22 @@ asn1@evs-broadcast/node-asn1: dependencies: "@sofie-automation/server-core-integration": 1.51.0-in-development "@sofie-automation/shared-lib": 1.51.0-in-development - debug: ^4.3.3 + debug: ^4.3.4 influx: ^5.9.3 timeline-state-resolver: 9.0.0-release50.5 - tslib: ^2.4.0 - underscore: ^1.13.4 - winston: ^3.8.2 + tslib: ^2.6.0 + underscore: ^1.13.6 + winston: ^3.9.0 languageName: unknown linkType: soft +"pony-cause@npm:^1.0.0": + version: 1.1.1 + resolution: "pony-cause@npm:1.1.1" + checksum: 5ff8878b808be48db801d52246a99d7e4789e52d20575ba504ede30c818fd85d38a033915e02c15fa9b6dce72448836dc1a47094acf8f1c21c4f04a4603b0cfb + languageName: node + linkType: hard + "postcss-calc@npm:^8.2.3": version: 8.2.4 resolution: "postcss-calc@npm:8.2.4" @@ -18960,7 +19664,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"prism-react-renderer@npm:^1.2.1, prism-react-renderer@npm:^1.3.1": +"prism-react-renderer@npm:^1.3.1, prism-react-renderer@npm:^1.3.5": version: 1.3.5 resolution: "prism-react-renderer@npm:1.3.5" peerDependencies: @@ -19306,7 +20010,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"ramldt2jsonschema@npm:^1.1.0": +"ramldt2jsonschema@npm:^1.2.3": version: 1.2.3 resolution: "ramldt2jsonschema@npm:1.2.3" dependencies: @@ -19414,7 +20118,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"react-dom@npm:^17.0.1, react-dom@npm:^17.0.2": +"react-dom@npm:^17.0.2": version: 17.0.2 resolution: "react-dom@npm:17.0.2" dependencies: @@ -20185,6 +20889,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"require-in-the-middle@npm:^7.0.1": + version: 7.1.1 + resolution: "require-in-the-middle@npm:7.1.1" + dependencies: + debug: ^4.1.1 + module-details-from-path: ^1.0.3 + resolve: ^1.22.1 + checksum: 00c7e28c271cefc219962b18c1803f6124c761238c1edbf588c68a7143bfeb5779df455d2f0791c2c1f6e841580c50080d4435156e72841fa0bc50c3f383d032 + languageName: node + linkType: hard + "require-like@npm:>= 0.1.1": version: 0.1.2 resolution: "require-like@npm:0.1.2" @@ -20468,6 +21183,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"safe-stable-stringify@npm:^1.1": + version: 1.1.1 + resolution: "safe-stable-stringify@npm:1.1.1" + checksum: e32a30720e8a2e3043b8b96733f015c1aa7a21a5a328074ce917b8afe4d26b4308c186c74fa92131e5f794b1efc63caa32defafceaa2981accaaedbc8b2c861c + languageName: node + linkType: hard + "safe-stable-stringify@npm:^2.3.1": version: 2.4.3 resolution: "safe-stable-stringify@npm:2.4.3" @@ -20628,7 +21350,16 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"semver@npm:7.x, semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.0": +"semver@npm:^6.0.0, semver@npm:^6.1.0, semver@npm:^6.1.1, semver@npm:^6.1.2, semver@npm:^6.2.0, semver@npm:^6.3.0": + version: 6.3.0 + resolution: "semver@npm:6.3.0" + bin: + semver: ./bin/semver.js + checksum: 1b26ecf6db9e8292dd90df4e781d91875c0dcc1b1909e70f5d12959a23c7eebb8f01ea581c00783bbee72ceeaad9505797c381756326073850dc36ed284b21b9 + languageName: node + linkType: hard + +"semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.0, semver@npm:^7.5.3": version: 7.5.3 resolution: "semver@npm:7.5.3" dependencies: @@ -20639,15 +21370,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"semver@npm:^6.0.0, semver@npm:^6.1.0, semver@npm:^6.1.1, semver@npm:^6.1.2, semver@npm:^6.2.0, semver@npm:^6.3.0": - version: 6.3.0 - resolution: "semver@npm:6.3.0" - bin: - semver: ./bin/semver.js - checksum: 1b26ecf6db9e8292dd90df4e781d91875c0dcc1b1909e70f5d12959a23c7eebb8f01ea581c00783bbee72ceeaad9505797c381756326073850dc36ed284b21b9 - languageName: node - linkType: hard - "semver@npm:~7.0.0": version: 7.0.0 resolution: "semver@npm:7.0.0" @@ -20759,13 +21481,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"set-cookie-serde@npm:^1.0.0": - version: 1.0.0 - resolution: "set-cookie-serde@npm:1.0.0" - checksum: 0ef01f2f54ecd72532f6c5ac131b7ed531d568b845ba73bf27a1017704acad96abc46e1bb1a5a5616bf3273acf4e735e09803738e0e1fa7e539ff9e5cc0fce40 - languageName: node - linkType: hard - "setimmediate@npm:^1.0.5": version: 1.0.5 resolution: "setimmediate@npm:1.0.5" @@ -20901,6 +21616,28 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"sigstore@npm:^1.4.0": + version: 1.7.0 + resolution: "sigstore@npm:1.7.0" + dependencies: + "@sigstore/protobuf-specs": ^0.1.0 + "@sigstore/tuf": ^1.0.1 + make-fetch-happen: ^11.0.1 + bin: + sigstore: bin/sigstore.js + checksum: d292e48a0150d4c31eb5d14dfd51638afde5af9f607c1c5a26dc49ea2ad43ac580afedb658eadff2afe1bd983afd99eb2ab318be8fbf6d2cabf370f6a356107e + languageName: node + linkType: hard + +"simple-eval@npm:1.0.0": + version: 1.0.0 + resolution: "simple-eval@npm:1.0.0" + dependencies: + jsep: ^1.1.2 + checksum: 0f0719ae3a84d4b9c19366dc03065b1fe9638c982ed3e9d44ba541d25e3454e99419e3239034974fd6c5074b79c119419168b8f343fef4da6d7e35227cfd1f87 + languageName: node + linkType: hard + "simple-git@npm:^3.3.0": version: 3.18.0 resolution: "simple-git@npm:3.18.0" @@ -21087,13 +21824,13 @@ asn1@evs-broadcast/node-asn1: dependencies: "@docusaurus/core": 2.0.0-beta.20 "@docusaurus/preset-classic": 2.0.0-beta.20 - "@mdx-js/react": ^1.6.21 + "@mdx-js/react": ^1.6.22 "@svgr/webpack": ^5.5.0 - clsx: ^1.1.1 + clsx: ^1.2.1 file-loader: ^6.2.0 - prism-react-renderer: ^1.2.1 - react: ^17.0.1 - react-dom: ^17.0.1 + prism-react-renderer: ^1.3.5 + react: ^17.0.2 + react-dom: ^17.0.2 url-loader: ^4.1.1 languageName: unknown linkType: soft @@ -21141,7 +21878,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"source-map-support@npm:^0.5.17, source-map-support@npm:^0.5.19, source-map-support@npm:~0.5.20": +"source-map-support@npm:^0.5.19, source-map-support@npm:~0.5.20": version: 0.5.21 resolution: "source-map-support@npm:0.5.21" dependencies: @@ -22053,7 +22790,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"threadedclass@npm:^1.1.2, threadedclass@npm:^1.2.1": +"threadedclass@npm:^1.2.1": version: 1.2.1 resolution: "threadedclass@npm:1.2.1" dependencies: @@ -22336,13 +23073,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"traverse@npm:^0.6.6": - version: 0.6.7 - resolution: "traverse@npm:0.6.7" - checksum: 21018085ab72f717991597e12e2b52446962ed59df591502e4d7e1a709bc0a989f7c3d451aa7d882666ad0634f1546d696c5edecda1f2fc228777df7bb529a1e - languageName: node - linkType: hard - "tree-kill@npm:^1.2.2": version: 1.2.2 resolution: "tree-kill@npm:1.2.2" @@ -22424,9 +23154,9 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"ts-jest@npm:^29.0.5": - version: 29.0.5 - resolution: "ts-jest@npm:29.0.5" +"ts-jest@npm:^29.1.1": + version: 29.1.1 + resolution: "ts-jest@npm:29.1.1" dependencies: bs-logger: 0.x fast-json-stable-stringify: 2.x @@ -22434,14 +23164,14 @@ asn1@evs-broadcast/node-asn1: json5: ^2.2.3 lodash.memoize: 4.x make-error: 1.x - semver: 7.x + semver: ^7.5.3 yargs-parser: ^21.0.1 peerDependencies: "@babel/core": ">=7.0.0-beta.0 <8" "@jest/types": ^29.0.0 babel-jest: ^29.0.0 jest: ^29.0.0 - typescript: ">=4.3" + typescript: ">=4.3 <6" peerDependenciesMeta: "@babel/core": optional: true @@ -22453,11 +23183,11 @@ asn1@evs-broadcast/node-asn1: optional: true bin: ts-jest: cli.js - checksum: f60f129c2287f4c963d9ee2677132496c5c5a5d39c27ad234199a1140c26318a7d5bda34890ab0e30636ec42a8de28f84487c09e9dcec639c9c67812b3a38373 + checksum: a8c9e284ed4f819526749f6e4dc6421ec666f20ab44d31b0f02b4ed979975f7580b18aea4813172d43e39b29464a71899f8893dd29b06b4a351a3af8ba47b402 languageName: node linkType: hard -"ts-node@npm:^10.8.0": +"ts-node@npm:^10.9.1": version: 10.9.1 resolution: "ts-node@npm:10.9.1" dependencies: @@ -22495,27 +23225,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"ts-node@npm:^9.1.1": - version: 9.1.1 - resolution: "ts-node@npm:9.1.1" - dependencies: - arg: ^4.1.0 - create-require: ^1.1.0 - diff: ^4.0.1 - make-error: ^1.1.1 - source-map-support: ^0.5.17 - yn: 3.1.1 - peerDependencies: - typescript: ">=2.7" - bin: - ts-node: dist/bin.js - ts-node-script: dist/bin-script.js - ts-node-transpile-only: dist/bin-transpile.js - ts-script: dist/bin-script-deprecated.js - checksum: 356e2647b8b1e6ab00380c0537fa569b63bd9b6f006cc40fd650f81fae1817bd8fecc075300036950d8f45c1d85b95be33cd1e48a1a424a7d86c3dbb42bf60e5 - languageName: node - linkType: hard - "tsconfig-paths@npm:^4.1.2": version: 4.1.2 resolution: "tsconfig-paths@npm:4.1.2" @@ -22541,7 +23250,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"tslib@npm:^1.11.1, tslib@npm:^1.13.0, tslib@npm:^1.8.1, tslib@npm:^1.9.0": +"tslib@npm:^1.11.1, tslib@npm:^1.13.0, tslib@npm:^1.14.1, tslib@npm:^1.8.1, tslib@npm:^1.9.0": version: 1.14.1 resolution: "tslib@npm:1.14.1" checksum: dbe628ef87f66691d5d2959b3e41b9ca0045c3ee3c7c7b906cc1e328b39f199bb1ad9e671c39025bd56122ac57dfbf7385a94843b1cc07c60a4db74795829acd @@ -22555,6 +23264,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"tslib@npm:^2.6.0": + version: 2.6.0 + resolution: "tslib@npm:2.6.0" + checksum: c01066038f950016a18106ddeca4649b4d76caa76ec5a31e2a26e10586a59fceb4ee45e96719bf6c715648e7c14085a81fee5c62f7e9ebee68e77a5396e5538f + languageName: node + linkType: hard + "tsutils@npm:^3.21.0": version: 3.21.0 resolution: "tsutils@npm:3.21.0" @@ -22576,6 +23292,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"tuf-js@npm:^1.1.7": + version: 1.1.7 + resolution: "tuf-js@npm:1.1.7" + dependencies: + "@tufjs/models": 1.0.4 + debug: ^4.3.4 + make-fetch-happen: ^11.1.1 + checksum: 089fc0dabe1fcaeca8b955b358b34272f23237ac9e074b5f983349eb44d9688fd137f28f493bbd8dfd865d1af4e76e0cc869d307eadd054d1b404914c3124ae5 + languageName: node + linkType: hard + "tunnel-agent@npm:^0.6.0": version: 0.6.0 resolution: "tunnel-agent@npm:0.6.0" @@ -22779,7 +23506,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"typescript@npm:^3 || ^4, typescript@npm:^4.2.2, typescript@npm:~4.9": +"typescript@npm:^3 || ^4, typescript@npm:^4.9.3, typescript@npm:~4.9": version: 4.9.5 resolution: "typescript@npm:4.9.5" bin: @@ -22789,7 +23516,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"typescript@patch:typescript@^3 || ^4#~builtin, typescript@patch:typescript@^4.2.2#~builtin, typescript@patch:typescript@~4.9#~builtin": +"typescript@patch:typescript@^3 || ^4#~builtin, typescript@patch:typescript@^4.9.3#~builtin, typescript@patch:typescript@~4.9#~builtin": version: 4.9.5 resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=23ec76" bin: @@ -22874,7 +23601,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"underscore@npm:^1.13.2, underscore@npm:^1.13.4, underscore@npm:^1.13.6": +"underscore@npm:^1.13.2, underscore@npm:^1.13.6": version: 1.13.6 resolution: "underscore@npm:1.13.6" checksum: d5cedd14a9d0d91dd38c1ce6169e4455bb931f0aaf354108e47bd46d3f2da7464d49b2171a5cf786d61963204a42d01ea1332a903b7342ad428deaafaf70ec36 @@ -23208,6 +23935,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"urijs@npm:^1.19.11": + version: 1.19.11 + resolution: "urijs@npm:1.19.11" + checksum: f9b95004560754d30fd7dbee44b47414d662dc9863f1cf5632a7c7983648df11d23c0be73b9b4f9554463b61d5b0a520b70df9e1ee963ebb4af02e6da2cc80f3 + languageName: node + linkType: hard + "url-loader@npm:^4.1.1": version: 4.1.1 resolution: "url-loader@npm:4.1.1" @@ -23489,7 +24223,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"vm2@npm:^3.9.14": +"vm2@npm:^3.9.19": version: 3.9.19 resolution: "vm2@npm:3.9.19" dependencies: @@ -23835,15 +24569,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"wget-improved@npm:^3.3.1": - version: 3.3.1 - resolution: "wget-improved@npm:3.3.1" +"wget-improved@npm:^3.4.0": + version: 3.4.0 + resolution: "wget-improved@npm:3.4.0" dependencies: minimist: 1.2.6 tunnel: 0.0.6 bin: nwget: bin/nwget - checksum: c5975bc93e8c262504e24297b705295a6b68f9eb34ea11241001e592dd0b4b6ebfe3df6d5001bcd7aa3658dd59fd55109775a26edad5033b76e90c6560a7bdd2 + checksum: b1e779499344c4f0037a62b0617fc757f9049900a1dc797382578c5c0996c2bfedb090fb2650db508c52b23de82aa08141a7ac2e4dafd0a0983deccc1560ee66 languageName: node linkType: hard @@ -24010,9 +24744,9 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"winston@npm:^3.8.1, winston@npm:^3.8.2": - version: 3.8.2 - resolution: "winston@npm:3.8.2" +"winston@npm:^3.9.0": + version: 3.9.0 + resolution: "winston@npm:3.9.0" dependencies: "@colors/colors": 1.5.0 "@dabh/diagnostics": ^2.0.2 @@ -24025,7 +24759,7 @@ asn1@evs-broadcast/node-asn1: stack-trace: 0.0.x triple-beam: ^1.3.0 winston-transport: ^4.5.0 - checksum: f7b901798b92ab9e93c850110bf6e98500e9a0e762b62dab410cf928b2a4145533dfa6d3d2b24f7bf0dc94b53808d5bd28aaaeff9a4b43b89ea4c798cce308ea + checksum: 410f82b7a502106e7d93e62cd21d7e9bcfd37884d0d95921b12526d2fe163e654ec9cd39e18f9884fad5cf6506a45d07bd2519c1dc9c88e82f0f12b2ce9fa510 languageName: node linkType: hard @@ -24168,7 +24902,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"ws@npm:8.13.0, ws@npm:^8.10.0, ws@npm:^8.13.0": +"ws@npm:8.13.0, ws@npm:^8.13.0": version: 8.13.0 resolution: "ws@npm:8.13.0" peerDependencies: @@ -24311,14 +25045,14 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"yaml-eslint-parser@npm:^1.1.0": - version: 1.2.0 - resolution: "yaml-eslint-parser@npm:1.2.0" +"yaml-eslint-parser@npm:^1.2.1": + version: 1.2.2 + resolution: "yaml-eslint-parser@npm:1.2.2" dependencies: eslint-visitor-keys: ^3.0.0 lodash: ^4.17.21 yaml: ^2.0.0 - checksum: 2e8a84b639991bdd0e4ad9984d96f3b6ae0d39852aa06e4fc594f5bfa2e68a64d7f8f07086e781bb6198310f8df7246354473d70dd294a07358a1d551a5ae9ee + checksum: 06e8b8e4f5624731e05618a11e42e4727f99ae75c7c9b1f9986ba020d194580d11fc660674320720e45c27e75bb0044954d95fa1a6b7d5f6f9250af1e43dbeec languageName: node linkType: hard diff --git a/yarn.lock b/yarn.lock index 2cc7aa1d0e..e9e8731d58 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14,6 +14,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.21.0": + version: 7.22.5 + resolution: "@babel/runtime@npm:7.22.5" + dependencies: + regenerator-runtime: ^0.13.11 + checksum: 12a50b7de2531beef38840d17af50c55a094253697600cee255311222390c68eed704829308d4fd305e1b3dfbce113272e428e9d9d45b1730e0fede997eaceb1 + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -398,11 +407,11 @@ __metadata: version: 0.0.0-use.local resolution: "automation-core@workspace:." dependencies: - concurrently: ^8.0.1 - lint-staged: ^13.2.2 + concurrently: ^8.2.0 + lint-staged: ^13.2.3 rimraf: ^4.4.1 - semver: ^7.3.8 - snyk-nodejs-lockfile-parser: ^1.48.2 + semver: ^7.5.3 + snyk-nodejs-lockfile-parser: ^1.52.1 languageName: unknown linkType: soft @@ -632,23 +641,23 @@ __metadata: languageName: node linkType: hard -"concurrently@npm:^8.0.1": - version: 8.0.1 - resolution: "concurrently@npm:8.0.1" +"concurrently@npm:^8.2.0": + version: 8.2.0 + resolution: "concurrently@npm:8.2.0" dependencies: chalk: ^4.1.2 - date-fns: ^2.29.3 + date-fns: ^2.30.0 lodash: ^4.17.21 - rxjs: ^7.8.0 - shell-quote: ^1.8.0 - spawn-command: 0.0.2-1 + rxjs: ^7.8.1 + shell-quote: ^1.8.1 + spawn-command: 0.0.2 supports-color: ^8.1.1 tree-kill: ^1.2.2 - yargs: ^17.7.1 + yargs: ^17.7.2 bin: conc: dist/bin/concurrently.js concurrently: dist/bin/concurrently.js - checksum: cce10ab1bbd7fd099300234637cdbc8e12622a5fe53f8dd31dcd9562d9803f25d3d0410bdb6fffd54e8f4e11b852366c03994bd319843cce70e449c8a8a69526 + checksum: eafe6a4d9b7fda87f55ea285cfc6acd937a5286ceec8991ab48e6cc27c45fce6a5c6f45e18d7555defa15dc7d7e8941bc5a9d1ceaf182e31441d420e00333434 languageName: node linkType: hard @@ -663,10 +672,12 @@ __metadata: languageName: node linkType: hard -"date-fns@npm:^2.29.3": - version: 2.29.3 - resolution: "date-fns@npm:2.29.3" - checksum: e01cf5b62af04e05dfff921bb9c9933310ed0e1ae9a81eb8653452e64dc841acf7f6e01e1a5ae5644d0337e9a7f936175fd2cb6819dc122fdd9c5e86c56be484 +"date-fns@npm:^2.30.0": + version: 2.30.0 + resolution: "date-fns@npm:2.30.0" + dependencies: + "@babel/runtime": ^7.21.0 + checksum: f7be01523282e9bb06c0cd2693d34f245247a29098527d4420628966a2d9aad154bd0e90a6b1cf66d37adcb769cd108cf8a7bd49d76db0fb119af5cdd13644f4 languageName: node linkType: hard @@ -1161,9 +1172,9 @@ __metadata: languageName: node linkType: hard -"lint-staged@npm:^13.2.2": - version: 13.2.2 - resolution: "lint-staged@npm:13.2.2" +"lint-staged@npm:^13.2.3": + version: 13.2.3 + resolution: "lint-staged@npm:13.2.3" dependencies: chalk: 5.2.0 cli-truncate: ^3.1.0 @@ -1180,7 +1191,7 @@ __metadata: yaml: ^2.2.2 bin: lint-staged: bin/lint-staged.js - checksum: f34f6e2e85e827364658ab8717bf8b35239473c2d4959d746b053a4cf158ac657348444c755820a8ef3eac2d4753a37c52e9db3e201ee20b085f26d2f2fbc9ed + checksum: ff51a1e33072f488b28b938ed47323816a1ff278ef6d0e5cbe1704b292773a6c8ce945b504eae3a9b5702917a979523a741f17023e16077bd5fa35be687cc067 languageName: node linkType: hard @@ -1745,6 +1756,13 @@ __metadata: languageName: node linkType: hard +"regenerator-runtime@npm:^0.13.11": + version: 0.13.11 + resolution: "regenerator-runtime@npm:0.13.11" + checksum: 27481628d22a1c4e3ff551096a683b424242a216fee44685467307f14d58020af1e19660bf2e26064de946bad7eff28950eae9f8209d55723e2d9351e632bbb4 + languageName: node + linkType: hard + "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -1821,6 +1839,15 @@ __metadata: languageName: node linkType: hard +"rxjs@npm:^7.8.1": + version: 7.8.1 + resolution: "rxjs@npm:7.8.1" + dependencies: + tslib: ^2.1.0 + checksum: de4b53db1063e618ec2eca0f7965d9137cabe98cf6be9272efe6c86b47c17b987383df8574861bcced18ebd590764125a901d5506082be84a8b8e364bf05f119 + languageName: node + linkType: hard + "safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" @@ -1835,7 +1862,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.0.0, semver@npm:^7.1.2, semver@npm:^7.3.5, semver@npm:^7.3.8": +"semver@npm:^7.0.0, semver@npm:^7.1.2, semver@npm:^7.3.5, semver@npm:^7.5.3": version: 7.5.3 resolution: "semver@npm:7.5.3" dependencies: @@ -1862,10 +1889,10 @@ __metadata: languageName: node linkType: hard -"shell-quote@npm:^1.8.0": - version: 1.8.0 - resolution: "shell-quote@npm:1.8.0" - checksum: 6ef7c5e308b9c77eedded882653a132214fa98b4a1512bb507588cf6cd2fc78bfee73e945d0c3211af028a1eabe09c6a19b96edd8977dc149810797e93809749 +"shell-quote@npm:^1.8.1": + version: 1.8.1 + resolution: "shell-quote@npm:1.8.1" + checksum: 5f01201f4ef504d4c6a9d0d283fa17075f6770bfbe4c5850b074974c68062f37929ca61700d95ad2ac8822e14e8c4b990ca0e6e9272e64befd74ce5e19f0736b languageName: node linkType: hard @@ -1927,9 +1954,9 @@ __metadata: languageName: node linkType: hard -"snyk-nodejs-lockfile-parser@npm:^1.48.2": - version: 1.48.2 - resolution: "snyk-nodejs-lockfile-parser@npm:1.48.2" +"snyk-nodejs-lockfile-parser@npm:^1.52.1": + version: 1.52.1 + resolution: "snyk-nodejs-lockfile-parser@npm:1.52.1" dependencies: "@snyk/dep-graph": ^2.3.0 "@snyk/graphlib": 2.1.9-patch.3 @@ -1942,20 +1969,21 @@ __metadata: lodash.isempty: ^4.4.0 lodash.topairs: ^4.3.0 micromatch: ^4.0.5 + p-map: ^4.0.0 semver: ^7.3.5 snyk-config: ^5.0.0 tslib: ^1.9.3 uuid: ^8.3.0 bin: parse-nodejs-lockfile: bin/index.js - checksum: bae0606d2c39f66c953c36d32c727a56aedabe8f6a81dfa0c2cee057ad86e27d0c7764f3a1cee247cc0903b3d63bcab3aadd6f98ec0758a08fe5f69275394196 + checksum: e7e1fca6182f7e11c8eca1ab39dd6fd3370cc3fe32d9bf8075ab0679959516193b9589e9c823937daa4984d721ca33f6c91aec8f7745837716a90bf20bdb9370 languageName: node linkType: hard -"spawn-command@npm:0.0.2-1": - version: 0.0.2-1 - resolution: "spawn-command@npm:0.0.2-1" - checksum: 2cac8519332193d1ed37d57298c4a1f73095e9edd20440fbab4aa47f531da83831734f2b51c44bb42b2747bf3485dec3fa2b0a1003f74c67561f2636622e328b +"spawn-command@npm:0.0.2": + version: 0.0.2 + resolution: "spawn-command@npm:0.0.2" + checksum: e35c5d28177b4d461d33c88cc11f6f3a5079e2b132c11e1746453bbb7a0c0b8a634f07541a2a234fa4758239d88203b758def509161b651e81958894c0b4b64b languageName: node linkType: hard @@ -2231,9 +2259,9 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^17.7.1": - version: 17.7.1 - resolution: "yargs@npm:17.7.1" +"yargs@npm:^17.7.2": + version: 17.7.2 + resolution: "yargs@npm:17.7.2" dependencies: cliui: ^8.0.1 escalade: ^3.1.1 @@ -2242,6 +2270,6 @@ __metadata: string-width: ^4.2.3 y18n: ^5.0.5 yargs-parser: ^21.1.1 - checksum: 3d8a43c336a4942bc68080768664aca85c7bd406f018bad362fd255c41c8f4e650277f42fd65d543fce99e084124ddafee7bbfc1a5c6a8fda4cec78609dcf8d4 + checksum: 73b572e863aa4a8cbef323dd911d79d193b772defd5a51aab0aca2d446655216f5002c42c5306033968193bdbf892a7a4c110b0d77954a7fdf563e653967b56a languageName: node linkType: hard From d47f534fdbc15c55a0ef865efb39265b7d1a9f66 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 3 Jul 2023 17:11:31 +0100 Subject: [PATCH 003/479] chore: fix test --- meteor/server/api/__tests__/rundownLayouts.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/meteor/server/api/__tests__/rundownLayouts.test.ts b/meteor/server/api/__tests__/rundownLayouts.test.ts index 58c48904c9..4c959a219c 100644 --- a/meteor/server/api/__tests__/rundownLayouts.test.ts +++ b/meteor/server/api/__tests__/rundownLayouts.test.ts @@ -116,13 +116,14 @@ describe('Rundown Layouts', () => { { // try not to send a request body - SupressLogMessages.suppressLogMessage(/Invalid request body/i) + SupressLogMessages.suppressLogMessage(/Missing request body/i) const ctx = await callKoaRoute(shelfLayoutsRouter, { method: 'POST', url: `/upload/${env.showStyleBaseId}`, headers: { 'content-type': 'application/json', }, + requestBody: null, }) expect(ctx.response.status).toBe(500) From d7dfb71d19405267cab5e2abc39794a80acb30b1 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 4 Jul 2023 09:52:27 +0100 Subject: [PATCH 004/479] feat: update meteor to 2.12 SOFIE-2368 (#931) MongoDB 6.0 is now recommended https://guide.meteor.com/2.11-migration.html --- .github/actions/setup-meteor/action.yaml | 2 +- .github/workflows/node.yaml | 8 + meteor/.meteor/packages | 1 + meteor/.meteor/release | 2 +- meteor/.meteor/versions | 86 +- meteor/Dockerfile | 2 +- meteor/__mocks__/meteor.ts | 49 +- meteor/client/lib/__tests__/rundown.test.ts | 6 +- .../lib/triggers/ActionAdLibHotkeyPreview.tsx | 5 +- meteor/client/ui/App.tsx | 2 +- .../TriggeredActionsEditor.tsx | 2 +- meteor/lib/ReactiveStore.ts | 2 +- meteor/lib/Rundown.ts | 15 +- meteor/lib/__tests__/lib.test.ts | 24 - meteor/lib/api/methods.ts | 5 +- meteor/lib/api/pubsub.ts | 5 +- .../triggers/actionFilterChainCompilers.ts | 4 +- meteor/lib/api/user.ts | 20 +- meteor/lib/collections/lib.ts | 3 +- meteor/lib/collections/rundownPlaylistUtil.ts | 2 +- meteor/lib/lib.ts | 33 - meteor/lib/memoizedIsolatedAutorun.ts | 2 +- meteor/lib/typings/meteor.d.ts | 54 +- meteor/package.json | 5 +- meteor/server/api/ExternalMessageQueue.ts | 2 +- .../server/api/blueprints/migrationContext.ts | 2 +- meteor/server/api/cleanup.ts | 2 +- .../deviceTriggers/RundownContentObserver.ts | 20 +- meteor/server/api/deviceTriggers/observer.ts | 2 +- .../deviceTriggers/reactiveContentCache.ts | 2 +- meteor/server/api/snapshot.ts | 2 +- meteor/server/api/user.ts | 6 +- meteor/server/collections/collection.ts | 23 +- .../implementations/asyncCollection.ts | 2 +- .../collections/implementations/base.ts | 7 +- .../collections/implementations/mock.ts | 11 +- meteor/server/lib/database.ts | 2 +- meteor/server/methods.ts | 5 +- meteor/server/migration/1_50_0.ts | 2 +- meteor/server/migration/lib.ts | 6 +- meteor/server/optimizations.ts | 2 +- meteor/server/publications/buckets.ts | 74 +- .../publications/deviceTriggersPreview.ts | 2 +- meteor/server/publications/lib.ts | 2 +- .../lib/ReactiveCacheCollection.ts | 100 +- meteor/server/publications/mountedTriggers.ts | 4 +- meteor/server/publications/organization.ts | 33 +- .../expectedPackages/contentObserver.ts | 6 +- .../server/publications/peripheralDevice.ts | 77 +- .../publications/peripheralDeviceForDevice.ts | 2 +- .../bucket/bucketContentCache.ts | 6 +- .../bucket/bucketContentObserver.ts | 4 +- .../rundown/reactiveContentCache.ts | 6 +- .../rundown/rundownContentObserver.ts | 18 +- meteor/server/publications/rundown.ts | 391 ++++--- meteor/server/publications/rundownPlaylist.ts | 40 +- .../rundownContentObserver.ts | 8 +- meteor/server/publications/showStyle.ts | 117 +- meteor/server/publications/showStyleUI.ts | 2 +- meteor/server/publications/studio.ts | 96 +- meteor/server/publications/system.ts | 8 +- meteor/server/publications/timeline.ts | 38 +- .../publications/translationsBundles.ts | 27 +- .../server/publications/triggeredActionsUI.ts | 8 +- meteor/server/security/lib/security.ts | 2 +- meteor/server/security/organization.ts | 2 +- meteor/server/security/peripheralDevice.ts | 2 +- meteor/server/security/rundown.ts | 7 +- meteor/server/security/showStyle.ts | 6 +- meteor/server/security/studio.ts | 2 +- meteor/server/webmanifest.ts | 2 +- meteor/server/worker/worker.ts | 2 +- meteor/tsconfig-base.json | 9 +- meteor/yarn.lock | 1041 +---------------- packages/blueprints-integration/package.json | 2 +- packages/corelib/package.json | 4 +- packages/corelib/src/mongo.ts | 3 +- packages/job-worker/package.json | 4 +- .../src/blueprints/context/adlibActions.ts | 2 - packages/job-worker/src/db/collection.ts | 8 +- packages/mos-gateway/package.json | 2 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 953 +-------------- 83 files changed, 910 insertions(+), 2651 deletions(-) diff --git a/.github/actions/setup-meteor/action.yaml b/.github/actions/setup-meteor/action.yaml index a3a1a2d571..a3a154bad7 100644 --- a/.github/actions/setup-meteor/action.yaml +++ b/.github/actions/setup-meteor/action.yaml @@ -3,7 +3,7 @@ description: "Setup Meteor" runs: using: "composite" steps: - - run: curl "https://install.meteor.com/?release=2.7.3" | sh + - run: curl "https://install.meteor.com/?release=2.12" | sh shell: bash - run: meteor npm install -g yarn shell: bash diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index 430b580d50..1ea16f95d0 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -40,6 +40,10 @@ jobs: - name: Run typecheck and linter run: | cd meteor + + # setup zodern:types. No linters are setup, so this simply installs the packages + meteor lint + meteor yarn ci:lint env: CI: true @@ -73,6 +77,10 @@ jobs: - name: Run Tests run: | cd meteor + + # setup zodern:types. No linters are setup, so this simply installs the packages + meteor lint + NODE_OPTIONS="--max-old-space-size=6144" meteor yarn unitci --force-exit env: CI: true diff --git a/meteor/.meteor/packages b/meteor/.meteor/packages index 4b4a9b98e8..d51d407720 100644 --- a/meteor/.meteor/packages +++ b/meteor/.meteor/packages @@ -33,3 +33,4 @@ ostrio:meteor-root accounts-password@2.3.1 julusian:meteor-elastic-apm@2.5.2 +zodern:types diff --git a/meteor/.meteor/release b/meteor/.meteor/release index 66dd7b6647..e8cfc7ec4c 100644 --- a/meteor/.meteor/release +++ b/meteor/.meteor/release @@ -1 +1 @@ -METEOR@2.7.3 +METEOR@2.12 diff --git a/meteor/.meteor/versions b/meteor/.meteor/versions index af512132e1..acc08f6d86 100644 --- a/meteor/.meteor/versions +++ b/meteor/.meteor/versions @@ -1,8 +1,8 @@ -accounts-base@2.2.3 -accounts-password@2.3.1 +accounts-base@2.2.8 +accounts-password@2.3.4 allow-deny@1.1.1 autoupdate@1.8.0 -babel-compiler@7.9.0 +babel-compiler@7.10.4 babel-runtime@1.5.1 base64@1.0.12 binary-heap@1.0.11 @@ -10,25 +10,25 @@ blaze-tools@1.1.3 boilerplate-generator@1.7.1 caching-compiler@1.2.2 caching-html-compiler@1.2.1 -callback-hook@1.4.0 -check@1.3.1 -ddp@1.4.0 -ddp-client@2.5.0 +callback-hook@1.5.1 +check@1.3.2 +ddp@1.4.1 +ddp-client@2.6.1 ddp-common@1.4.0 -ddp-rate-limiter@1.1.0 -ddp-server@2.5.0 -diff-sequence@1.1.1 -dynamic-import@0.7.2 -ecmascript@0.16.2 -ecmascript-runtime@0.8.0 +ddp-rate-limiter@1.2.0 +ddp-server@2.6.1 +diff-sequence@1.1.2 +dynamic-import@0.7.3 +ecmascript@0.16.7 +ecmascript-runtime@0.8.1 ecmascript-runtime-client@0.12.1 ecmascript-runtime-server@0.11.0 -ejson@1.1.2 -email@2.2.1 +ejson@1.1.3 +email@2.2.5 es5-shim@4.8.0 -fetch@0.1.1 +fetch@0.1.3 fourseven:scss@4.15.0 -geojson-utils@1.0.10 +geojson-utils@1.0.11 hot-code-push@1.0.4 html-tools@1.1.3 htmljs@1.1.1 @@ -38,47 +38,47 @@ julusian:meteor-elastic-apm@2.5.2 kschingiz:meteor-measured@1.0.3 launch-screen@1.3.0 localstorage@1.2.0 -logging@1.3.1 -meteor@1.10.0 +logging@1.3.2 +meteor@1.11.2 meteor-base@1.5.1 -minifier-css@1.6.0 -minifier-js@2.7.4 -minimongo@1.8.0 +minifier-css@1.6.4 +minifier-js@2.7.5 +minimongo@1.9.3 mobile-experience@1.1.0 mobile-status-bar@1.1.0 -modern-browsers@0.1.8 -modules@0.18.0 -modules-runtime@0.13.0 -mongo@1.15.0 +modern-browsers@0.1.9 +modules@0.19.0 +modules-runtime@0.13.1 +mongo@1.16.6 mongo-decimal@0.1.3 mongo-dev-server@1.1.0 mongo-id@1.0.8 -npm-mongo@4.3.1 +npm-mongo@4.16.0 ordered-dict@1.1.0 ostrio:meteor-root@1.1.1 -promise@0.12.0 -random@1.2.0 -rate-limit@1.0.9 -react-fast-refresh@0.2.3 +promise@0.12.2 +random@1.2.1 +rate-limit@1.1.1 +react-fast-refresh@0.2.7 react-meteor-data@2.5.1 -reactive-dict@1.3.0 -reactive-var@1.0.11 +reactive-dict@1.3.1 +reactive-var@1.0.12 reload@1.3.1 retry@1.1.0 routepolicy@1.1.1 -service-configuration@1.3.0 -session@1.2.0 +session@1.2.1 sha@1.0.9 shell-server@0.5.0 -socket-stream-client@0.5.0 +socket-stream-client@0.5.1 spacebars-compiler@1.3.1 -standard-minifier-css@1.8.1 -standard-minifier-js@2.8.0 +standard-minifier-css@1.9.2 +standard-minifier-js@2.8.1 static-html@1.3.2 templating-tools@1.2.2 -tracker@1.2.0 -typescript@4.5.4 -underscore@1.0.10 +tracker@1.3.2 +typescript@4.9.4 +underscore@1.0.13 url@1.3.2 -webapp@1.13.1 -webapp-hashing@1.1.0 +webapp@1.13.5 +webapp-hashing@1.1.1 +zodern:types@1.0.9 diff --git a/meteor/Dockerfile b/meteor/Dockerfile index eaccd9d35f..d42eed41eb 100644 --- a/meteor/Dockerfile +++ b/meteor/Dockerfile @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:experimental # BUILD IMAGE FROM node:14 -RUN curl "https://install.meteor.com/?release=2.7.3" | sh +RUN curl "https://install.meteor.com/?release=2.12" | sh # Temporary change the NODE_ENV env variable, so that all libraries are installed: ENV NODE_ENV_TMP $NODE_ENV diff --git a/meteor/__mocks__/meteor.ts b/meteor/__mocks__/meteor.ts index 9eef515b57..7dcdce5030 100644 --- a/meteor/__mocks__/meteor.ts +++ b/meteor/__mocks__/meteor.ts @@ -1,5 +1,7 @@ -import { stringifyError } from '@sofie-automation/corelib/dist/lib' +import { getRandomString, stringifyError } from '@sofie-automation/corelib/dist/lib' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' import * as _ from 'underscore' +import type { MethodContext } from '../lib/api/methods' import { Fiber } from './Fibers' import { MongoMock } from './mongo' @@ -117,11 +119,26 @@ export namespace MeteorMock { export function userId(): string | undefined { return mockUser ? mockUser._id : undefined } - function getMethodContext() { + function getMethodContext(): MethodContext { return { - userId: mockUser ? mockUser._id : undefined, + userId: protectString(mockUser?._id) ?? null, connection: { + id: getRandomString(), clientAddress: '1.1.1.1', + close: () => { + /* no-op */ + }, + onClose: () => { + /* no-op */ + }, + httpHeaders: {}, + }, + isSimulation: false, + setUserId: () => { + /* no-op */ + }, + unblock: () => { + /* no-op */ }, } } @@ -194,6 +211,17 @@ export namespace MeteorMock { return waitForPromiseLocal(Promise.resolve(fcn.call(getMethodContext(), ...args))) } } + export async function callAsync(methodName: string, ...args: any[]): Promise { + const fcn: Function = mockMethods[methodName] + if (!fcn) { + console.log(methodName) + console.log(mockMethods) + console.log(new Error(1).stack) + throw new Error(404, `Method '${methodName}' not found`) + } + + return waitForPromiseLocal(Promise.resolve(fcn.call(getMethodContext(), ...args))) + } export function apply( methodName: string, args: any[], @@ -210,6 +238,21 @@ export namespace MeteorMock { // but it'll do for now: call(methodName, ...args, asyncCallback) } + export function applyAsync( + methodName: string, + args: any[], + _options?: { + wait?: boolean + onResultReceived?: Function + returnStubValue?: boolean + throwStubExceptions?: boolean + } + ): any { + // ? + // This is a bad mock, since it doesn't support any of the options.. + // but it'll do for now: + return callAsync(methodName, ...args) + } export function absoluteUrl(path?: string): string { return path + '' // todo } diff --git a/meteor/client/lib/__tests__/rundown.test.ts b/meteor/client/lib/__tests__/rundown.test.ts index 7e2ef52688..e0de86a68a 100644 --- a/meteor/client/lib/__tests__/rundown.test.ts +++ b/meteor/client/lib/__tests__/rundown.test.ts @@ -9,7 +9,7 @@ import { import { RundownUtils } from '../rundown' import { Piece } from '../../../lib/collections/Pieces' import { defaultPartInstance, defaultPiece, defaultPieceInstance } from '../../../__mocks__/defaultCollectionObjects' -import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { PieceLifespan } from '@sofie-automation/blueprints-integration' import { PartInstance } from '../../../lib/collections/PartInstances' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' @@ -245,7 +245,7 @@ describe('client/lib/rundown', () => { const outputLayerIds = Object.keys(showStyleBase.outputLayers) const playlistActivationId = protectString('mock_activation_0') - mockRundownPlaylistsCollection.update(playlistId, { + mockRundownPlaylistsCollection.update(unprotectString(playlistId), { $set: { activationId: playlistActivationId, }, @@ -339,7 +339,7 @@ describe('client/lib/rundown', () => { mockPieceInstancesCollection.insert(followingPieceInstance) - mockRundownPlaylistsCollection.update(playlistId, { + mockRundownPlaylistsCollection.update(unprotectString(playlistId), { $set: { currentPartInfo: { partInstanceId: mockCurrentPartInstance._id, diff --git a/meteor/client/lib/triggers/ActionAdLibHotkeyPreview.tsx b/meteor/client/lib/triggers/ActionAdLibHotkeyPreview.tsx index 5dbcc79f00..1a8f3c2144 100644 --- a/meteor/client/lib/triggers/ActionAdLibHotkeyPreview.tsx +++ b/meteor/client/lib/triggers/ActionAdLibHotkeyPreview.tsx @@ -1,14 +1,13 @@ -import { Mongo } from 'meteor/mongo' import { ISourceLayer } from '@sofie-automation/blueprints-integration' import React, { useContext, useState, useEffect } from 'react' import { assertNever } from '../../../lib/lib' -import { MongoQuery } from '../../../lib/typings/meteor' import { useTracker } from '../ReactMeteorData/ReactMeteorData' import { SorensenContext } from '../SorensenContext' import { MountedAdLibTriggers } from './TriggersHandler' import { codesToKeyLabels } from './codesToKeyLabels' import { AdLibActionId, PieceId, RundownBaselineAdLibActionId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { MountedAdLibTrigger, MountedHotkeyMixin } from '../../../lib/api/triggers/MountedTriggers' +import { FindOptions, MongoQuery } from '@sofie-automation/corelib/dist/mongo' type IProps = | { @@ -53,7 +52,7 @@ export const ActionAdLibHotkeyPreview: React.FC = function AdLibActionHo } }, [Sorensen]) - const findOptions: Mongo.Options = { + const findOptions: FindOptions = { fields: { keys: 1, }, diff --git a/meteor/client/ui/App.tsx b/meteor/client/ui/App.tsx index bf008d2d0c..374af83ded 100644 --- a/meteor/client/ui/App.tsx +++ b/meteor/client/ui/App.tsx @@ -83,7 +83,7 @@ export const App: React.FC = function App() { const [requestedRoute, setRequestedRoute] = useState() const userReady = useSubscription(PubSub.loggedInUser) - const orgReady = useSubscription(PubSub.organization, { _id: user?.organizationId ?? protectString('__never') }) + const orgReady = useSubscription(PubSub.organization, user?.organizationId ?? null) const subsReady = userReady && orgReady diff --git a/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx b/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx index 9d331d6ac7..69072a3110 100644 --- a/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx +++ b/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx @@ -24,7 +24,7 @@ import { fetchFrom } from '../../../../lib/lib' import { NotificationCenter, Notification, NoticeLevel } from '../../../../../lib/notifications/notifications' import { Meteor } from 'meteor/meteor' import { doModalDialog } from '../../../../lib/ModalDialog' -import { MongoQuery } from '../../../../../lib/typings/meteor' +import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import _ from 'underscore' import { PartId, RundownId, ShowStyleBaseId, TriggeredActionId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PartInstances, Parts, RundownPlaylists, Rundowns, TriggeredActions } from '../../../../collections' diff --git a/meteor/lib/ReactiveStore.ts b/meteor/lib/ReactiveStore.ts index 168cb7b205..005b35ab20 100644 --- a/meteor/lib/ReactiveStore.ts +++ b/meteor/lib/ReactiveStore.ts @@ -70,7 +70,7 @@ export class ReactiveStore | string, Value> { } } - if (Tracker.active) { + if (Tracker.active && Tracker.currentComputation) { Tracker.currentComputation.onStop(() => { // Called when the reactive context of the caller of this.getValue is invalidated. diff --git a/meteor/lib/Rundown.ts b/meteor/lib/Rundown.ts index 14cbd14bbf..aa77f31ed9 100644 --- a/meteor/lib/Rundown.ts +++ b/meteor/lib/Rundown.ts @@ -11,13 +11,12 @@ import { buildPastInfinitePiecesForThisPartQuery, } from '@sofie-automation/corelib/dist/playout/infinites' import { PieceInstanceWithTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' -import { MongoQuery } from './typings/meteor' import { invalidateAfter } from '../lib/invalidatingTime' -import { convertCorelibToMeteorMongoQuery, getCurrentTime, groupByToMap, ProtectedString, protectString } from './lib' +import { getCurrentTime, groupByToMap, ProtectedString, protectString } from './lib' import { RundownPlaylist } from './collections/RundownPlaylists' import { Rundown } from './collections/Rundowns' import { isTranslatableMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' -import { mongoWhereFilter } from '@sofie-automation/corelib/dist/mongo' +import { mongoWhereFilter, MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { FindOptions } from './collections/lib' import { PartId, @@ -105,7 +104,7 @@ export function fetchPiecesThatMayBeActiveForPart( // Fast-path: if we already have the pieces, we can use them directly: piecesStartingInPart = mongoWhereFilter(allPieces, selector) } else { - piecesStartingInPart = Pieces.find(convertCorelibToMeteorMongoQuery(selector)).fetch() + piecesStartingInPart = Pieces.find(selector).fetch() } const partsBeforeThisInSegment = Array.from(partsBeforeThisInSegmentSet.values()) @@ -122,9 +121,7 @@ export function fetchPiecesThatMayBeActiveForPart( // Fast-path: if we already have the pieces, we can use them directly: infinitePieces = infinitePieceQuery ? mongoWhereFilter(allPieces, infinitePieceQuery) : [] } else { - infinitePieces = infinitePieceQuery - ? Pieces.find(convertCorelibToMeteorMongoQuery(infinitePieceQuery)).fetch() - : [] + infinitePieces = infinitePieceQuery ? Pieces.find(infinitePieceQuery).fetch() : [] } return piecesStartingInPart.concat(infinitePieces) // replace spread with concat, as 3x is faster (https://stackoverflow.com/questions/48865710/spread-operator-vs-array-concat) @@ -247,8 +244,8 @@ export function getPieceInstancesForPartInstance( * * @export * @param {RundownPlaylist} playlist - * @param {(MongoQuery | Mongo.QueryWithModifiers)} [segmentsQuery] - * @param {(MongoQuery | Mongo.QueryWithModifiers)} [partsQuery] + * @param {(MongoQuery)} [segmentsQuery] + * @param {(MongoQuery)} [partsQuery] * @param {MongoQuery} [partInstancesQuery] * @param {FindOptions} [segmentsOptions] * @param {FindOptions} [partsOptions] diff --git a/meteor/lib/__tests__/lib.test.ts b/meteor/lib/__tests__/lib.test.ts index cf75a51335..03b53c2453 100644 --- a/meteor/lib/__tests__/lib.test.ts +++ b/meteor/lib/__tests__/lib.test.ts @@ -15,11 +15,8 @@ import { equivalentArrays, LogLevel, makePromise, - MeteorPromiseApply, } from '../lib' import { MeteorMock } from '../../__mocks__/meteor' -import { MeteorDebugMethods } from '../../server/methods' -import { Settings } from '../Settings' // require('../../../../../server/api/ingest/mosDevice/api.ts') // include in order to create the Meteor methods needed @@ -28,27 +25,6 @@ describe('lib/lib', () => { MeteorMock.mockSetServerEnvironment() }) - testInFiber('MeteorPromiseApply', async () => { - // set up method: - Settings.enableUserAccounts = false - MeteorDebugMethods({ - myMethod: async (value1: string, value2: string) => { - // Do an async operation, to ensure that asynchronous operations work: - const v = await new Promise((resolve) => { - setTimeout(() => { - resolve(value1 + value2) - }, 10) - }) - return v - }, - }) - const pValue: any = MeteorPromiseApply('myMethod', ['myValue', 'AAA']).catch((e) => { - throw e - }) - expect(pValue).toHaveProperty('then') // be a promise - const value = await pValue - expect(value).toEqual('myValueAAA') - }) testInFiber('getCurrentTime', () => { systemTime.diff = 5439 MeteorMock.mockSetClientEnvironment() diff --git a/meteor/lib/api/methods.ts b/meteor/lib/api/methods.ts index f6aa1ced79..57e221d7d6 100644 --- a/meteor/lib/api/methods.ts +++ b/meteor/lib/api/methods.ts @@ -1,5 +1,4 @@ import * as _ from 'underscore' -import { MeteorPromiseApply } from '../lib' import { NewBlueprintAPI, BlueprintAPIMethods } from './blueprint' import { NewClientAPI, ClientAPIMethods } from './client' import { NewExternalMessageQueueAPI, ExternalMessageQueueAPIMethods } from './ExternalMessageQueue' @@ -74,11 +73,11 @@ function makeMethods( _.each(methods, (serverMethodName: any, methodName: string) => { if (listOfMethodsThatShouldNotRetry?.includes(methodName)) { resultingMethods[methodName] = async (...args) => - MeteorPromiseApply(serverMethodName, args, { + Meteor.applyAsync(serverMethodName, args, { noRetry: true, }) } else { - resultingMethods[methodName] = async (...args) => MeteorPromiseApply(serverMethodName, args) + resultingMethods[methodName] = async (...args) => Meteor.applyAsync(serverMethodName, args) } }) return resultingMethods diff --git a/meteor/lib/api/pubsub.ts b/meteor/lib/api/pubsub.ts index ca0369dcd4..5b6dd9b97c 100644 --- a/meteor/lib/api/pubsub.ts +++ b/meteor/lib/api/pubsub.ts @@ -1,6 +1,7 @@ import { IngestDataCacheObj } from '@sofie-automation/corelib/dist/dataModel/IngestDataCache' import { BucketId, + OrganizationId, PeripheralDeviceId, RundownId, RundownPlaylistActivationId, @@ -48,7 +49,6 @@ import { TranslationsBundle } from '../collections/TranslationsBundles' import { DBTriggeredActions, UITriggeredActionsObj } from '../collections/TriggeredActions' import { UserActionsLogItem } from '../collections/UserActionsLog' import { DBUser } from '../collections/Users' -import { MongoQuery } from '../typings/meteor' import { UIBucketContentStatus, UIPieceContentStatus, UISegmentPartNote } from './rundownNotifications' import { UIShowStyleBase } from './showStyles' import { UIStudio } from './studios' @@ -60,6 +60,7 @@ import { PackageManagerPackageContainers, PackageManagerPlayoutContext, } from '@sofie-automation/shared-lib/dist/package-manager/publications' +import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' /** * Ids of possible DDP subscriptions @@ -205,7 +206,7 @@ export interface PubSubTypes { [PubSub.rundownLayouts]: (selector: MongoQuery, token?: string) => RundownLayoutBase [PubSub.loggedInUser]: (token?: string) => DBUser [PubSub.usersInOrganization]: (selector: MongoQuery, token?: string) => DBUser - [PubSub.organization]: (selector: MongoQuery, token?: string) => DBOrganization + [PubSub.organization]: (organizationId: OrganizationId | null, token?: string) => DBOrganization [PubSub.buckets]: (studioId: StudioId, bucketId: BucketId | null, token?: string) => Bucket [PubSub.bucketAdLibPieces]: (selector: MongoQuery, token?: string) => BucketAdLib [PubSub.bucketAdLibActions]: (selector: MongoQuery, token?: string) => BucketAdLibAction diff --git a/meteor/lib/api/triggers/actionFilterChainCompilers.ts b/meteor/lib/api/triggers/actionFilterChainCompilers.ts index 43e010d0ce..67941c48ca 100644 --- a/meteor/lib/api/triggers/actionFilterChainCompilers.ts +++ b/meteor/lib/api/triggers/actionFilterChainCompilers.ts @@ -16,7 +16,7 @@ import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataMod import { DBRundownPlaylist, RundownPlaylist } from '../../collections/RundownPlaylists' import { SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { assertNever, generateTranslation } from '../../lib' -import { MongoQuery } from '../../typings/meteor' +import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { DBRundown } from '../../collections/Rundowns' import { DBSegment } from '../../collections/Segments' import { sortAdlibs } from '../../Rundown' @@ -788,7 +788,7 @@ export function rundownPlaylistFilter( case 'studioId': selector['$and']?.push({ studioId: { - $regex: link.value, + $regex: link.value as any, }, }) break diff --git a/meteor/lib/api/user.ts b/meteor/lib/api/user.ts index 5347409882..053ce718e2 100644 --- a/meteor/lib/api/user.ts +++ b/meteor/lib/api/user.ts @@ -1,7 +1,6 @@ import { Accounts } from 'meteor/accounts-base' import { UserProfile } from '../../lib/collections/Users' import { protectString } from '../lib' -import { DBOrganizationBase } from '../collections/Organization' import { UserId } from '@sofie-automation/corelib/dist/dataModel/Ids' export interface NewUserAPI { @@ -15,22 +14,21 @@ export enum UserAPIMethods { 'removeUser' = 'user.removeUser', } -interface NewUser { +export interface CreateNewUserData { email: string profile: UserProfile password?: string - createOrganization?: DBOrganizationBase + createOrganization?: { + name: string + applications: string[] + broadcastMediums: string[] + } } -export async function createUser(newUser: NewUser): Promise { +export async function createUser(newUser: CreateNewUserData): Promise { // This is available both client-side and server side. // The reason for that is that the client-side should use Accounts.createUser right away // so that the password aren't sent in "plaintext" to the server. - return new Promise((resolve, reject) => { - const userId = Accounts.createUser(newUser, (error) => { - if (error) reject(error) - else if (!userId) reject(new Error('Missing UserId')) - else resolve(protectString(userId)) - }) - }) + const userId = await Accounts.createUserAsync(newUser) + return protectString(userId) } diff --git a/meteor/lib/collections/lib.ts b/meteor/lib/collections/lib.ts index 64f8591531..0361564e85 100644 --- a/meteor/lib/collections/lib.ts +++ b/meteor/lib/collections/lib.ts @@ -1,10 +1,9 @@ import { Meteor } from 'meteor/meteor' import { Mongo } from 'meteor/mongo' -import { MongoModifier, MongoQuery } from '../typings/meteor' import { ProtectedString, protectString } from '../lib' import type { Collection as RawCollection, Db as RawDb } from 'mongodb' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' -import { MongoFieldSpecifier, SortSpecifier } from '@sofie-automation/corelib/dist/mongo' +import { MongoFieldSpecifier, MongoModifier, MongoQuery, SortSpecifier } from '@sofie-automation/corelib/dist/mongo' import { CustomCollectionName, CustomCollectionType } from '../api/pubsub' export const ClientCollections = new Map | WrappedMongoReadOnlyCollection>() diff --git a/meteor/lib/collections/rundownPlaylistUtil.ts b/meteor/lib/collections/rundownPlaylistUtil.ts index 1982851d9e..8b6146daf6 100644 --- a/meteor/lib/collections/rundownPlaylistUtil.ts +++ b/meteor/lib/collections/rundownPlaylistUtil.ts @@ -18,7 +18,7 @@ import { PartInstance } from './PartInstances' import { Part } from './Parts' import { RundownPlaylist } from './RundownPlaylists' import { Segment } from './Segments' -import { MongoQuery } from '../typings/meteor' +import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { Piece } from './Pieces' /** diff --git a/meteor/lib/lib.ts b/meteor/lib/lib.ts index 60badde4cf..b740f61c61 100644 --- a/meteor/lib/lib.ts +++ b/meteor/lib/lib.ts @@ -3,8 +3,6 @@ import { ITranslatableMessage } from '@sofie-automation/corelib/dist/Translatabl import { Meteor } from 'meteor/meteor' import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' import { logger } from './logging' -import { MongoQuery } from './typings/meteor' -import { MongoQuery as CoreLibMongoQuery } from '@sofie-automation/corelib/dist/mongo' import { Time, TimeDuration } from '@sofie-automation/shared-lib/dist/lib/lib' import { stringifyError } from '@sofie-automation/corelib/dist/lib' @@ -22,26 +20,6 @@ type PromisifyFunction = T extends (...args: any) => any ? (...args: Parameters) => Promise> | ReturnType : T -/** - * Convenience method to convert a Meteor.apply() into a Promise - * @param callName {string} Method name - * @param args {Array} An array of arguments for the method call - * @param options (Optional) An object with options for the call. See Meteor documentation. - * @returns {Promise} A promise containing the result of the called method. - */ -export async function MeteorPromiseApply( - callName: Parameters[0], - args: Parameters[1], - options?: Parameters[2] -): Promise { - return new Promise((resolve, reject) => { - Meteor.apply(callName, args, options, (err, res) => { - if (err) reject(err) - else resolve(res) - }) - }) -} - // The diff is currently only used client-side const systemTime = { hasBeenSet: false, @@ -484,17 +462,6 @@ export enum LocalStorageProperty { PROTO_ONE_PART_PER_LINE = 'proto:onePartPerLine', } -/** - * Convert a MongoQuery from @sofie-automation/corelib typings to Meteor typings. - * They aren't compatible yet because Meteor is using some 'loose' custom typings, rather than corelib which uses the strong typings given by the mongodb library - * Note: This assumes the queries are compatible. Due to how meteor uses the query they should be, but this has not been verified - * @param query MongoQuery as written in @sofie-automation/corelib syntax - * @returns MongoQuery as written in Meteor syntax - */ -export function convertCorelibToMeteorMongoQuery(query: CoreLibMongoQuery): MongoQuery { - return query as any -} - /** * This just looks like a ReactiveVar, but is not reactive. * It's used to use the same interface/typings, but when code is run on both client and server side. diff --git a/meteor/lib/memoizedIsolatedAutorun.ts b/meteor/lib/memoizedIsolatedAutorun.ts index 3afa26d4c7..9d38c39da1 100644 --- a/meteor/lib/memoizedIsolatedAutorun.ts +++ b/meteor/lib/memoizedIsolatedAutorun.ts @@ -58,7 +58,7 @@ export function memoizedIsolatedAutorun any>( value: result, } - if (!Tracker.currentComputation.firstRun) { + if (Tracker.currentComputation && !Tracker.currentComputation.firstRun) { if (!_.isEqual(oldValue, result)) { dep.changed() } diff --git a/meteor/lib/typings/meteor.d.ts b/meteor/lib/typings/meteor.d.ts index 584d6953fe..287ec3d077 100644 --- a/meteor/lib/typings/meteor.d.ts +++ b/meteor/lib/typings/meteor.d.ts @@ -1,15 +1,43 @@ -import { Mongo } from 'meteor/mongo' +declare module 'meteor/meteor' { + export namespace Meteor { + /** + * Pending https://github.com/meteor/meteor/pull/12645 + */ -import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' + interface MethodApplyOptions { + /** + * (Client only) If true, don't send this method until all previous method calls have completed, and don't send any subsequent method calls until this one is completed. + */ + wait?: boolean | undefined + /** + * (Client only) This callback is invoked with the error or result of the method (just like `asyncCallback`) as soon as the error or result is available. The local cache may not yet reflect the writes performed by the method. + */ + onResultReceived?: ((error: global_Error | Meteor.Error | undefined, result?: Result) => void) | undefined + /** + * (Client only) if true, don't send this method again on reload, simply call the callback an error with the error code 'invocation-failed'. + */ + noRetry?: boolean | undefined + /** + * (Client only) If true then in cases where we would have otherwise discarded the stub's return value and returned undefined, instead we go ahead and return it. Specifically, this is any time other than when (a) we are already inside a stub or (b) we are in Node and no callback was provided. Currently we require this flag to be explicitly passed to reduce the likelihood that stub return values will be confused with server return values; we may improve this in future. + */ + returnStubValue?: boolean | undefined + /** + * (Client only) If true, exceptions thrown by method stubs will be thrown instead of logged, and the method will not be invoked on the server. + */ + throwStubExceptions?: boolean | undefined + } -// Note: This file is temporary, we should replace these types with ones which are stronger, or move them to a better home if they are good - -// export { MongoQuery, MongoQueryKey, MongoModifier } from '@sofie-automation/corelib/dist/mongo' - -/** - * Subset of MongoSelector, only allows direct queries, not QueryWithModifiers such as $explain etc. - * Used for simplified expressions (ie not using $and, $or etc..) - * */ -export type MongoQuery = Mongo.Query -export type MongoQueryKey = RegExp | T | Mongo.FieldExpression // Allowed properties in a Mongo.Query -export type MongoModifier = Mongo.Modifier + /** + * Invokes a method with an async stub, passing any number of arguments. + * @param name Name of method to invoke + * @param args Method arguments + * @param options Optional execution options + * @param asyncCallback Optional callback + */ + function applyAsync( + name: string, + args: ReadonlyArray, + options?: MethodApplyOptions + ): Promise + } +} diff --git a/meteor/package.json b/meteor/package.json index 140932e00d..25ccb6fa87 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -102,7 +102,7 @@ "superfly-timeline": "^8.3.1", "threadedclass": "^1.2.1", "timecode": "0.0.4", - "type-fest": "^2.19.0", + "type-fest": "^3.10.0", "underscore": "^1.13.6", "velocity-animate": "^1.5.2", "velocity-react": "^1.4.3", @@ -127,7 +127,6 @@ "@types/koa-bodyparser": "^4.3.10", "@types/koa__cors": "^3.3.1", "@types/koa__router": "^12.0.0", - "@types/meteor": "^2.9.2", "@types/node": "^14.18.53", "@types/prop-types": "^15.7.5", "@types/react": "^18.2.14", @@ -169,7 +168,7 @@ "sinon": "^14.0.2", "standard-version": "^9.5.0", "ts-jest": "^29.1.1", - "typescript": "~4.5", + "typescript": "~4.9", "xml2js": "^0.4.23", "yargs": "^17.7.2" }, diff --git a/meteor/server/api/ExternalMessageQueue.ts b/meteor/server/api/ExternalMessageQueue.ts index 408886e011..3fcf474acb 100644 --- a/meteor/server/api/ExternalMessageQueue.ts +++ b/meteor/server/api/ExternalMessageQueue.ts @@ -10,7 +10,7 @@ import { StudioContentWriteAccess } from '../security/studio' import { ExternalMessageQueueObjId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ExternalMessageQueue } from '../collections' import { ExternalMessageQueueObj } from '@sofie-automation/corelib/dist/dataModel/ExternalMessageQueue' -import { MongoQuery } from '../../lib/typings/meteor' +import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' let updateExternalMessageQueueStatusTimeout = 0 function updateExternalMessageQueueStatus(): void { diff --git a/meteor/server/api/blueprints/migrationContext.ts b/meteor/server/api/blueprints/migrationContext.ts index 893e18dd3a..ba07041c4e 100644 --- a/meteor/server/api/blueprints/migrationContext.ts +++ b/meteor/server/api/blueprints/migrationContext.ts @@ -35,7 +35,7 @@ import { check } from '../../../lib/check' import { PERIPHERAL_SUBTYPE_PROCESS, PeripheralDeviceType } from '../../../lib/collections/PeripheralDevices' import { TriggeredActionsObj } from '../../../lib/collections/TriggeredActions' import { Match } from 'meteor/check' -import { MongoModifier } from '../../../lib/typings/meteor' +import { MongoModifier } from '@sofie-automation/corelib/dist/mongo' import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { ShowStyleBaseId, ShowStyleVariantId, TriggeredActionId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PeripheralDevices, ShowStyleBases, ShowStyleVariants, Studios, TriggeredActions } from '../../collections' diff --git a/meteor/server/api/cleanup.ts b/meteor/server/api/cleanup.ts index ffcc4235bc..c6224f2293 100644 --- a/meteor/server/api/cleanup.ts +++ b/meteor/server/api/cleanup.ts @@ -1,6 +1,6 @@ import { ProtectedString, getCurrentTime } from '../../lib/lib' import { CollectionCleanupResult } from '../../lib/api/system' -import { MongoQuery } from '../../lib/typings/meteor' +import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { RundownPlaylist } from '../../lib/collections/RundownPlaylists' import { getActiveRundownPlaylistsInStudioFromDb, diff --git a/meteor/server/api/deviceTriggers/RundownContentObserver.ts b/meteor/server/api/deviceTriggers/RundownContentObserver.ts index e19a15d422..270973bf9f 100644 --- a/meteor/server/api/deviceTriggers/RundownContentObserver.ts +++ b/meteor/server/api/deviceTriggers/RundownContentObserver.ts @@ -55,11 +55,11 @@ export class RundownContentObserver { this.#cancelCache = cancelCache this.#observers = [ - RundownPlaylists.observe(rundownPlaylistId, cache.RundownPlaylists.link(), { + RundownPlaylists.observeChanges(rundownPlaylistId, cache.RundownPlaylists.link(), { projection: rundownPlaylistFieldSpecifier, }), - ShowStyleBases.observe(showStyleBaseId, cache.ShowStyleBases.link()), - TriggeredActions.observe( + ShowStyleBases.observeChanges(showStyleBaseId, cache.ShowStyleBases.link()), + TriggeredActions.observeChanges( { showStyleBaseId: { $in: [showStyleBaseId, null], @@ -67,7 +67,7 @@ export class RundownContentObserver { }, cache.TriggeredActions.link() ), - Segments.observe( + Segments.observeChanges( { rundownId: { $in: rundownIds, @@ -78,7 +78,7 @@ export class RundownContentObserver { projection: segmentFieldSpecifier, } ), - Parts.observe( + Parts.observeChanges( { rundownId: { $in: rundownIds, @@ -89,7 +89,7 @@ export class RundownContentObserver { projection: partFieldSpecifier, } ), - PartInstances.observe( + PartInstances.observeChanges( { playlistActivationId: activationId, reset: { @@ -101,7 +101,7 @@ export class RundownContentObserver { projection: partInstanceFieldSpecifier, } ), - RundownBaselineAdLibActions.observe( + RundownBaselineAdLibActions.observeChanges( { rundownId: { $in: rundownIds, @@ -112,7 +112,7 @@ export class RundownContentObserver { projection: adLibActionFieldSpecifier, } ), - RundownBaselineAdLibPieces.observe( + RundownBaselineAdLibPieces.observeChanges( { rundownId: { $in: rundownIds, @@ -123,7 +123,7 @@ export class RundownContentObserver { projection: adLibPieceFieldSpecifier, } ), - AdLibActions.observe( + AdLibActions.observeChanges( { rundownId: { $in: rundownIds, @@ -134,7 +134,7 @@ export class RundownContentObserver { projection: adLibActionFieldSpecifier, } ), - AdLibPieces.observe( + AdLibPieces.observeChanges( { rundownId: { $in: rundownIds, diff --git a/meteor/server/api/deviceTriggers/observer.ts b/meteor/server/api/deviceTriggers/observer.ts index 70c24d32d2..62ae1b6ca8 100644 --- a/meteor/server/api/deviceTriggers/observer.ts +++ b/meteor/server/api/deviceTriggers/observer.ts @@ -13,10 +13,10 @@ import { logger } from '../../logging' import { checkAccessAndGetPeripheralDevice } from '../ingest/lib' import { StudioActionManagers } from './StudioActionManagers' import { JobQueueWithClasses } from '@sofie-automation/shared-lib/dist/lib/JobQueueWithClasses' -import { ReactiveCacheCollection } from '../../publications/lib/ReactiveCacheCollection' import { StudioDeviceTriggerManager } from './StudioDeviceTriggerManager' import { StudioObserver } from './StudioObserver' import { Studios } from '../../collections' +import { ReactiveCacheCollection } from '../../publications/lib/ReactiveCacheCollection' type ObserverAndManager = { observer: StudioObserver diff --git a/meteor/server/api/deviceTriggers/reactiveContentCache.ts b/meteor/server/api/deviceTriggers/reactiveContentCache.ts index c2e374eb67..c3606fe849 100644 --- a/meteor/server/api/deviceTriggers/reactiveContentCache.ts +++ b/meteor/server/api/deviceTriggers/reactiveContentCache.ts @@ -10,9 +10,9 @@ import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/Rund import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBTriggeredActions } from '../../../lib/collections/TriggeredActions' -import { ReactiveCacheCollection } from '../../publications/lib/ReactiveCacheCollection' import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mongo' import { literal } from '@sofie-automation/corelib/dist/lib' +import { ReactiveCacheCollection } from '../../publications/lib/ReactiveCacheCollection' export type RundownPlaylistFields = '_id' | 'name' | 'activationId' | 'currentPartInfo' | 'nextPartInfo' export const rundownPlaylistFieldSpecifier = literal< diff --git a/meteor/server/api/snapshot.ts b/meteor/server/api/snapshot.ts index 270a8e8180..955bf2a3e6 100644 --- a/meteor/server/api/snapshot.ts +++ b/meteor/server/api/snapshot.ts @@ -41,7 +41,7 @@ import { isVersionSupported } from '../migration/databaseMigration' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' import { Blueprint } from '../../lib/collections/Blueprints' import { IngestRundown, VTContent } from '@sofie-automation/blueprints-integration' -import { MongoQuery } from '../../lib/typings/meteor' +import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { importIngestRundown } from './ingest/http' import { RundownPlaylist } from '../../lib/collections/RundownPlaylists' import { RundownLayoutBase } from '../../lib/collections/RundownLayouts' diff --git a/meteor/server/api/user.ts b/meteor/server/api/user.ts index 7efddfeac5..0e3626a557 100644 --- a/meteor/server/api/user.ts +++ b/meteor/server/api/user.ts @@ -3,7 +3,7 @@ import * as _ from 'underscore' import { Accounts } from 'meteor/accounts-base' import { unprotectString, protectString, deferAsync, sleep } from '../../lib/lib' import { MethodContextAPI, MethodContext } from '../../lib/api/methods' -import { NewUserAPI, UserAPIMethods, createUser } from '../../lib/api/user' +import { NewUserAPI, UserAPIMethods, createUser, CreateNewUserData } from '../../lib/api/user' import { registerClassToMeteorMethods } from '../methods' import { SystemWriteAccess } from '../security/system' import { triggerWriteAccess, triggerWriteAccessBecauseNoCheckNecessary } from '../security/lib/securityVerify' @@ -102,10 +102,10 @@ class ServerUserAPI extends MethodContextAPI implements NewUserAPI { registerClassToMeteorMethods(UserAPIMethods, ServerUserAPI, false) -Accounts.onCreateUser((options, user) => { +Accounts.onCreateUser((options0, user) => { + const options = options0 as Partial user.profile = options.profile - // @ts-expect-error hack, add the property "createOrganization" to trigger creation of an org const createOrganization = options.createOrganization if (createOrganization) { deferAsync(async () => { diff --git a/meteor/server/collections/collection.ts b/meteor/server/collections/collection.ts index bb43a29d41..7bab6044c6 100644 --- a/meteor/server/collections/collection.ts +++ b/meteor/server/collections/collection.ts @@ -1,8 +1,9 @@ import { UserId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { MongoModifier, MongoQuery } from '../../lib/typings/meteor' -import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' +import { MongoModifier, MongoQuery } from '@sofie-automation/corelib/dist/mongo' +import { ProtectedString, protectString } from '@sofie-automation/corelib/dist/protectedString' import { Meteor } from 'meteor/meteor' import { Mongo } from 'meteor/mongo' +import { NpmModuleMongodb } from 'meteor/npm-mongo' import { UpdateOptions, UpsertOptions, @@ -15,13 +16,13 @@ import { ObserveChangesCallbacks, ObserveCallbacks, } from '../../lib/collections/lib' -import type { AnyBulkWriteOperation, Collection as RawCollection, CreateIndexesOptions } from 'mongodb' +import { PromisifyCallbacks, waitForPromise } from '../../lib/lib' +import type { AnyBulkWriteOperation, Collection as RawCollection } from 'mongodb' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' import { registerCollection } from './lib' import { WrappedMockCollection } from './implementations/mock' import { WrappedAsyncMongoCollection } from './implementations/asyncCollection' import { WrappedReadOnlyMongoCollection } from './implementations/readonlyWrapper' -import { PromisifyCallbacks, protectString, waitForPromise } from '../../lib/lib' export interface MongoAllowRules { insert?: (userId: UserId, doc: DBInterface) => Promise | boolean @@ -52,7 +53,7 @@ export function wrapMongoCollection(collection, name) - registerCollection(name, wrapped) + registerCollection(name, wrapped as WrappedAsyncMongoCollection) return wrapped } @@ -70,7 +71,7 @@ export function createAsyncOnlyMongoCollection(collection, name) registerCollection(name, wrappedCollection) @@ -87,8 +88,8 @@ export function createAsyncOnlyReadOnlyMongoCollection { const collection = getOrCreateMongoCollection(name) - const mutableCollection = wrapMeteorCollectionIntoAsyncCollection(collection, name) - const readonlyCollection = new WrappedReadOnlyMongoCollection(mutableCollection) + const mutableCollection = wrapMeteorCollectionIntoAsyncCollection(collection, name) + const readonlyCollection = new WrappedReadOnlyMongoCollection(mutableCollection) registerCollection(name, readonlyCollection) @@ -103,10 +104,10 @@ function wrapMeteorCollectionIntoAsyncCollection(collection, name) } else { // Override the default mongodb methods, because the errors thrown by them doesn't contain the proper call stack - return new WrappedAsyncMongoCollection(collection, name) + return new WrappedAsyncMongoCollection(collection, name) } } @@ -274,5 +275,5 @@ export interface AsyncOnlyReadOnlyMongoCollection, options?: FindOptions): Promise - _ensureIndex(keys: IndexSpecifier | string, options?: CreateIndexesOptions): void + _ensureIndex(keys: IndexSpecifier | string, options?: NpmModuleMongodb.CreateIndexesOptions): void } diff --git a/meteor/server/collections/implementations/asyncCollection.ts b/meteor/server/collections/implementations/asyncCollection.ts index 0d9a3d230c..7c21e75188 100644 --- a/meteor/server/collections/implementations/asyncCollection.ts +++ b/meteor/server/collections/implementations/asyncCollection.ts @@ -1,4 +1,4 @@ -import { MongoModifier, MongoQuery } from '../../../lib/typings/meteor' +import { MongoModifier, MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' import { Meteor } from 'meteor/meteor' import { diff --git a/meteor/server/collections/implementations/base.ts b/meteor/server/collections/implementations/base.ts index 103433f607..6f58af9433 100644 --- a/meteor/server/collections/implementations/base.ts +++ b/meteor/server/collections/implementations/base.ts @@ -1,11 +1,12 @@ -import { MongoModifier, MongoQuery } from '../../../lib/typings/meteor' +import { MongoModifier, MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { ProtectedString, protectString } from '@sofie-automation/corelib/dist/protectedString' import { Meteor } from 'meteor/meteor' import { Mongo } from 'meteor/mongo' import { UpdateOptions, UpsertOptions, FindOptions, IndexSpecifier, MongoCursor } from '../../../lib/collections/lib' -import type { Collection as RawCollection, Db as RawDb, CreateIndexesOptions } from 'mongodb' +import type { Collection as RawCollection, Db as RawDb } from 'mongodb' import { stringifyError } from '@sofie-automation/corelib/dist/lib' import { PromisifyCallbacks, waitForPromise } from '../../../lib/lib' +import { NpmModuleMongodb } from 'meteor/npm-mongo' export class WrappedMongoCollectionBase }> { protected readonly _collection: Mongo.Collection @@ -95,7 +96,7 @@ export class WrappedMongoCollectionBase | string, options?: CreateIndexesOptions): void { + _ensureIndex(keys: IndexSpecifier | string, options?: NpmModuleMongodb.CreateIndexesOptions): void { try { return this._collection._ensureIndex(keys as any, options) } catch (e) { diff --git a/meteor/server/collections/implementations/mock.ts b/meteor/server/collections/implementations/mock.ts index cf3b6da390..471052e6f6 100644 --- a/meteor/server/collections/implementations/mock.ts +++ b/meteor/server/collections/implementations/mock.ts @@ -1,4 +1,4 @@ -import { MongoModifier, MongoQuery } from '../../../lib/typings/meteor' +import { MongoModifier, MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' import { Meteor } from 'meteor/meteor' import { Mongo } from 'meteor/mongo' @@ -12,7 +12,6 @@ import { } from '../../../lib/collections/lib' import { PromisifyCallbacks } from '../../../lib/lib' import type { AnyBulkWriteOperation } from 'mongodb' -import _ from 'underscore' import { AsyncOnlyMongoCollection } from '../collection' import { WrappedMongoCollectionBase, dePromiseObjectOfFunctions } from './base' @@ -137,14 +136,10 @@ export class WrappedMockCollection), and makes anything optional be nullable instead */ export type ReplaceOptionalWithNullInArray = { - [K in keyof T]: undefined extends T[K] ? NonNullable | null : T[K] + [K in keyof T]-?: undefined extends T[K] ? NonNullable | null : T[K] } /** @@ -108,6 +108,9 @@ function setMeteorMethods(orgMethods: MethodsInner, secret?: boolean): void { const result = method.apply(this, args) if (isPromise(result)) { + // Don't block execution of other methods while waiting for this to resolve. (This is how meteor 2.7 behaved, added to avoid breaking Sofie) + this.unblock() + // The method result is a promise return Promise.resolve(result) .finally(() => { diff --git a/meteor/server/migration/1_50_0.ts b/meteor/server/migration/1_50_0.ts index ec6b66e47f..00aeef1718 100644 --- a/meteor/server/migration/1_50_0.ts +++ b/meteor/server/migration/1_50_0.ts @@ -754,7 +754,7 @@ function updatePlayoutDeviceTypeInOverride(op: SomeObjectOverrideOp): ObjectOver if (op.op !== 'set') return undefined if (!op.path.includes('.')) { // Root level - const value = op.value as Partial + const value = op.value as any if (typeof value.options?.type === 'number') { value.options.type = oldDeviceTypeToNewMapping[value.options.type] ?? TSR.DeviceType.ABSTRACT return op diff --git a/meteor/server/migration/lib.ts b/meteor/server/migration/lib.ts index 7592b68a63..7bf4c525f2 100644 --- a/meteor/server/migration/lib.ts +++ b/meteor/server/migration/lib.ts @@ -1,4 +1,3 @@ -import { Mongo } from 'meteor/mongo' import * as _ from 'underscore' import { MigrationStepCore } from '@sofie-automation/blueprints-integration' import { objectPathGet, ProtectedString } from '../../lib/lib' @@ -7,13 +6,14 @@ import { logger } from '../logging' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' import { AsyncOnlyMongoCollection } from '../collections/collection' import { Collections } from '../collections/lib' +import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' /** * Returns a migration step that ensures the provided property is set in the collection */ export function ensureCollectionProperty( collectionName: CollectionName, - selector: Mongo.Selector, + selector: MongoQuery, property: string, // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types defaultValue: any, @@ -56,7 +56,7 @@ export function ensureCollectionProperty( export function removeCollectionProperty( collectionName: CollectionName, - selector: Mongo.Selector, + selector: MongoQuery, property: string, dependOnResultFrom?: string ): Omit { diff --git a/meteor/server/optimizations.ts b/meteor/server/optimizations.ts index fa28008a06..25c0274469 100644 --- a/meteor/server/optimizations.ts +++ b/meteor/server/optimizations.ts @@ -1,5 +1,5 @@ import { ShowStyleBaseId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { MongoQuery } from '../lib/typings/meteor' +import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBStudio, StudioLight } from '../lib/collections/Studios' import { ShowStyleBases, Studios } from './collections' diff --git a/meteor/server/publications/buckets.ts b/meteor/server/publications/buckets.ts index 8cc54cbe9d..3e5cac7384 100644 --- a/meteor/server/publications/buckets.ts +++ b/meteor/server/publications/buckets.ts @@ -10,34 +10,39 @@ import { StudioReadAccess } from '../security/studio' import { isProtectedString } from '@sofie-automation/corelib/dist/protectedString' import { BucketAdLibActions, BucketAdLibs, Buckets } from '../collections' import { check, Match } from 'meteor/check' +import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' +import { StudioId, BucketId } from '@sofie-automation/corelib/dist/dataModel/Ids' -meteorPublish(PubSub.buckets, async function (studioId, bucketId, _token) { - check(studioId, String) - check(bucketId, Match.Maybe(String)) +meteorPublish( + PubSub.buckets, + async function (studioId: StudioId, bucketId: BucketId | null, _token: string | undefined) { + check(studioId, String) + check(bucketId, Match.Maybe(String)) - const modifier: FindOptions = { - fields: {}, - } - if ( - (await StudioReadAccess.studioContent(studioId, this)) || - (isProtectedString(bucketId) && bucketId && (await BucketSecurity.allowReadAccess(this, bucketId))) - ) { - return Buckets.findWithCursor( - bucketId - ? { - _id: bucketId, - studioId, - } - : { - studioId, - }, - modifier - ) + const modifier: FindOptions = { + fields: {}, + } + if ( + (await StudioReadAccess.studioContent(studioId, this)) || + (isProtectedString(bucketId) && bucketId && (await BucketSecurity.allowReadAccess(this, bucketId))) + ) { + return Buckets.findWithCursor( + bucketId + ? { + _id: bucketId, + studioId, + } + : { + studioId, + }, + modifier + ) + } + return null } - return null -}) +) -meteorPublish(PubSub.bucketAdLibPieces, async function (selector, _token) { +meteorPublish(PubSub.bucketAdLibPieces, async function (selector: MongoQuery, _token: string | undefined) { if (!selector) throw new Meteor.Error(400, 'selector argument missing') const modifier: FindOptions = { fields: {}, @@ -48,13 +53,16 @@ meteorPublish(PubSub.bucketAdLibPieces, async function (selector, _token) { return null }) -meteorPublish(PubSub.bucketAdLibActions, async function (selector, _token) { - if (!selector) throw new Meteor.Error(400, 'selector argument missing') - const modifier: FindOptions = { - fields: {}, - } - if (isProtectedString(selector.bucketId) && (await BucketSecurity.allowReadAccess(this, selector.bucketId))) { - return BucketAdLibActions.findWithCursor(selector, modifier) +meteorPublish( + PubSub.bucketAdLibActions, + async function (selector: MongoQuery, _token: string | undefined) { + if (!selector) throw new Meteor.Error(400, 'selector argument missing') + const modifier: FindOptions = { + fields: {}, + } + if (isProtectedString(selector.bucketId) && (await BucketSecurity.allowReadAccess(this, selector.bucketId))) { + return BucketAdLibActions.findWithCursor(selector, modifier) + } + return null } - return null -}) +) diff --git a/meteor/server/publications/deviceTriggersPreview.ts b/meteor/server/publications/deviceTriggersPreview.ts index 4724d0bf3c..1141578111 100644 --- a/meteor/server/publications/deviceTriggersPreview.ts +++ b/meteor/server/publications/deviceTriggersPreview.ts @@ -30,7 +30,7 @@ export interface UIDeviceTriggerPreview { meteorCustomPublish( PubSub.deviceTriggersPreview, CustomCollectionName.UIDeviceTriggerPreviews, - async function (pub, studioId: StudioId, token) { + async function (pub, studioId: StudioId, token: string | undefined) { check(studioId, String) if (!studioId) throw new Meteor.Error(400, 'One of studioId must be provided') diff --git a/meteor/server/publications/lib.ts b/meteor/server/publications/lib.ts index 0739b574d0..16a48c5e16 100644 --- a/meteor/server/publications/lib.ts +++ b/meteor/server/publications/lib.ts @@ -1,7 +1,7 @@ import { Meteor, Subscription } from 'meteor/meteor' import { PubSubTypes } from '../../lib/api/pubsub' import { extractFunctionSignature } from '../lib' -import { MongoQuery } from '../../lib/typings/meteor' +import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { ResolvedCredentials, resolveCredentials } from '../security/lib/credentials' import { Settings } from '../../lib/Settings' import { PeripheralDevice } from '../../lib/collections/PeripheralDevices' diff --git a/meteor/server/publications/lib/ReactiveCacheCollection.ts b/meteor/server/publications/lib/ReactiveCacheCollection.ts index f62a7d1af8..e537cd4108 100644 --- a/meteor/server/publications/lib/ReactiveCacheCollection.ts +++ b/meteor/server/publications/lib/ReactiveCacheCollection.ts @@ -1,7 +1,8 @@ import { omit } from '@sofie-automation/corelib/dist/lib' -import { ProtectedString, isProtectedString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' +import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' import { Mongo } from 'meteor/mongo' -import { ObserveCallbacks } from '../../../lib/collections/lib' +import { ObserveChangesCallbacks } from '../../../lib/collections/lib' +import { MongoModifier, MongoQuery } from '@sofie-automation/corelib/dist/mongo' type Reaction = () => void @@ -16,14 +17,14 @@ export class ReactiveCacheCollection, options?: Mongo.Options ): Mongo.Cursor { - return this.#collection.find(isProtectedString(selector) ? unprotectString(selector) : selector, options) + return this.#collection.find(selector as any, options) } findOne( selector: Document['_id'] | Mongo.Selector, options?: Omit, 'limit'> ): Document | undefined { - return this.#collection.findOne(isProtectedString(selector) ? unprotectString(selector) : selector, options) + return this.#collection.findOne(selector as any, options) } insert(doc: Mongo.OptionalId): string { @@ -32,8 +33,8 @@ export class ReactiveCacheCollection): number { - const num = this.#collection.remove(isProtectedString(selector) ? unprotectString(selector) : selector) + remove(selector: Document['_id'] | MongoQuery): number { + const num = this.#collection.remove(selector as any) if (num > 0) { this.runReaction() } @@ -41,19 +42,15 @@ export class ReactiveCacheCollection, - modifier: Mongo.Modifier, + selector: Document['_id'] | MongoQuery, + modifier: MongoModifier, options?: { multi?: boolean upsert?: boolean arrayFilters?: { [identifier: string]: any }[] } ): number { - const num = this.#collection.update( - isProtectedString(selector) ? unprotectString(selector) : selector, - modifier, - options - ) + const num = this.#collection.update(selector as any, modifier as any, options) if (num > 0) { this.runReaction() } @@ -65,81 +62,30 @@ export class ReactiveCacheCollection, options?: { multi?: boolean } ): { numberAffected?: number; insertedId?: string } { - const res = this.#collection.upsert( - isProtectedString(selector) ? unprotectString(selector) : selector, - modifier, - options - ) + const res = this.#collection.upsert(selector as any, modifier as any, options) if (res.numberAffected || res.insertedId) { this.runReaction() } return res } - async insertAsync(doc: Mongo.OptionalId): Promise { - const result = await this.#collection.insertAsync(doc) - this.runReaction() - return result - } - - async removeAsync(selector: Document['_id'] | Mongo.Selector): Promise { - const result = await this.#collection.removeAsync( - isProtectedString(selector) ? unprotectString(selector) : selector - ) - if (result > 0) { - this.runReaction() - } - return result - } - - async updateAsync( - selector: Document['_id'] | Mongo.Selector, - modifier: Mongo.Modifier, - options?: { - multi?: boolean - upsert?: boolean - arrayFilters?: { [identifier: string]: any }[] - } - ): Promise { - const result = await this.#collection.updateAsync( - isProtectedString(selector) ? unprotectString(selector) : selector, - modifier, - options - ) - if (result > 0) { - this.runReaction() - } - return result - } - - async upsertAsync( - selector: Document['_id'] | Mongo.Selector, - modifier: Mongo.Modifier, - options?: { multi?: boolean } - ): Promise<{ numberAffected?: number; insertedId?: string }> { - const result = await this.#collection.upsertAsync( - isProtectedString(selector) ? unprotectString(selector) : selector, - modifier, - options - ) - if (result.numberAffected || result.insertedId) { - this.runReaction() - } - return result - } - - link(cb?: () => void): ObserveCallbacks { + link(cb?: () => void): ObserveChangesCallbacks { return { - added: (doc: Document) => { - this.upsert(doc._id, { $set: omit(doc, '_id') as Partial }) + added: (id: Document['_id'], fields: Partial) => { + this.upsert(id, { $set: omit(fields, '_id') as any }) cb?.() }, - changed: (doc: Document) => { - this.upsert(doc._id, { $set: omit(doc, '_id') as Partial }) + changed: (id: Document['_id'], fields: Partial) => { + const unset: Partial> = {} + for (const [key, value] of Object.entries(fields)) { + if (value !== undefined) continue + unset[key] = 1 + } + this.upsert(id, { $set: omit(fields, '_id') as any, $unset: unset as any }) cb?.() }, - removed: (doc: Document) => { - this.remove(doc._id) + removed: (id: Document['_id']) => { + this.remove(id) cb?.() }, } diff --git a/meteor/server/publications/mountedTriggers.ts b/meteor/server/publications/mountedTriggers.ts index ec1581d3ac..27e756cf9c 100644 --- a/meteor/server/publications/mountedTriggers.ts +++ b/meteor/server/publications/mountedTriggers.ts @@ -16,7 +16,7 @@ const PUBLICATION_DEBOUNCE = 20 meteorCustomPublish( PubSub.mountedTriggersForDevice, CustomCollectionName.MountedTriggers, - async function (pub, deviceId: PeripheralDeviceId, deviceIds: string[], token) { + async function (pub, deviceId: PeripheralDeviceId, deviceIds: string[], token: string | undefined) { check(deviceId, String) check(deviceIds, [String]) @@ -46,7 +46,7 @@ meteorCustomPublish( meteorCustomPublish( PubSub.mountedTriggersForDevicePreview, CustomCollectionName.MountedTriggersPreviews, - async function (pub, deviceId: PeripheralDeviceId, token) { + async function (pub, deviceId: PeripheralDeviceId, token: string | undefined) { check(deviceId, String) if (await PeripheralDeviceReadAccess.peripheralDeviceContent(deviceId, { userId: this.userId, token })) { diff --git a/meteor/server/publications/organization.ts b/meteor/server/publications/organization.ts index 8a1577dad5..1548658f3c 100644 --- a/meteor/server/publications/organization.ts +++ b/meteor/server/publications/organization.ts @@ -9,9 +9,13 @@ import { FindOptions } from '../../lib/collections/lib' import { DBOrganization } from '../../lib/collections/Organization' import { isProtectedString } from '@sofie-automation/corelib/dist/protectedString' import { Blueprints, Evaluations, Organizations, Snapshots, UserActionsLog } from '../collections' +import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' +import { OrganizationId } from '@sofie-automation/corelib/dist/dataModel/Ids' -meteorPublish(PubSub.organization, async function (selector0, token) { - const { cred, selector } = await AutoFillSelector.organizationId(this.userId, selector0, token) +meteorPublish(PubSub.organization, async function (organizationId: OrganizationId | null, token: string | undefined) { + if (!organizationId) return null + + const { cred, selector } = await AutoFillSelector.organizationId(this.userId, { _id: organizationId }, token) const modifier: FindOptions = { fields: { name: 1, @@ -29,7 +33,7 @@ meteorPublish(PubSub.organization, async function (selector0, token) { return null }) -meteorPublish(PubSub.blueprints, async function (selector0, token) { +meteorPublish(PubSub.blueprints, async function (selector0: MongoQuery, token: string | undefined) { const { cred, selector } = await AutoFillSelector.organizationId(this.userId, selector0, token) const modifier: FindOptions = { fields: { @@ -41,24 +45,31 @@ meteorPublish(PubSub.blueprints, async function (selector0, token) { } return null }) -meteorPublish(PubSub.evaluations, async function (selector0, token) { +meteorPublish(PubSub.evaluations, async function (selector0: MongoQuery, token: string | undefined) { const { cred, selector } = await AutoFillSelector.organizationId(this.userId, selector0, token) if (!cred || (await OrganizationReadAccess.organizationContent(selector.organizationId, cred))) { return Evaluations.findWithCursor(selector) } return null }) -meteorPublish(PubSub.snapshots, async function (selector0, token) { +meteorPublish(PubSub.snapshots, async function (selector0: MongoQuery, token: string | undefined) { const { cred, selector } = await AutoFillSelector.organizationId(this.userId, selector0, token) if (!cred || (await OrganizationReadAccess.organizationContent(selector.organizationId, cred))) { return Snapshots.findWithCursor(selector) } return null }) -meteorPublish(PubSub.userActionsLog, async function (selector0, token) { - const { cred, selector } = await AutoFillSelector.organizationId(this.userId, selector0, token) - if (!cred || (await OrganizationReadAccess.organizationContent(selector.organizationId, cred))) { - return UserActionsLog.findWithCursor(selector) +meteorPublish( + PubSub.userActionsLog, + async function (selector0: MongoQuery, token: string | undefined) { + const { cred, selector } = await AutoFillSelector.organizationId( + this.userId, + selector0, + token + ) + if (!cred || (await OrganizationReadAccess.organizationContent(selector.organizationId, cred))) { + return UserActionsLog.findWithCursor(selector) + } + return null } - return null -}) +) diff --git a/meteor/server/publications/packageManager/expectedPackages/contentObserver.ts b/meteor/server/publications/packageManager/expectedPackages/contentObserver.ts index 24d88f4af8..4507bc47b3 100644 --- a/meteor/server/publications/packageManager/expectedPackages/contentObserver.ts +++ b/meteor/server/publications/packageManager/expectedPackages/contentObserver.ts @@ -31,7 +31,7 @@ export class ExpectedPackagesContentObserver implements Meteor.LiveQueryHandle { cache.PieceInstances.remove({}) return [ - PieceInstances.observe( + PieceInstances.observeChanges( { // We can use the `this.#partInstanceIds` here, as this is restarted every time that property changes partInstanceId: { $in: this.#partInstanceIds }, @@ -47,14 +47,14 @@ export class ExpectedPackagesContentObserver implements Meteor.LiveQueryHandle { // Subscribe to the database, and pipe any updates into the ReactiveCacheCollections this.#observers = [ - ExpectedPackages.observe( + ExpectedPackages.observeChanges( { studioId: studioId, }, cache.ExpectedPackages.link() ), - RundownPlaylists.observe( + RundownPlaylists.observeChanges( { studioId: studioId, }, diff --git a/meteor/server/publications/peripheralDevice.ts b/meteor/server/publications/peripheralDevice.ts index 7c7b8d79a2..07c442a164 100644 --- a/meteor/server/publications/peripheralDevice.ts +++ b/meteor/server/publications/peripheralDevice.ts @@ -6,12 +6,14 @@ import { PeripheralDeviceReadAccess } from '../security/peripheralDevice' import { PeripheralDevice } from '../../lib/collections/PeripheralDevices' import { OrganizationReadAccess } from '../security/organization' import { StudioReadAccess } from '../security/studio' -import { MongoQuery } from '../../lib/typings/meteor' +import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { Credentials, ResolvedCredentials } from '../security/lib/credentials' import { NoSecurityReadAccess } from '../security/noSecurity' import { FindOptions } from '../../lib/collections/lib' import { PeripheralDeviceId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { MediaWorkFlows, MediaWorkFlowSteps, PeripheralDeviceCommands, PeripheralDevices } from '../collections' +import { MediaWorkFlow } from '@sofie-automation/shared-lib/dist/core/model/MediaWorkFlows' +import { MediaWorkFlowStep } from '@sofie-automation/shared-lib/dist/core/model/MediaWorkFlowSteps' /* * This file contains publications for the peripheralDevices, such as playout-gateway, mos-gateway and package-manager @@ -28,25 +30,32 @@ async function checkAccess(cred: Credentials | ResolvedCredentials | null, selec (selector.studioId && (await StudioReadAccess.studioContent(selector.studioId, cred))) ) } -meteorPublish(PubSub.peripheralDevices, async function (selector0, token) { - const { cred, selector } = await AutoFillSelector.organizationId(this.userId, selector0, token) - if (await checkAccess(cred, selector)) { - const modifier: FindOptions = { - fields: { - token: 0, - secretSettings: 0, - }, - } - if (selector._id && token && modifier.fields) { - // in this case, send the secretSettings: - delete modifier.fields.secretSettings +meteorPublish( + PubSub.peripheralDevices, + async function (selector0: MongoQuery, token: string | undefined) { + const { cred, selector } = await AutoFillSelector.organizationId( + this.userId, + selector0, + token + ) + if (await checkAccess(cred, selector)) { + const modifier: FindOptions = { + fields: { + token: 0, + secretSettings: 0, + }, + } + if (selector._id && token && modifier.fields) { + // in this case, send the secretSettings: + delete modifier.fields.secretSettings + } + return PeripheralDevices.findWithCursor(selector, modifier) } - return PeripheralDevices.findWithCursor(selector, modifier) + return null } - return null -}) +) -meteorPublish(PubSub.peripheralDevicesAndSubDevices, async function (selector0) { +meteorPublish(PubSub.peripheralDevicesAndSubDevices, async function (selector0: MongoQuery) { const { cred, selector } = await AutoFillSelector.organizationId( this.userId, selector0, @@ -76,25 +85,31 @@ meteorPublish(PubSub.peripheralDevicesAndSubDevices, async function (selector0) } return null }) -meteorPublish(PubSub.peripheralDeviceCommands, async function (deviceId: PeripheralDeviceId, token) { - if (!deviceId) throw new Meteor.Error(400, 'deviceId argument missing') - check(deviceId, String) - if (await PeripheralDeviceReadAccess.peripheralDeviceContent(deviceId, { userId: this.userId, token })) { - return PeripheralDeviceCommands.findWithCursor({ deviceId: deviceId }) +meteorPublish( + PubSub.peripheralDeviceCommands, + async function (deviceId: PeripheralDeviceId, token: string | undefined) { + if (!deviceId) throw new Meteor.Error(400, 'deviceId argument missing') + check(deviceId, String) + if (await PeripheralDeviceReadAccess.peripheralDeviceContent(deviceId, { userId: this.userId, token })) { + return PeripheralDeviceCommands.findWithCursor({ deviceId: deviceId }) + } + return null } - return null -}) -meteorPublish(PubSub.mediaWorkFlows, async function (selector0, token) { +) +meteorPublish(PubSub.mediaWorkFlows, async function (selector0: MongoQuery, token: string | undefined) { const { cred, selector } = await AutoFillSelector.deviceId(this.userId, selector0, token) if (!cred || (await PeripheralDeviceReadAccess.peripheralDeviceContent(selector.deviceId, cred))) { return MediaWorkFlows.findWithCursor(selector) } return null }) -meteorPublish(PubSub.mediaWorkFlowSteps, async function (selector0, token) { - const { cred, selector } = await AutoFillSelector.deviceId(this.userId, selector0, token) - if (!cred || (await PeripheralDeviceReadAccess.peripheralDeviceContent(selector.deviceId, cred))) { - return MediaWorkFlowSteps.findWithCursor(selector) +meteorPublish( + PubSub.mediaWorkFlowSteps, + async function (selector0: MongoQuery, token: string | undefined) { + const { cred, selector } = await AutoFillSelector.deviceId(this.userId, selector0, token) + if (!cred || (await PeripheralDeviceReadAccess.peripheralDeviceContent(selector.deviceId, cred))) { + return MediaWorkFlowSteps.findWithCursor(selector) + } + return null } - return null -}) +) diff --git a/meteor/server/publications/peripheralDeviceForDevice.ts b/meteor/server/publications/peripheralDeviceForDevice.ts index ebf8bf47f9..52b31cdef8 100644 --- a/meteor/server/publications/peripheralDeviceForDevice.ts +++ b/meteor/server/publications/peripheralDeviceForDevice.ts @@ -191,7 +191,7 @@ async function manipulatePeripheralDevicePublicationData( meteorCustomPublish( PubSub.peripheralDeviceForDevice, CustomCollectionName.PeripheralDeviceForDevice, - async function (pub, deviceId: PeripheralDeviceId, token) { + async function (pub, deviceId: PeripheralDeviceId, token: string | undefined) { check(deviceId, String) if (await PeripheralDeviceReadAccess.peripheralDeviceContent(deviceId, { userId: this.userId, token })) { diff --git a/meteor/server/publications/pieceContentStatusUI/bucket/bucketContentCache.ts b/meteor/server/publications/pieceContentStatusUI/bucket/bucketContentCache.ts index 6ca11aee61..0f9cdb13fe 100644 --- a/meteor/server/publications/pieceContentStatusUI/bucket/bucketContentCache.ts +++ b/meteor/server/publications/pieceContentStatusUI/bucket/bucketContentCache.ts @@ -1,15 +1,13 @@ import { ReactiveCacheCollection } from '../../lib/ReactiveCacheCollection' import { DBShowStyleBase, SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' -import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' import { literal } from '@sofie-automation/corelib/dist/lib' import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mongo' import { BucketAdLibAction } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibAction' import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' -import { BlueprintId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { BlueprintId, ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids' -export type SourceLayersDocId = ProtectedString<'SourceLayersDocId'> export interface SourceLayersDoc { - _id: SourceLayersDocId + _id: ShowStyleBaseId blueprintId: BlueprintId sourceLayers: SourceLayers } diff --git a/meteor/server/publications/pieceContentStatusUI/bucket/bucketContentObserver.ts b/meteor/server/publications/pieceContentStatusUI/bucket/bucketContentObserver.ts index e883dbc4fd..f5f116ae78 100644 --- a/meteor/server/publications/pieceContentStatusUI/bucket/bucketContentObserver.ts +++ b/meteor/server/publications/pieceContentStatusUI/bucket/bucketContentObserver.ts @@ -71,7 +71,7 @@ export class BucketContentObserver implements Meteor.LiveQueryHandle { // Subscribe to the database, and pipe any updates into the ReactiveCacheCollections this.#observers = [ - BucketAdLibs.observe( + BucketAdLibs.observeChanges( { bucketId: bucketId, }, @@ -84,7 +84,7 @@ export class BucketContentObserver implements Meteor.LiveQueryHandle { projection: bucketAdlibFieldSpecifier, } ), - BucketAdLibActions.observe( + BucketAdLibActions.observeChanges( { bucketId: bucketId, }, diff --git a/meteor/server/publications/pieceContentStatusUI/rundown/reactiveContentCache.ts b/meteor/server/publications/pieceContentStatusUI/rundown/reactiveContentCache.ts index ba20fd77ef..253e51fef7 100644 --- a/meteor/server/publications/pieceContentStatusUI/rundown/reactiveContentCache.ts +++ b/meteor/server/publications/pieceContentStatusUI/rundown/reactiveContentCache.ts @@ -3,7 +3,6 @@ import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { ReactiveCacheCollection } from '../../lib/ReactiveCacheCollection' import { DBShowStyleBase, SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' -import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' import { literal } from '@sofie-automation/corelib/dist/lib' import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mongo' import { DBRundown, Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' @@ -11,12 +10,11 @@ import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' -import { BlueprintId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { BlueprintId, ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -export type SourceLayersDocId = ProtectedString<'SourceLayersDocId'> export interface SourceLayersDoc { - _id: SourceLayersDocId + _id: ShowStyleBaseId blueprintId: BlueprintId sourceLayers: SourceLayers } diff --git a/meteor/server/publications/pieceContentStatusUI/rundown/rundownContentObserver.ts b/meteor/server/publications/pieceContentStatusUI/rundown/rundownContentObserver.ts index 877fdc35e9..824be9e785 100644 --- a/meteor/server/publications/pieceContentStatusUI/rundown/rundownContentObserver.ts +++ b/meteor/server/publications/pieceContentStatusUI/rundown/rundownContentObserver.ts @@ -89,7 +89,7 @@ export class RundownContentObserver { // Subscribe to the database, and pipe any updates into the ReactiveCacheCollections this.#observers = [ - Rundowns.observe( + Rundowns.observeChanges( { _id: { $in: rundownIds, @@ -105,7 +105,7 @@ export class RundownContentObserver { ), this.#showStyleBaseIdObserver, - Segments.observe( + Segments.observeChanges( { rundownId: { $in: rundownIds, @@ -116,7 +116,7 @@ export class RundownContentObserver { projection: segmentFieldSpecifier, } ), - Parts.observe( + Parts.observeChanges( { rundownId: { $in: rundownIds, @@ -127,7 +127,7 @@ export class RundownContentObserver { projection: partFieldSpecifier, } ), - Pieces.observe( + Pieces.observeChanges( { startRundownId: { $in: rundownIds, @@ -138,7 +138,7 @@ export class RundownContentObserver { projection: pieceFieldSpecifier, } ), - PieceInstances.observe( + PieceInstances.observeChanges( { rundownId: { $in: rundownIds, @@ -150,7 +150,7 @@ export class RundownContentObserver { projection: pieceInstanceFieldSpecifier, } ), - AdLibPieces.observe( + AdLibPieces.observeChanges( { rundownId: { $in: rundownIds, @@ -161,7 +161,7 @@ export class RundownContentObserver { projection: adLibPieceFieldSpecifier, } ), - AdLibActions.observe( + AdLibActions.observeChanges( { rundownId: { $in: rundownIds, @@ -172,7 +172,7 @@ export class RundownContentObserver { projection: adLibActionFieldSpecifier, } ), - RundownBaselineAdLibPieces.observe( + RundownBaselineAdLibPieces.observeChanges( { rundownId: { $in: rundownIds, @@ -183,7 +183,7 @@ export class RundownContentObserver { projection: adLibPieceFieldSpecifier, } ), - RundownBaselineAdLibActions.observe( + RundownBaselineAdLibActions.observeChanges( { rundownId: { $in: rundownIds, diff --git a/meteor/server/publications/rundown.ts b/meteor/server/publications/rundown.ts index 7d51b2c2fa..a33739c149 100644 --- a/meteor/server/publications/rundown.ts +++ b/meteor/server/publications/rundown.ts @@ -2,14 +2,14 @@ import { Meteor } from 'meteor/meteor' import * as _ from 'underscore' import { meteorPublish, AutoFillSelector } from './lib' import { PubSub } from '../../lib/api/pubsub' -import { MongoQuery } from '../../lib/typings/meteor' +import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { RundownReadAccess } from '../security/rundown' import { DBSegment } from '../../lib/collections/Segments' import { DBPart } from '../../lib/collections/Parts' import { Piece } from '../../lib/collections/Pieces' import { PieceInstance } from '../../lib/collections/PieceInstances' -import { DBPartInstance } from '../../lib/collections/PartInstances' +import { DBPartInstance, PartInstance } from '../../lib/collections/PartInstances' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' import { NoSecurityReadAccess } from '../security/noSecurity' import { OrganizationReadAccess } from '../security/organization' @@ -37,8 +37,16 @@ import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { IngestDataCacheObj } from '@sofie-automation/corelib/dist/dataModel/IngestDataCache' import { literal } from '@sofie-automation/corelib/dist/lib' import { MongoFieldSpecifierZeroes } from '@sofie-automation/corelib/dist/mongo' - -meteorPublish(PubSub.rundownsForDevice, async function (deviceId, token) { +import { + RundownId, + RundownPlaylistActivationId, + RundownPlaylistId, + ShowStyleBaseId, +} from '@sofie-automation/corelib/dist/dataModel/Ids' +import { ExpectedMediaItem } from '@sofie-automation/corelib/dist/dataModel/ExpectedMediaItem' +import { ExpectedPlayoutItem } from '@sofie-automation/corelib/dist/dataModel/ExpectedPlayoutItem' + +meteorPublish(PubSub.rundownsForDevice, async function (deviceId, token: string | undefined) { check(deviceId, String) check(token, String) @@ -66,42 +74,49 @@ meteorPublish(PubSub.rundownsForDevice, async function (deviceId, token) { return null }) -meteorPublish(PubSub.rundowns, async function (playlistIds, showStyleBaseIds, token) { - check(playlistIds, Match.Maybe(Array)) - check(showStyleBaseIds, Match.Maybe(Array)) +meteorPublish( + PubSub.rundowns, + async function ( + playlistIds: RundownPlaylistId[] | null, + showStyleBaseIds: ShowStyleBaseId[] | null, + token: string | undefined + ) { + check(playlistIds, Match.Maybe(Array)) + check(showStyleBaseIds, Match.Maybe(Array)) - if (!playlistIds && !showStyleBaseIds) - throw new Meteor.Error(400, 'One of playlistIds and showStyleBaseIds must be provided') + if (!playlistIds && !showStyleBaseIds) + throw new Meteor.Error(400, 'One of playlistIds and showStyleBaseIds must be provided') - // If values were provided, they must have values - if (playlistIds && playlistIds.length === 0) return null - if (showStyleBaseIds && showStyleBaseIds.length === 0) return null + // If values were provided, they must have values + if (playlistIds && playlistIds.length === 0) return null + if (showStyleBaseIds && showStyleBaseIds.length === 0) return null - const { cred, selector } = await AutoFillSelector.organizationId(this.userId, {}, token) + const { cred, selector } = await AutoFillSelector.organizationId(this.userId, {}, token) - // Add the requested filter - if (playlistIds) selector.playlistId = { $in: playlistIds } - if (showStyleBaseIds) selector.showStyleBaseId = { $in: showStyleBaseIds } + // Add the requested filter + if (playlistIds) selector.playlistId = { $in: playlistIds } + if (showStyleBaseIds) selector.showStyleBaseId = { $in: showStyleBaseIds } - const modifier: FindOptions = { - fields: { - metaData: 0, - }, - } + const modifier: FindOptions = { + fields: { + metaData: 0, + }, + } - if ( - !cred || - NoSecurityReadAccess.any() || - (selector.organizationId && - (await OrganizationReadAccess.organizationContent(selector.organizationId, cred))) || - (selector.studioId && (await StudioReadAccess.studioContent(selector.studioId, cred))) || - (selector._id && (await RundownReadAccess.rundown(selector._id, cred))) - ) { - return Rundowns.findWithCursor(selector, modifier) + if ( + !cred || + NoSecurityReadAccess.any() || + (selector.organizationId && + (await OrganizationReadAccess.organizationContent(selector.organizationId, cred))) || + (selector.studioId && (await StudioReadAccess.studioContent(selector.studioId, cred))) || + (selector._id && (await RundownReadAccess.rundown(selector._id, cred))) + ) { + return Rundowns.findWithCursor(selector, modifier) + } + return null } - return null -}) -meteorPublish(PubSub.segments, async function (selector, token) { +) +meteorPublish(PubSub.segments, async function (selector: MongoQuery, token: string | undefined) { if (!selector) throw new Meteor.Error(400, 'selector argument missing') const modifier: FindOptions = { fields: { @@ -119,7 +134,7 @@ meteorPublish(PubSub.segments, async function (selector, token) { return null }) -meteorPublish(PubSub.parts, async function (rundownIds, token) { +meteorPublish(PubSub.parts, async function (rundownIds: RundownId[], token: string | undefined) { check(rundownIds, Array) if (rundownIds.length === 0) return null @@ -145,79 +160,92 @@ meteorPublish(PubSub.parts, async function (rundownIds, token) { } return null }) -meteorPublish(PubSub.partInstances, async function (rundownIds, playlistActivationId, token?: string) { - check(rundownIds, Array) - check(playlistActivationId, Match.Maybe(String)) +meteorPublish( + PubSub.partInstances, + async function ( + rundownIds: RundownId[], + playlistActivationId: RundownPlaylistActivationId | undefined, + token: string | undefined + ) { + check(rundownIds, Array) + check(playlistActivationId, Match.Maybe(String)) - if (rundownIds.length === 0 || !playlistActivationId) return null + if (rundownIds.length === 0 || !playlistActivationId) return null - const modifier: FindOptions = { - fields: { - // @ts-expect-error Mongo typings aren't clever enough yet - 'part.metaData': 0, - }, - } + const modifier: FindOptions = { + fields: { + // @ts-expect-error Mongo typings aren't clever enough yet + 'part.metaData': 0, + }, + } - const selector: MongoQuery = { - rundownId: { $in: rundownIds }, - playlistActivationId: playlistActivationId, - reset: { $ne: true }, - } + const selector: MongoQuery = { + rundownId: { $in: rundownIds }, + playlistActivationId: playlistActivationId, + reset: { $ne: true }, + } - if ( - NoSecurityReadAccess.any() || - (await RundownReadAccess.rundownContent(selector.rundownId, { userId: this.userId, token })) - ) { - return PartInstances.findWithCursor(selector, modifier) - } - return null -}) -meteorPublish(PubSub.partInstancesSimple, async function (selector, token) { - if (!selector) throw new Meteor.Error(400, 'selector argument missing') - const modifier: FindOptions = { - fields: literal>({ - // @ts-expect-error Mongo typings aren't clever enough yet - 'part.metaData': 0, - isTaken: 0, - timings: 0, - }), + if ( + NoSecurityReadAccess.any() || + (await RundownReadAccess.rundownContent(selector.rundownId, { userId: this.userId, token })) + ) { + return PartInstances.findWithCursor(selector, modifier) + } + return null } +) +meteorPublish( + PubSub.partInstancesSimple, + async function (selector: MongoQuery, token: string | undefined) { + if (!selector) throw new Meteor.Error(400, 'selector argument missing') + const modifier: FindOptions = { + fields: literal>({ + // @ts-expect-error Mongo typings aren't clever enough yet + 'part.metaData': 0, + isTaken: 0, + timings: 0, + }), + } - // Enforce only not-reset - selector.reset = { $ne: true } + // Enforce only not-reset + selector.reset = { $ne: true } - if ( - NoSecurityReadAccess.any() || - (await RundownReadAccess.rundownContent(selector.rundownId, { userId: this.userId, token })) - ) { - return PartInstances.findWithCursor(selector, modifier) - } - return null -}) -meteorPublish(PubSub.partInstancesForSegmentPlayout, async function (selector, token) { - if (!selector) throw new Meteor.Error(400, 'selector argument missing') - const modifier: FindOptions = { - fields: { - // @ts-expect-error Mongo typings aren't clever enough yet - 'part.metaData': 0, - }, - sort: { - takeCount: 1, - }, - limit: 1, + if ( + NoSecurityReadAccess.any() || + (await RundownReadAccess.rundownContent(selector.rundownId, { userId: this.userId, token })) + ) { + return PartInstances.findWithCursor(selector, modifier) + } + return null } +) +meteorPublish( + PubSub.partInstancesForSegmentPlayout, + async function (selector: MongoQuery, token: string | undefined) { + if (!selector) throw new Meteor.Error(400, 'selector argument missing') + const modifier: FindOptions = { + fields: { + // @ts-expect-error Mongo typings aren't clever enough yet + 'part.metaData': 0, + }, + sort: { + takeCount: 1, + }, + limit: 1, + } - if ( - NoSecurityReadAccess.any() || - (selector.segmentPlayoutId && - (await RundownReadAccess.rundownContent(selector.rundownId, { userId: this.userId, token }))) - ) { - return PartInstances.findWithCursor(selector, modifier) + if ( + NoSecurityReadAccess.any() || + (selector.segmentPlayoutId && + (await RundownReadAccess.rundownContent(selector.rundownId, { userId: this.userId, token }))) + ) { + return PartInstances.findWithCursor(selector, modifier) + } + return null } - return null -}) +) -meteorPublish(PubSub.pieces, async function (selector: MongoQuery, token?: string) { +meteorPublish(PubSub.pieces, async function (selector: MongoQuery, token: string | undefined) { if (!selector) throw new Meteor.Error(400, 'selector argument missing') const modifier: FindOptions = { fields: { @@ -234,7 +262,7 @@ meteorPublish(PubSub.pieces, async function (selector: MongoQuery, token? return null }) -meteorPublish(PubSub.adLibPieces, async function (selector, token) { +meteorPublish(PubSub.adLibPieces, async function (selector: MongoQuery, token: string | undefined) { if (!selector) throw new Meteor.Error(400, 'selector argument missing') const modifier: FindOptions = { fields: { @@ -250,7 +278,7 @@ meteorPublish(PubSub.adLibPieces, async function (selector, token) { } return null }) -meteorPublish(PubSub.pieceInstances, async function (selector, token) { +meteorPublish(PubSub.pieceInstances, async function (selector: MongoQuery, token: string | undefined) { if (!selector) throw new Meteor.Error(400, 'selector argument missing') const modifier: FindOptions = { fields: { @@ -272,80 +300,92 @@ meteorPublish(PubSub.pieceInstances, async function (selector, token) { return null }) -meteorPublish(PubSub.pieceInstancesSimple, async function (selector, token) { - if (!selector) throw new Meteor.Error(400, 'selector argument missing') - const modifier: FindOptions = { - fields: literal>({ - // @ts-expect-error Mongo typings aren't clever enough yet - 'piece.metaData': 0, - 'piece.timelineObjectsString': 0, - plannedStartedPlayback: 0, - plannedStoppedPlayback: 0, - }), - } +meteorPublish( + PubSub.pieceInstancesSimple, + async function (selector: MongoQuery, token: string | undefined) { + if (!selector) throw new Meteor.Error(400, 'selector argument missing') + const modifier: FindOptions = { + fields: literal>({ + // @ts-expect-error Mongo typings aren't clever enough yet + 'piece.metaData': 0, + 'piece.timelineObjectsString': 0, + plannedStartedPlayback: 0, + plannedStoppedPlayback: 0, + }), + } - // Enforce only not-reset - selector.reset = { $ne: true } + // Enforce only not-reset + selector.reset = { $ne: true } - if ( - NoSecurityReadAccess.any() || - (await RundownReadAccess.rundownContent(selector.rundownId, { userId: this.userId, token })) - ) { - return PieceInstances.findWithCursor(selector, modifier) + if ( + NoSecurityReadAccess.any() || + (await RundownReadAccess.rundownContent(selector.rundownId, { userId: this.userId, token })) + ) { + return PieceInstances.findWithCursor(selector, modifier) + } + return null } - return null -}) -meteorPublish(PubSub.expectedMediaItems, async function (selector, token) { - const allowed = - NoSecurityReadAccess.any() || - (await RundownReadAccess.expectedMediaItems(selector, { userId: this.userId, token })) - if (!allowed) { +) +meteorPublish( + PubSub.expectedMediaItems, + async function (selector: MongoQuery, token: string | undefined) { + const allowed = + NoSecurityReadAccess.any() || + (await RundownReadAccess.expectedMediaItems(selector, { userId: this.userId, token })) + if (!allowed) { + return null + } else if (allowed === true) { + return ExpectedMediaItems.findWithCursor(selector) + } else if (typeof allowed === 'object') { + return ExpectedMediaItems.findWithCursor( + _.extend(selector, { + studioId: allowed.studioId, + }) + ) + } return null - } else if (allowed === true) { - return ExpectedMediaItems.findWithCursor(selector) - } else if (typeof allowed === 'object') { - return ExpectedMediaItems.findWithCursor( - _.extend(selector, { - studioId: allowed.studioId, - }) - ) } - return null -}) -meteorPublish(PubSub.expectedPlayoutItems, async function (selector, token) { - const allowed = - NoSecurityReadAccess.any() || - (await RundownReadAccess.expectedPlayoutItems(selector, { userId: this.userId, token })) - if (!allowed) { +) +meteorPublish( + PubSub.expectedPlayoutItems, + async function (selector: MongoQuery, token: string | undefined) { + const allowed = + NoSecurityReadAccess.any() || + (await RundownReadAccess.expectedPlayoutItems(selector, { userId: this.userId, token })) + if (!allowed) { + return null + } else if (allowed === true) { + return ExpectedPlayoutItems.findWithCursor(selector) + } else if (typeof allowed === 'object') { + return ExpectedPlayoutItems.findWithCursor( + _.extend(selector, { + studioId: allowed.studioId, + }) + ) + } return null - } else if (allowed === true) { - return ExpectedPlayoutItems.findWithCursor(selector) - } else if (typeof allowed === 'object') { - return ExpectedPlayoutItems.findWithCursor( - _.extend(selector, { - studioId: allowed.studioId, - }) - ) } - return null -}) +) // Note: this publication is for dev purposes only: -meteorPublish(PubSub.ingestDataCache, async function (selector, token) { - if (!selector) throw new Meteor.Error(400, 'selector argument missing') - const modifier: FindOptions = { - fields: {}, - } - if ( - NoSecurityReadAccess.any() || - (await RundownReadAccess.rundownContent(selector.rundownId, { userId: this.userId, token })) - ) { - return IngestDataCache.findWithCursor(selector, modifier) +meteorPublish( + PubSub.ingestDataCache, + async function (selector: MongoQuery, token: string | undefined) { + if (!selector) throw new Meteor.Error(400, 'selector argument missing') + const modifier: FindOptions = { + fields: {}, + } + if ( + NoSecurityReadAccess.any() || + (await RundownReadAccess.rundownContent(selector.rundownId, { userId: this.userId, token })) + ) { + return IngestDataCache.findWithCursor(selector, modifier) + } + return null } - return null -}) +) meteorPublish( PubSub.rundownBaselineAdLibPieces, - async function (selector: MongoQuery, token?: string) { + async function (selector: MongoQuery, token: string | undefined) { if (!selector) throw new Meteor.Error(400, 'selector argument missing') const modifier: FindOptions = { fields: { @@ -361,7 +401,7 @@ meteorPublish( return null } ) -meteorPublish(PubSub.adLibActions, async function (selector, token) { +meteorPublish(PubSub.adLibActions, async function (selector: MongoQuery, token: string | undefined) { if (!selector) throw new Meteor.Error(400, 'selector argument missing') const modifier: FindOptions = { fields: {}, @@ -374,16 +414,19 @@ meteorPublish(PubSub.adLibActions, async function (selector, token) { } return null }) -meteorPublish(PubSub.rundownBaselineAdLibActions, async function (selector, token) { - if (!selector) throw new Meteor.Error(400, 'selector argument missing') - const modifier: FindOptions = { - fields: {}, - } - if ( - NoSecurityReadAccess.any() || - (await RundownReadAccess.rundownContent(selector.rundownId, { userId: this.userId, token })) - ) { - return RundownBaselineAdLibActions.findWithCursor(selector, modifier) +meteorPublish( + PubSub.rundownBaselineAdLibActions, + async function (selector: MongoQuery, token: string | undefined) { + if (!selector) throw new Meteor.Error(400, 'selector argument missing') + const modifier: FindOptions = { + fields: {}, + } + if ( + NoSecurityReadAccess.any() || + (await RundownReadAccess.rundownContent(selector.rundownId, { userId: this.userId, token })) + ) { + return RundownBaselineAdLibActions.findWithCursor(selector, modifier) + } + return null } - return null -}) +) diff --git a/meteor/server/publications/rundownPlaylist.ts b/meteor/server/publications/rundownPlaylist.ts index 54448a6ac2..724212cb0b 100644 --- a/meteor/server/publications/rundownPlaylist.ts +++ b/meteor/server/publications/rundownPlaylist.ts @@ -7,21 +7,29 @@ import { NoSecurityReadAccess } from '../security/noSecurity' import { isProtectedString } from '@sofie-automation/corelib/dist/protectedString' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { RundownPlaylists } from '../collections' +import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' -meteorPublish(PubSub.rundownPlaylists, async function (selector0, token) { - const { cred, selector } = await AutoFillSelector.organizationId(this.userId, selector0, token) - const modifier = { - fields: {}, +meteorPublish( + PubSub.rundownPlaylists, + async function (selector0: MongoQuery, token: string | undefined) { + const { cred, selector } = await AutoFillSelector.organizationId( + this.userId, + selector0, + token + ) + const modifier = { + fields: {}, + } + if ( + !cred || + NoSecurityReadAccess.any() || + (selector.organizationId && + (await OrganizationReadAccess.organizationContent(selector.organizationId, cred))) || + (selector.studioId && (await StudioReadAccess.studioContent(selector.studioId, cred))) || + (isProtectedString(selector._id) && (await RundownPlaylistReadAccess.rundownPlaylist(selector._id, cred))) + ) { + return RundownPlaylists.findWithCursor(selector, modifier) + } + return null } - if ( - !cred || - NoSecurityReadAccess.any() || - (selector.organizationId && - (await OrganizationReadAccess.organizationContent(selector.organizationId, cred))) || - (selector.studioId && (await StudioReadAccess.studioContent(selector.studioId, cred))) || - (isProtectedString(selector._id) && (await RundownPlaylistReadAccess.rundownPlaylist(selector._id, cred))) - ) { - return RundownPlaylists.findWithCursor(selector, modifier) - } - return null -}) +) diff --git a/meteor/server/publications/segmentPartNotesUI/rundownContentObserver.ts b/meteor/server/publications/segmentPartNotesUI/rundownContentObserver.ts index 2a687fd9b3..43f9c25270 100644 --- a/meteor/server/publications/segmentPartNotesUI/rundownContentObserver.ts +++ b/meteor/server/publications/segmentPartNotesUI/rundownContentObserver.ts @@ -19,7 +19,7 @@ export class RundownContentObserver { this.#cache = cache this.#observers = [ - Rundowns.observe( + Rundowns.observeChanges( { _id: { $in: rundownIds, @@ -30,7 +30,7 @@ export class RundownContentObserver { projection: rundownFieldSpecifier, } ), - Segments.observe( + Segments.observeChanges( { rundownId: { $in: rundownIds, @@ -41,7 +41,7 @@ export class RundownContentObserver { projection: segmentFieldSpecifier, } ), - Parts.observe( + Parts.observeChanges( { rundownId: { $in: rundownIds, @@ -52,7 +52,7 @@ export class RundownContentObserver { projection: partFieldSpecifier, } ), - PartInstances.observe( + PartInstances.observeChanges( { rundownId: { $in: rundownIds }, reset: { $ne: true }, orphaned: 'deleted' }, cache.DeletedPartInstances.link(), { fields: partInstanceFieldSpecifier } diff --git a/meteor/server/publications/showStyle.ts b/meteor/server/publications/showStyle.ts index aedb92c8fc..f5aa35b902 100644 --- a/meteor/server/publications/showStyle.ts +++ b/meteor/server/publications/showStyle.ts @@ -8,67 +8,80 @@ import { OrganizationReadAccess } from '../security/organization' import { FindOptions } from '../../lib/collections/lib' import { NoSecurityReadAccess } from '../security/noSecurity' import { RundownLayouts, ShowStyleBases, ShowStyleVariants, TriggeredActions } from '../collections' -import { TriggeredActionsObj } from '../../lib/collections/TriggeredActions' +import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' +import { DBTriggeredActions } from '../../lib/collections/TriggeredActions' -meteorPublish(PubSub.showStyleBases, async function (selector0, token) { - const { cred, selector } = await AutoFillSelector.organizationId(this.userId, selector0, token) - const modifier: FindOptions = { - fields: {}, +meteorPublish( + PubSub.showStyleBases, + async function (selector0: MongoQuery, token: string | undefined) { + const { cred, selector } = await AutoFillSelector.organizationId(this.userId, selector0, token) + const modifier: FindOptions = { + fields: {}, + } + if ( + !cred || + NoSecurityReadAccess.any() || + (selector.organizationId && + (await OrganizationReadAccess.organizationContent(selector.organizationId, cred))) || + (selector._id && (await ShowStyleReadAccess.showStyleBase(selector.id, cred))) + ) { + return ShowStyleBases.findWithCursor(selector, modifier) + } + return null } - if ( - !cred || - NoSecurityReadAccess.any() || - (selector.organizationId && - (await OrganizationReadAccess.organizationContent(selector.organizationId, cred))) || - (selector._id && (await ShowStyleReadAccess.showStyleBase(selector, cred))) - ) { - return ShowStyleBases.findWithCursor(selector, modifier) - } - return null -}) +) -meteorPublish(PubSub.showStyleVariants, async function (selector0, token) { - const { cred, selector } = await AutoFillSelector.showStyleBaseId(this.userId, selector0, token) +meteorPublish( + PubSub.showStyleVariants, + async function (selector0: MongoQuery, token: string | undefined) { + const { cred, selector } = await AutoFillSelector.showStyleBaseId(this.userId, selector0, token) - const modifier: FindOptions = { - fields: {}, - } - if ( - !cred || - NoSecurityReadAccess.any() || - (selector.showStyleBaseId && (await ShowStyleReadAccess.showStyleBaseContent(selector, cred))) || - (selector._id && (await ShowStyleReadAccess.showStyleVariant(selector._id, cred))) - ) { - return ShowStyleVariants.findWithCursor(selector, modifier) + const modifier: FindOptions = { + fields: {}, + } + if ( + !cred || + NoSecurityReadAccess.any() || + (selector.showStyleBaseId && (await ShowStyleReadAccess.showStyleBaseContent(selector, cred))) || + (selector._id && (await ShowStyleReadAccess.showStyleVariant(selector._id, cred))) + ) { + return ShowStyleVariants.findWithCursor(selector, modifier) + } + return null } - return null -}) +) -meteorPublish(PubSub.rundownLayouts, async function (selector0, token) { - const { cred, selector } = await AutoFillSelector.showStyleBaseId(this.userId, selector0, token) +meteorPublish( + PubSub.rundownLayouts, + async function (selector0: MongoQuery, token: string | undefined) { + const { cred, selector } = await AutoFillSelector.showStyleBaseId(this.userId, selector0, token) - const modifier: FindOptions = { - fields: {}, + const modifier: FindOptions = { + fields: {}, + } + if (!cred || (await ShowStyleReadAccess.showStyleBaseContent(selector, cred))) { + return RundownLayouts.findWithCursor(selector, modifier) + } + return null } - if (!cred || (await ShowStyleReadAccess.showStyleBaseContent(selector, cred))) { - return RundownLayouts.findWithCursor(selector, modifier) - } - return null -}) +) -meteorPublish(PubSub.triggeredActions, async function (selector0, token) { - const { cred, selector } = await AutoFillSelector.showStyleBaseId(this.userId, selector0, token) +meteorPublish( + PubSub.triggeredActions, + async function (selector0: MongoQuery, token: string | undefined) { + const { cred, selector } = await AutoFillSelector.showStyleBaseId(this.userId, selector0, token) - const modifier: FindOptions = { - fields: {}, - } + const modifier: FindOptions = { + fields: {}, + } - if ( - !cred || - NoSecurityReadAccess.any() || - (selector.showStyleBaseId && (await ShowStyleReadAccess.showStyleBaseContent(selector, cred))) - ) { - return TriggeredActions.findWithCursor(selector, modifier) + if ( + !cred || + NoSecurityReadAccess.any() || + (selector.showStyleBaseId && (await ShowStyleReadAccess.showStyleBaseContent(selector, cred))) + ) { + return TriggeredActions.findWithCursor(selector, modifier) + } + return null } - return null -}) +) diff --git a/meteor/server/publications/showStyleUI.ts b/meteor/server/publications/showStyleUI.ts index 66fb68fc07..5ca3e50863 100644 --- a/meteor/server/publications/showStyleUI.ts +++ b/meteor/server/publications/showStyleUI.ts @@ -99,7 +99,7 @@ meteorCustomPublish( NoSecurityReadAccess.any() || (selector.organizationId && (await OrganizationReadAccess.organizationContent(selector.organizationId, cred))) || - (selector._id && (await ShowStyleReadAccess.showStyleBase(selector, cred))) + (selector._id && (await ShowStyleReadAccess.showStyleBase(selector._id, cred))) ) { await setUpOptimizedObserverArray< UIShowStyleBase, diff --git a/meteor/server/publications/studio.ts b/meteor/server/publications/studio.ts index 0f96e5cabc..09974176f8 100644 --- a/meteor/server/publications/studio.ts +++ b/meteor/server/publications/studio.ts @@ -14,7 +14,7 @@ import { setUpOptimizedObserverArray, TriggerUpdate, } from '../lib/customPublication' -import { ExpectedPackageDBBase } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' +import { ExpectedPackageDB, ExpectedPackageDBBase } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' import { ExpectedPackageWorkStatus } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackageWorkStatuses' import { literal } from '../../lib/lib' import { ReadonlyDeep } from 'type-fest' @@ -29,8 +29,10 @@ import { PeripheralDevices, Studios, } from '../collections' +import { PackageContainerStatusDB } from '@sofie-automation/corelib/dist/dataModel/PackageContainerStatus' +import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' -meteorPublish(PubSub.studios, async function (selector0, token) { +meteorPublish(PubSub.studios, async function (selector0: MongoQuery, token: string | undefined) { const { cred, selector } = await AutoFillSelector.organizationId(this.userId, selector0, token) const modifier: FindOptions = { fields: {}, @@ -46,53 +48,65 @@ meteorPublish(PubSub.studios, async function (selector0, token) { return null }) -meteorPublish(PubSub.externalMessageQueue, async function (selector, token) { - if (!selector) throw new Meteor.Error(400, 'selector argument missing') - const modifier: FindOptions = { - fields: {}, - } - if (await StudioReadAccess.studioContent(selector.studioId, { userId: this.userId, token })) { - return ExternalMessageQueue.findWithCursor(selector, modifier) +meteorPublish( + PubSub.externalMessageQueue, + async function (selector: MongoQuery, token: string | undefined) { + if (!selector) throw new Meteor.Error(400, 'selector argument missing') + const modifier: FindOptions = { + fields: {}, + } + if (await StudioReadAccess.studioContent(selector.studioId, { userId: this.userId, token })) { + return ExternalMessageQueue.findWithCursor(selector, modifier) + } + return null } - return null -}) +) -meteorPublish(PubSub.expectedPackages, async function (selector, token) { - // Note: This differs from the expected packages sent to the Package Manager, instead @see PubSub.expectedPackagesForDevice - if (!selector) throw new Meteor.Error(400, 'selector argument missing') - const modifier: FindOptions = { - fields: {}, - } - if (await StudioReadAccess.studioContent(selector.studioId, { userId: this.userId, token })) { - return ExpectedPackages.findWithCursor(selector, modifier) - } - return null -}) -meteorPublish(PubSub.expectedPackageWorkStatuses, async function (selector, token) { - if (!selector) throw new Meteor.Error(400, 'selector argument missing') - const modifier: FindOptions = { - fields: {}, - } - if (await StudioReadAccess.studioContent(selector.studioId, { userId: this.userId, token })) { - return ExpectedPackageWorkStatuses.findWithCursor(selector, modifier) +meteorPublish( + PubSub.expectedPackages, + async function (selector: MongoQuery, token: string | undefined) { + // Note: This differs from the expected packages sent to the Package Manager, instead @see PubSub.expectedPackagesForDevice + if (!selector) throw new Meteor.Error(400, 'selector argument missing') + const modifier: FindOptions = { + fields: {}, + } + if (await StudioReadAccess.studioContent(selector.studioId, { userId: this.userId, token })) { + return ExpectedPackages.findWithCursor(selector, modifier) + } + return null } - return null -}) -meteorPublish(PubSub.packageContainerStatuses, async function (selector, token) { - if (!selector) throw new Meteor.Error(400, 'selector argument missing') - const modifier: FindOptions = { - fields: {}, +) +meteorPublish( + PubSub.expectedPackageWorkStatuses, + async function (selector: MongoQuery, token: string | undefined) { + if (!selector) throw new Meteor.Error(400, 'selector argument missing') + const modifier: FindOptions = { + fields: {}, + } + if (await StudioReadAccess.studioContent(selector.studioId, { userId: this.userId, token })) { + return ExpectedPackageWorkStatuses.findWithCursor(selector, modifier) + } + return null } - if (await StudioReadAccess.studioContent(selector.studioId, { userId: this.userId, token })) { - return PackageContainerStatuses.findWithCursor(selector, modifier) +) +meteorPublish( + PubSub.packageContainerStatuses, + async function (selector: MongoQuery, token: string | undefined) { + if (!selector) throw new Meteor.Error(400, 'selector argument missing') + const modifier: FindOptions = { + fields: {}, + } + if (await StudioReadAccess.studioContent(selector.studioId, { userId: this.userId, token })) { + return PackageContainerStatuses.findWithCursor(selector, modifier) + } + return null } - return null -}) +) meteorCustomPublish( PubSub.mappingsForDevice, CustomCollectionName.StudioMappings, - async function (pub, deviceId: PeripheralDeviceId, token) { + async function (pub, deviceId: PeripheralDeviceId, token: string | undefined) { check(deviceId, String) if (await PeripheralDeviceReadAccess.peripheralDeviceContent(deviceId, { userId: this.userId, token })) { @@ -111,7 +125,7 @@ meteorCustomPublish( meteorCustomPublish( PubSub.mappingsForStudio, CustomCollectionName.StudioMappings, - async function (pub, studioId: StudioId, token) { + async function (pub, studioId: StudioId, token: string | undefined) { check(studioId, String) if (await StudioReadAccess.studio(studioId, { userId: this.userId, token })) { diff --git a/meteor/server/publications/system.ts b/meteor/server/publications/system.ts index 74df9f9267..b028840ae2 100644 --- a/meteor/server/publications/system.ts +++ b/meteor/server/publications/system.ts @@ -5,8 +5,10 @@ import { SystemReadAccess } from '../security/system' import { OrganizationReadAccess } from '../security/organization' import { CoreSystem, Users } from '../collections' import { SYSTEM_ID } from '../../lib/collections/CoreSystem' +import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' +import { DBUser } from '../../lib/collections/Users' -meteorPublish(PubSub.coreSystem, async function (token) { +meteorPublish(PubSub.coreSystem, async function (token: string | undefined) { if (await SystemReadAccess.coreSystem({ userId: this.userId, token })) { return CoreSystem.findWithCursor(SYSTEM_ID, { fields: { @@ -26,7 +28,7 @@ meteorPublish(PubSub.coreSystem, async function (token) { return null }) -meteorPublish(PubSub.loggedInUser, async function (token) { +meteorPublish(PubSub.loggedInUser, async function (token: string | undefined) { const currentUserId = this.userId if (!currentUserId) return null @@ -49,7 +51,7 @@ meteorPublish(PubSub.loggedInUser, async function (token) { } return null }) -meteorPublish(PubSub.usersInOrganization, async function (selector, token) { +meteorPublish(PubSub.usersInOrganization, async function (selector: MongoQuery, token: string | undefined) { if (!selector) throw new Meteor.Error(400, 'selector argument missing') if (await OrganizationReadAccess.adminUsers(selector.organizationId, { userId: this.userId, token })) { return Users.findWithCursor(selector, { diff --git a/meteor/server/publications/timeline.ts b/meteor/server/publications/timeline.ts index b00661f015..2da744645b 100644 --- a/meteor/server/publications/timeline.ts +++ b/meteor/server/publications/timeline.ts @@ -30,8 +30,9 @@ import { PeripheralDeviceId, StudioId } from '@sofie-automation/corelib/dist/dat import { DBTimelineDatastoreEntry } from '@sofie-automation/corelib/dist/dataModel/TimelineDatastore' import { PeripheralDevices, Studios, Timeline, TimelineDatastore } from '../collections' import { check } from 'meteor/check' +import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' -meteorPublish(PubSub.timeline, async function (selector, token) { +meteorPublish(PubSub.timeline, async function (selector: MongoQuery, token: string | undefined) { if (!selector) throw new Meteor.Error(400, 'selector argument missing') const modifier: FindOptions = { fields: {}, @@ -41,7 +42,7 @@ meteorPublish(PubSub.timeline, async function (selector, token) { } return null }) -meteorPublish(PubSub.timelineDatastore, async function (studioId, token) { +meteorPublish(PubSub.timelineDatastore, async function (studioId: StudioId, token: string | undefined) { if (!studioId) throw new Meteor.Error(400, 'selector argument missing') const modifier: FindOptions = { fields: {}, @@ -55,7 +56,7 @@ meteorPublish(PubSub.timelineDatastore, async function (studioId, token) { meteorCustomPublish( PubSub.timelineForDevice, CustomCollectionName.StudioTimeline, - async function (pub, deviceId: PeripheralDeviceId, token) { + async function (pub, deviceId: PeripheralDeviceId, token: string | undefined) { check(deviceId, String) if (await PeripheralDeviceReadAccess.peripheralDeviceContent(deviceId, { userId: this.userId, token })) { @@ -70,29 +71,32 @@ meteorCustomPublish( } } ) -meteorPublish(PubSub.timelineDatastoreForDevice, async function (deviceId, token) { - check(deviceId, String) +meteorPublish( + PubSub.timelineDatastoreForDevice, + async function (deviceId: PeripheralDeviceId, token: string | undefined) { + check(deviceId, String) - if (await PeripheralDeviceReadAccess.peripheralDeviceContent(deviceId, { userId: this.userId, token })) { - const peripheralDevice = await PeripheralDevices.findOneAsync(deviceId) + if (await PeripheralDeviceReadAccess.peripheralDeviceContent(deviceId, { userId: this.userId, token })) { + const peripheralDevice = await PeripheralDevices.findOneAsync(deviceId) - if (!peripheralDevice) throw new Meteor.Error('PeripheralDevice "' + deviceId + '" not found') + if (!peripheralDevice) throw new Meteor.Error('PeripheralDevice "' + deviceId + '" not found') - const studioId = peripheralDevice.studioId - if (!studioId) return null - const modifier: FindOptions = { - fields: {}, - } + const studioId = peripheralDevice.studioId + if (!studioId) return null + const modifier: FindOptions = { + fields: {}, + } - return TimelineDatastore.findWithCursor({ studioId }, modifier) + return TimelineDatastore.findWithCursor({ studioId }, modifier) + } + return null } - return null -}) +) meteorCustomPublish( PubSub.timelineForStudio, CustomCollectionName.StudioTimeline, - async function (pub, studioId: StudioId, token) { + async function (pub, studioId: StudioId, token: string | undefined) { if (await StudioReadAccess.studio(studioId, { userId: this.userId, token })) { await createObserverForTimelinePublication(pub, studioId) } diff --git a/meteor/server/publications/translationsBundles.ts b/meteor/server/publications/translationsBundles.ts index af187e89b7..78ef535d9f 100644 --- a/meteor/server/publications/translationsBundles.ts +++ b/meteor/server/publications/translationsBundles.ts @@ -4,17 +4,22 @@ import { TranslationsBundlesSecurity } from '../security/translationsBundles' import { meteorPublish } from './lib' import { PubSub } from '../../lib/api/pubsub' import { TranslationsBundles } from '../collections' +import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' +import { TranslationsBundle } from '../../lib/collections/TranslationsBundles' -meteorPublish(PubSub.translationsBundles, async (selector, token) => { - if (!selector) throw new Meteor.Error(400, 'selector argument missing') +meteorPublish( + PubSub.translationsBundles, + async (selector: MongoQuery, token: string | undefined) => { + if (!selector) throw new Meteor.Error(400, 'selector argument missing') - if (TranslationsBundlesSecurity.allowReadAccess(selector, token, this)) { - return TranslationsBundles.findWithCursor(selector, { - fields: { - data: 0, - }, - }) - } + if (TranslationsBundlesSecurity.allowReadAccess(selector, token, this)) { + return TranslationsBundles.findWithCursor(selector, { + fields: { + data: 0, + }, + }) + } - return null -}) + return null + } +) diff --git a/meteor/server/publications/triggeredActionsUI.ts b/meteor/server/publications/triggeredActionsUI.ts index da80b87519..4c87db2271 100644 --- a/meteor/server/publications/triggeredActionsUI.ts +++ b/meteor/server/publications/triggeredActionsUI.ts @@ -1,7 +1,6 @@ import { ShowStyleBaseId, TriggeredActionId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { Meteor } from 'meteor/meteor' -import { Mongo } from 'meteor/mongo' import { ReadonlyDeep } from 'type-fest' import { CustomCollectionName, PubSub } from '../../lib/api/pubsub' import { DBTriggeredActions, UITriggeredActionsObj } from '../../lib/collections/TriggeredActions' @@ -18,6 +17,7 @@ import { NoSecurityReadAccess } from '../security/noSecurity' import { ShowStyleReadAccess } from '../security/showStyle' import { TriggeredActions } from '../collections' import { check, Match } from 'meteor/check' +import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' interface UITriggeredActionsArgs { readonly showStyleBaseId: ShowStyleBaseId | null @@ -32,8 +32,8 @@ interface UITriggeredActionsUpdateProps { function compileMongoSelector( showStyleBaseId: ShowStyleBaseId | null, docIds?: readonly TriggeredActionId[] -): Mongo.Selector { - const selector: Mongo.Selector = { showStyleBaseId: null } +): MongoQuery { + const selector: MongoQuery = { showStyleBaseId: null } if (showStyleBaseId) { selector.showStyleBaseId = { $in: [null, showStyleBaseId] } } @@ -114,7 +114,7 @@ meteorCustomPublish( if ( !cred || NoSecurityReadAccess.any() || - (showStyleBaseId && (await ShowStyleReadAccess.showStyleBase({ _id: showStyleBaseId }, cred))) + (showStyleBaseId && (await ShowStyleReadAccess.showStyleBase(showStyleBaseId, cred))) ) { await setUpCollectionOptimizedObserver< UITriggeredActionsObj, diff --git a/meteor/server/security/lib/security.ts b/meteor/server/security/lib/security.ts index c9e0cdb12f..98f3923c61 100644 --- a/meteor/server/security/lib/security.ts +++ b/meteor/server/security/lib/security.ts @@ -1,5 +1,5 @@ import * as _ from 'underscore' -import { MongoQueryKey } from '../../../lib/typings/meteor' +import { MongoQueryKey } from '@sofie-automation/corelib/dist/mongo' import { Settings } from '../../../lib/Settings' import { resolveCredentials, ResolvedCredentials, Credentials, isResolvedCredentials } from './credentials' import { allAccess, noAccess, combineAccess, Access } from './access' diff --git a/meteor/server/security/organization.ts b/meteor/server/security/organization.ts index a1af8cc84c..2a9045137d 100644 --- a/meteor/server/security/organization.ts +++ b/meteor/server/security/organization.ts @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor' import { SnapshotItem } from '../../lib/collections/Snapshots' import { Blueprint } from '../../lib/collections/Blueprints' import { logNotAllowed } from './lib/lib' -import { MongoQueryKey } from '../../lib/typings/meteor' +import { MongoQueryKey } from '@sofie-automation/corelib/dist/mongo' import { allowAccessToOrganization } from './lib/security' import { Credentials, ResolvedCredentials, resolveCredentials } from './lib/credentials' import { Settings } from '../../lib/Settings' diff --git a/meteor/server/security/peripheralDevice.ts b/meteor/server/security/peripheralDevice.ts index 7bb1c0e7a8..c818732796 100644 --- a/meteor/server/security/peripheralDevice.ts +++ b/meteor/server/security/peripheralDevice.ts @@ -4,7 +4,7 @@ import { PeripheralDevice } from '../../lib/collections/PeripheralDevices' import { isProtectedString } from '../../lib/lib' import { logNotAllowed } from './lib/lib' import { MediaWorkFlow } from '@sofie-automation/shared-lib/dist/core/model/MediaWorkFlows' -import { MongoQueryKey } from '../../lib/typings/meteor' +import { MongoQueryKey } from '@sofie-automation/corelib/dist/mongo' import { Credentials, ResolvedCredentials, resolveCredentials } from './lib/credentials' import { allowAccessToPeripheralDevice, allowAccessToPeripheralDeviceContent } from './lib/security' import { Settings } from '../../lib/Settings' diff --git a/meteor/server/security/rundown.ts b/meteor/server/security/rundown.ts index 4250e23cf6..1520ad5360 100644 --- a/meteor/server/security/rundown.ts +++ b/meteor/server/security/rundown.ts @@ -1,8 +1,6 @@ import { Meteor } from 'meteor/meteor' import { check } from '../../lib/check' -import { Mongo } from 'meteor/mongo' import * as _ from 'underscore' -import { MongoQueryKey } from '../../lib/typings/meteor' import { Credentials, ResolvedCredentials } from './lib/credentials' import { logNotAllowed } from './lib/lib' import { allowAccessToRundown } from './lib/security' @@ -14,6 +12,7 @@ import { Settings } from '../../lib/Settings' import { RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PeripheralDevices, Segments } from '../collections' import { getStudioIdFromDevice } from '../api/studio/lib' +import { MongoQuery, MongoQueryKey } from '@sofie-automation/corelib/dist/mongo' export namespace RundownReadAccess { /** Check for read access to the rundown collection */ @@ -66,7 +65,7 @@ export namespace RundownReadAccess { } /** Check for read access for exoected media items in a rundown */ export async function expectedMediaItems( - selector: Mongo.Query | any, + selector: MongoQuery | any, cred: Credentials ): Promise { check(selector, Object) @@ -100,7 +99,7 @@ export namespace RundownReadAccess { /** Check for read access to expectedPlayoutItems */ export async function expectedPlayoutItems( - selector: Mongo.Query | any, + selector: MongoQuery | any, cred: Credentials ): Promise { check(selector, Object) diff --git a/meteor/server/security/showStyle.ts b/meteor/server/security/showStyle.ts index 61127a1cd3..0097216601 100644 --- a/meteor/server/security/showStyle.ts +++ b/meteor/server/security/showStyle.ts @@ -3,7 +3,7 @@ import { check } from '../../lib/check' import { logNotAllowed } from './lib/lib' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' import { RundownLayoutBase } from '../../lib/collections/RundownLayouts' -import { MongoQuery, MongoQueryKey } from '../../lib/typings/meteor' +import { MongoQuery, MongoQueryKey } from '@sofie-automation/corelib/dist/mongo' import { Credentials, ResolvedCredentials, resolveCredentials } from './lib/credentials' import { allowAccessToShowStyleBase, allowAccessToShowStyleVariant } from './lib/security' import { triggerWriteAccess } from './lib/securityVerify' @@ -33,10 +33,10 @@ export interface ShowStyleContentAccess { export namespace ShowStyleReadAccess { /** Handles read access for all showstyle document */ export async function showStyleBase( - selector: MongoQuery<{ _id: ShowStyleBaseId }>, + showStyleBaseId: MongoQueryKey, cred: Credentials | ResolvedCredentials ): Promise { - return showStyleBaseContent({ showStyleBaseId: selector._id }, cred) + return showStyleBaseContent({ showStyleBaseId }, cred) } /** Handles read access for all showstyle content */ diff --git a/meteor/server/security/studio.ts b/meteor/server/security/studio.ts index b8ba46680e..d77039c253 100644 --- a/meteor/server/security/studio.ts +++ b/meteor/server/security/studio.ts @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor' import { allowAccessToStudio } from './lib/security' -import { MongoQueryKey } from '../../lib/typings/meteor' +import { MongoQueryKey } from '@sofie-automation/corelib/dist/mongo' import { logNotAllowed } from './lib/lib' import { ExternalMessageQueueObj } from '../../lib/collections/ExternalMessageQueue' import { Credentials, ResolvedCredentials, resolveCredentials } from './lib/credentials' diff --git a/meteor/server/webmanifest.ts b/meteor/server/webmanifest.ts index 40e80f3602..81fbbe92fd 100644 --- a/meteor/server/webmanifest.ts +++ b/meteor/server/webmanifest.ts @@ -5,7 +5,7 @@ import type { ShortcutItem, } from '../lib/typings/webmanifest' import { logger } from '../lib/logging' -import { MongoQuery } from '../lib/typings/meteor' +import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { DBStudio } from '../lib/collections/Studios' import { RundownPlaylists, Rundowns } from './collections' import { getLocale, Translations } from './lib' diff --git a/meteor/server/worker/worker.ts b/meteor/server/worker/worker.ts index 2732597bfd..4ac02f4ab7 100644 --- a/meteor/server/worker/worker.ts +++ b/meteor/server/worker/worker.ts @@ -24,7 +24,7 @@ import { fetchStudioLight } from '../optimizations' import * as path from 'path' import { LogEntry } from 'winston' import { initializeWorkerStatus, setWorkerStatus } from './workerStatus' -import { MongoQuery } from '../../lib/typings/meteor' +import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { UserActionsLog } from '../collections' import { MetricsCounter } from '@sofie-automation/corelib/dist/prometheus' diff --git a/meteor/tsconfig-base.json b/meteor/tsconfig-base.json index 9e827652c1..6c233014bd 100644 --- a/meteor/tsconfig-base.json +++ b/meteor/tsconfig-base.json @@ -9,7 +9,14 @@ "skipLibCheck": true, "sourceMap": true, "allowJs": false, - "lib": ["dom", "es6", "dom.iterable", "scripthost", "es2017", "es2018", "es2019", "ES2020.Promise"] + "lib": ["dom", "es6", "dom.iterable", "scripthost", "es2017", "es2018", "es2019", "ES2020.Promise"], + + "paths": { + "meteor/*": [ + // "./node_modules/@types/meteor/*", + "./.meteor/local/types/packages.d.ts" + ] + } }, "include": ["client/**/*", "server/**/*", "lib/**/*", "__mocks__/**/*", "tslint-rules/**/*"], "exclude": ["node_modules", "**/.coverage/**/*"] diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 6dca08b175..b952415c1f 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -33,869 +33,6 @@ __metadata: languageName: node linkType: hard -"@aws-crypto/ie11-detection@npm:^3.0.0": - version: 3.0.0 - resolution: "@aws-crypto/ie11-detection@npm:3.0.0" - dependencies: - tslib: ^1.11.1 - checksum: 299b2ddd46eddac1f2d54d91386ceb37af81aef8a800669281c73d634ed17fd855dcfb8b3157f2879344b93a2666a6d602550eb84b71e4d7868100ad6da8f803 - languageName: node - linkType: hard - -"@aws-crypto/sha256-browser@npm:3.0.0": - version: 3.0.0 - resolution: "@aws-crypto/sha256-browser@npm:3.0.0" - dependencies: - "@aws-crypto/ie11-detection": ^3.0.0 - "@aws-crypto/sha256-js": ^3.0.0 - "@aws-crypto/supports-web-crypto": ^3.0.0 - "@aws-crypto/util": ^3.0.0 - "@aws-sdk/types": ^3.222.0 - "@aws-sdk/util-locate-window": ^3.0.0 - "@aws-sdk/util-utf8-browser": ^3.0.0 - tslib: ^1.11.1 - checksum: ca89456bf508db2e08060a7f656460db97ac9a15b11e39d6fa7665e2b156508a1758695bff8e82d0a00178d6ac5c36f35eb4bcfac2e48621265224ca14a19bd2 - languageName: node - linkType: hard - -"@aws-crypto/sha256-js@npm:3.0.0, @aws-crypto/sha256-js@npm:^3.0.0": - version: 3.0.0 - resolution: "@aws-crypto/sha256-js@npm:3.0.0" - dependencies: - "@aws-crypto/util": ^3.0.0 - "@aws-sdk/types": ^3.222.0 - tslib: ^1.11.1 - checksum: 644ded32ea310237811afae873d3c7320739cb6f6cc39dced9c94801379e68e5ee2cca0c34f0384793fa9e750a7e0a5e2468f95754bd08e6fd72ab833c8fe23c - languageName: node - linkType: hard - -"@aws-crypto/supports-web-crypto@npm:^3.0.0": - version: 3.0.0 - resolution: "@aws-crypto/supports-web-crypto@npm:3.0.0" - dependencies: - tslib: ^1.11.1 - checksum: 35479a1558db9e9a521df6877a99f95670e972c602f2a0349303477e5d638a5baf569fb037c853710e382086e6fd77e8ed58d3fb9b49f6e1186a9d26ce7be006 - languageName: node - linkType: hard - -"@aws-crypto/util@npm:^3.0.0": - version: 3.0.0 - resolution: "@aws-crypto/util@npm:3.0.0" - dependencies: - "@aws-sdk/types": ^3.222.0 - "@aws-sdk/util-utf8-browser": ^3.0.0 - tslib: ^1.11.1 - checksum: d29d5545048721aae3d60b236708535059733019a105f8a64b4e4a8eab7cf8dde1546dc56bff7de20d36140a4d1f0f4693e639c5732a7059273a7b1e56354776 - languageName: node - linkType: hard - -"@aws-sdk/abort-controller@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/abort-controller@npm:3.296.0" - dependencies: - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: bfaf89f703f3be0b2c79574e3bd67f7f8272c88e1f99edaba51fa592a70d82f391380fdec703d8b31eea5488b285797848f7c6d187e87872ec0faf2df8284d47 - languageName: node - linkType: hard - -"@aws-sdk/client-cognito-identity@npm:3.301.0": - version: 3.301.0 - resolution: "@aws-sdk/client-cognito-identity@npm:3.301.0" - dependencies: - "@aws-crypto/sha256-browser": 3.0.0 - "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.301.0 - "@aws-sdk/config-resolver": 3.300.0 - "@aws-sdk/credential-provider-node": 3.301.0 - "@aws-sdk/fetch-http-handler": 3.296.0 - "@aws-sdk/hash-node": 3.296.0 - "@aws-sdk/invalid-dependency": 3.296.0 - "@aws-sdk/middleware-content-length": 3.296.0 - "@aws-sdk/middleware-endpoint": 3.299.0 - "@aws-sdk/middleware-host-header": 3.296.0 - "@aws-sdk/middleware-logger": 3.296.0 - "@aws-sdk/middleware-recursion-detection": 3.296.0 - "@aws-sdk/middleware-retry": 3.300.0 - "@aws-sdk/middleware-serde": 3.296.0 - "@aws-sdk/middleware-signing": 3.299.0 - "@aws-sdk/middleware-stack": 3.296.0 - "@aws-sdk/middleware-user-agent": 3.299.0 - "@aws-sdk/node-config-provider": 3.300.0 - "@aws-sdk/node-http-handler": 3.296.0 - "@aws-sdk/protocol-http": 3.296.0 - "@aws-sdk/smithy-client": 3.296.0 - "@aws-sdk/types": 3.296.0 - "@aws-sdk/url-parser": 3.296.0 - "@aws-sdk/util-base64": 3.295.0 - "@aws-sdk/util-body-length-browser": 3.295.0 - "@aws-sdk/util-body-length-node": 3.295.0 - "@aws-sdk/util-defaults-mode-browser": 3.296.0 - "@aws-sdk/util-defaults-mode-node": 3.300.0 - "@aws-sdk/util-endpoints": 3.296.0 - "@aws-sdk/util-retry": 3.296.0 - "@aws-sdk/util-user-agent-browser": 3.299.0 - "@aws-sdk/util-user-agent-node": 3.300.0 - "@aws-sdk/util-utf8": 3.295.0 - tslib: ^2.5.0 - checksum: 0a77c20d5be67f2d7abc0d118af0f2bd71b3d2989aa0e2b484e0f5612cb6001c6f54ae3934f827c37e3aad53ea6fba43e4c7f93ea83e9db8ea505b2d167c8553 - languageName: node - linkType: hard - -"@aws-sdk/client-sso-oidc@npm:3.301.0": - version: 3.301.0 - resolution: "@aws-sdk/client-sso-oidc@npm:3.301.0" - dependencies: - "@aws-crypto/sha256-browser": 3.0.0 - "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/config-resolver": 3.300.0 - "@aws-sdk/fetch-http-handler": 3.296.0 - "@aws-sdk/hash-node": 3.296.0 - "@aws-sdk/invalid-dependency": 3.296.0 - "@aws-sdk/middleware-content-length": 3.296.0 - "@aws-sdk/middleware-endpoint": 3.299.0 - "@aws-sdk/middleware-host-header": 3.296.0 - "@aws-sdk/middleware-logger": 3.296.0 - "@aws-sdk/middleware-recursion-detection": 3.296.0 - "@aws-sdk/middleware-retry": 3.300.0 - "@aws-sdk/middleware-serde": 3.296.0 - "@aws-sdk/middleware-stack": 3.296.0 - "@aws-sdk/middleware-user-agent": 3.299.0 - "@aws-sdk/node-config-provider": 3.300.0 - "@aws-sdk/node-http-handler": 3.296.0 - "@aws-sdk/protocol-http": 3.296.0 - "@aws-sdk/smithy-client": 3.296.0 - "@aws-sdk/types": 3.296.0 - "@aws-sdk/url-parser": 3.296.0 - "@aws-sdk/util-base64": 3.295.0 - "@aws-sdk/util-body-length-browser": 3.295.0 - "@aws-sdk/util-body-length-node": 3.295.0 - "@aws-sdk/util-defaults-mode-browser": 3.296.0 - "@aws-sdk/util-defaults-mode-node": 3.300.0 - "@aws-sdk/util-endpoints": 3.296.0 - "@aws-sdk/util-retry": 3.296.0 - "@aws-sdk/util-user-agent-browser": 3.299.0 - "@aws-sdk/util-user-agent-node": 3.300.0 - "@aws-sdk/util-utf8": 3.295.0 - tslib: ^2.5.0 - checksum: d4bc3b6cf2e870005c474caf699ce739162f077c6d8b5caa846ec9585649e694fd43f50a864355dbf6cf87e2f9a0747ef462492013d3a91843cd194bd9526d56 - languageName: node - linkType: hard - -"@aws-sdk/client-sso@npm:3.301.0": - version: 3.301.0 - resolution: "@aws-sdk/client-sso@npm:3.301.0" - dependencies: - "@aws-crypto/sha256-browser": 3.0.0 - "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/config-resolver": 3.300.0 - "@aws-sdk/fetch-http-handler": 3.296.0 - "@aws-sdk/hash-node": 3.296.0 - "@aws-sdk/invalid-dependency": 3.296.0 - "@aws-sdk/middleware-content-length": 3.296.0 - "@aws-sdk/middleware-endpoint": 3.299.0 - "@aws-sdk/middleware-host-header": 3.296.0 - "@aws-sdk/middleware-logger": 3.296.0 - "@aws-sdk/middleware-recursion-detection": 3.296.0 - "@aws-sdk/middleware-retry": 3.300.0 - "@aws-sdk/middleware-serde": 3.296.0 - "@aws-sdk/middleware-stack": 3.296.0 - "@aws-sdk/middleware-user-agent": 3.299.0 - "@aws-sdk/node-config-provider": 3.300.0 - "@aws-sdk/node-http-handler": 3.296.0 - "@aws-sdk/protocol-http": 3.296.0 - "@aws-sdk/smithy-client": 3.296.0 - "@aws-sdk/types": 3.296.0 - "@aws-sdk/url-parser": 3.296.0 - "@aws-sdk/util-base64": 3.295.0 - "@aws-sdk/util-body-length-browser": 3.295.0 - "@aws-sdk/util-body-length-node": 3.295.0 - "@aws-sdk/util-defaults-mode-browser": 3.296.0 - "@aws-sdk/util-defaults-mode-node": 3.300.0 - "@aws-sdk/util-endpoints": 3.296.0 - "@aws-sdk/util-retry": 3.296.0 - "@aws-sdk/util-user-agent-browser": 3.299.0 - "@aws-sdk/util-user-agent-node": 3.300.0 - "@aws-sdk/util-utf8": 3.295.0 - tslib: ^2.5.0 - checksum: 21cb421bb1cbe57f859466ae33a7e46986510c5cab519ba012727ec553ca7e70f351e3fafc1f8b9600235ece352c0d8b209b1943030f34c13f4a70f0e3befdb4 - languageName: node - linkType: hard - -"@aws-sdk/client-sts@npm:3.301.0": - version: 3.301.0 - resolution: "@aws-sdk/client-sts@npm:3.301.0" - dependencies: - "@aws-crypto/sha256-browser": 3.0.0 - "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/config-resolver": 3.300.0 - "@aws-sdk/credential-provider-node": 3.301.0 - "@aws-sdk/fetch-http-handler": 3.296.0 - "@aws-sdk/hash-node": 3.296.0 - "@aws-sdk/invalid-dependency": 3.296.0 - "@aws-sdk/middleware-content-length": 3.296.0 - "@aws-sdk/middleware-endpoint": 3.299.0 - "@aws-sdk/middleware-host-header": 3.296.0 - "@aws-sdk/middleware-logger": 3.296.0 - "@aws-sdk/middleware-recursion-detection": 3.296.0 - "@aws-sdk/middleware-retry": 3.300.0 - "@aws-sdk/middleware-sdk-sts": 3.299.0 - "@aws-sdk/middleware-serde": 3.296.0 - "@aws-sdk/middleware-signing": 3.299.0 - "@aws-sdk/middleware-stack": 3.296.0 - "@aws-sdk/middleware-user-agent": 3.299.0 - "@aws-sdk/node-config-provider": 3.300.0 - "@aws-sdk/node-http-handler": 3.296.0 - "@aws-sdk/protocol-http": 3.296.0 - "@aws-sdk/smithy-client": 3.296.0 - "@aws-sdk/types": 3.296.0 - "@aws-sdk/url-parser": 3.296.0 - "@aws-sdk/util-base64": 3.295.0 - "@aws-sdk/util-body-length-browser": 3.295.0 - "@aws-sdk/util-body-length-node": 3.295.0 - "@aws-sdk/util-defaults-mode-browser": 3.296.0 - "@aws-sdk/util-defaults-mode-node": 3.300.0 - "@aws-sdk/util-endpoints": 3.296.0 - "@aws-sdk/util-retry": 3.296.0 - "@aws-sdk/util-user-agent-browser": 3.299.0 - "@aws-sdk/util-user-agent-node": 3.300.0 - "@aws-sdk/util-utf8": 3.295.0 - fast-xml-parser: 4.1.2 - tslib: ^2.5.0 - checksum: f5ac1755831b8bc596e7d678beff52d086c29d9acef1150c0cb29f4e1f9395a57c21b0106efe7e39aa19eeede4a14d9b9410997490bd0e778ab6e8bf05371454 - languageName: node - linkType: hard - -"@aws-sdk/config-resolver@npm:3.300.0": - version: 3.300.0 - resolution: "@aws-sdk/config-resolver@npm:3.300.0" - dependencies: - "@aws-sdk/types": 3.296.0 - "@aws-sdk/util-config-provider": 3.295.0 - "@aws-sdk/util-middleware": 3.296.0 - tslib: ^2.5.0 - checksum: c8c065520c9b8e628d2971cb3c22f48b35a600a429d8b1031ff732058cb19e6f3d83ce00927c37726ef72c4b5a3e2a385a01fd8e4d3d69526f0808f51e77042f - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-cognito-identity@npm:3.301.0": - version: 3.301.0 - resolution: "@aws-sdk/credential-provider-cognito-identity@npm:3.301.0" - dependencies: - "@aws-sdk/client-cognito-identity": 3.301.0 - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 6bc9542814e4efb64d42b5ed943293ca4d2dbd5519d4cb027ee22331d1e63f2e00d8e2ffd33bf56bc8171f9abead9dc8e9cc74cc8a049cb442d9969b7c7a32fa - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-env@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/credential-provider-env@npm:3.296.0" - dependencies: - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 2695b2431f68a0faea47e41bef6ad38a5ccaa48a85729b7b533af63daa30e0645e9aee6d138abfbb2175c361b2afdf6a8acb6bd19a6cc7c2648d7c6a1ca84917 - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-imds@npm:3.300.0": - version: 3.300.0 - resolution: "@aws-sdk/credential-provider-imds@npm:3.300.0" - dependencies: - "@aws-sdk/node-config-provider": 3.300.0 - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/types": 3.296.0 - "@aws-sdk/url-parser": 3.296.0 - tslib: ^2.5.0 - checksum: 1fa76d5143ad8738db82ffc62f81218f6dcc42afedf1a95de0c5ad08e76a011204367fb085eab4088e9d3754ae28cd449bafd3b424abd20e86f7144e3235523b - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-ini@npm:3.301.0": - version: 3.301.0 - resolution: "@aws-sdk/credential-provider-ini@npm:3.301.0" - dependencies: - "@aws-sdk/credential-provider-env": 3.296.0 - "@aws-sdk/credential-provider-imds": 3.300.0 - "@aws-sdk/credential-provider-process": 3.300.0 - "@aws-sdk/credential-provider-sso": 3.301.0 - "@aws-sdk/credential-provider-web-identity": 3.296.0 - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/shared-ini-file-loader": 3.300.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 8ff07f5ddabf5b0a1e903b5d7e8932627a21b8ec385bd759ee4bef2d57841c99e52ec10cbea6a0987125c04147562a55eecf079544594c489329b831b4cd3115 - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-node@npm:3.301.0": - version: 3.301.0 - resolution: "@aws-sdk/credential-provider-node@npm:3.301.0" - dependencies: - "@aws-sdk/credential-provider-env": 3.296.0 - "@aws-sdk/credential-provider-imds": 3.300.0 - "@aws-sdk/credential-provider-ini": 3.301.0 - "@aws-sdk/credential-provider-process": 3.300.0 - "@aws-sdk/credential-provider-sso": 3.301.0 - "@aws-sdk/credential-provider-web-identity": 3.296.0 - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/shared-ini-file-loader": 3.300.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 06930d0deb34006845ff12bf9959dbfb35300bf40d062fa8e745bb393402a13bf28be33a6515d42e88aeaff078ee9beba7d5af31f4dd1468c9bcd1ca50fc161a - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-process@npm:3.300.0": - version: 3.300.0 - resolution: "@aws-sdk/credential-provider-process@npm:3.300.0" - dependencies: - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/shared-ini-file-loader": 3.300.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 228bf0716beda6c78a13f66ffb02c94da14408d5cdb48edf851c139a6fd851e71c9a2b57ac5c87bc6137f2fb067c6b5800241694bc7aed21c35273a5040a1291 - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-sso@npm:3.301.0": - version: 3.301.0 - resolution: "@aws-sdk/credential-provider-sso@npm:3.301.0" - dependencies: - "@aws-sdk/client-sso": 3.301.0 - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/shared-ini-file-loader": 3.300.0 - "@aws-sdk/token-providers": 3.301.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: a728565b4089c49f98e361d1848454cbb763a8858e9cd6ac82310b1c6edac23806a1e7cd9f3709728f086f02e08f9fabde4db3f73077e34394d03a9f02d454bd - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-web-identity@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/credential-provider-web-identity@npm:3.296.0" - dependencies: - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 35fadfbc623cb47eae1b189a6384ae545d4a5d203c70a12e57bb236c645d61d31dd3ffaafc323d878cde6061ec5be5c285c34b3296e7c4a8992c479b4485518a - languageName: node - linkType: hard - -"@aws-sdk/credential-providers@npm:^3.186.0": - version: 3.301.0 - resolution: "@aws-sdk/credential-providers@npm:3.301.0" - dependencies: - "@aws-sdk/client-cognito-identity": 3.301.0 - "@aws-sdk/client-sso": 3.301.0 - "@aws-sdk/client-sts": 3.301.0 - "@aws-sdk/credential-provider-cognito-identity": 3.301.0 - "@aws-sdk/credential-provider-env": 3.296.0 - "@aws-sdk/credential-provider-imds": 3.300.0 - "@aws-sdk/credential-provider-ini": 3.301.0 - "@aws-sdk/credential-provider-node": 3.301.0 - "@aws-sdk/credential-provider-process": 3.300.0 - "@aws-sdk/credential-provider-sso": 3.301.0 - "@aws-sdk/credential-provider-web-identity": 3.296.0 - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: c246080b6c5214e41a64471088fec68ed9f13ffa2bce94e2a12fc249d17c25dbf326251f728d0129dc8298a627587b2a6361770d449bb1e76848cb895641155d - languageName: node - linkType: hard - -"@aws-sdk/fetch-http-handler@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/fetch-http-handler@npm:3.296.0" - dependencies: - "@aws-sdk/protocol-http": 3.296.0 - "@aws-sdk/querystring-builder": 3.296.0 - "@aws-sdk/types": 3.296.0 - "@aws-sdk/util-base64": 3.295.0 - tslib: ^2.5.0 - checksum: cc57acffff2a1dd96ed752a21f76d35f6c597a09ad2a7d5be5b03dba93386a1c3cfaf6c3c5f41b33e9734005424444892f4a173e58ce06dcf6e78421fb7b511b - languageName: node - linkType: hard - -"@aws-sdk/hash-node@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/hash-node@npm:3.296.0" - dependencies: - "@aws-sdk/types": 3.296.0 - "@aws-sdk/util-buffer-from": 3.295.0 - "@aws-sdk/util-utf8": 3.295.0 - tslib: ^2.5.0 - checksum: 728680b1bc04764dc710003e6b967e176d65ba46c03e53ffebf25f7d87bad2354203e1c1e754ef6b6bcebbc3bde725aac6a1e00cb47118f78419cefed13b5724 - languageName: node - linkType: hard - -"@aws-sdk/invalid-dependency@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/invalid-dependency@npm:3.296.0" - dependencies: - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 5dc57b4396cbde9f594f14dc2a38e0c12e8cca4557b8e78fb5d23e1429e6dfef0b5ed1382da84862bb19abeaf39b92bc4dc7cba7c7a04dbc212cb6971f0553b0 - languageName: node - linkType: hard - -"@aws-sdk/is-array-buffer@npm:3.295.0": - version: 3.295.0 - resolution: "@aws-sdk/is-array-buffer@npm:3.295.0" - dependencies: - tslib: ^2.5.0 - checksum: 23de81a3ab63a51ae61183792db4a9faf74f02c7c9cb5cfa6d4b36781d7832070090bb406dc8591dd74a07fa3d3c27bac11d7a931e75163f8e018987a995f3ce - languageName: node - linkType: hard - -"@aws-sdk/middleware-content-length@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/middleware-content-length@npm:3.296.0" - dependencies: - "@aws-sdk/protocol-http": 3.296.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 649c0d3f0c1ec607ea9558df09f318bffc763f66501263136791571206c148ca7ca691cb3211fe18bd09aacdc69545fdc5ffff4daeac73323864b5cb76b5c072 - languageName: node - linkType: hard - -"@aws-sdk/middleware-endpoint@npm:3.299.0": - version: 3.299.0 - resolution: "@aws-sdk/middleware-endpoint@npm:3.299.0" - dependencies: - "@aws-sdk/middleware-serde": 3.296.0 - "@aws-sdk/types": 3.296.0 - "@aws-sdk/url-parser": 3.296.0 - "@aws-sdk/util-middleware": 3.296.0 - tslib: ^2.5.0 - checksum: 7e8c6a85575019871c87dc5c1b19c47999bd5ae3fc2546b72e3e617bf8bdea1392fb385e25a51f382b37d01d171de51dfaf2f53b79fb09adc10049ee571fbc13 - languageName: node - linkType: hard - -"@aws-sdk/middleware-host-header@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/middleware-host-header@npm:3.296.0" - dependencies: - "@aws-sdk/protocol-http": 3.296.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 8b5b2b26b204bcc8587cbd659a052b896c4d76dd1633e99d7c035b7b779d2b90beff4878481b8b5b28deeb363bf276584f371a5f7c88cecda43a18c5ae1ab7aa - languageName: node - linkType: hard - -"@aws-sdk/middleware-logger@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/middleware-logger@npm:3.296.0" - dependencies: - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: c006c03d24e26bbc1f9089987a0987b7627e1f7e1a5215d75e7132d2905f77f867d78806b968bee6e6830ddb94ddae8c1cead785b3fe4762e66da10584218ca9 - languageName: node - linkType: hard - -"@aws-sdk/middleware-recursion-detection@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/middleware-recursion-detection@npm:3.296.0" - dependencies: - "@aws-sdk/protocol-http": 3.296.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 2d8b8e130b6410e95340bca961e966c36f54154041aba3b0a308b0b6786cea36e026f28b910ac13411f333e445dbcc122801366bb55f64fab5cd76e05bb70257 - languageName: node - linkType: hard - -"@aws-sdk/middleware-retry@npm:3.300.0": - version: 3.300.0 - resolution: "@aws-sdk/middleware-retry@npm:3.300.0" - dependencies: - "@aws-sdk/protocol-http": 3.296.0 - "@aws-sdk/service-error-classification": 3.296.0 - "@aws-sdk/types": 3.296.0 - "@aws-sdk/util-middleware": 3.296.0 - "@aws-sdk/util-retry": 3.296.0 - tslib: ^2.5.0 - uuid: ^8.3.2 - checksum: 16b76246701eadda581a3a666f27d6100e75cf4d872e83a7e183ab2ec421d689f7c3f86a6dca2b7f0062208340ab578d4285af7d647a279d837891e3ef84b48b - languageName: node - linkType: hard - -"@aws-sdk/middleware-sdk-sts@npm:3.299.0": - version: 3.299.0 - resolution: "@aws-sdk/middleware-sdk-sts@npm:3.299.0" - dependencies: - "@aws-sdk/middleware-signing": 3.299.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: b1e2b28d29d924bbe176203c9a0abbe05a48748845da970a5aa61723b5ad118d50b3790baf63cecb69db63b89dc6e4abca218f14ce258bce87858acf339696de - languageName: node - linkType: hard - -"@aws-sdk/middleware-serde@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/middleware-serde@npm:3.296.0" - dependencies: - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 337bf9c4a621d6ef3a5c5d16273633f098e12e3581e08a9543d48e3a63ba6b70472f26f4e4e46040ed43fc1a498f8f046b66c28fc629f4a3a74b6a331609fd52 - languageName: node - linkType: hard - -"@aws-sdk/middleware-signing@npm:3.299.0": - version: 3.299.0 - resolution: "@aws-sdk/middleware-signing@npm:3.299.0" - dependencies: - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/protocol-http": 3.296.0 - "@aws-sdk/signature-v4": 3.299.0 - "@aws-sdk/types": 3.296.0 - "@aws-sdk/util-middleware": 3.296.0 - tslib: ^2.5.0 - checksum: 4eaa0a3b51eefa0f5e839a55ffa98f7cc79644fb7e8e3a3fbbdc4de2e2d10fb07cbd5201f44464a5550462af5efe47977feb6f057e6bd2160eed0904cd9d1752 - languageName: node - linkType: hard - -"@aws-sdk/middleware-stack@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/middleware-stack@npm:3.296.0" - dependencies: - tslib: ^2.5.0 - checksum: 1fb787c066aa48612e0f7ddd4932c3a9fbfd37eef5838bd83c06bd113d11af76f098e2b09a431ab039fb3f3628b1b80bdaca1200a10d9ec9cde56134e40b6995 - languageName: node - linkType: hard - -"@aws-sdk/middleware-user-agent@npm:3.299.0": - version: 3.299.0 - resolution: "@aws-sdk/middleware-user-agent@npm:3.299.0" - dependencies: - "@aws-sdk/protocol-http": 3.296.0 - "@aws-sdk/types": 3.296.0 - "@aws-sdk/util-endpoints": 3.296.0 - tslib: ^2.5.0 - checksum: 7ae91b41d2a9ab6d016e749458c30eb99f7fc1b11093410e6e2b0f0258aba8269fd5e22a980eb54bcbf8456679bf65c7e76fbf5bb145a13ff7d31398a82821ae - languageName: node - linkType: hard - -"@aws-sdk/node-config-provider@npm:3.300.0": - version: 3.300.0 - resolution: "@aws-sdk/node-config-provider@npm:3.300.0" - dependencies: - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/shared-ini-file-loader": 3.300.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 1ad2536961b9a735278635f8c1dc5c6c260d4e44d490e189a5115f6a41b5f8ae0643a3b213b66f39be457eb6a65cfcbcca07459608e1ac9876af7b92ebe5fbb2 - languageName: node - linkType: hard - -"@aws-sdk/node-http-handler@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/node-http-handler@npm:3.296.0" - dependencies: - "@aws-sdk/abort-controller": 3.296.0 - "@aws-sdk/protocol-http": 3.296.0 - "@aws-sdk/querystring-builder": 3.296.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 8766f46047f1667363a34433c9a867b4e5b27b400252418df2d5b110b6f85ff46a8aeff8b96132f7b39fc2bafa54fb1ae0e0fe44da84c8222349cea05cc7cc38 - languageName: node - linkType: hard - -"@aws-sdk/property-provider@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/property-provider@npm:3.296.0" - dependencies: - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: b456e82002c8302b2ebe328446346381c6d34b63d64f797ffd5b24d9f5cb0d739127d21657587c76a351fa6f56f43585d66020df370ae64f7cd9718c26175fff - languageName: node - linkType: hard - -"@aws-sdk/protocol-http@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/protocol-http@npm:3.296.0" - dependencies: - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: b283a1e6b6a6ba544bf833929d19f988747a451a1e1232f440b9412918932d099f29c0459dd398163b7f7bbae4c372166cc36a5e5afc1343d3085884e36879bc - languageName: node - linkType: hard - -"@aws-sdk/querystring-builder@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/querystring-builder@npm:3.296.0" - dependencies: - "@aws-sdk/types": 3.296.0 - "@aws-sdk/util-uri-escape": 3.295.0 - tslib: ^2.5.0 - checksum: 54028a5087126cdf48cf7abf4cc12c5d761c30aa97b23bab001ae387179cd95974a9332aacc6a74936f3ce818067ded67383231d5839b0456c5ed326bcaeeac5 - languageName: node - linkType: hard - -"@aws-sdk/querystring-parser@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/querystring-parser@npm:3.296.0" - dependencies: - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: b60b003302b4823609f9f585acdc3b7e48444aeaaf941549626066a6a74579201b473cdaa0ab1d59e63c4c8eec5bc9380464ccfeca47077ec4b5bc20fea4b190 - languageName: node - linkType: hard - -"@aws-sdk/service-error-classification@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/service-error-classification@npm:3.296.0" - checksum: f4f53591c636971bb2766340137a4816619c8ede0955dff37f795f03a0ed18534042f3ab9ec898d9f3fba98309322854b7442caeb919590f241ac21b3b18de74 - languageName: node - linkType: hard - -"@aws-sdk/shared-ini-file-loader@npm:3.300.0": - version: 3.300.0 - resolution: "@aws-sdk/shared-ini-file-loader@npm:3.300.0" - dependencies: - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: a85fc99d74b8323dd3fa771244aa072c123355a4f895b8a60d23950971bc3463a5826f70eeb4ac78a5c8395579a8828ad3da64db050888c2a900c85ca95d8a97 - languageName: node - linkType: hard - -"@aws-sdk/signature-v4@npm:3.299.0": - version: 3.299.0 - resolution: "@aws-sdk/signature-v4@npm:3.299.0" - dependencies: - "@aws-sdk/is-array-buffer": 3.295.0 - "@aws-sdk/types": 3.296.0 - "@aws-sdk/util-hex-encoding": 3.295.0 - "@aws-sdk/util-middleware": 3.296.0 - "@aws-sdk/util-uri-escape": 3.295.0 - "@aws-sdk/util-utf8": 3.295.0 - tslib: ^2.5.0 - checksum: 642e49ef1cb87649a0fdceb82c55f5153f9d662d5429d49fb18601c3ff8c12a1b050d8856fb18c041511be9d4d38fa741628c23f4be508cb816be696446139cd - languageName: node - linkType: hard - -"@aws-sdk/smithy-client@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/smithy-client@npm:3.296.0" - dependencies: - "@aws-sdk/middleware-stack": 3.296.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 8b7a626c1fd8b253f1f5b365c340626a9495aa6ba83d246be50f559d8274259daa6386f6931c9db41d3250ee3fd9d2d63646f4f0d7c2f793fed5aa7c87593676 - languageName: node - linkType: hard - -"@aws-sdk/token-providers@npm:3.301.0": - version: 3.301.0 - resolution: "@aws-sdk/token-providers@npm:3.301.0" - dependencies: - "@aws-sdk/client-sso-oidc": 3.301.0 - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/shared-ini-file-loader": 3.300.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: f8dac8a46272dfd966a285f005ab27f3f60f61bd0afd2cb21317aaa86083a3297e7d8dee5f9528c2f1bf8a186a7e681ab66ba7e5d0e85f7fee14de8c78633474 - languageName: node - linkType: hard - -"@aws-sdk/types@npm:3.296.0, @aws-sdk/types@npm:^3.222.0": - version: 3.296.0 - resolution: "@aws-sdk/types@npm:3.296.0" - dependencies: - tslib: ^2.5.0 - checksum: 3cff062f8cb08cd5473c40076ca06f8d93c80fa11e64a7c2d8273fcc0107e709e08a669779f9e3e70d35b3ae7fc3abafd2885f74fa9f8ba57042e5167f72bd2a - languageName: node - linkType: hard - -"@aws-sdk/url-parser@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/url-parser@npm:3.296.0" - dependencies: - "@aws-sdk/querystring-parser": 3.296.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: eb76e25571d9d13f7a335f36dc5f7466a7d7e8a6e26a5a0ea2529cda36d0ad29d0d6c5d418ac8a81a53a480a57a1b8aced93a43a08060eb0fb8e4259ab2e5048 - languageName: node - linkType: hard - -"@aws-sdk/util-base64@npm:3.295.0": - version: 3.295.0 - resolution: "@aws-sdk/util-base64@npm:3.295.0" - dependencies: - "@aws-sdk/util-buffer-from": 3.295.0 - tslib: ^2.5.0 - checksum: 047329e37dd6946f63b47ae415a988c32022b883b9fcf113120973b2d5e97679bc19f5183b264474f3201cb0560d3a4cb4b9d07d4d70b823945387e20869b41d - languageName: node - linkType: hard - -"@aws-sdk/util-body-length-browser@npm:3.295.0": - version: 3.295.0 - resolution: "@aws-sdk/util-body-length-browser@npm:3.295.0" - dependencies: - tslib: ^2.5.0 - checksum: 15100fa717f98a58475c934944108f98b811025039a04eeaaed71380bb2d4444fe44af7c527b74bb6ebd5a3ce277e28896d2e60ea73084f2b9e6cce6873b5592 - languageName: node - linkType: hard - -"@aws-sdk/util-body-length-node@npm:3.295.0": - version: 3.295.0 - resolution: "@aws-sdk/util-body-length-node@npm:3.295.0" - dependencies: - tslib: ^2.5.0 - checksum: e44fa83139d0e7e61152cce8f7709e772c920996fbd3ae4acc545df75ea3f7f28fd2959c97029411802b3e12277f775f3e2dbb34c797369635d0881bcf6b0a12 - languageName: node - linkType: hard - -"@aws-sdk/util-buffer-from@npm:3.295.0": - version: 3.295.0 - resolution: "@aws-sdk/util-buffer-from@npm:3.295.0" - dependencies: - "@aws-sdk/is-array-buffer": 3.295.0 - tslib: ^2.5.0 - checksum: c93e6f0cf66927230588365995d0e94ee874857dae6753529f44ebd7f2d9c1bc6e8fdef0a6459bd96f0c29a2fdc9eaa35145102d074249f4e8ff8bb070708f24 - languageName: node - linkType: hard - -"@aws-sdk/util-config-provider@npm:3.295.0": - version: 3.295.0 - resolution: "@aws-sdk/util-config-provider@npm:3.295.0" - dependencies: - tslib: ^2.5.0 - checksum: 2b6cd2b118465a36c9908dfc330f9f107a0a67cbde2293101af8b3ca0cb2d9f29f76394261880535d62da74b10cf89c5433a2d4524272d5b8776ad8085b0489b - languageName: node - linkType: hard - -"@aws-sdk/util-defaults-mode-browser@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/util-defaults-mode-browser@npm:3.296.0" - dependencies: - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/types": 3.296.0 - bowser: ^2.11.0 - tslib: ^2.5.0 - checksum: ae8c6b03d9de2fc9c320ae4aa65027604658aff95483d3eccdc87307e683da2c6943f863a98f74af7c7f9cae74d0b1df7e6f83592cd0b5eff0ff8eba48fca127 - languageName: node - linkType: hard - -"@aws-sdk/util-defaults-mode-node@npm:3.300.0": - version: 3.300.0 - resolution: "@aws-sdk/util-defaults-mode-node@npm:3.300.0" - dependencies: - "@aws-sdk/config-resolver": 3.300.0 - "@aws-sdk/credential-provider-imds": 3.300.0 - "@aws-sdk/node-config-provider": 3.300.0 - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: a6c4f5cd85485e23032ab45ea05f9cc943f07b23090c527b7ca159031300a3838c2ce4eb0e545f34a5bdbb2ffce494e85ad7cc86b9b631877fd5f21274bd65f4 - languageName: node - linkType: hard - -"@aws-sdk/util-endpoints@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/util-endpoints@npm:3.296.0" - dependencies: - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 9ff3b1c39123e8a88c2dcc1e81757f99cfd787be2039b9172e6a1a8b48b04d78712c2082dd1762e41b8b0a6c2f8d79aa06c571ea7ee622351634e5c40a069643 - languageName: node - linkType: hard - -"@aws-sdk/util-hex-encoding@npm:3.295.0": - version: 3.295.0 - resolution: "@aws-sdk/util-hex-encoding@npm:3.295.0" - dependencies: - tslib: ^2.5.0 - checksum: 4b85f087de5c2a8317ff13df4947e355b4c4acae1dd283133e53139457252fb83951194c85e07b89a1e12cecec1b3c0dbd11b7d0f9f2a7775d8c6d3d9c21371e - languageName: node - linkType: hard - -"@aws-sdk/util-locate-window@npm:^3.0.0": - version: 3.295.0 - resolution: "@aws-sdk/util-locate-window@npm:3.295.0" - dependencies: - tslib: ^2.5.0 - checksum: 81546c251a4f58915059d9d46b207f90ce44890e48fd87507ca280d70a719d8f963864afcffc676fd10ffa55e9b272fc5a522bd0e3d6dde379739adb5b429501 - languageName: node - linkType: hard - -"@aws-sdk/util-middleware@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/util-middleware@npm:3.296.0" - dependencies: - tslib: ^2.5.0 - checksum: 9743a6279208cdb25fc6a5d5800477dfa7f10c25e65cfcb7e0721613c0640df21bef7fe6cb18f27ee3ff470c0be2ff06cb6afdefe234d418c7ac17d9a3563301 - languageName: node - linkType: hard - -"@aws-sdk/util-retry@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/util-retry@npm:3.296.0" - dependencies: - "@aws-sdk/service-error-classification": 3.296.0 - tslib: ^2.5.0 - checksum: f225fc4eb0933eda3df069d2b0028d8eed76be9b7b871b7bf601c4f5f17012ebb6ebb194df26fc141c9e77bad86d5cbdb79113fe5624d3c2c9d3441f96f580b3 - languageName: node - linkType: hard - -"@aws-sdk/util-uri-escape@npm:3.295.0": - version: 3.295.0 - resolution: "@aws-sdk/util-uri-escape@npm:3.295.0" - dependencies: - tslib: ^2.5.0 - checksum: 2334baedd339161aa2fb6ae880c04730b072a217ed42b40aa0c0df526aba5a663302da50ba550ad657a4b3cf44070696d91501bf2ba33722f452247f5c2d0fde - languageName: node - linkType: hard - -"@aws-sdk/util-user-agent-browser@npm:3.299.0": - version: 3.299.0 - resolution: "@aws-sdk/util-user-agent-browser@npm:3.299.0" - dependencies: - "@aws-sdk/types": 3.296.0 - bowser: ^2.11.0 - tslib: ^2.5.0 - checksum: 60ebb1c0f5de624c212f00641fdcc8b667dac6442413d0d882745b660f72a6d1107cd6b04e7a5eee4013f0444b9fedfe151317937ac1f7d333dd3feeefa027b0 - languageName: node - linkType: hard - -"@aws-sdk/util-user-agent-node@npm:3.300.0": - version: 3.300.0 - resolution: "@aws-sdk/util-user-agent-node@npm:3.300.0" - dependencies: - "@aws-sdk/node-config-provider": 3.300.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - peerDependencies: - aws-crt: ">=1.0.0" - peerDependenciesMeta: - aws-crt: - optional: true - checksum: a42670de913d554d9a102a49920f52eb158fec64fb12d83ce610d6259da17d0ce56cf2e96299c35d019109ca514cd94adb9e8e1a103ce844ee8d4c17236dd35e - languageName: node - linkType: hard - -"@aws-sdk/util-utf8-browser@npm:^3.0.0": - version: 3.259.0 - resolution: "@aws-sdk/util-utf8-browser@npm:3.259.0" - dependencies: - tslib: ^2.3.1 - checksum: b6a1e580da1c9b62c749814182a7649a748ca4253edb4063aa521df97d25b76eae3359eb1680b86f71aac668e05cc05c514379bca39ebf4ba998ae4348412da8 - languageName: node - linkType: hard - -"@aws-sdk/util-utf8@npm:3.295.0": - version: 3.295.0 - resolution: "@aws-sdk/util-utf8@npm:3.295.0" - dependencies: - "@aws-sdk/util-buffer-from": 3.295.0 - tslib: ^2.5.0 - checksum: 098058651aa48bb2a6652ea6a1a0a1520e9964a91920e67eed023eacc0956b75475d25025e888ee193140aa800ed89faafa67653914b78c7ffb98e8f96f6d54c - languageName: node - linkType: hard - "@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.18.6": version: 7.18.6 resolution: "@babel/code-frame@npm:7.18.6" @@ -2453,7 +1590,7 @@ __metadata: dependencies: "@sofie-automation/shared-lib": 1.51.0-in-development tslib: ^2.6.0 - type-fest: ^2.19.0 + type-fest: ^3.10.0 languageName: node linkType: soft @@ -2502,10 +1639,10 @@ __metadata: prom-client: ^14.2.0 timecode: 0.0.4 tslib: ^2.6.0 - type-fest: ^2.19.0 + type-fest: ^3.10.0 underscore: ^1.13.6 peerDependencies: - mongodb: ^4.13.0 + mongodb: ^5.5.0 languageName: node linkType: soft @@ -2531,13 +1668,13 @@ __metadata: deepmerge: ^4.3.1 elastic-apm-node: ^3.47.0 eventemitter3: ^4.0.7 - mongodb: ^4.16.0 + mongodb: ^5.5.0 p-lazy: ^3.1.0 p-timeout: ^4.1.0 superfly-timeline: ^8.3.1 threadedclass: ^1.2.1 tslib: ^2.6.0 - type-fest: ^2.19.0 + type-fest: ^3.10.0 underscore: ^1.13.6 vm2: ^3.9.19 languageName: node @@ -2550,7 +1687,7 @@ __metadata: "@mos-connection/model": ^3.0.4 timeline-state-resolver-types: 9.0.0-release50.5 tslib: ^2.6.0 - type-fest: ^2.19.0 + type-fest: ^3.10.0 languageName: node linkType: soft @@ -2789,15 +1926,6 @@ __metadata: languageName: node linkType: hard -"@types/jquery@npm:*": - version: 3.5.16 - resolution: "@types/jquery@npm:3.5.16" - dependencies: - "@types/sizzle": "*" - checksum: 13c995f15d1c2f1d322103dc1cb0a22b95eecc3e7546f00279b8731aea21d7ec04550af40e609ee48e755d4e11bf61c25b4aa9f53df3bcbec4b8fe8e81471732 - languageName: node - linkType: hard - "@types/jsdom@npm:^20.0.0": version: 20.0.1 resolution: "@types/jsdom@npm:20.0.1" @@ -2875,20 +2003,6 @@ __metadata: languageName: node linkType: hard -"@types/meteor@npm:^2.9.2": - version: 2.9.2 - resolution: "@types/meteor@npm:2.9.2" - dependencies: - "@types/connect": "*" - "@types/jquery": "*" - "@types/nodemailer": "*" - "@types/react": "*" - "@types/underscore": "*" - mongodb: ^4.3.1 - checksum: 6395578e5d5f139aad8dfd0f70cb489dd70984b78ed2ab791bfabc59c70f94196cdb3cfc40659da0498ae1962f135782067579e5c320341c9a1a331553329f56 - languageName: node - linkType: hard - "@types/mime@npm:*": version: 3.0.1 resolution: "@types/mime@npm:3.0.1" @@ -2924,15 +2038,6 @@ __metadata: languageName: node linkType: hard -"@types/nodemailer@npm:*": - version: 6.4.7 - resolution: "@types/nodemailer@npm:6.4.7" - dependencies: - "@types/node": "*" - checksum: dc2a33a89135e04a5bea4921e8645e8453b90e3c3b05f0646f05071c5951ab697ea49ea1e503a690f04cb0a6abfc54967325c5a4036356793cfbb64ba64fb141 - languageName: node - linkType: hard - "@types/normalize-package-data@npm:^2.4.0, @types/normalize-package-data@npm:^2.4.1": version: 2.4.1 resolution: "@types/normalize-package-data@npm:2.4.1" @@ -3109,13 +2214,6 @@ __metadata: languageName: node linkType: hard -"@types/sizzle@npm:*": - version: 2.3.3 - resolution: "@types/sizzle@npm:2.3.3" - checksum: 586a9fb1f6ff3e325e0f2cc1596a460615f0bc8a28f6e276ac9b509401039dd242fa8b34496d3a30c52f5b495873922d09a9e76c50c2ab2bcc70ba3fb9c4e160 - languageName: node - linkType: hard - "@types/stack-utils@npm:^2.0.0": version: 2.0.1 resolution: "@types/stack-utils@npm:2.0.1" @@ -3137,13 +2235,6 @@ __metadata: languageName: node linkType: hard -"@types/underscore@npm:*": - version: 1.11.4 - resolution: "@types/underscore@npm:1.11.4" - checksum: db9f8486bc851b732259e51f42d62aad1ae2158be5724612dc125ece5f5d61c51447f9dea28284c2a0f79cb95e788d01cb5ce97709880019213e69fab0dd1696 - languageName: node - linkType: hard - "@types/underscore@npm:1.11.5": version: 1.11.5 resolution: "@types/underscore@npm:1.11.5" @@ -4069,7 +3160,6 @@ __metadata: "@types/koa-bodyparser": ^4.3.10 "@types/koa__cors": ^3.3.1 "@types/koa__router": ^12.0.0 - "@types/meteor": ^2.9.2 "@types/node": ^14.18.53 "@types/prop-types": ^15.7.5 "@types/react": ^18.2.14 @@ -4156,8 +3246,8 @@ __metadata: threadedclass: ^1.2.1 timecode: 0.0.4 ts-jest: ^29.1.1 - type-fest: ^2.19.0 - typescript: ~4.5 + type-fest: ^3.10.0 + typescript: ~4.9 underscore: ^1.13.6 velocity-animate: ^1.5.2 velocity-react: ^1.4.3 @@ -4350,13 +3440,6 @@ __metadata: languageName: node linkType: hard -"bowser@npm:^2.11.0": - version: 2.11.0 - resolution: "bowser@npm:2.11.0" - checksum: 29c3f01f22e703fa6644fc3b684307442df4240b6e10f6cfe1b61c6ca5721073189ca97cdeedb376081148c8518e33b1d818a57f781d70b0b70e1f31fb48814f - languageName: node - linkType: hard - "bplist-parser@npm:^0.2.0": version: 0.2.0 resolution: "bplist-parser@npm:0.2.0" @@ -4515,12 +3598,10 @@ __metadata: languageName: node linkType: hard -"bson@npm:^4.7.0, bson@npm:^4.7.2": - version: 4.7.2 - resolution: "bson@npm:4.7.2" - dependencies: - buffer: ^5.6.0 - checksum: f357d12c5679c8eb029a62e410ad40fb862b7b91f0fc12a3399fb3668e14aecaa63205ffeeee48735a01d393171743607dcd527eb8c058b6f2bd294079ee4125 +"bson@npm:^5.3.0": + version: 5.3.0 + resolution: "bson@npm:5.3.0" + checksum: c1e57630fea6b82ad8bc0fb83fd8f16a2620a836ab69f6b9433250cd19704c0c8995681a3bfab9f967eae30512f0c8ee921d42ace4c1781726600c4a367593cf languageName: node linkType: hard @@ -4552,7 +3633,7 @@ __metadata: languageName: node linkType: hard -"buffer@npm:^5.6.0, buffer@npm:^5.7.1": +"buffer@npm:^5.7.1": version: 5.7.1 resolution: "buffer@npm:5.7.1" dependencies: @@ -6771,17 +5852,6 @@ __metadata: languageName: node linkType: hard -"fast-xml-parser@npm:4.1.2": - version: 4.1.2 - resolution: "fast-xml-parser@npm:4.1.2" - dependencies: - strnum: ^1.0.5 - bin: - fxparser: src/cli/cli.js - checksum: 6a7d1b17057f8470e70603eddfa75f990625735d068d57ece861d0154ad8d27fda63c2831d07e1ecd7e68e993738b2448925cb9277d8c0ed68009623bbcd63c6 - languageName: node - linkType: hard - "fastq@npm:^1.6.0": version: 1.15.0 resolution: "fastq@npm:1.15.0" @@ -10115,7 +9185,7 @@ __metadata: languageName: node linkType: hard -"mongodb-connection-string-url@npm:^2.5.4": +"mongodb-connection-string-url@npm:^2.6.0": version: 2.6.0 resolution: "mongodb-connection-string-url@npm:2.6.0" dependencies: @@ -10125,39 +9195,29 @@ __metadata: languageName: node linkType: hard -"mongodb@npm:^4.16.0": - version: 4.16.0 - resolution: "mongodb@npm:4.16.0" +"mongodb@npm:^5.5.0": + version: 5.5.0 + resolution: "mongodb@npm:5.5.0" dependencies: - "@aws-sdk/credential-providers": ^3.186.0 - bson: ^4.7.2 - mongodb-connection-string-url: ^2.5.4 + bson: ^5.3.0 + mongodb-connection-string-url: ^2.6.0 saslprep: ^1.0.3 socks: ^2.7.1 + peerDependencies: + "@aws-sdk/credential-providers": ^3.201.0 + mongodb-client-encryption: ">=2.3.0 <3" + snappy: ^7.2.2 dependenciesMeta: - "@aws-sdk/credential-providers": - optional: true saslprep: optional: true - checksum: f0b1347739cc362b82b3aabc7e7d4d74bc7a344ed1bbafd6f92681bcab440f6cc618ffa0438d41d2789cb34818f3b09d4c78f517b42160ebae55bf2c96f13953 - languageName: node - linkType: hard - -"mongodb@npm:^4.3.1": - version: 4.14.0 - resolution: "mongodb@npm:4.14.0" - dependencies: - "@aws-sdk/credential-providers": ^3.186.0 - bson: ^4.7.0 - mongodb-connection-string-url: ^2.5.4 - saslprep: ^1.0.3 - socks: ^2.7.1 - dependenciesMeta: + peerDependenciesMeta: "@aws-sdk/credential-providers": optional: true - saslprep: + mongodb-client-encryption: + optional: true + snappy: optional: true - checksum: ab3b8f27e99fab4ea0c163067158ad360a161edf9ebf74368b9b561da7b75d23c09282d530cb4e2fac32ea30eae29620e861d837d334ddbdfa50d57240c1bd8e + checksum: fafb75195d605767cf7269aa82a1f4704c3357240d0fef7296d55e83eab592158844988af3b94165769459ff1b2a0af2fc49bc44c9bbabd10d6ddc319668a0e0 languageName: node linkType: hard @@ -13202,13 +12262,6 @@ __metadata: languageName: node linkType: hard -"strnum@npm:^1.0.5": - version: 1.0.5 - resolution: "strnum@npm:1.0.5" - checksum: 651b2031db5da1bf4a77fdd2f116a8ac8055157c5420f5569f64879133825915ad461513e7202a16d7fec63c54fd822410d0962f8ca12385c4334891b9ae6dd2 - languageName: node - linkType: hard - "strtok3@npm:^7.0.0": version: 7.0.0 resolution: "strtok3@npm:7.0.0" @@ -13604,14 +12657,14 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^1.11.1, tslib@npm:^1.13.0, tslib@npm:^1.8.1": +"tslib@npm:^1.13.0, tslib@npm:^1.8.1": version: 1.14.1 resolution: "tslib@npm:1.14.1" checksum: dbe628ef87f66691d5d2959b3e41b9ca0045c3ee3c7c7b906cc1e328b39f199bb1ad9e671c39025bd56122ac57dfbf7385a94843b1cc07c60a4db74795829acd languageName: node linkType: hard -"tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.5.0": +"tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.4.0": version: 2.5.0 resolution: "tslib@npm:2.5.0" checksum: ae3ed5f9ce29932d049908ebfdf21b3a003a85653a9a140d614da6b767a93ef94f460e52c3d787f0e4f383546981713f165037dc2274df212ea9f8a4541004e1 @@ -13724,7 +12777,7 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^2.0.0, type-fest@npm:^2.12.2, type-fest@npm:^2.13.0, type-fest@npm:^2.19.0, type-fest@npm:^2.5.0": +"type-fest@npm:^2.0.0, type-fest@npm:^2.12.2, type-fest@npm:^2.13.0, type-fest@npm:^2.5.0": version: 2.19.0 resolution: "type-fest@npm:2.19.0" checksum: a4ef07ece297c9fba78fc1bd6d85dff4472fe043ede98bd4710d2615d15776902b595abf62bd78339ed6278f021235fb28a96361f8be86ed754f778973a0d278 @@ -13738,6 +12791,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^3.10.0": + version: 3.12.0 + resolution: "type-fest@npm:3.12.0" + checksum: 11cb6f40e42f92c462a13677eafedf5c48353eaefa8e489a146e55fe0ae5cecd59a2ba03cd6b294345b5ea304361891a6313c23ed959fa0117f63fb71ee6cbab + languageName: node + linkType: hard + "type-is@npm:^1.6.16, type-is@npm:^1.6.18, type-is@npm:~1.6.18": version: 1.6.18 resolution: "type-is@npm:1.6.18" @@ -13773,23 +12833,23 @@ __metadata: languageName: node linkType: hard -"typescript@npm:~4.5": - version: 4.5.5 - resolution: "typescript@npm:4.5.5" +"typescript@npm:~4.9": + version: 4.9.5 + resolution: "typescript@npm:4.9.5" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 506f4c919dc8aeaafa92068c997f1d213b9df4d9756d0fae1a1e7ab66b585ab3498050e236113a1c9e57ee08c21ec6814ca7a7f61378c058d79af50a4b1f5a5e + checksum: ee000bc26848147ad423b581bd250075662a354d84f0e06eb76d3b892328d8d4440b7487b5a83e851b12b255f55d71835b008a66cbf8f255a11e4400159237db languageName: node linkType: hard -"typescript@patch:typescript@~4.5#~builtin": - version: 4.5.5 - resolution: "typescript@patch:typescript@npm%3A4.5.5#~builtin::version=4.5.5&hash=bcec9a" +"typescript@patch:typescript@~4.9#~builtin": + version: 4.9.5 + resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=23ec76" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 858c61fa63f7274ca4aaaffeced854d550bf416cff6e558c4884041b3311fb662f476f167cf5c9f8680c607239797e26a2ee0bcc6467fbc05bfcb218e1c6c671 + checksum: ab417a2f398380c90a6cf5a5f74badd17866adf57f1165617d6a551f059c3ba0a3e4da0d147b3ac5681db9ac76a303c5876394b13b3de75fdd5b1eaa06181c9d languageName: node linkType: hard @@ -13991,15 +13051,6 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^8.3.2": - version: 8.3.2 - resolution: "uuid@npm:8.3.2" - bin: - uuid: dist/bin/uuid - checksum: 5575a8a75c13120e2f10e6ddc801b2c7ed7d8f3c8ac22c7ed0c7b2ba6383ec0abda88c905085d630e251719e0777045ae3236f04c812184b7c765f63a70e58df - languageName: node - linkType: hard - "v8-to-istanbul@npm:^9.0.1": version: 9.1.0 resolution: "v8-to-istanbul@npm:9.1.0" diff --git a/packages/blueprints-integration/package.json b/packages/blueprints-integration/package.json index 5468de7efe..3c2ab8b965 100644 --- a/packages/blueprints-integration/package.json +++ b/packages/blueprints-integration/package.json @@ -40,7 +40,7 @@ "dependencies": { "@sofie-automation/shared-lib": "1.51.0-in-development", "tslib": "^2.6.0", - "type-fest": "^2.19.0" + "type-fest": "^3.10.0" }, "lint-staged": { "*.{css,json,md,scss}": [ diff --git a/packages/corelib/package.json b/packages/corelib/package.json index ec7d3fe3d4..675306e632 100644 --- a/packages/corelib/package.json +++ b/packages/corelib/package.json @@ -49,11 +49,11 @@ "prom-client": "^14.2.0", "timecode": "0.0.4", "tslib": "^2.6.0", - "type-fest": "^2.19.0", + "type-fest": "^3.10.0", "underscore": "^1.13.6" }, "peerDependencies": { - "mongodb": "^4.13.0" + "mongodb": "^5.5.0" }, "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", "lint-staged": { diff --git a/packages/corelib/src/mongo.ts b/packages/corelib/src/mongo.ts index 631aad1936..04a3f2c270 100644 --- a/packages/corelib/src/mongo.ts +++ b/packages/corelib/src/mongo.ts @@ -2,7 +2,7 @@ import * as _ from 'underscore' import { ProtectedString } from './protectedString' import * as objectPath from 'object-path' // eslint-disable-next-line node/no-extraneous-import -import type { Filter, UpdateFilter } from 'mongodb' +import type { Condition, Filter, UpdateFilter } from 'mongodb' /** Hack's using typings pulled from meteor */ @@ -49,6 +49,7 @@ export interface FindOptions extends FindOneOptions { * Used for simplified expressions (ie not using $and, $or etc..) * */ export type MongoQuery = Filter +export type MongoQueryKey = RegExp | T | Condition // Allowed properties in a MongoQuery export type MongoModifier = UpdateFilter /** End of hacks */ diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index dfe90e8492..597344cb41 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -48,13 +48,13 @@ "deepmerge": "^4.3.1", "elastic-apm-node": "^3.47.0", "eventemitter3": "^4.0.7", - "mongodb": "^4.16.0", + "mongodb": "^5.5.0", "p-lazy": "^3.1.0", "p-timeout": "^4.1.0", "superfly-timeline": "^8.3.1", "threadedclass": "^1.2.1", "tslib": "^2.6.0", - "type-fest": "^2.19.0", + "type-fest": "^3.10.0", "underscore": "^1.13.6", "vm2": "^3.9.19" }, diff --git a/packages/job-worker/src/blueprints/context/adlibActions.ts b/packages/job-worker/src/blueprints/context/adlibActions.ts index ae35a16786..fd17e3c10c 100644 --- a/packages/job-worker/src/blueprints/context/adlibActions.ts +++ b/packages/job-worker/src/blueprints/context/adlibActions.ts @@ -221,7 +221,6 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct for (const [key, value] of Object.entries(options.pieceMetaDataFilter)) { // TODO do we need better validation here? // It should be pretty safe as we are working with the cache version (for now) - // @ts-expect-error metaData is `unknown` so no subkeys are known to be valid query[`piece.metaData.${key}`] = value } } @@ -255,7 +254,6 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct for (const [key, value] of Object.entries(options.pieceMetaDataFilter)) { // TODO do we need better validation here? // It should be pretty safe as we are working with the cache version (for now) - // @ts-expect-error metaData is `unknown` so no subkeys are known to be valid query[`metaData.${key}`] = value } } diff --git a/packages/job-worker/src/db/collection.ts b/packages/job-worker/src/db/collection.ts index 21bf883b6d..681981ebca 100644 --- a/packages/job-worker/src/db/collection.ts +++ b/packages/job-worker/src/db/collection.ts @@ -155,12 +155,8 @@ class WrappedCollection }> implements I const bulkWriteResult = await this.#collection.bulkWrite(ops, { ordered: false, }) - if ( - bulkWriteResult && - Array.isArray(bulkWriteResult.result?.writeErrors) && - bulkWriteResult.result.writeErrors.length - ) { - throw new Error(`Errors in rawCollection.bulkWrite: ${bulkWriteResult.result.writeErrors.join(',')}`) + if (bulkWriteResult && bulkWriteResult.hasWriteErrors()) { + throw new Error(`Errors in rawCollection.bulkWrite: ${bulkWriteResult.getWriteErrors().join(',')}`) } } diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index 5e5633a8bc..ec84b1ce7f 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -69,7 +69,7 @@ "@sofie-automation/server-core-integration": "1.51.0-in-development", "@sofie-automation/shared-lib": "1.51.0-in-development", "tslib": "^2.6.0", - "type-fest": "^2.19.0", + "type-fest": "^3.10.0", "underscore": "^1.13.6", "winston": "^3.9.0" }, diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 9ea0a4884c..b1129113e1 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -41,7 +41,7 @@ "@mos-connection/model": "^3.0.4", "timeline-state-resolver-types": "9.0.0-release50.5", "tslib": "^2.6.0", - "type-fest": "^2.19.0" + "type-fest": "^3.10.0" }, "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", "lint-staged": { diff --git a/packages/yarn.lock b/packages/yarn.lock index 3b3431ad4e..18a712d731 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -452,869 +452,6 @@ __metadata: languageName: node linkType: hard -"@aws-crypto/ie11-detection@npm:^3.0.0": - version: 3.0.0 - resolution: "@aws-crypto/ie11-detection@npm:3.0.0" - dependencies: - tslib: ^1.11.1 - checksum: 299b2ddd46eddac1f2d54d91386ceb37af81aef8a800669281c73d634ed17fd855dcfb8b3157f2879344b93a2666a6d602550eb84b71e4d7868100ad6da8f803 - languageName: node - linkType: hard - -"@aws-crypto/sha256-browser@npm:3.0.0": - version: 3.0.0 - resolution: "@aws-crypto/sha256-browser@npm:3.0.0" - dependencies: - "@aws-crypto/ie11-detection": ^3.0.0 - "@aws-crypto/sha256-js": ^3.0.0 - "@aws-crypto/supports-web-crypto": ^3.0.0 - "@aws-crypto/util": ^3.0.0 - "@aws-sdk/types": ^3.222.0 - "@aws-sdk/util-locate-window": ^3.0.0 - "@aws-sdk/util-utf8-browser": ^3.0.0 - tslib: ^1.11.1 - checksum: ca89456bf508db2e08060a7f656460db97ac9a15b11e39d6fa7665e2b156508a1758695bff8e82d0a00178d6ac5c36f35eb4bcfac2e48621265224ca14a19bd2 - languageName: node - linkType: hard - -"@aws-crypto/sha256-js@npm:3.0.0, @aws-crypto/sha256-js@npm:^3.0.0": - version: 3.0.0 - resolution: "@aws-crypto/sha256-js@npm:3.0.0" - dependencies: - "@aws-crypto/util": ^3.0.0 - "@aws-sdk/types": ^3.222.0 - tslib: ^1.11.1 - checksum: 644ded32ea310237811afae873d3c7320739cb6f6cc39dced9c94801379e68e5ee2cca0c34f0384793fa9e750a7e0a5e2468f95754bd08e6fd72ab833c8fe23c - languageName: node - linkType: hard - -"@aws-crypto/supports-web-crypto@npm:^3.0.0": - version: 3.0.0 - resolution: "@aws-crypto/supports-web-crypto@npm:3.0.0" - dependencies: - tslib: ^1.11.1 - checksum: 35479a1558db9e9a521df6877a99f95670e972c602f2a0349303477e5d638a5baf569fb037c853710e382086e6fd77e8ed58d3fb9b49f6e1186a9d26ce7be006 - languageName: node - linkType: hard - -"@aws-crypto/util@npm:^3.0.0": - version: 3.0.0 - resolution: "@aws-crypto/util@npm:3.0.0" - dependencies: - "@aws-sdk/types": ^3.222.0 - "@aws-sdk/util-utf8-browser": ^3.0.0 - tslib: ^1.11.1 - checksum: d29d5545048721aae3d60b236708535059733019a105f8a64b4e4a8eab7cf8dde1546dc56bff7de20d36140a4d1f0f4693e639c5732a7059273a7b1e56354776 - languageName: node - linkType: hard - -"@aws-sdk/abort-controller@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/abort-controller@npm:3.296.0" - dependencies: - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: bfaf89f703f3be0b2c79574e3bd67f7f8272c88e1f99edaba51fa592a70d82f391380fdec703d8b31eea5488b285797848f7c6d187e87872ec0faf2df8284d47 - languageName: node - linkType: hard - -"@aws-sdk/client-cognito-identity@npm:3.301.0": - version: 3.301.0 - resolution: "@aws-sdk/client-cognito-identity@npm:3.301.0" - dependencies: - "@aws-crypto/sha256-browser": 3.0.0 - "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.301.0 - "@aws-sdk/config-resolver": 3.300.0 - "@aws-sdk/credential-provider-node": 3.301.0 - "@aws-sdk/fetch-http-handler": 3.296.0 - "@aws-sdk/hash-node": 3.296.0 - "@aws-sdk/invalid-dependency": 3.296.0 - "@aws-sdk/middleware-content-length": 3.296.0 - "@aws-sdk/middleware-endpoint": 3.299.0 - "@aws-sdk/middleware-host-header": 3.296.0 - "@aws-sdk/middleware-logger": 3.296.0 - "@aws-sdk/middleware-recursion-detection": 3.296.0 - "@aws-sdk/middleware-retry": 3.300.0 - "@aws-sdk/middleware-serde": 3.296.0 - "@aws-sdk/middleware-signing": 3.299.0 - "@aws-sdk/middleware-stack": 3.296.0 - "@aws-sdk/middleware-user-agent": 3.299.0 - "@aws-sdk/node-config-provider": 3.300.0 - "@aws-sdk/node-http-handler": 3.296.0 - "@aws-sdk/protocol-http": 3.296.0 - "@aws-sdk/smithy-client": 3.296.0 - "@aws-sdk/types": 3.296.0 - "@aws-sdk/url-parser": 3.296.0 - "@aws-sdk/util-base64": 3.295.0 - "@aws-sdk/util-body-length-browser": 3.295.0 - "@aws-sdk/util-body-length-node": 3.295.0 - "@aws-sdk/util-defaults-mode-browser": 3.296.0 - "@aws-sdk/util-defaults-mode-node": 3.300.0 - "@aws-sdk/util-endpoints": 3.296.0 - "@aws-sdk/util-retry": 3.296.0 - "@aws-sdk/util-user-agent-browser": 3.299.0 - "@aws-sdk/util-user-agent-node": 3.300.0 - "@aws-sdk/util-utf8": 3.295.0 - tslib: ^2.5.0 - checksum: 0a77c20d5be67f2d7abc0d118af0f2bd71b3d2989aa0e2b484e0f5612cb6001c6f54ae3934f827c37e3aad53ea6fba43e4c7f93ea83e9db8ea505b2d167c8553 - languageName: node - linkType: hard - -"@aws-sdk/client-sso-oidc@npm:3.301.0": - version: 3.301.0 - resolution: "@aws-sdk/client-sso-oidc@npm:3.301.0" - dependencies: - "@aws-crypto/sha256-browser": 3.0.0 - "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/config-resolver": 3.300.0 - "@aws-sdk/fetch-http-handler": 3.296.0 - "@aws-sdk/hash-node": 3.296.0 - "@aws-sdk/invalid-dependency": 3.296.0 - "@aws-sdk/middleware-content-length": 3.296.0 - "@aws-sdk/middleware-endpoint": 3.299.0 - "@aws-sdk/middleware-host-header": 3.296.0 - "@aws-sdk/middleware-logger": 3.296.0 - "@aws-sdk/middleware-recursion-detection": 3.296.0 - "@aws-sdk/middleware-retry": 3.300.0 - "@aws-sdk/middleware-serde": 3.296.0 - "@aws-sdk/middleware-stack": 3.296.0 - "@aws-sdk/middleware-user-agent": 3.299.0 - "@aws-sdk/node-config-provider": 3.300.0 - "@aws-sdk/node-http-handler": 3.296.0 - "@aws-sdk/protocol-http": 3.296.0 - "@aws-sdk/smithy-client": 3.296.0 - "@aws-sdk/types": 3.296.0 - "@aws-sdk/url-parser": 3.296.0 - "@aws-sdk/util-base64": 3.295.0 - "@aws-sdk/util-body-length-browser": 3.295.0 - "@aws-sdk/util-body-length-node": 3.295.0 - "@aws-sdk/util-defaults-mode-browser": 3.296.0 - "@aws-sdk/util-defaults-mode-node": 3.300.0 - "@aws-sdk/util-endpoints": 3.296.0 - "@aws-sdk/util-retry": 3.296.0 - "@aws-sdk/util-user-agent-browser": 3.299.0 - "@aws-sdk/util-user-agent-node": 3.300.0 - "@aws-sdk/util-utf8": 3.295.0 - tslib: ^2.5.0 - checksum: d4bc3b6cf2e870005c474caf699ce739162f077c6d8b5caa846ec9585649e694fd43f50a864355dbf6cf87e2f9a0747ef462492013d3a91843cd194bd9526d56 - languageName: node - linkType: hard - -"@aws-sdk/client-sso@npm:3.301.0": - version: 3.301.0 - resolution: "@aws-sdk/client-sso@npm:3.301.0" - dependencies: - "@aws-crypto/sha256-browser": 3.0.0 - "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/config-resolver": 3.300.0 - "@aws-sdk/fetch-http-handler": 3.296.0 - "@aws-sdk/hash-node": 3.296.0 - "@aws-sdk/invalid-dependency": 3.296.0 - "@aws-sdk/middleware-content-length": 3.296.0 - "@aws-sdk/middleware-endpoint": 3.299.0 - "@aws-sdk/middleware-host-header": 3.296.0 - "@aws-sdk/middleware-logger": 3.296.0 - "@aws-sdk/middleware-recursion-detection": 3.296.0 - "@aws-sdk/middleware-retry": 3.300.0 - "@aws-sdk/middleware-serde": 3.296.0 - "@aws-sdk/middleware-stack": 3.296.0 - "@aws-sdk/middleware-user-agent": 3.299.0 - "@aws-sdk/node-config-provider": 3.300.0 - "@aws-sdk/node-http-handler": 3.296.0 - "@aws-sdk/protocol-http": 3.296.0 - "@aws-sdk/smithy-client": 3.296.0 - "@aws-sdk/types": 3.296.0 - "@aws-sdk/url-parser": 3.296.0 - "@aws-sdk/util-base64": 3.295.0 - "@aws-sdk/util-body-length-browser": 3.295.0 - "@aws-sdk/util-body-length-node": 3.295.0 - "@aws-sdk/util-defaults-mode-browser": 3.296.0 - "@aws-sdk/util-defaults-mode-node": 3.300.0 - "@aws-sdk/util-endpoints": 3.296.0 - "@aws-sdk/util-retry": 3.296.0 - "@aws-sdk/util-user-agent-browser": 3.299.0 - "@aws-sdk/util-user-agent-node": 3.300.0 - "@aws-sdk/util-utf8": 3.295.0 - tslib: ^2.5.0 - checksum: 21cb421bb1cbe57f859466ae33a7e46986510c5cab519ba012727ec553ca7e70f351e3fafc1f8b9600235ece352c0d8b209b1943030f34c13f4a70f0e3befdb4 - languageName: node - linkType: hard - -"@aws-sdk/client-sts@npm:3.301.0": - version: 3.301.0 - resolution: "@aws-sdk/client-sts@npm:3.301.0" - dependencies: - "@aws-crypto/sha256-browser": 3.0.0 - "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/config-resolver": 3.300.0 - "@aws-sdk/credential-provider-node": 3.301.0 - "@aws-sdk/fetch-http-handler": 3.296.0 - "@aws-sdk/hash-node": 3.296.0 - "@aws-sdk/invalid-dependency": 3.296.0 - "@aws-sdk/middleware-content-length": 3.296.0 - "@aws-sdk/middleware-endpoint": 3.299.0 - "@aws-sdk/middleware-host-header": 3.296.0 - "@aws-sdk/middleware-logger": 3.296.0 - "@aws-sdk/middleware-recursion-detection": 3.296.0 - "@aws-sdk/middleware-retry": 3.300.0 - "@aws-sdk/middleware-sdk-sts": 3.299.0 - "@aws-sdk/middleware-serde": 3.296.0 - "@aws-sdk/middleware-signing": 3.299.0 - "@aws-sdk/middleware-stack": 3.296.0 - "@aws-sdk/middleware-user-agent": 3.299.0 - "@aws-sdk/node-config-provider": 3.300.0 - "@aws-sdk/node-http-handler": 3.296.0 - "@aws-sdk/protocol-http": 3.296.0 - "@aws-sdk/smithy-client": 3.296.0 - "@aws-sdk/types": 3.296.0 - "@aws-sdk/url-parser": 3.296.0 - "@aws-sdk/util-base64": 3.295.0 - "@aws-sdk/util-body-length-browser": 3.295.0 - "@aws-sdk/util-body-length-node": 3.295.0 - "@aws-sdk/util-defaults-mode-browser": 3.296.0 - "@aws-sdk/util-defaults-mode-node": 3.300.0 - "@aws-sdk/util-endpoints": 3.296.0 - "@aws-sdk/util-retry": 3.296.0 - "@aws-sdk/util-user-agent-browser": 3.299.0 - "@aws-sdk/util-user-agent-node": 3.300.0 - "@aws-sdk/util-utf8": 3.295.0 - fast-xml-parser: 4.1.2 - tslib: ^2.5.0 - checksum: f5ac1755831b8bc596e7d678beff52d086c29d9acef1150c0cb29f4e1f9395a57c21b0106efe7e39aa19eeede4a14d9b9410997490bd0e778ab6e8bf05371454 - languageName: node - linkType: hard - -"@aws-sdk/config-resolver@npm:3.300.0": - version: 3.300.0 - resolution: "@aws-sdk/config-resolver@npm:3.300.0" - dependencies: - "@aws-sdk/types": 3.296.0 - "@aws-sdk/util-config-provider": 3.295.0 - "@aws-sdk/util-middleware": 3.296.0 - tslib: ^2.5.0 - checksum: c8c065520c9b8e628d2971cb3c22f48b35a600a429d8b1031ff732058cb19e6f3d83ce00927c37726ef72c4b5a3e2a385a01fd8e4d3d69526f0808f51e77042f - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-cognito-identity@npm:3.301.0": - version: 3.301.0 - resolution: "@aws-sdk/credential-provider-cognito-identity@npm:3.301.0" - dependencies: - "@aws-sdk/client-cognito-identity": 3.301.0 - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 6bc9542814e4efb64d42b5ed943293ca4d2dbd5519d4cb027ee22331d1e63f2e00d8e2ffd33bf56bc8171f9abead9dc8e9cc74cc8a049cb442d9969b7c7a32fa - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-env@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/credential-provider-env@npm:3.296.0" - dependencies: - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 2695b2431f68a0faea47e41bef6ad38a5ccaa48a85729b7b533af63daa30e0645e9aee6d138abfbb2175c361b2afdf6a8acb6bd19a6cc7c2648d7c6a1ca84917 - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-imds@npm:3.300.0": - version: 3.300.0 - resolution: "@aws-sdk/credential-provider-imds@npm:3.300.0" - dependencies: - "@aws-sdk/node-config-provider": 3.300.0 - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/types": 3.296.0 - "@aws-sdk/url-parser": 3.296.0 - tslib: ^2.5.0 - checksum: 1fa76d5143ad8738db82ffc62f81218f6dcc42afedf1a95de0c5ad08e76a011204367fb085eab4088e9d3754ae28cd449bafd3b424abd20e86f7144e3235523b - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-ini@npm:3.301.0": - version: 3.301.0 - resolution: "@aws-sdk/credential-provider-ini@npm:3.301.0" - dependencies: - "@aws-sdk/credential-provider-env": 3.296.0 - "@aws-sdk/credential-provider-imds": 3.300.0 - "@aws-sdk/credential-provider-process": 3.300.0 - "@aws-sdk/credential-provider-sso": 3.301.0 - "@aws-sdk/credential-provider-web-identity": 3.296.0 - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/shared-ini-file-loader": 3.300.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 8ff07f5ddabf5b0a1e903b5d7e8932627a21b8ec385bd759ee4bef2d57841c99e52ec10cbea6a0987125c04147562a55eecf079544594c489329b831b4cd3115 - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-node@npm:3.301.0": - version: 3.301.0 - resolution: "@aws-sdk/credential-provider-node@npm:3.301.0" - dependencies: - "@aws-sdk/credential-provider-env": 3.296.0 - "@aws-sdk/credential-provider-imds": 3.300.0 - "@aws-sdk/credential-provider-ini": 3.301.0 - "@aws-sdk/credential-provider-process": 3.300.0 - "@aws-sdk/credential-provider-sso": 3.301.0 - "@aws-sdk/credential-provider-web-identity": 3.296.0 - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/shared-ini-file-loader": 3.300.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 06930d0deb34006845ff12bf9959dbfb35300bf40d062fa8e745bb393402a13bf28be33a6515d42e88aeaff078ee9beba7d5af31f4dd1468c9bcd1ca50fc161a - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-process@npm:3.300.0": - version: 3.300.0 - resolution: "@aws-sdk/credential-provider-process@npm:3.300.0" - dependencies: - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/shared-ini-file-loader": 3.300.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 228bf0716beda6c78a13f66ffb02c94da14408d5cdb48edf851c139a6fd851e71c9a2b57ac5c87bc6137f2fb067c6b5800241694bc7aed21c35273a5040a1291 - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-sso@npm:3.301.0": - version: 3.301.0 - resolution: "@aws-sdk/credential-provider-sso@npm:3.301.0" - dependencies: - "@aws-sdk/client-sso": 3.301.0 - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/shared-ini-file-loader": 3.300.0 - "@aws-sdk/token-providers": 3.301.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: a728565b4089c49f98e361d1848454cbb763a8858e9cd6ac82310b1c6edac23806a1e7cd9f3709728f086f02e08f9fabde4db3f73077e34394d03a9f02d454bd - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-web-identity@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/credential-provider-web-identity@npm:3.296.0" - dependencies: - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 35fadfbc623cb47eae1b189a6384ae545d4a5d203c70a12e57bb236c645d61d31dd3ffaafc323d878cde6061ec5be5c285c34b3296e7c4a8992c479b4485518a - languageName: node - linkType: hard - -"@aws-sdk/credential-providers@npm:^3.186.0": - version: 3.301.0 - resolution: "@aws-sdk/credential-providers@npm:3.301.0" - dependencies: - "@aws-sdk/client-cognito-identity": 3.301.0 - "@aws-sdk/client-sso": 3.301.0 - "@aws-sdk/client-sts": 3.301.0 - "@aws-sdk/credential-provider-cognito-identity": 3.301.0 - "@aws-sdk/credential-provider-env": 3.296.0 - "@aws-sdk/credential-provider-imds": 3.300.0 - "@aws-sdk/credential-provider-ini": 3.301.0 - "@aws-sdk/credential-provider-node": 3.301.0 - "@aws-sdk/credential-provider-process": 3.300.0 - "@aws-sdk/credential-provider-sso": 3.301.0 - "@aws-sdk/credential-provider-web-identity": 3.296.0 - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: c246080b6c5214e41a64471088fec68ed9f13ffa2bce94e2a12fc249d17c25dbf326251f728d0129dc8298a627587b2a6361770d449bb1e76848cb895641155d - languageName: node - linkType: hard - -"@aws-sdk/fetch-http-handler@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/fetch-http-handler@npm:3.296.0" - dependencies: - "@aws-sdk/protocol-http": 3.296.0 - "@aws-sdk/querystring-builder": 3.296.0 - "@aws-sdk/types": 3.296.0 - "@aws-sdk/util-base64": 3.295.0 - tslib: ^2.5.0 - checksum: cc57acffff2a1dd96ed752a21f76d35f6c597a09ad2a7d5be5b03dba93386a1c3cfaf6c3c5f41b33e9734005424444892f4a173e58ce06dcf6e78421fb7b511b - languageName: node - linkType: hard - -"@aws-sdk/hash-node@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/hash-node@npm:3.296.0" - dependencies: - "@aws-sdk/types": 3.296.0 - "@aws-sdk/util-buffer-from": 3.295.0 - "@aws-sdk/util-utf8": 3.295.0 - tslib: ^2.5.0 - checksum: 728680b1bc04764dc710003e6b967e176d65ba46c03e53ffebf25f7d87bad2354203e1c1e754ef6b6bcebbc3bde725aac6a1e00cb47118f78419cefed13b5724 - languageName: node - linkType: hard - -"@aws-sdk/invalid-dependency@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/invalid-dependency@npm:3.296.0" - dependencies: - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 5dc57b4396cbde9f594f14dc2a38e0c12e8cca4557b8e78fb5d23e1429e6dfef0b5ed1382da84862bb19abeaf39b92bc4dc7cba7c7a04dbc212cb6971f0553b0 - languageName: node - linkType: hard - -"@aws-sdk/is-array-buffer@npm:3.295.0": - version: 3.295.0 - resolution: "@aws-sdk/is-array-buffer@npm:3.295.0" - dependencies: - tslib: ^2.5.0 - checksum: 23de81a3ab63a51ae61183792db4a9faf74f02c7c9cb5cfa6d4b36781d7832070090bb406dc8591dd74a07fa3d3c27bac11d7a931e75163f8e018987a995f3ce - languageName: node - linkType: hard - -"@aws-sdk/middleware-content-length@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/middleware-content-length@npm:3.296.0" - dependencies: - "@aws-sdk/protocol-http": 3.296.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 649c0d3f0c1ec607ea9558df09f318bffc763f66501263136791571206c148ca7ca691cb3211fe18bd09aacdc69545fdc5ffff4daeac73323864b5cb76b5c072 - languageName: node - linkType: hard - -"@aws-sdk/middleware-endpoint@npm:3.299.0": - version: 3.299.0 - resolution: "@aws-sdk/middleware-endpoint@npm:3.299.0" - dependencies: - "@aws-sdk/middleware-serde": 3.296.0 - "@aws-sdk/types": 3.296.0 - "@aws-sdk/url-parser": 3.296.0 - "@aws-sdk/util-middleware": 3.296.0 - tslib: ^2.5.0 - checksum: 7e8c6a85575019871c87dc5c1b19c47999bd5ae3fc2546b72e3e617bf8bdea1392fb385e25a51f382b37d01d171de51dfaf2f53b79fb09adc10049ee571fbc13 - languageName: node - linkType: hard - -"@aws-sdk/middleware-host-header@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/middleware-host-header@npm:3.296.0" - dependencies: - "@aws-sdk/protocol-http": 3.296.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 8b5b2b26b204bcc8587cbd659a052b896c4d76dd1633e99d7c035b7b779d2b90beff4878481b8b5b28deeb363bf276584f371a5f7c88cecda43a18c5ae1ab7aa - languageName: node - linkType: hard - -"@aws-sdk/middleware-logger@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/middleware-logger@npm:3.296.0" - dependencies: - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: c006c03d24e26bbc1f9089987a0987b7627e1f7e1a5215d75e7132d2905f77f867d78806b968bee6e6830ddb94ddae8c1cead785b3fe4762e66da10584218ca9 - languageName: node - linkType: hard - -"@aws-sdk/middleware-recursion-detection@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/middleware-recursion-detection@npm:3.296.0" - dependencies: - "@aws-sdk/protocol-http": 3.296.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 2d8b8e130b6410e95340bca961e966c36f54154041aba3b0a308b0b6786cea36e026f28b910ac13411f333e445dbcc122801366bb55f64fab5cd76e05bb70257 - languageName: node - linkType: hard - -"@aws-sdk/middleware-retry@npm:3.300.0": - version: 3.300.0 - resolution: "@aws-sdk/middleware-retry@npm:3.300.0" - dependencies: - "@aws-sdk/protocol-http": 3.296.0 - "@aws-sdk/service-error-classification": 3.296.0 - "@aws-sdk/types": 3.296.0 - "@aws-sdk/util-middleware": 3.296.0 - "@aws-sdk/util-retry": 3.296.0 - tslib: ^2.5.0 - uuid: ^8.3.2 - checksum: 16b76246701eadda581a3a666f27d6100e75cf4d872e83a7e183ab2ec421d689f7c3f86a6dca2b7f0062208340ab578d4285af7d647a279d837891e3ef84b48b - languageName: node - linkType: hard - -"@aws-sdk/middleware-sdk-sts@npm:3.299.0": - version: 3.299.0 - resolution: "@aws-sdk/middleware-sdk-sts@npm:3.299.0" - dependencies: - "@aws-sdk/middleware-signing": 3.299.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: b1e2b28d29d924bbe176203c9a0abbe05a48748845da970a5aa61723b5ad118d50b3790baf63cecb69db63b89dc6e4abca218f14ce258bce87858acf339696de - languageName: node - linkType: hard - -"@aws-sdk/middleware-serde@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/middleware-serde@npm:3.296.0" - dependencies: - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 337bf9c4a621d6ef3a5c5d16273633f098e12e3581e08a9543d48e3a63ba6b70472f26f4e4e46040ed43fc1a498f8f046b66c28fc629f4a3a74b6a331609fd52 - languageName: node - linkType: hard - -"@aws-sdk/middleware-signing@npm:3.299.0": - version: 3.299.0 - resolution: "@aws-sdk/middleware-signing@npm:3.299.0" - dependencies: - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/protocol-http": 3.296.0 - "@aws-sdk/signature-v4": 3.299.0 - "@aws-sdk/types": 3.296.0 - "@aws-sdk/util-middleware": 3.296.0 - tslib: ^2.5.0 - checksum: 4eaa0a3b51eefa0f5e839a55ffa98f7cc79644fb7e8e3a3fbbdc4de2e2d10fb07cbd5201f44464a5550462af5efe47977feb6f057e6bd2160eed0904cd9d1752 - languageName: node - linkType: hard - -"@aws-sdk/middleware-stack@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/middleware-stack@npm:3.296.0" - dependencies: - tslib: ^2.5.0 - checksum: 1fb787c066aa48612e0f7ddd4932c3a9fbfd37eef5838bd83c06bd113d11af76f098e2b09a431ab039fb3f3628b1b80bdaca1200a10d9ec9cde56134e40b6995 - languageName: node - linkType: hard - -"@aws-sdk/middleware-user-agent@npm:3.299.0": - version: 3.299.0 - resolution: "@aws-sdk/middleware-user-agent@npm:3.299.0" - dependencies: - "@aws-sdk/protocol-http": 3.296.0 - "@aws-sdk/types": 3.296.0 - "@aws-sdk/util-endpoints": 3.296.0 - tslib: ^2.5.0 - checksum: 7ae91b41d2a9ab6d016e749458c30eb99f7fc1b11093410e6e2b0f0258aba8269fd5e22a980eb54bcbf8456679bf65c7e76fbf5bb145a13ff7d31398a82821ae - languageName: node - linkType: hard - -"@aws-sdk/node-config-provider@npm:3.300.0": - version: 3.300.0 - resolution: "@aws-sdk/node-config-provider@npm:3.300.0" - dependencies: - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/shared-ini-file-loader": 3.300.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 1ad2536961b9a735278635f8c1dc5c6c260d4e44d490e189a5115f6a41b5f8ae0643a3b213b66f39be457eb6a65cfcbcca07459608e1ac9876af7b92ebe5fbb2 - languageName: node - linkType: hard - -"@aws-sdk/node-http-handler@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/node-http-handler@npm:3.296.0" - dependencies: - "@aws-sdk/abort-controller": 3.296.0 - "@aws-sdk/protocol-http": 3.296.0 - "@aws-sdk/querystring-builder": 3.296.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 8766f46047f1667363a34433c9a867b4e5b27b400252418df2d5b110b6f85ff46a8aeff8b96132f7b39fc2bafa54fb1ae0e0fe44da84c8222349cea05cc7cc38 - languageName: node - linkType: hard - -"@aws-sdk/property-provider@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/property-provider@npm:3.296.0" - dependencies: - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: b456e82002c8302b2ebe328446346381c6d34b63d64f797ffd5b24d9f5cb0d739127d21657587c76a351fa6f56f43585d66020df370ae64f7cd9718c26175fff - languageName: node - linkType: hard - -"@aws-sdk/protocol-http@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/protocol-http@npm:3.296.0" - dependencies: - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: b283a1e6b6a6ba544bf833929d19f988747a451a1e1232f440b9412918932d099f29c0459dd398163b7f7bbae4c372166cc36a5e5afc1343d3085884e36879bc - languageName: node - linkType: hard - -"@aws-sdk/querystring-builder@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/querystring-builder@npm:3.296.0" - dependencies: - "@aws-sdk/types": 3.296.0 - "@aws-sdk/util-uri-escape": 3.295.0 - tslib: ^2.5.0 - checksum: 54028a5087126cdf48cf7abf4cc12c5d761c30aa97b23bab001ae387179cd95974a9332aacc6a74936f3ce818067ded67383231d5839b0456c5ed326bcaeeac5 - languageName: node - linkType: hard - -"@aws-sdk/querystring-parser@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/querystring-parser@npm:3.296.0" - dependencies: - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: b60b003302b4823609f9f585acdc3b7e48444aeaaf941549626066a6a74579201b473cdaa0ab1d59e63c4c8eec5bc9380464ccfeca47077ec4b5bc20fea4b190 - languageName: node - linkType: hard - -"@aws-sdk/service-error-classification@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/service-error-classification@npm:3.296.0" - checksum: f4f53591c636971bb2766340137a4816619c8ede0955dff37f795f03a0ed18534042f3ab9ec898d9f3fba98309322854b7442caeb919590f241ac21b3b18de74 - languageName: node - linkType: hard - -"@aws-sdk/shared-ini-file-loader@npm:3.300.0": - version: 3.300.0 - resolution: "@aws-sdk/shared-ini-file-loader@npm:3.300.0" - dependencies: - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: a85fc99d74b8323dd3fa771244aa072c123355a4f895b8a60d23950971bc3463a5826f70eeb4ac78a5c8395579a8828ad3da64db050888c2a900c85ca95d8a97 - languageName: node - linkType: hard - -"@aws-sdk/signature-v4@npm:3.299.0": - version: 3.299.0 - resolution: "@aws-sdk/signature-v4@npm:3.299.0" - dependencies: - "@aws-sdk/is-array-buffer": 3.295.0 - "@aws-sdk/types": 3.296.0 - "@aws-sdk/util-hex-encoding": 3.295.0 - "@aws-sdk/util-middleware": 3.296.0 - "@aws-sdk/util-uri-escape": 3.295.0 - "@aws-sdk/util-utf8": 3.295.0 - tslib: ^2.5.0 - checksum: 642e49ef1cb87649a0fdceb82c55f5153f9d662d5429d49fb18601c3ff8c12a1b050d8856fb18c041511be9d4d38fa741628c23f4be508cb816be696446139cd - languageName: node - linkType: hard - -"@aws-sdk/smithy-client@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/smithy-client@npm:3.296.0" - dependencies: - "@aws-sdk/middleware-stack": 3.296.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 8b7a626c1fd8b253f1f5b365c340626a9495aa6ba83d246be50f559d8274259daa6386f6931c9db41d3250ee3fd9d2d63646f4f0d7c2f793fed5aa7c87593676 - languageName: node - linkType: hard - -"@aws-sdk/token-providers@npm:3.301.0": - version: 3.301.0 - resolution: "@aws-sdk/token-providers@npm:3.301.0" - dependencies: - "@aws-sdk/client-sso-oidc": 3.301.0 - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/shared-ini-file-loader": 3.300.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: f8dac8a46272dfd966a285f005ab27f3f60f61bd0afd2cb21317aaa86083a3297e7d8dee5f9528c2f1bf8a186a7e681ab66ba7e5d0e85f7fee14de8c78633474 - languageName: node - linkType: hard - -"@aws-sdk/types@npm:3.296.0, @aws-sdk/types@npm:^3.222.0": - version: 3.296.0 - resolution: "@aws-sdk/types@npm:3.296.0" - dependencies: - tslib: ^2.5.0 - checksum: 3cff062f8cb08cd5473c40076ca06f8d93c80fa11e64a7c2d8273fcc0107e709e08a669779f9e3e70d35b3ae7fc3abafd2885f74fa9f8ba57042e5167f72bd2a - languageName: node - linkType: hard - -"@aws-sdk/url-parser@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/url-parser@npm:3.296.0" - dependencies: - "@aws-sdk/querystring-parser": 3.296.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: eb76e25571d9d13f7a335f36dc5f7466a7d7e8a6e26a5a0ea2529cda36d0ad29d0d6c5d418ac8a81a53a480a57a1b8aced93a43a08060eb0fb8e4259ab2e5048 - languageName: node - linkType: hard - -"@aws-sdk/util-base64@npm:3.295.0": - version: 3.295.0 - resolution: "@aws-sdk/util-base64@npm:3.295.0" - dependencies: - "@aws-sdk/util-buffer-from": 3.295.0 - tslib: ^2.5.0 - checksum: 047329e37dd6946f63b47ae415a988c32022b883b9fcf113120973b2d5e97679bc19f5183b264474f3201cb0560d3a4cb4b9d07d4d70b823945387e20869b41d - languageName: node - linkType: hard - -"@aws-sdk/util-body-length-browser@npm:3.295.0": - version: 3.295.0 - resolution: "@aws-sdk/util-body-length-browser@npm:3.295.0" - dependencies: - tslib: ^2.5.0 - checksum: 15100fa717f98a58475c934944108f98b811025039a04eeaaed71380bb2d4444fe44af7c527b74bb6ebd5a3ce277e28896d2e60ea73084f2b9e6cce6873b5592 - languageName: node - linkType: hard - -"@aws-sdk/util-body-length-node@npm:3.295.0": - version: 3.295.0 - resolution: "@aws-sdk/util-body-length-node@npm:3.295.0" - dependencies: - tslib: ^2.5.0 - checksum: e44fa83139d0e7e61152cce8f7709e772c920996fbd3ae4acc545df75ea3f7f28fd2959c97029411802b3e12277f775f3e2dbb34c797369635d0881bcf6b0a12 - languageName: node - linkType: hard - -"@aws-sdk/util-buffer-from@npm:3.295.0": - version: 3.295.0 - resolution: "@aws-sdk/util-buffer-from@npm:3.295.0" - dependencies: - "@aws-sdk/is-array-buffer": 3.295.0 - tslib: ^2.5.0 - checksum: c93e6f0cf66927230588365995d0e94ee874857dae6753529f44ebd7f2d9c1bc6e8fdef0a6459bd96f0c29a2fdc9eaa35145102d074249f4e8ff8bb070708f24 - languageName: node - linkType: hard - -"@aws-sdk/util-config-provider@npm:3.295.0": - version: 3.295.0 - resolution: "@aws-sdk/util-config-provider@npm:3.295.0" - dependencies: - tslib: ^2.5.0 - checksum: 2b6cd2b118465a36c9908dfc330f9f107a0a67cbde2293101af8b3ca0cb2d9f29f76394261880535d62da74b10cf89c5433a2d4524272d5b8776ad8085b0489b - languageName: node - linkType: hard - -"@aws-sdk/util-defaults-mode-browser@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/util-defaults-mode-browser@npm:3.296.0" - dependencies: - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/types": 3.296.0 - bowser: ^2.11.0 - tslib: ^2.5.0 - checksum: ae8c6b03d9de2fc9c320ae4aa65027604658aff95483d3eccdc87307e683da2c6943f863a98f74af7c7f9cae74d0b1df7e6f83592cd0b5eff0ff8eba48fca127 - languageName: node - linkType: hard - -"@aws-sdk/util-defaults-mode-node@npm:3.300.0": - version: 3.300.0 - resolution: "@aws-sdk/util-defaults-mode-node@npm:3.300.0" - dependencies: - "@aws-sdk/config-resolver": 3.300.0 - "@aws-sdk/credential-provider-imds": 3.300.0 - "@aws-sdk/node-config-provider": 3.300.0 - "@aws-sdk/property-provider": 3.296.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: a6c4f5cd85485e23032ab45ea05f9cc943f07b23090c527b7ca159031300a3838c2ce4eb0e545f34a5bdbb2ffce494e85ad7cc86b9b631877fd5f21274bd65f4 - languageName: node - linkType: hard - -"@aws-sdk/util-endpoints@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/util-endpoints@npm:3.296.0" - dependencies: - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - checksum: 9ff3b1c39123e8a88c2dcc1e81757f99cfd787be2039b9172e6a1a8b48b04d78712c2082dd1762e41b8b0a6c2f8d79aa06c571ea7ee622351634e5c40a069643 - languageName: node - linkType: hard - -"@aws-sdk/util-hex-encoding@npm:3.295.0": - version: 3.295.0 - resolution: "@aws-sdk/util-hex-encoding@npm:3.295.0" - dependencies: - tslib: ^2.5.0 - checksum: 4b85f087de5c2a8317ff13df4947e355b4c4acae1dd283133e53139457252fb83951194c85e07b89a1e12cecec1b3c0dbd11b7d0f9f2a7775d8c6d3d9c21371e - languageName: node - linkType: hard - -"@aws-sdk/util-locate-window@npm:^3.0.0": - version: 3.295.0 - resolution: "@aws-sdk/util-locate-window@npm:3.295.0" - dependencies: - tslib: ^2.5.0 - checksum: 81546c251a4f58915059d9d46b207f90ce44890e48fd87507ca280d70a719d8f963864afcffc676fd10ffa55e9b272fc5a522bd0e3d6dde379739adb5b429501 - languageName: node - linkType: hard - -"@aws-sdk/util-middleware@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/util-middleware@npm:3.296.0" - dependencies: - tslib: ^2.5.0 - checksum: 9743a6279208cdb25fc6a5d5800477dfa7f10c25e65cfcb7e0721613c0640df21bef7fe6cb18f27ee3ff470c0be2ff06cb6afdefe234d418c7ac17d9a3563301 - languageName: node - linkType: hard - -"@aws-sdk/util-retry@npm:3.296.0": - version: 3.296.0 - resolution: "@aws-sdk/util-retry@npm:3.296.0" - dependencies: - "@aws-sdk/service-error-classification": 3.296.0 - tslib: ^2.5.0 - checksum: f225fc4eb0933eda3df069d2b0028d8eed76be9b7b871b7bf601c4f5f17012ebb6ebb194df26fc141c9e77bad86d5cbdb79113fe5624d3c2c9d3441f96f580b3 - languageName: node - linkType: hard - -"@aws-sdk/util-uri-escape@npm:3.295.0": - version: 3.295.0 - resolution: "@aws-sdk/util-uri-escape@npm:3.295.0" - dependencies: - tslib: ^2.5.0 - checksum: 2334baedd339161aa2fb6ae880c04730b072a217ed42b40aa0c0df526aba5a663302da50ba550ad657a4b3cf44070696d91501bf2ba33722f452247f5c2d0fde - languageName: node - linkType: hard - -"@aws-sdk/util-user-agent-browser@npm:3.299.0": - version: 3.299.0 - resolution: "@aws-sdk/util-user-agent-browser@npm:3.299.0" - dependencies: - "@aws-sdk/types": 3.296.0 - bowser: ^2.11.0 - tslib: ^2.5.0 - checksum: 60ebb1c0f5de624c212f00641fdcc8b667dac6442413d0d882745b660f72a6d1107cd6b04e7a5eee4013f0444b9fedfe151317937ac1f7d333dd3feeefa027b0 - languageName: node - linkType: hard - -"@aws-sdk/util-user-agent-node@npm:3.300.0": - version: 3.300.0 - resolution: "@aws-sdk/util-user-agent-node@npm:3.300.0" - dependencies: - "@aws-sdk/node-config-provider": 3.300.0 - "@aws-sdk/types": 3.296.0 - tslib: ^2.5.0 - peerDependencies: - aws-crt: ">=1.0.0" - peerDependenciesMeta: - aws-crt: - optional: true - checksum: a42670de913d554d9a102a49920f52eb158fec64fb12d83ce610d6259da17d0ce56cf2e96299c35d019109ca514cd94adb9e8e1a103ce844ee8d4c17236dd35e - languageName: node - linkType: hard - -"@aws-sdk/util-utf8-browser@npm:^3.0.0": - version: 3.259.0 - resolution: "@aws-sdk/util-utf8-browser@npm:3.259.0" - dependencies: - tslib: ^2.3.1 - checksum: b6a1e580da1c9b62c749814182a7649a748ca4253edb4063aa521df97d25b76eae3359eb1680b86f71aac668e05cc05c514379bca39ebf4ba998ae4348412da8 - languageName: node - linkType: hard - -"@aws-sdk/util-utf8@npm:3.295.0": - version: 3.295.0 - resolution: "@aws-sdk/util-utf8@npm:3.295.0" - dependencies: - "@aws-sdk/util-buffer-from": 3.295.0 - tslib: ^2.5.0 - checksum: 098058651aa48bb2a6652ea6a1a0a1520e9964a91920e67eed023eacc0956b75475d25025e888ee193140aa800ed89faafa67653914b78c7ffb98e8f96f6d54c - languageName: node - linkType: hard - "@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.0, @babel/code-frame@npm:^7.18.6, @babel/code-frame@npm:^7.8.3": version: 7.18.6 resolution: "@babel/code-frame@npm:7.18.6" @@ -5694,7 +4831,7 @@ __metadata: dependencies: "@sofie-automation/shared-lib": 1.51.0-in-development tslib: ^2.6.0 - type-fest: ^2.19.0 + type-fest: ^3.10.0 languageName: unknown linkType: soft @@ -5743,10 +4880,10 @@ __metadata: prom-client: ^14.2.0 timecode: 0.0.4 tslib: ^2.6.0 - type-fest: ^2.19.0 + type-fest: ^3.10.0 underscore: ^1.13.6 peerDependencies: - mongodb: ^4.13.0 + mongodb: ^5.5.0 languageName: unknown linkType: soft @@ -5772,13 +4909,13 @@ __metadata: deepmerge: ^4.3.1 elastic-apm-node: ^3.47.0 eventemitter3: ^4.0.7 - mongodb: ^4.16.0 + mongodb: ^5.5.0 p-lazy: ^3.1.0 p-timeout: ^4.1.0 superfly-timeline: ^8.3.1 threadedclass: ^1.2.1 tslib: ^2.6.0 - type-fest: ^2.19.0 + type-fest: ^3.10.0 underscore: ^1.13.6 vm2: ^3.9.19 languageName: unknown @@ -5817,7 +4954,7 @@ __metadata: "@mos-connection/model": ^3.0.4 timeline-state-resolver-types: 9.0.0-release50.5 tslib: ^2.6.0 - type-fest: ^2.19.0 + type-fest: ^3.10.0 languageName: unknown linkType: soft @@ -8670,13 +7807,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"bowser@npm:^2.11.0": - version: 2.11.0 - resolution: "bowser@npm:2.11.0" - checksum: 29c3f01f22e703fa6644fc3b684307442df4240b6e10f6cfe1b61c6ca5721073189ca97cdeedb376081148c8518e33b1d818a57f781d70b0b70e1f31fb48814f - languageName: node - linkType: hard - "boxen@npm:^5.0.0": version: 5.1.2 resolution: "boxen@npm:5.1.2" @@ -8794,12 +7924,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"bson@npm:^4.7.2": - version: 4.7.2 - resolution: "bson@npm:4.7.2" - dependencies: - buffer: ^5.6.0 - checksum: f357d12c5679c8eb029a62e410ad40fb862b7b91f0fc12a3399fb3668e14aecaa63205ffeeee48735a01d393171743607dcd527eb8c058b6f2bd294079ee4125 +"bson@npm:^5.3.0": + version: 5.3.0 + resolution: "bson@npm:5.3.0" + checksum: c1e57630fea6b82ad8bc0fb83fd8f16a2620a836ab69f6b9433250cd19704c0c8995681a3bfab9f967eae30512f0c8ee921d42ace4c1781726600c4a367593cf languageName: node linkType: hard @@ -8824,7 +7952,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"buffer@npm:^5.2.1, buffer@npm:^5.5.0, buffer@npm:^5.6.0": +"buffer@npm:^5.2.1, buffer@npm:^5.5.0": version: 5.7.1 resolution: "buffer@npm:5.7.1" dependencies: @@ -12180,17 +11308,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"fast-xml-parser@npm:4.1.2": - version: 4.1.2 - resolution: "fast-xml-parser@npm:4.1.2" - dependencies: - strnum: ^1.0.5 - bin: - fxparser: src/cli/cli.js - checksum: 6a7d1b17057f8470e70603eddfa75f990625735d068d57ece861d0154ad8d27fda63c2831d07e1ecd7e68e993738b2448925cb9277d8c0ed68009623bbcd63c6 - languageName: node - linkType: hard - "fastq@npm:^1.6.0": version: 1.15.0 resolution: "fastq@npm:1.15.0" @@ -17154,7 +16271,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"mongodb-connection-string-url@npm:^2.5.4": +"mongodb-connection-string-url@npm:^2.6.0": version: 2.6.0 resolution: "mongodb-connection-string-url@npm:2.6.0" dependencies: @@ -17164,21 +16281,29 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"mongodb@npm:^4.16.0": - version: 4.16.0 - resolution: "mongodb@npm:4.16.0" +"mongodb@npm:^5.5.0": + version: 5.5.0 + resolution: "mongodb@npm:5.5.0" dependencies: - "@aws-sdk/credential-providers": ^3.186.0 - bson: ^4.7.2 - mongodb-connection-string-url: ^2.5.4 + bson: ^5.3.0 + mongodb-connection-string-url: ^2.6.0 saslprep: ^1.0.3 socks: ^2.7.1 + peerDependencies: + "@aws-sdk/credential-providers": ^3.201.0 + mongodb-client-encryption: ">=2.3.0 <3" + snappy: ^7.2.2 dependenciesMeta: + saslprep: + optional: true + peerDependenciesMeta: "@aws-sdk/credential-providers": optional: true - saslprep: + mongodb-client-encryption: optional: true - checksum: f0b1347739cc362b82b3aabc7e7d4d74bc7a344ed1bbafd6f92681bcab440f6cc618ffa0438d41d2789cb34818f3b09d4c78f517b42160ebae55bf2c96f13953 + snappy: + optional: true + checksum: fafb75195d605767cf7269aa82a1f4704c3357240d0fef7296d55e83eab592158844988af3b94165769459ff1b2a0af2fc49bc44c9bbabd10d6ddc319668a0e0 languageName: node linkType: hard @@ -17197,7 +16322,7 @@ asn1@evs-broadcast/node-asn1: "@sofie-automation/server-core-integration": 1.51.0-in-development "@sofie-automation/shared-lib": 1.51.0-in-development tslib: ^2.6.0 - type-fest: ^2.19.0 + type-fest: ^3.10.0 underscore: ^1.13.6 winston: ^3.9.0 languageName: unknown @@ -22414,13 +21539,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"strnum@npm:^1.0.5": - version: 1.0.5 - resolution: "strnum@npm:1.0.5" - checksum: 651b2031db5da1bf4a77fdd2f116a8ac8055157c5420f5569f64879133825915ad461513e7202a16d7fec63c54fd822410d0962f8ca12385c4334891b9ae6dd2 - languageName: node - linkType: hard - "strong-log-transformer@npm:2.1.0, strong-log-transformer@npm:^2.1.0": version: 2.1.0 resolution: "strong-log-transformer@npm:2.1.0" @@ -23250,7 +22368,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"tslib@npm:^1.11.1, tslib@npm:^1.13.0, tslib@npm:^1.14.1, tslib@npm:^1.8.1, tslib@npm:^1.9.0": +"tslib@npm:^1.13.0, tslib@npm:^1.14.1, tslib@npm:^1.8.1, tslib@npm:^1.9.0": version: 1.14.1 resolution: "tslib@npm:1.14.1" checksum: dbe628ef87f66691d5d2959b3e41b9ca0045c3ee3c7c7b906cc1e328b39f199bb1ad9e671c39025bd56122ac57dfbf7385a94843b1cc07c60a4db74795829acd @@ -23425,7 +22543,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"type-fest@npm:^2.0.0, type-fest@npm:^2.12.2, type-fest@npm:^2.13.0, type-fest@npm:^2.19.0, type-fest@npm:^2.5.0": +"type-fest@npm:^2.0.0, type-fest@npm:^2.12.2, type-fest@npm:^2.13.0, type-fest@npm:^2.5.0": version: 2.19.0 resolution: "type-fest@npm:2.19.0" checksum: a4ef07ece297c9fba78fc1bd6d85dff4472fe043ede98bd4710d2615d15776902b595abf62bd78339ed6278f021235fb28a96361f8be86ed754f778973a0d278 @@ -23439,6 +22557,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"type-fest@npm:^3.10.0": + version: 3.10.0 + resolution: "type-fest@npm:3.10.0" + peerDependencies: + typescript: ">=4.7.0" + checksum: df3bd25809ea79ac7712da7c11648251118c23f65b226eba263797e42be60ba34085874ec492202b8ecf2e5a2273b27ddcbfe3be2c275dfcef3f0e2ee90ea179 + languageName: node + linkType: hard + "type-is@npm:~1.6.18": version: 1.6.18 resolution: "type-is@npm:1.6.18" From a7f7d4f19fbedee373c7afe3bef62e8c2d531e8a Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 4 Jul 2023 16:24:59 +0100 Subject: [PATCH 005/479] chore: tidy up Studio collection rexports --- meteor/__mocks__/defaultCollectionObjects.ts | 2 +- meteor/__mocks__/helpers/database.ts | 8 +++---- meteor/client/collections/index.ts | 4 ++-- .../VTFloatingInspector.tsx | 2 +- meteor/client/ui/RundownView.tsx | 2 +- .../RundownView/RundownRightHandControls.tsx | 6 ++++- .../ui/RundownView/SwitchboardPopUp.tsx | 6 ++++- .../Settings/BlueprintConfigSchema/index.tsx | 2 +- .../client/ui/Settings/BlueprintSettings.tsx | 4 ++-- .../ui/Settings/RundownLayoutEditor.tsx | 4 ++-- meteor/client/ui/Settings/SettingsMenu.tsx | 6 ++--- .../client/ui/Settings/ShowStyle/Generic.tsx | 4 ++-- .../ui/Settings/ShowStyleBaseSettings.tsx | 4 ++-- meteor/client/ui/Settings/SnapshotsView.tsx | 4 ++-- meteor/client/ui/Settings/Studio/Generic.tsx | 4 ++-- meteor/client/ui/Settings/Studio/Mappings.tsx | 5 +++-- .../ui/Settings/Studio/PackageManager.tsx | 4 ++-- meteor/client/ui/Settings/Studio/Routings.tsx | 7 +++--- meteor/client/ui/TestTools/Mappings.tsx | 3 ++- meteor/lib/api/pubsub.ts | 3 ++- meteor/lib/api/studios.ts | 7 +++++- meteor/lib/api/triggers/actionFactory.ts | 4 ++-- meteor/lib/collections/ExpectedPackages.ts | 2 +- meteor/lib/collections/Studios.ts | 5 ----- meteor/lib/collections/Timeline.ts | 2 +- .../server/__tests__/_testEnvironment.test.ts | 2 +- .../api/__tests__/userActions/system.test.ts | 8 +++---- meteor/server/api/blueprintConfigPresets.ts | 4 ++-- .../__tests__/migrationContext.test.ts | 22 +++++++++---------- meteor/server/api/blueprints/api.ts | 4 ++-- .../server/api/blueprints/migrationContext.ts | 6 ++--- meteor/server/api/playout/playout.ts | 2 +- meteor/server/api/rest/v1/index.ts | 4 ++-- meteor/server/api/rest/v1/typeConversion.ts | 5 ++--- meteor/server/api/rundown.ts | 4 ++-- meteor/server/api/snapshot.ts | 6 ++--- meteor/server/api/studio/api.ts | 2 +- meteor/server/collections/index.ts | 4 ++-- .../coreSystem/checkDatabaseVersions.ts | 4 ++-- meteor/server/migration/1_42_0.ts | 2 +- meteor/server/migration/1_47_0.ts | 2 +- meteor/server/migration/1_50_0.ts | 6 ++--- .../migration/__tests__/migrations.test.ts | 4 ++-- .../server/migration/upgrades/checkStatus.ts | 4 ++-- meteor/server/migration/upgrades/studio.ts | 6 ++--- meteor/server/optimizations.ts | 4 +--- .../expectedPackages/generate.ts | 6 ++--- .../expectedPackages/publication.ts | 8 +++---- .../packageManager/packageContainers.ts | 3 +-- .../publications/peripheralDeviceForDevice.ts | 13 +++++++---- .../__tests__/checkPieceContentStatus.test.ts | 4 ++-- .../checkPieceContentStatus.ts | 5 +++-- .../pieceContentStatusUI/common.ts | 6 ++--- meteor/server/publications/studio.ts | 4 +++- meteor/server/publications/studioUI.ts | 2 +- meteor/server/publications/timeline.ts | 5 +++-- meteor/server/security/lib/security.ts | 3 ++- meteor/server/security/organization.ts | 3 ++- meteor/server/security/studio.ts | 3 ++- .../server/systemStatus/blueprintVersions.ts | 4 ++-- meteor/server/webmanifest.ts | 2 +- 61 files changed, 149 insertions(+), 131 deletions(-) diff --git a/meteor/__mocks__/defaultCollectionObjects.ts b/meteor/__mocks__/defaultCollectionObjects.ts index 3179bde198..8fa02c3d9d 100644 --- a/meteor/__mocks__/defaultCollectionObjects.ts +++ b/meteor/__mocks__/defaultCollectionObjects.ts @@ -1,4 +1,4 @@ -import { DBStudio } from '../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { clone, getCurrentTime, unprotectString } from '../lib/lib' import { DBRundownPlaylist } from '../lib/collections/RundownPlaylists' import { DBRundown } from '../lib/collections/Rundowns' diff --git a/meteor/__mocks__/helpers/database.ts b/meteor/__mocks__/helpers/database.ts index 23c08c2b98..68537077e8 100644 --- a/meteor/__mocks__/helpers/database.ts +++ b/meteor/__mocks__/helpers/database.ts @@ -6,7 +6,7 @@ import { PERIPHERAL_SUBTYPE_PROCESS, PeripheralDeviceSubType, } from '../../lib/collections/PeripheralDevices' -import { Studio, DBStudio } from '../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { PieceLifespan, IOutputLayer, @@ -118,7 +118,7 @@ export async function setupMockPeripheralDevice( category: PeripheralDeviceCategory, type: PeripheralDeviceType, subType: PeripheralDeviceSubType, - studio?: Pick, + studio?: Pick, doc?: Partial ): Promise { doc = doc || {} @@ -220,7 +220,7 @@ export async function setupMockTriggeredActions( } return mocks } -export async function setupMockStudio(doc?: Partial): Promise { +export async function setupMockStudio(doc?: Partial): Promise { doc = doc || {} const studio: DBStudio = { @@ -519,7 +519,7 @@ export interface DefaultEnvironment { showStyleBase: DBShowStyleBase triggeredActions: DBTriggeredActions[] showStyleVariant: DBShowStyleVariant - studio: Studio + studio: DBStudio core: ICoreSystem systemTriggeredActions: DBTriggeredActions[] diff --git a/meteor/client/collections/index.ts b/meteor/client/collections/index.ts index 683b30abd0..8a3871f2b3 100644 --- a/meteor/client/collections/index.ts +++ b/meteor/client/collections/index.ts @@ -31,7 +31,7 @@ import { RundownLayoutBase } from '../../lib/collections/RundownLayouts' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' import { SnapshotItem } from '../../lib/collections/Snapshots' -import { Studio } from '../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { TranslationsBundle } from '../../lib/collections/TranslationsBundles' import { DBTriggeredActions } from '../../lib/collections/TriggeredActions' import { UserActionsLogItem } from '../../lib/collections/UserActionsLog' @@ -84,7 +84,7 @@ export const ShowStyleVariants = createSyncMongoCollection(C export const Snapshots = createSyncMongoCollection(CollectionName.Snapshots) -export const Studios = createSyncMongoCollection(CollectionName.Studios) +export const Studios = createSyncMongoCollection(CollectionName.Studios) export const TranslationsBundles = createSyncReadOnlyMongoCollection( CollectionName.TranslationsBundles diff --git a/meteor/client/ui/FloatingInspectors/VTFloatingInspector.tsx b/meteor/client/ui/FloatingInspectors/VTFloatingInspector.tsx index 1ec52ac50c..291403e55e 100644 --- a/meteor/client/ui/FloatingInspectors/VTFloatingInspector.tsx +++ b/meteor/client/ui/FloatingInspectors/VTFloatingInspector.tsx @@ -5,7 +5,7 @@ import { CriticalIconSmall, WarningIconSmall } from '../../lib/ui/icons/notifica import { FloatingInspector } from '../FloatingInspector' import { NoticeLevel } from '../../../lib/notifications/notifications' import { VTContent } from '@sofie-automation/blueprints-integration' -import { IStudioSettings } from '../../../lib/collections/Studios' +import { IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' import { PieceStatusCode } from '../../../lib/collections/Pieces' import { VideoPreviewPlayer } from '../../lib/VideoPreviewPlayer' import classNames from 'classnames' diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx index e542158eca..9fedb10232 100644 --- a/meteor/client/ui/RundownView.tsx +++ b/meteor/client/ui/RundownView.tsx @@ -18,7 +18,7 @@ import { NavLink, Route, Prompt } from 'react-router-dom' import { RundownPlaylist } from '../../lib/collections/RundownPlaylists' import { Rundown } from '../../lib/collections/Rundowns' import { DBSegment, Segment } from '../../lib/collections/Segments' -import { StudioRouteSet } from '../../lib/collections/Studios' +import { StudioRouteSet } from '@sofie-automation/corelib/dist/dataModel/Studio' import { Part } from '../../lib/collections/Parts' import { ContextMenu, MenuItem, ContextMenuTrigger } from '@jstarpl/react-contextmenu' import { RundownTimingProvider } from './RundownView/RundownTiming/RundownTimingProvider' diff --git a/meteor/client/ui/RundownView/RundownRightHandControls.tsx b/meteor/client/ui/RundownView/RundownRightHandControls.tsx index d62db5adee..a6868fd7f4 100644 --- a/meteor/client/ui/RundownView/RundownRightHandControls.tsx +++ b/meteor/client/ui/RundownView/RundownRightHandControls.tsx @@ -1,7 +1,11 @@ import React, { useEffect, useState } from 'react' import * as VelocityReact from 'velocity-react' -import { StudioRouteSet, StudioRouteBehavior, StudioRouteSetExclusivityGroup } from '../../../lib/collections/Studios' +import { + StudioRouteSet, + StudioRouteBehavior, + StudioRouteSetExclusivityGroup, +} from '@sofie-automation/corelib/dist/dataModel/Studio' import { RewindAllSegmentsIcon } from '../../lib/ui/icons/rewindAllSegmentsIcon' import { Lottie } from '@crello/react-lottie' diff --git a/meteor/client/ui/RundownView/SwitchboardPopUp.tsx b/meteor/client/ui/RundownView/SwitchboardPopUp.tsx index d970a9b201..89a2219863 100644 --- a/meteor/client/ui/RundownView/SwitchboardPopUp.tsx +++ b/meteor/client/ui/RundownView/SwitchboardPopUp.tsx @@ -1,7 +1,11 @@ import * as React from 'react' import { withTranslation } from 'react-i18next' import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData' -import { StudioRouteSet, StudioRouteSetExclusivityGroup, StudioRouteBehavior } from '../../../lib/collections/Studios' +import { + StudioRouteSet, + StudioRouteSetExclusivityGroup, + StudioRouteBehavior, +} from '@sofie-automation/corelib/dist/dataModel/Studio' import classNames from 'classnames' import { RouteSetOverrideIcon } from '../../lib/ui/icons/switchboard' import Tooltip from 'rc-tooltip' diff --git a/meteor/client/ui/Settings/BlueprintConfigSchema/index.tsx b/meteor/client/ui/Settings/BlueprintConfigSchema/index.tsx index 667087c13d..d2a2630389 100644 --- a/meteor/client/ui/Settings/BlueprintConfigSchema/index.tsx +++ b/meteor/client/ui/Settings/BlueprintConfigSchema/index.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useMemo } from 'react' -import { MappingExt, MappingsExt } from '../../../../lib/collections/Studios' +import { MappingExt, MappingsExt } from '@sofie-automation/corelib/dist/dataModel/Studio' import { IBlueprintConfig, ISourceLayer, SchemaFormUIField } from '@sofie-automation/blueprints-integration' import { groupByToMapFunc, literal } from '../../../../lib/lib' import { useTranslation } from 'react-i18next' diff --git a/meteor/client/ui/Settings/BlueprintSettings.tsx b/meteor/client/ui/Settings/BlueprintSettings.tsx index 0020148516..217395ea64 100644 --- a/meteor/client/ui/Settings/BlueprintSettings.tsx +++ b/meteor/client/ui/Settings/BlueprintSettings.tsx @@ -7,7 +7,7 @@ import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { Blueprint } from '../../../lib/collections/Blueprints' import Moment from 'react-moment' import { Link } from 'react-router-dom' -import { Studio } from '../../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { ICoreSystem } from '../../../lib/collections/CoreSystem' import { BlueprintManifestType } from '@sofie-automation/blueprints-integration' @@ -31,7 +31,7 @@ interface IState { } interface ITrackedProps { blueprint?: Blueprint - assignedStudios: Studio[] + assignedStudios: DBStudio[] assignedShowStyles: DBShowStyleBase[] assignedSystem: ICoreSystem | undefined } diff --git a/meteor/client/ui/Settings/RundownLayoutEditor.tsx b/meteor/client/ui/Settings/RundownLayoutEditor.tsx index 610fd653fc..aac1f39a52 100644 --- a/meteor/client/ui/Settings/RundownLayoutEditor.tsx +++ b/meteor/client/ui/Settings/RundownLayoutEditor.tsx @@ -26,7 +26,7 @@ import { UploadButton } from '../../lib/uploadButton' import { doModalDialog } from '../../lib/ModalDialog' import { NotificationCenter, Notification, NoticeLevel } from '../../../lib/notifications/notifications' import { fetchFrom } from '../../lib/lib' -import { Studio } from '../../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { Link } from 'react-router-dom' import { MeteorCall } from '../../../lib/api/methods' import { defaultColorPickerPalette } from '../../lib/colorPicker' @@ -43,7 +43,7 @@ export interface IProps { showStyleBaseId: ShowStyleBaseId sourceLayers: SourceLayers outputLayers: OutputLayers - studios: Studio[] + studios: DBStudio[] customRegion: CustomizableRegionSettingsManifest } diff --git a/meteor/client/ui/Settings/SettingsMenu.tsx b/meteor/client/ui/Settings/SettingsMenu.tsx index f8bc35f57f..d882d00c87 100644 --- a/meteor/client/ui/Settings/SettingsMenu.tsx +++ b/meteor/client/ui/Settings/SettingsMenu.tsx @@ -4,7 +4,7 @@ import { unprotectString } from '../../../lib/lib' import { doModalDialog } from '../../lib/ModalDialog' import { NavLink, useLocation } from 'react-router-dom' -import { DBStudio, Studio } from '../../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { PeripheralDevice, PERIPHERAL_SUBTYPE_PROCESS } from '../../../lib/collections/PeripheralDevices' import { NotificationCenter, Notification, NoticeLevel } from '../../../lib/notifications/notifications' @@ -28,7 +28,7 @@ interface ISettingsMenuProps { } interface ISettingsMenuState {} interface ISettingsMenuTrackedProps { - studios: Array + studios: Array showStyleBases: Array blueprints: Array peripheralDevices: Array @@ -280,7 +280,7 @@ function SettingsMenuStudio({ studio }: SettingsMenuStudioProps) { ) } -function studioHasError(studio: Studio): boolean { +function studioHasError(studio: DBStudio): boolean { if (!studio.name) return true if (!studio.supportedShowStyleBase.length) return true if (!studio.blueprintId) return true diff --git a/meteor/client/ui/Settings/ShowStyle/Generic.tsx b/meteor/client/ui/Settings/ShowStyle/Generic.tsx index 87cb8622c7..f63301a2f5 100644 --- a/meteor/client/ui/Settings/ShowStyle/Generic.tsx +++ b/meteor/client/ui/Settings/ShowStyle/Generic.tsx @@ -6,13 +6,13 @@ import { unprotectString } from '../../../../lib/lib' import { EditAttribute } from '../../../lib/EditAttribute' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { Link } from 'react-router-dom' -import { Studio } from '../../../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { ShowStyleBases } from '../../../collections' import { LabelActual } from '../../../lib/Components/LabelAndOverrides' interface IShowStyleGenericPropertiesProps { showStyleBase: DBShowStyleBase - compatibleStudios: Array + compatibleStudios: Array } export function ShowStyleGenericProperties({ showStyleBase, diff --git a/meteor/client/ui/Settings/ShowStyleBaseSettings.tsx b/meteor/client/ui/Settings/ShowStyleBaseSettings.tsx index 15ab48d727..ac03be3e5c 100644 --- a/meteor/client/ui/Settings/ShowStyleBaseSettings.tsx +++ b/meteor/client/ui/Settings/ShowStyleBaseSettings.tsx @@ -5,7 +5,7 @@ import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { OutputLayers, DBShowStyleBase, SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' import RundownLayoutEditor from './RundownLayoutEditor' -import { Studio, MappingsExt } from '../../../lib/collections/Studios' +import { DBStudio, MappingsExt } from '@sofie-automation/corelib/dist/dataModel/Studio' import { BlueprintManifestType, IShowStyleConfigPreset } from '@sofie-automation/blueprints-integration' import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts' import { TriggeredActionsEditor } from './components/triggeredActions/TriggeredActionsEditor' @@ -41,7 +41,7 @@ interface IState { interface ITrackedProps { showStyleBase?: DBShowStyleBase showStyleVariants: Array - compatibleStudios: Array + compatibleStudios: Array blueprintConfigSchema: JSONSchema | undefined blueprintConfigPreset: IShowStyleConfigPreset | undefined sourceLayers: SourceLayers diff --git a/meteor/client/ui/Settings/SnapshotsView.tsx b/meteor/client/ui/Settings/SnapshotsView.tsx index ff5277197b..3e5fd407ec 100644 --- a/meteor/client/ui/Settings/SnapshotsView.tsx +++ b/meteor/client/ui/Settings/SnapshotsView.tsx @@ -9,7 +9,7 @@ import { logger } from '../../../lib/logging' import { EditAttribute } from '../../lib/EditAttribute' import { faWindowClose, faUpload } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { Studio } from '../../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { multilineText, fetchFrom } from '../../lib/lib' import { NotificationCenter, Notification, NoticeLevel } from '../../../lib/notifications/notifications' import { UploadButton } from '../../lib/uploadButton' @@ -34,7 +34,7 @@ interface IState { } interface ITrackedProps { snapshots: Array - studios: Array + studios: Array } export default translateWithTracker(() => { return { diff --git a/meteor/client/ui/Settings/Studio/Generic.tsx b/meteor/client/ui/Settings/Studio/Generic.tsx index b8e3fe7282..5698e4409d 100644 --- a/meteor/client/ui/Settings/Studio/Generic.tsx +++ b/meteor/client/ui/Settings/Studio/Generic.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Studio } from '../../../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { Translated } from '../../../lib/ReactMeteorData/react-meteor-data' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons' @@ -14,7 +14,7 @@ import { MeteorCall } from '../../../../lib/api/methods' import { LabelActual } from '../../../lib/Components/LabelAndOverrides' interface IStudioGenericPropertiesProps { - studio: Studio + studio: DBStudio availableShowStyleBases: Array<{ name: string value: ShowStyleBaseId diff --git a/meteor/client/ui/Settings/Studio/Mappings.tsx b/meteor/client/ui/Settings/Studio/Mappings.tsx index 719a61288d..371c5721d8 100644 --- a/meteor/client/ui/Settings/Studio/Mappings.tsx +++ b/meteor/client/ui/Settings/Studio/Mappings.tsx @@ -1,7 +1,8 @@ import ClassNames from 'classnames' import React, { useCallback, useMemo } from 'react' import Tooltip from 'rc-tooltip' -import { Studio, MappingExt, getActiveRoutes, ResultingMappingRoutes } from '../../../../lib/collections/Studios' +import { getActiveRoutes } from '../../../../lib/collections/Studios' +import { DBStudio, MappingExt, ResultingMappingRoutes } from '@sofie-automation/corelib/dist/dataModel/Studio' import { doModalDialog } from '../../../lib/ModalDialog' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faTrash, faPencilAlt, faCheck, faPlus, faSync } from '@fortawesome/free-solid-svg-icons' @@ -51,7 +52,7 @@ export interface MappingsSettingsManifest { export type MappingsSettingsManifests = Record interface IStudioMappingsProps { - studio: Studio + studio: DBStudio manifest: MappingsSettingsManifests | undefined translationNamespaces: string[] } diff --git a/meteor/client/ui/Settings/Studio/PackageManager.tsx b/meteor/client/ui/Settings/Studio/PackageManager.tsx index d56ec1cb95..d5cb8125c9 100644 --- a/meteor/client/ui/Settings/Studio/PackageManager.tsx +++ b/meteor/client/ui/Settings/Studio/PackageManager.tsx @@ -2,7 +2,7 @@ import ClassNames from 'classnames' import * as React from 'react' import { Meteor } from 'meteor/meteor' import * as _ from 'underscore' -import { Studio, DBStudio, StudioPackageContainer } from '../../../../lib/collections/Studios' +import { DBStudio, StudioPackageContainer } from '@sofie-automation/corelib/dist/dataModel/Studio' import { EditAttribute, EditAttributeBase } from '../../../lib/EditAttribute' import { doModalDialog } from '../../../lib/ModalDialog' import { Translated } from '../../../lib/ReactMeteorData/react-meteor-data' @@ -15,7 +15,7 @@ import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settin import { LabelActual } from '../../../lib/Components/LabelAndOverrides' interface IStudioPackageManagerSettingsProps { - studio: Studio + studio: DBStudio } interface IStudioPackageManagerSettingsState { editedPackageContainer: Array diff --git a/meteor/client/ui/Settings/Studio/Routings.tsx b/meteor/client/ui/Settings/Studio/Routings.tsx index 9cf623c74f..8a4175fb63 100644 --- a/meteor/client/ui/Settings/Studio/Routings.tsx +++ b/meteor/client/ui/Settings/Studio/Routings.tsx @@ -3,7 +3,6 @@ import * as React from 'react' import { Meteor } from 'meteor/meteor' import * as _ from 'underscore' import { - Studio, DBStudio, StudioRouteSet, StudioRouteBehavior, @@ -12,7 +11,7 @@ import { StudioRouteType, MappingsExt, MappingExt, -} from '../../../../lib/collections/Studios' +} from '@sofie-automation/corelib/dist/dataModel/Studio' import { EditAttribute, EditAttributeBase } from '../../../lib/EditAttribute' import { doModalDialog } from '../../../lib/ModalDialog' import { Translated } from '../../../lib/ReactMeteorData/react-meteor-data' @@ -33,7 +32,7 @@ import { LabelActual } from '../../../lib/Components/LabelAndOverrides' interface IStudioRoutingsProps { translationNamespaces: string[] - studio: Studio + studio: DBStudio studioMappings: ReadonlyDeep manifest: MappingsSettingsManifests | undefined } @@ -733,7 +732,7 @@ export const StudioRoutings = withTranslation()( interface IDeviceMappingSettingsProps { translationNamespaces: string[] - studio: Studio + studio: DBStudio attribute: string manifest: MappingsSettingsManifest | undefined mappedLayer: ReadonlyDeep | undefined diff --git a/meteor/client/ui/TestTools/Mappings.tsx b/meteor/client/ui/TestTools/Mappings.tsx index 04ad4f5a1b..11ebf344aa 100644 --- a/meteor/client/ui/TestTools/Mappings.tsx +++ b/meteor/client/ui/TestTools/Mappings.tsx @@ -6,10 +6,11 @@ import { omit, Time, unprotectString } from '../../../lib/lib' import { CustomCollectionName, PubSub } from '../../../lib/api/pubsub' import { makeTableOfObject } from '../../lib/utilComponents' import { StudioSelect } from './StudioSelect' -import { MappingExt, RoutedMappings } from '../../../lib/collections/Studios' +import { MappingExt } from '@sofie-automation/corelib/dist/dataModel/Studio' import { LookaheadMode, TSR } from '@sofie-automation/blueprints-integration' import { createSyncCustomPublicationMongoCollection } from '../../../lib/collections/lib' import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { RoutedMappings } from '@sofie-automation/shared-lib/dist/core/model/Timeline' const StudioMappings = createSyncCustomPublicationMongoCollection(CustomCollectionName.StudioMappings) diff --git a/meteor/lib/api/pubsub.ts b/meteor/lib/api/pubsub.ts index 5b6dd9b97c..69bd97af2e 100644 --- a/meteor/lib/api/pubsub.ts +++ b/meteor/lib/api/pubsub.ts @@ -43,7 +43,7 @@ import { DBSegment } from '../collections/Segments' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' import { SnapshotItem } from '../collections/Snapshots' -import { DBStudio, RoutedMappings } from '../collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { RoutedTimeline, TimelineComplete } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { TranslationsBundle } from '../collections/TranslationsBundles' import { DBTriggeredActions, UITriggeredActionsObj } from '../collections/TriggeredActions' @@ -61,6 +61,7 @@ import { PackageManagerPlayoutContext, } from '@sofie-automation/shared-lib/dist/package-manager/publications' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' +import { RoutedMappings } from '@sofie-automation/shared-lib/dist/core/model/Timeline' /** * Ids of possible DDP subscriptions diff --git a/meteor/lib/api/studios.ts b/meteor/lib/api/studios.ts index 0370a7e4b1..ee232fe436 100644 --- a/meteor/lib/api/studios.ts +++ b/meteor/lib/api/studios.ts @@ -1,5 +1,10 @@ import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { IStudioSettings, MappingsExt, StudioRouteSet, StudioRouteSetExclusivityGroup } from '../collections/Studios' +import { + IStudioSettings, + MappingsExt, + StudioRouteSet, + StudioRouteSetExclusivityGroup, +} from '@sofie-automation/corelib/dist/dataModel/Studio' export interface NewStudiosAPI { insertStudio(): Promise diff --git a/meteor/lib/api/triggers/actionFactory.ts b/meteor/lib/api/triggers/actionFactory.ts index 3857b09958..e91e3bbeee 100644 --- a/meteor/lib/api/triggers/actionFactory.ts +++ b/meteor/lib/api/triggers/actionFactory.ts @@ -16,7 +16,7 @@ import { MeteorCall } from '../methods' import { PartInstance } from '../../collections/PartInstances' import { RundownPlaylist } from '../../collections/RundownPlaylists' import { DBShowStyleBase, SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' -import { Studio } from '../../collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { assertNever, DummyReactiveVar } from '../../lib' import { logger } from '../../logging' import RundownViewEventBus, { RundownViewEvents } from './RundownViewEventBus' @@ -64,7 +64,7 @@ interface PlainPlaylistContext { } interface PlainStudioContext { - studio: Studio + studio: DBStudio showStyleBase: DBShowStyleBase } diff --git a/meteor/lib/collections/ExpectedPackages.ts b/meteor/lib/collections/ExpectedPackages.ts index 0916d0b55e..b4349899cd 100644 --- a/meteor/lib/collections/ExpectedPackages.ts +++ b/meteor/lib/collections/ExpectedPackages.ts @@ -1,6 +1,6 @@ import { ExpectedPackage } from '@sofie-automation/blueprints-integration' import { assertNever, literal } from '../lib' -import { StudioLight } from './Studios' +import { StudioLight } from '@sofie-automation/corelib/dist/dataModel/Studio' import deepExtend from 'deep-extend' export function getPreviewPackageSettings( diff --git a/meteor/lib/collections/Studios.ts b/meteor/lib/collections/Studios.ts index dddd35b9ef..b41c07df57 100644 --- a/meteor/lib/collections/Studios.ts +++ b/meteor/lib/collections/Studios.ts @@ -2,15 +2,12 @@ import { omit, protectString } from '../lib' import { LookaheadMode } from '@sofie-automation/blueprints-integration' import { ResultingMappingRoutes, - DBStudio, MappingExt, StudioRouteType, StudioRouteSet, RouteMapping, } from '@sofie-automation/corelib/dist/dataModel/Studio' import { ReadonlyDeep } from 'type-fest' -export * from '@sofie-automation/corelib/dist/dataModel/Studio' -export { RoutedMappings } from '@sofie-automation/shared-lib/dist/core/model/Timeline' export function getActiveRoutes(routeSets: ReadonlyDeep>): ResultingMappingRoutes { const routes: ResultingMappingRoutes = { @@ -99,5 +96,3 @@ export function getRoutedMappings( } return outputMappings } - -export type Studio = DBStudio diff --git a/meteor/lib/collections/Timeline.ts b/meteor/lib/collections/Timeline.ts index 0bd2ab1d56..e8ffdcb2f4 100644 --- a/meteor/lib/collections/Timeline.ts +++ b/meteor/lib/collections/Timeline.ts @@ -1,4 +1,4 @@ -import { ResultingMappingRoutes } from './Studios' +import { ResultingMappingRoutes } from '@sofie-automation/corelib/dist/dataModel/Studio' import { TimelineObjGeneric, updateLookaheadLayer } from '@sofie-automation/corelib/dist/dataModel/Timeline' diff --git a/meteor/server/__tests__/_testEnvironment.test.ts b/meteor/server/__tests__/_testEnvironment.test.ts index 80ef6fd8bb..28a2bc98a0 100644 --- a/meteor/server/__tests__/_testEnvironment.test.ts +++ b/meteor/server/__tests__/_testEnvironment.test.ts @@ -29,7 +29,7 @@ import { Timeline, UserActionsLog, } from '../collections' -import { DBStudio } from '../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { isInFiber } from '../../__mocks__/Fibers' import { Mongo } from 'meteor/mongo' import { defaultStudio } from '../../__mocks__/defaultCollectionObjects' diff --git a/meteor/server/api/__tests__/userActions/system.test.ts b/meteor/server/api/__tests__/userActions/system.test.ts index e81afcbc24..56cf606c76 100644 --- a/meteor/server/api/__tests__/userActions/system.test.ts +++ b/meteor/server/api/__tests__/userActions/system.test.ts @@ -21,7 +21,7 @@ import { } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { literal } from '@sofie-automation/shared-lib/dist/lib/lib' import { StudioPlayoutDevice } from '@sofie-automation/corelib/dist/dataModel/Studio' -import { Studio } from '../../../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' require('../../userActions') // include in order to create the Meteor methods needed @@ -91,7 +91,7 @@ describe('User Actions - Disable Peripheral SubDevice', () => { success: 200, }) - const studio = (await Studios.findOneAsync(env.studio._id)) as Studio + const studio = (await Studios.findOneAsync(env.studio._id)) as DBStudio expect(studio).toBeDefined() const playoutDevices = applyAndValidateOverrides(studio.peripheralDeviceSettings.playoutDevices).obj expect(playoutDevices[mockSubDeviceId].options.disable).toBe(true) @@ -110,7 +110,7 @@ describe('User Actions - Disable Peripheral SubDevice', () => { success: 200, }) - const studio = (await Studios.findOneAsync(env.studio._id)) as Studio + const studio = (await Studios.findOneAsync(env.studio._id)) as DBStudio expect(studio).toBeDefined() const playoutDevices = applyAndValidateOverrides(studio.peripheralDeviceSettings.playoutDevices).obj expect(playoutDevices[mockSubDeviceId].options.disable).toBe(true) @@ -129,7 +129,7 @@ describe('User Actions - Disable Peripheral SubDevice', () => { success: 200, }) - const studio = (await Studios.findOneAsync(env.studio._id)) as Studio + const studio = (await Studios.findOneAsync(env.studio._id)) as DBStudio expect(studio).toBeDefined() const playoutDevices = applyAndValidateOverrides(studio.peripheralDeviceSettings.playoutDevices).obj expect(playoutDevices[mockSubDeviceId].options.disable).toBe(false) diff --git a/meteor/server/api/blueprintConfigPresets.ts b/meteor/server/api/blueprintConfigPresets.ts index 2e189b36bf..96be628601 100644 --- a/meteor/server/api/blueprintConfigPresets.ts +++ b/meteor/server/api/blueprintConfigPresets.ts @@ -3,7 +3,7 @@ import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { Blueprints, ShowStyleBases, ShowStyleVariants, Studios } from '../collections' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' -import { Studio } from '../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { ObserveChangesHelper } from '../collections/lib' import { MeteorStartupAsync } from '../../lib/lib' @@ -19,7 +19,7 @@ const ObserveChangeBufferTimeout = 100 * We want it synced across, so that if the config-preset is removed, then there is some config that can be used */ MeteorStartupAsync(async () => { - const doUpdate = async (doc: Studio): Promise => { + const doUpdate = async (doc: DBStudio): Promise => { const markUnlinked = async () => { await Studios.updateAsync(doc._id, { $set: { diff --git a/meteor/server/api/blueprints/__tests__/migrationContext.test.ts b/meteor/server/api/blueprints/__tests__/migrationContext.test.ts index 2e280d03b3..9fa9e303a8 100644 --- a/meteor/server/api/blueprints/__tests__/migrationContext.test.ts +++ b/meteor/server/api/blueprints/__tests__/migrationContext.test.ts @@ -22,7 +22,7 @@ import { PlayoutActions, IBlueprintTriggeredActions, } from '@sofie-automation/blueprints-integration' -import { Studio, MappingExt } from '../../../../lib/collections/Studios' +import { DBStudio, MappingExt } from '@sofie-automation/corelib/dist/dataModel/Studio' import { MigrationContextStudio, MigrationContextShowStyle, MigrationContextSystem } from '../migrationContext' import { DBShowStyleBase, SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' @@ -47,18 +47,18 @@ describe('Test blueprint migrationContext', () => { describe('MigrationContextStudio', () => { async function getContext() { - const studio = (await Studios.findOneAsync({})) as Studio + const studio = (await Studios.findOneAsync({})) as DBStudio expect(studio).toBeTruthy() return new MigrationContextStudio(studio) } - function getStudio(context: MigrationContextStudio): Studio { + function getStudio(context: MigrationContextStudio): DBStudio { const studio = (context as any).studio expect(studio).toBeTruthy() return studio } describe('mappings', () => { - async function getMappingFromDb(studio: Studio, mappingId: string): Promise { - const studio2 = (await Studios.findOneAsync(studio._id)) as Studio + async function getMappingFromDb(studio: DBStudio, mappingId: string): Promise { + const studio2 = (await Studios.findOneAsync(studio._id)) as DBStudio expect(studio2).toBeTruthy() return studio2.mappingsWithOverrides.defaults[mappingId] } @@ -253,8 +253,8 @@ describe('Test blueprint migrationContext', () => { }) describe('config', () => { - async function getAllConfigFromDb(studio: Studio): Promise { - const studio2 = (await Studios.findOneAsync(studio._id)) as Studio + async function getAllConfigFromDb(studio: DBStudio): Promise { + const studio2 = (await Studios.findOneAsync(studio._id)) as DBStudio expect(studio2).toBeTruthy() return studio2.blueprintConfigWithOverrides.defaults } @@ -414,13 +414,13 @@ describe('Test blueprint migrationContext', () => { }) describe('devices', () => { - async function getStudio(context: MigrationContextStudio): Promise { + async function getStudio(context: MigrationContextStudio): Promise { const studioId = (context as any).studio._id - const studio = (await Studios.findOneAsync(studioId)) as Studio + const studio = (await Studios.findOneAsync(studioId)) as DBStudio expect(studio).toBeTruthy() return studio } - async function createPlayoutDevice(studio: Studio) { + async function createPlayoutDevice(studio: DBStudio) { const peripheralDeviceId = getRandomId() studio.peripheralDeviceSettings.playoutDevices.defaults = { device01: { @@ -458,7 +458,7 @@ describe('Test blueprint migrationContext', () => { }, }) } - async function getPlayoutDevice(studio: Studio): Promise { + async function getPlayoutDevice(studio: DBStudio): Promise { const device = await PeripheralDevices.findOneAsync({ studioId: studio._id, type: PeripheralDeviceType.PLAYOUT, diff --git a/meteor/server/api/blueprints/api.ts b/meteor/server/api/blueprints/api.ts index 279f6136e0..71f0bcdd39 100644 --- a/meteor/server/api/blueprints/api.ts +++ b/meteor/server/api/blueprints/api.ts @@ -29,7 +29,7 @@ import { fetchBlueprintLight, BlueprintLight } from '../../serverOptimisations' import { getSystemStorePath } from '../../coreSystem' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' -import { Studio } from '../../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' export async function insertBlueprint( methodContext: MethodContext, @@ -331,7 +331,7 @@ async function syncConfigPresetsToStudios(blueprint: Blueprint): Promise { blueprintConfigPresetId: 1, }, } - )) as Pick[] + )) as Pick[] const configPresets = blueprint.studioConfigPresets || {} diff --git a/meteor/server/api/blueprints/migrationContext.ts b/meteor/server/api/blueprints/migrationContext.ts index ba07041c4e..dcd23d4940 100644 --- a/meteor/server/api/blueprints/migrationContext.ts +++ b/meteor/server/api/blueprints/migrationContext.ts @@ -12,7 +12,7 @@ import { Complete, waitForPromise, } from '../../../lib/lib' -import { Studio, DBStudio, StudioPlayoutDevice } from '../../../lib/collections/Studios' +import { DBStudio, StudioPlayoutDevice } from '@sofie-automation/corelib/dist/dataModel/Studio' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { Meteor } from 'meteor/meteor' import { @@ -160,9 +160,9 @@ export class MigrationContextSystem implements IMigrationContextSystem {} export class MigrationContextStudio implements IMigrationContextStudio { - private studio: Studio + private studio: DBStudio - constructor(studio: Studio) { + constructor(studio: DBStudio) { this.studio = studio } diff --git a/meteor/server/api/playout/playout.ts b/meteor/server/api/playout/playout.ts index 4a1a233b62..30491a8288 100644 --- a/meteor/server/api/playout/playout.ts +++ b/meteor/server/api/playout/playout.ts @@ -1,7 +1,7 @@ /* tslint:disable:no-use-before-declare */ import { Meteor } from 'meteor/meteor' import * as _ from 'underscore' -import { StudioRouteBehavior } from '../../../lib/collections/Studios' +import { StudioRouteBehavior } from '@sofie-automation/corelib/dist/dataModel/Studio' import { PackageInfo } from '../../coreSystem' import { StudioContentAccess } from '../../security/studio' import { shouldUpdateStudioBaselineInner } from '@sofie-automation/corelib/dist/studio/baseline' diff --git a/meteor/server/api/rest/v1/index.ts b/meteor/server/api/rest/v1/index.ts index 49b7d321ca..b156a37e05 100644 --- a/meteor/server/api/rest/v1/index.ts +++ b/meteor/server/api/rest/v1/index.ts @@ -84,7 +84,7 @@ import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/Perip import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' -import { Studio } from '../../../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { executePeripheralDeviceFunction } from '../../peripheralDevice/executeFunction' @@ -965,7 +965,7 @@ class ServerRestAPI implements RestAPI { _connection: Meteor.Connection, _event: string ): Promise>> { - const studios = (await Studios.findFetchAsync({}, { projection: { _id: 1 } })) as Array> + const studios = (await Studios.findFetchAsync({}, { projection: { _id: 1 } })) as Array> return ClientAPI.responseSuccess(studios.map((studio) => ({ id: unprotectString(studio._id) }))) } diff --git a/meteor/server/api/rest/v1/typeConversion.ts b/meteor/server/api/rest/v1/typeConversion.ts index d445174fe6..d03d284da6 100644 --- a/meteor/server/api/rest/v1/typeConversion.ts +++ b/meteor/server/api/rest/v1/typeConversion.ts @@ -30,7 +30,6 @@ import { } from '../../../../lib/api/rest' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' -import { Studio } from '../../../../lib/collections/Studios' import { Blueprints, ShowStyleBases, Studios } from '../../../collections' import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants' @@ -238,7 +237,7 @@ export function APISourceLayerFrom(sourceLayer: ISourceLayer): APISourceLayer { } } -export async function studioFrom(apiStudio: APIStudio, existingId?: StudioId): Promise { +export async function studioFrom(apiStudio: APIStudio, existingId?: StudioId): Promise { let blueprint: Blueprint | undefined if (apiStudio.blueprintId) { blueprint = await Blueprints.findOneAsync(protectString(apiStudio.blueprintId)) @@ -278,7 +277,7 @@ export async function studioFrom(apiStudio: APIStudio, existingId?: StudioId): P } } -export function APIStudioFrom(studio: Studio): APIStudio { +export function APIStudioFrom(studio: DBStudio): APIStudio { const studioSettings = APIStudioSettingsFrom(studio.settings) return { diff --git a/meteor/server/api/rundown.ts b/meteor/server/api/rundown.ts index 3bf423f353..5430620f9c 100644 --- a/meteor/server/api/rundown.ts +++ b/meteor/server/api/rundown.ts @@ -14,7 +14,7 @@ import { runIngestOperation } from './ingest/lib' import { IngestJobs } from '@sofie-automation/corelib/dist/worker/ingest' import { VerifiedRundownContentAccess, VerifiedRundownPlaylistContentAccess } from './lib' import { Blueprint } from '../../lib/collections/Blueprints' -import { Studio } from '../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { Blueprints, Rundowns, ShowStyleBases, ShowStyleVariants, Studios } from '../collections' @@ -117,7 +117,7 @@ export namespace ClientRundownAPI { _id: 1, _rundownVersionHash: 1, }, - })) as Pick + })) as Pick if (!studio) return 'missing studio' if (rundown.importVersions.studio !== (studio._rundownVersionHash || 0)) return 'studio' }) diff --git a/meteor/server/api/snapshot.ts b/meteor/server/api/snapshot.ts index 955bf2a3e6..9b994f7bd8 100644 --- a/meteor/server/api/snapshot.ts +++ b/meteor/server/api/snapshot.ts @@ -5,7 +5,7 @@ import Koa from 'koa' import KoaRouter from '@koa/router' import bodyParser from 'koa-bodyparser' import { check } from '../../lib/check' -import { Studio } from '../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { SnapshotType, SnapshotSystem, @@ -113,7 +113,7 @@ interface SystemSnapshot { versionExtended?: string studioId: StudioId | null snapshot: SnapshotSystem - studios: Array + studios: Array showStyleBases: Array showStyleVariants: Array blueprints?: Array // optional, to be backwards compatible @@ -161,7 +161,7 @@ async function createSystemSnapshot( if (Settings.enableUserAccounts && !organizationId) throw new Meteor.Error(500, 'Not able to create a systemSnaphost without organizationId') - let queryStudio: MongoQuery = {} + let queryStudio: MongoQuery = {} let queryShowStyleBases: MongoQuery = {} let queryShowStyleVariants: MongoQuery = {} let queryRundownLayouts: MongoQuery = {} diff --git a/meteor/server/api/studio/api.ts b/meteor/server/api/studio/api.ts index 825daffceb..c7179fea3f 100644 --- a/meteor/server/api/studio/api.ts +++ b/meteor/server/api/studio/api.ts @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor' import { check } from '../../../lib/check' import { registerClassToMeteorMethods } from '../../methods' import { NewStudiosAPI, StudiosAPIMethods } from '../../../lib/api/studios' -import { DBStudio } from '../../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { literal, getRandomId, lazyIgnore, stringifyError } from '../../../lib/lib' import { ExpectedPackages, diff --git a/meteor/server/collections/index.ts b/meteor/server/collections/index.ts index 713bb81e86..4c6df5b3a5 100644 --- a/meteor/server/collections/index.ts +++ b/meteor/server/collections/index.ts @@ -18,7 +18,7 @@ import { RundownLayoutBase } from '../../lib/collections/RundownLayouts' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' import { SnapshotItem } from '../../lib/collections/Snapshots' -import { Studio } from '../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { TimelineComplete } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { DBTimelineDatastoreEntry } from '@sofie-automation/corelib/dist/dataModel/TimelineDatastore' import { TranslationsBundle } from '../../lib/collections/TranslationsBundles' @@ -187,7 +187,7 @@ registerIndex(Snapshots, { created: 1, }) -export const Studios = createAsyncOnlyMongoCollection(CollectionName.Studios, { +export const Studios = createAsyncOnlyMongoCollection(CollectionName.Studios, { async update(userId, doc, fields, _modifier) { const access = await allowAccessToStudio({ userId: userId }, doc._id) if (!access.update) return logNotAllowed('Studio', access.reason) diff --git a/meteor/server/coreSystem/checkDatabaseVersions.ts b/meteor/server/coreSystem/checkDatabaseVersions.ts index 95c18324cb..b4bf362402 100644 --- a/meteor/server/coreSystem/checkDatabaseVersions.ts +++ b/meteor/server/coreSystem/checkDatabaseVersions.ts @@ -11,7 +11,7 @@ import { parseCoreIntegrationCompatabilityRange, } from '../../lib/collections/CoreSystem' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' -import { Studio } from '../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { lazyIgnore } from '../../lib/lib' import { logger } from '../logging' import { CURRENT_SYSTEM_VERSION } from '../migration/currentSystemVersion' @@ -126,7 +126,7 @@ export function checkDatabaseVersions(): void { { fields: { _id: 1 }, } - )) as Array> + )) as Array> for (const studio of studiosForShowStyleBase) { if (!checkedStudioIds.has(studio._id)) { // only run once per blueprint and studio diff --git a/meteor/server/migration/1_42_0.ts b/meteor/server/migration/1_42_0.ts index bc0b870837..e5f454d5bc 100644 --- a/meteor/server/migration/1_42_0.ts +++ b/meteor/server/migration/1_42_0.ts @@ -1,5 +1,5 @@ import { addMigrationSteps } from './databaseMigration' -import { StudioRouteSet, StudioRouteType } from '../../lib/collections/Studios' +import { StudioRouteSet, StudioRouteType } from '@sofie-automation/corelib/dist/dataModel/Studio' import { Studios } from '../collections' export const addSteps = addMigrationSteps('1.42.0', [ diff --git a/meteor/server/migration/1_47_0.ts b/meteor/server/migration/1_47_0.ts index 4d9aa4b355..9a2e5af290 100644 --- a/meteor/server/migration/1_47_0.ts +++ b/meteor/server/migration/1_47_0.ts @@ -1,5 +1,5 @@ import { addMigrationSteps } from './databaseMigration' -import { DBStudio, MappingsExt } from '../../lib/collections/Studios' +import { DBStudio, MappingsExt } from '@sofie-automation/corelib/dist/dataModel/Studio' import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' import { DBShowStyleBase, OutputLayers, SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' diff --git a/meteor/server/migration/1_50_0.ts b/meteor/server/migration/1_50_0.ts index 00aeef1718..f73386c339 100644 --- a/meteor/server/migration/1_50_0.ts +++ b/meteor/server/migration/1_50_0.ts @@ -13,7 +13,7 @@ import { PeripheralDeviceType, } from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI' import _ from 'underscore' -import { Studio } from '../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { wrapDefaultObject, ObjectOverrideSetOp, @@ -42,7 +42,7 @@ const mappingBaseOptions: Array = [ 'lookaheadMaxSearchDistance', ] -function convertMappingsOverrideOps(studio: Studio) { +function convertMappingsOverrideOps(studio: DBStudio) { let changed = false const newOverrides = clone(studio.mappingsWithOverrides.overrides) @@ -74,7 +74,7 @@ function convertMappingsOverrideOps(studio: Studio) { return changed && newOverrides } -function convertRouteSetMappings(studio: Studio) { +function convertRouteSetMappings(studio: DBStudio) { let changed = false const newRouteSets = clone(studio.routeSets || {}) diff --git a/meteor/server/migration/__tests__/migrations.test.ts b/meteor/server/migration/__tests__/migrations.test.ts index 868ff14f72..b4b88196a3 100644 --- a/meteor/server/migration/__tests__/migrations.test.ts +++ b/meteor/server/migration/__tests__/migrations.test.ts @@ -16,7 +16,7 @@ import { ShowStyleBlueprintManifest, StudioBlueprintManifest, } from '@sofie-automation/blueprints-integration' -import { Studio } from '../../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { generateFakeBlueprint } from '../../api/blueprints/__tests__/lib' import { MeteorCall } from '../../../lib/api/methods' import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' @@ -234,7 +234,7 @@ describe('Migrations', () => { expect(_.find(migration.steps, (s) => !!s.id.match(/myCoreMockStep2/))).toBeTruthy() expect(_.find(migration.steps, (s) => !!s.id.match(/myCoreMockStep3/))).toBeTruthy() - const studio = (await Studios.findOneAsync({})) as Studio + const studio = (await Studios.findOneAsync({})) as DBStudio expect(studio).toBeTruthy() const studioManifest = (): StudioBlueprintManifest => ({ diff --git a/meteor/server/migration/upgrades/checkStatus.ts b/meteor/server/migration/upgrades/checkStatus.ts index 02cc830f18..065210e382 100644 --- a/meteor/server/migration/upgrades/checkStatus.ts +++ b/meteor/server/migration/upgrades/checkStatus.ts @@ -22,7 +22,7 @@ import { } from '../../../lib/api/migration' import { Blueprints, ShowStyleBases, Studios } from '../../collections' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' -import { Studio } from '../../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { generateTranslation } from '../../../lib/lib' import { JSONBlob, JSONBlobParse } from '@sofie-automation/shared-lib/dist/lib/JSONBlob' import { JSONSchema } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes' @@ -149,7 +149,7 @@ async function checkShowStyleBaseUpgradeStatus(): Promise type ShowStyleBaseForUpgradeCheck = Pick< diff --git a/meteor/server/migration/upgrades/studio.ts b/meteor/server/migration/upgrades/studio.ts index c8548dc3ca..46fba1d821 100644 --- a/meteor/server/migration/upgrades/studio.ts +++ b/meteor/server/migration/upgrades/studio.ts @@ -1,7 +1,7 @@ import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { BlueprintValidateConfigForStudioResult, StudioJobs } from '@sofie-automation/corelib/dist/worker/studio' import { Meteor } from 'meteor/meteor' -import { Studio } from '../../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { profiler } from '../../api/profiler' import { Studios } from '../../collections' import { logger } from '../../logging' @@ -12,7 +12,7 @@ export async function validateConfigForStudio(studioId: StudioId): Promise | undefined + })) as Pick | undefined if (!studio) throw new Meteor.Error(404, `Studio "${studioId}" not found!`) const queuedJob = await QueueStudioJob(StudioJobs.BlueprintValidateConfigForStudio, studioId, undefined) @@ -33,7 +33,7 @@ export async function runUpgradeForStudio(studioId: StudioId): Promise { fields: { _id: 1, }, - })) as Pick | undefined + })) as Pick | undefined if (!studio) throw new Meteor.Error(404, `Studio "${studioId}" not found!`) const queuedJob = await QueueStudioJob(StudioJobs.BlueprintUpgradeForStudio, studioId, undefined) diff --git a/meteor/server/optimizations.ts b/meteor/server/optimizations.ts index 25c0274469..e5e59ec4b6 100644 --- a/meteor/server/optimizations.ts +++ b/meteor/server/optimizations.ts @@ -1,11 +1,9 @@ import { ShowStyleBaseId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' -import { DBStudio, StudioLight } from '../lib/collections/Studios' +import { DBStudio, StudioLight } from '@sofie-automation/corelib/dist/dataModel/Studio' import { ShowStyleBases, Studios } from './collections' -export { StudioLight } from '../lib/collections/Studios' // TODO: Legacy - /** * Returns a "light" version of the Studio, where the most heavy/large properties are omitted. */ diff --git a/meteor/server/publications/packageManager/expectedPackages/generate.ts b/meteor/server/publications/packageManager/expectedPackages/generate.ts index 951ffde379..8059586078 100644 --- a/meteor/server/publications/packageManager/expectedPackages/generate.ts +++ b/meteor/server/publications/packageManager/expectedPackages/generate.ts @@ -11,7 +11,7 @@ import deepExtend from 'deep-extend' import { ReadonlyDeep } from 'type-fest' import _ from 'underscore' import { getSideEffect } from '../../../../lib/collections/ExpectedPackages' -import { Studio, StudioLight, StudioPackageContainer } from '../../../../lib/collections/Studios' +import { DBStudio, StudioLight, StudioPackageContainer } from '@sofie-automation/corelib/dist/dataModel/Studio' import { clone, omit } from '../../../../lib/lib' import { CustomPublishCollection } from '../../../lib/customPublication' import { logger } from '../../../logging' @@ -29,7 +29,7 @@ import type { StudioFields } from './publication' */ export async function updateCollectionForExpectedPackageIds( contentCache: ReadonlyDeep, - studio: Pick, + studio: Pick, layerNameToDeviceIds: Map, collection: CustomPublishCollection, filterPlayoutDeviceIds: ReadonlyDeep | undefined, @@ -90,7 +90,7 @@ export async function updateCollectionForExpectedPackageIds( */ export async function updateCollectionForPieceInstanceIds( contentCache: ReadonlyDeep, - studio: Pick, + studio: Pick, layerNameToDeviceIds: Map, collection: CustomPublishCollection, filterPlayoutDeviceIds: ReadonlyDeep | undefined, diff --git a/meteor/server/publications/packageManager/expectedPackages/publication.ts b/meteor/server/publications/packageManager/expectedPackages/publication.ts index 3c377a859b..8d9e1f03b0 100644 --- a/meteor/server/publications/packageManager/expectedPackages/publication.ts +++ b/meteor/server/publications/packageManager/expectedPackages/publication.ts @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor' import { CustomCollectionName, PubSub } from '../../../../lib/api/pubsub' import { PeripheralDeviceReadAccess } from '../../../security/peripheralDevice' -import { Studio } from '../../../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { TriggerUpdate, meteorCustomPublish, @@ -43,7 +43,7 @@ interface ExpectedPackagesPublicationUpdateProps { } interface ExpectedPackagesPublicationState { - studio: Pick | undefined + studio: Pick | undefined layerNameToDeviceIds: Map contentCache: ReadonlyDeep @@ -56,7 +56,7 @@ export type StudioFields = | 'packageContainers' | 'previewContainerIds' | 'thumbnailContainerIds' -const studioFieldSpecifier = literal>>({ +const studioFieldSpecifier = literal>>({ _id: 1, routeSets: 1, mappingsWithOverrides: 1, @@ -133,7 +133,7 @@ async function manipulateExpectedPackagesPublicationData( // Reload the studio, and the layerNameToDeviceIds lookup if (!updateProps || updateProps.invalidateStudio) { state.studio = (await Studios.findOneAsync(args.studioId, { fields: studioFieldSpecifier })) as - | Pick + | Pick | undefined if (!state.studio) { logger.warn(`Pub.expectedPackagesForDevice: studio "${args.studioId}" not found!`) diff --git a/meteor/server/publications/packageManager/packageContainers.ts b/meteor/server/publications/packageManager/packageContainers.ts index c40b19afc1..516f1b4f6c 100644 --- a/meteor/server/publications/packageManager/packageContainers.ts +++ b/meteor/server/publications/packageManager/packageContainers.ts @@ -8,7 +8,6 @@ import { check } from 'meteor/check' import { Meteor } from 'meteor/meteor' import { ReadonlyDeep } from 'type-fest' import { PubSub, CustomCollectionName } from '../../../lib/api/pubsub' -import { Studio } from '../../../lib/collections/Studios' import { PeripheralDevices, Studios } from '../../collections' import { meteorCustomPublish, setUpOptimizedObserverArray, TriggerUpdate } from '../../lib/customPublication' import { logger } from '../../logging' @@ -61,7 +60,7 @@ async function manipulateExpectedPackagesPublicationData( // Future: this may want to cache on the state, but with only a single observer there feels little point const studio = (await Studios.findOneAsync(args.studioId, { fields: studioFieldSpecifier })) as - | Pick + | Pick | undefined const packageContainers: { [containerId: string]: PackageContainer } = {} diff --git a/meteor/server/publications/peripheralDeviceForDevice.ts b/meteor/server/publications/peripheralDeviceForDevice.ts index 52b31cdef8..6cd83897df 100644 --- a/meteor/server/publications/peripheralDeviceForDevice.ts +++ b/meteor/server/publications/peripheralDeviceForDevice.ts @@ -10,7 +10,12 @@ import { ReadonlyDeep } from 'type-fest' import { ReactiveMongoObserverGroup } from './lib/observerGroup' import { Complete, assertNever, literal } from '@sofie-automation/corelib/dist/lib' import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mongo' -import { Studio, StudioIngestDevice, StudioInputDevice, StudioPlayoutDevice } from '../../lib/collections/Studios' +import { + DBStudio, + StudioIngestDevice, + StudioInputDevice, + StudioPlayoutDevice, +} from '@sofie-automation/corelib/dist/dataModel/Studio' import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { check } from 'meteor/check' @@ -26,7 +31,7 @@ interface PeripheralDeviceForDeviceUpdateProps { } type StudioFields = '_id' | 'peripheralDeviceSettings' -const studioFieldsSpecifier = literal>>({ +const studioFieldsSpecifier = literal>>({ _id: 1, peripheralDeviceSettings: 1, }) @@ -44,7 +49,7 @@ const peripheralDeviceFieldsSpecifier = literal< export function convertPeripheralDeviceForGateway( peripheralDevice: Pick, - studio: Pick | undefined + studio: Pick | undefined ): PeripheralDeviceForDevice { const playoutDevices: PeripheralDeviceForDevice['playoutDevices'] = {} const ingestDevices: PeripheralDeviceForDevice['ingestDevices'] = {} @@ -182,7 +187,7 @@ async function manipulatePeripheralDevicePublicationData( const studio = peripheralDevice.studioId && ((await Studios.findOneAsync(peripheralDevice.studioId, { projection: studioFieldsSpecifier })) as - | Pick + | Pick | undefined) return [convertPeripheralDeviceForGateway(peripheralDevice, studio)] diff --git a/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts b/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts index e3eff3711c..87e04f7a95 100644 --- a/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts +++ b/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts @@ -36,7 +36,7 @@ import { defaultStudio } from '../../../../__mocks__/defaultCollectionObjects' import { testInFiber } from '../../../../__mocks__/helpers/jest' import { MediaObjects } from '../../../collections' import { PieceDependencies } from '../common' -import { Studio } from '../../../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants' const mockMediaObjectsCollection = MongoMock.getInnerMockCollection(MediaObjects) @@ -176,7 +176,7 @@ describe('lib/mediaObjects', () => { const mockDefaultStudio = defaultStudio(protectString('studio0')) const mockStudio: Complete< Pick< - Studio, + DBStudio, '_id' | 'settings' | 'packageContainers' | 'previewContainerIds' | 'thumbnailContainerIds' | 'routeSets' > & Pick diff --git a/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts index 0e99d71f3b..3161f18639 100644 --- a/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts +++ b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts @@ -18,6 +18,7 @@ import { } from '@sofie-automation/corelib/dist/dataModel/PackageContainerPackageStatus' import { PieceGeneric, PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' import { + DBStudio, IStudioSettings, MappingExt, MappingsExt, @@ -28,7 +29,7 @@ import { literal, Complete, assertNever } from '@sofie-automation/corelib/dist/l import { ReadonlyDeep } from 'type-fest' import _ from 'underscore' import { getSideEffect } from '../../../lib/collections/ExpectedPackages' -import { getActiveRoutes, getRoutedMappings, Studio } from '../../../lib/collections/Studios' +import { getActiveRoutes, getRoutedMappings } from '../../../lib/collections/Studios' import { ensureHasTrailingSlash, generateTranslation, unprotectString } from '../../../lib/lib' import { PieceContentStatusObj, ScanInfoForPackage, ScanInfoForPackages } from '../../../lib/mediaObjects' import { MediaObjects, PackageContainerPackageStatuses, PackageInfos } from '../../collections' @@ -159,7 +160,7 @@ export function getMediaObjectMediaId( export type PieceContentStatusPiece = Pick export interface PieceContentStatusStudio extends Pick< - Studio, + DBStudio, '_id' | 'settings' | 'packageContainers' | 'previewContainerIds' | 'thumbnailContainerIds' | 'routeSets' > { /** Mappings between the physical devices / outputs and logical ones */ diff --git a/meteor/server/publications/pieceContentStatusUI/common.ts b/meteor/server/publications/pieceContentStatusUI/common.ts index 7142d9313c..962dae251b 100644 --- a/meteor/server/publications/pieceContentStatusUI/common.ts +++ b/meteor/server/publications/pieceContentStatusUI/common.ts @@ -6,7 +6,7 @@ import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { MediaObject } from '@sofie-automation/shared-lib/dist/core/model/MediaObjects' import { ReadonlyDeep } from 'type-fest' -import { Studio } from '../../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { literal } from '../../../lib/lib' import { Studios } from '../../collections' import { PieceContentStatusStudio } from './checkPieceContentStatus' @@ -19,7 +19,7 @@ export type StudioFields = | 'thumbnailContainerIds' | 'mappingsWithOverrides' | 'routeSets' -export const studioFieldSpecifier = literal>>({ +export const studioFieldSpecifier = literal>>({ _id: 1, settings: 1, packageContainers: 1, @@ -104,7 +104,7 @@ export function addItemsWithDependenciesChangesToChangedSet { const studio = (await Studios.findOneAsync(studioId, { projection: studioFieldSpecifier, - })) as Pick | undefined + })) as Pick | undefined if (!studio) { return undefined diff --git a/meteor/server/publications/studio.ts b/meteor/server/publications/studio.ts index 09974176f8..e4ecc0b742 100644 --- a/meteor/server/publications/studio.ts +++ b/meteor/server/publications/studio.ts @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor' import { check } from '../../lib/check' import { meteorPublish, AutoFillSelector } from './lib' import { CustomCollectionName, PubSub } from '../../lib/api/pubsub' -import { DBStudio, getActiveRoutes, getRoutedMappings, RoutedMappings } from '../../lib/collections/Studios' +import { getActiveRoutes, getRoutedMappings } from '../../lib/collections/Studios' import { PeripheralDeviceReadAccess } from '../security/peripheralDevice' import { ExternalMessageQueueObj } from '../../lib/collections/ExternalMessageQueue' import { StudioReadAccess } from '../security/studio' @@ -31,6 +31,8 @@ import { } from '../collections' import { PackageContainerStatusDB } from '@sofie-automation/corelib/dist/dataModel/PackageContainerStatus' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' +import { RoutedMappings } from '@sofie-automation/shared-lib/dist/core/model/Timeline' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' meteorPublish(PubSub.studios, async function (selector0: MongoQuery, token: string | undefined) { const { cred, selector } = await AutoFillSelector.organizationId(this.userId, selector0, token) diff --git a/meteor/server/publications/studioUI.ts b/meteor/server/publications/studioUI.ts index 03634b2abc..7d4222a2e8 100644 --- a/meteor/server/publications/studioUI.ts +++ b/meteor/server/publications/studioUI.ts @@ -5,7 +5,7 @@ import { Meteor } from 'meteor/meteor' import { ReadonlyDeep } from 'type-fest' import { CustomCollectionName, PubSub } from '../../lib/api/pubsub' import { UIStudio } from '../../lib/api/studios' -import { DBStudio } from '../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { Complete, literal } from '../../lib/lib' import { CustomPublishCollection, diff --git a/meteor/server/publications/timeline.ts b/meteor/server/publications/timeline.ts index 2da744645b..a710f4caae 100644 --- a/meteor/server/publications/timeline.ts +++ b/meteor/server/publications/timeline.ts @@ -17,10 +17,10 @@ import { setUpOptimizedObserverArray, TriggerUpdate, } from '../lib/customPublication' -import { getActiveRoutes, ResultingMappingRoutes } from '../../lib/collections/Studios' +import { getActiveRoutes } from '../../lib/collections/Studios' import { PeripheralDeviceReadAccess } from '../security/peripheralDevice' import { StudioReadAccess } from '../security/studio' -import { fetchStudioLight, StudioLight } from '../optimizations' +import { fetchStudioLight } from '../optimizations' import { FastTrackObservers, setupFastTrackObserver } from './fastTrack' import { logger } from '../logging' import { getRandomId, literal } from '@sofie-automation/corelib/dist/lib' @@ -31,6 +31,7 @@ import { DBTimelineDatastoreEntry } from '@sofie-automation/corelib/dist/dataMod import { PeripheralDevices, Studios, Timeline, TimelineDatastore } from '../collections' import { check } from 'meteor/check' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' +import { ResultingMappingRoutes, StudioLight } from '@sofie-automation/corelib/dist/dataModel/Studio' meteorPublish(PubSub.timeline, async function (selector: MongoQuery, token: string | undefined) { if (!selector) throw new Meteor.Error(400, 'selector argument missing') diff --git a/meteor/server/security/lib/security.ts b/meteor/server/security/lib/security.ts index 98f3923c61..fdb5da9594 100644 --- a/meteor/server/security/lib/security.ts +++ b/meteor/server/security/lib/security.ts @@ -10,7 +10,7 @@ import { DBOrganization } from '../../../lib/collections/Organization' import { PeripheralDevice } from '../../../lib/collections/PeripheralDevices' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' import { profiler } from '../../api/profiler' -import { fetchShowStyleBasesLight, fetchStudioLight, ShowStyleBaseLight, StudioLight } from '../../optimizations' +import { fetchShowStyleBasesLight, fetchStudioLight, ShowStyleBaseLight } from '../../optimizations' import { Organizations, PeripheralDevices, RundownPlaylists, Rundowns, ShowStyleVariants } from '../../collections' import { OrganizationId, @@ -22,6 +22,7 @@ import { StudioId, UserId, } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { StudioLight } from '@sofie-automation/corelib/dist/dataModel/Studio' export const LIMIT_CACHE_TIME = 1000 * 60 * 15 // 15 minutes diff --git a/meteor/server/security/organization.ts b/meteor/server/security/organization.ts index 2a9045137d..71a0ebda93 100644 --- a/meteor/server/security/organization.ts +++ b/meteor/server/security/organization.ts @@ -9,7 +9,7 @@ import { Settings } from '../../lib/Settings' import { MethodContext } from '../../lib/api/methods' import { triggerWriteAccess } from './lib/securityVerify' import { isProtectedString } from '../../lib/lib' -import { fetchShowStyleBaseLight, fetchStudioLight, ShowStyleBaseLight, StudioLight } from '../optimizations' +import { fetchShowStyleBaseLight, fetchStudioLight, ShowStyleBaseLight } from '../optimizations' import { BlueprintId, OrganizationId, @@ -19,6 +19,7 @@ import { UserId, } from '@sofie-automation/corelib/dist/dataModel/Ids' import { Blueprints, Snapshots } from '../collections' +import { StudioLight } from '@sofie-automation/corelib/dist/dataModel/Studio' export type BasicAccessContext = { organizationId: OrganizationId | null; userId: UserId | null } diff --git a/meteor/server/security/studio.ts b/meteor/server/security/studio.ts index d77039c253..390d6cb88d 100644 --- a/meteor/server/security/studio.ts +++ b/meteor/server/security/studio.ts @@ -8,7 +8,7 @@ import { RundownPlaylist } from '../../lib/collections/RundownPlaylists' import { Settings } from '../../lib/Settings' import { triggerWriteAccess } from './lib/securityVerify' import { isProtectedString } from '../../lib/lib' -import { fetchStudioLight, StudioLight } from '../optimizations' +import { fetchStudioLight } from '../optimizations' import { ExternalMessageQueueObjId, OrganizationId, @@ -17,6 +17,7 @@ import { UserId, } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ExternalMessageQueue, RundownPlaylists } from '../collections' +import { StudioLight } from '@sofie-automation/corelib/dist/dataModel/Studio' export namespace StudioReadAccess { /** Handles read access for all studio document */ diff --git a/meteor/server/systemStatus/blueprintVersions.ts b/meteor/server/systemStatus/blueprintVersions.ts index 55ab4d288d..cf52d571e2 100644 --- a/meteor/server/systemStatus/blueprintVersions.ts +++ b/meteor/server/systemStatus/blueprintVersions.ts @@ -2,7 +2,7 @@ import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { BlueprintId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' -import { Studio } from '../../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { Blueprints, ShowStyleBases, Studios } from '../collections' import { getCoreSystemAsync } from '../coreSystem/collection' @@ -32,7 +32,7 @@ export async function getBlueprintVersions(): Promise<{ blueprintId: 1, }, } - ) as Promise>> + ) as Promise>> const pShowStyleBases = ShowStyleBases.findFetchAsync( { blueprintId: { $exists: true } }, diff --git a/meteor/server/webmanifest.ts b/meteor/server/webmanifest.ts index bb7447f515..c4a59aa5f7 100644 --- a/meteor/server/webmanifest.ts +++ b/meteor/server/webmanifest.ts @@ -6,7 +6,7 @@ import type { } from '../lib/typings/webmanifest' import { logger } from '../lib/logging' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' -import { DBStudio } from '../lib/collections/Studios' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { RundownPlaylists, Rundowns, Studios } from './collections' import { getLocale, Translations } from './lib' import { generateTranslation } from '../lib/lib' From f900fcab8bd2b224d44bac3300a12d4b96822e48 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 4 Jul 2023 16:28:59 +0100 Subject: [PATCH 006/479] chore: tidy up Segment collection rexports --- meteor/__mocks__/defaultCollectionObjects.ts | 2 +- meteor/__mocks__/helpers/database.ts | 2 +- .../lib/__tests__/rundownTiming.test.ts | 2 +- meteor/client/lib/rundown.ts | 6 ++--- meteor/client/lib/shelf.ts | 2 +- .../ui/ClockView/CameraScreen/Rundown.tsx | 4 ++-- .../client/ui/ClockView/PresenterScreen.tsx | 4 ++-- meteor/client/ui/RundownView.tsx | 4 ++-- meteor/client/ui/Shelf/AdLibPanel.tsx | 4 ++-- meteor/client/ui/Shelf/MiniRundownPanel.tsx | 16 +++++++------- meteor/client/ui/Shelf/NextInfoPanel.tsx | 6 ++--- meteor/client/ui/Shelf/SegmentNamePanel.tsx | 8 +++---- meteor/client/ui/Shelf/SegmentTimingPanel.tsx | 4 ++-- meteor/lib/Rundown.ts | 4 ++-- meteor/lib/api/pubsub.ts | 2 +- .../triggers/actionFilterChainCompilers.ts | 2 +- meteor/lib/collections/Segments.ts | 4 ---- meteor/lib/collections/libCollections.ts | 4 ++-- meteor/lib/collections/rundownPlaylistUtil.ts | 21 +++++++++--------- meteor/lib/main.ts | 1 - meteor/server/collections/rundown.ts | 4 ++-- meteor/server/publications/rundown.ts | 2 +- .../__tests__/generateNotesForSegment.test.ts | 22 +++++++++---------- .../generateNotesForSegment.ts | 4 ++-- .../segmentPartNotesUI/publication.ts | 2 +- meteor/server/security/rundown.ts | 2 +- 26 files changed, 66 insertions(+), 72 deletions(-) delete mode 100644 meteor/lib/collections/Segments.ts diff --git a/meteor/__mocks__/defaultCollectionObjects.ts b/meteor/__mocks__/defaultCollectionObjects.ts index 8fa02c3d9d..b884cbbf4d 100644 --- a/meteor/__mocks__/defaultCollectionObjects.ts +++ b/meteor/__mocks__/defaultCollectionObjects.ts @@ -2,7 +2,7 @@ import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { clone, getCurrentTime, unprotectString } from '../lib/lib' import { DBRundownPlaylist } from '../lib/collections/RundownPlaylists' import { DBRundown } from '../lib/collections/Rundowns' -import { DBSegment } from '../lib/collections/Segments' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart } from '../lib/collections/Parts' import { IBlueprintPieceType, PieceLifespan } from '@sofie-automation/blueprints-integration' import { Piece, EmptyPieceTimelineObjectsBlob } from '../lib/collections/Pieces' diff --git a/meteor/__mocks__/helpers/database.ts b/meteor/__mocks__/helpers/database.ts index 68537077e8..781ad41382 100644 --- a/meteor/__mocks__/helpers/database.ts +++ b/meteor/__mocks__/helpers/database.ts @@ -49,7 +49,7 @@ import { normalizeArray, } from '../../lib/lib' import { DBRundown } from '../../lib/collections/Rundowns' -import { DBSegment } from '../../lib/collections/Segments' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart } from '../../lib/collections/Parts' import { EmptyPieceTimelineObjectsBlob, Piece } from '../../lib/collections/Pieces' import { DBRundownPlaylist } from '../../lib/collections/RundownPlaylists' diff --git a/meteor/client/lib/__tests__/rundownTiming.test.ts b/meteor/client/lib/__tests__/rundownTiming.test.ts index 31ba885420..60bbe5f91e 100644 --- a/meteor/client/lib/__tests__/rundownTiming.test.ts +++ b/meteor/client/lib/__tests__/rundownTiming.test.ts @@ -1,7 +1,7 @@ import { RundownPlaylist, DBRundownPlaylist } from '../../../lib/collections/RundownPlaylists' import { PartInstance, wrapPartToTemporaryInstance } from '../../../lib/collections/PartInstances' import { DBPart, Part } from '../../../lib/collections/Parts' -import { DBSegment } from '../../../lib/collections/Segments' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBRundown } from '../../../lib/collections/Rundowns' import { literal, protectString } from '../../../lib/lib' import { RundownTimingCalculator, RundownTimingContext } from '../rundownTiming' diff --git a/meteor/client/lib/rundown.ts b/meteor/client/lib/rundown.ts index bc52c61166..d0d4a7fa9f 100644 --- a/meteor/client/lib/rundown.ts +++ b/meteor/client/lib/rundown.ts @@ -24,7 +24,7 @@ import { getSegmentsWithPartInstances, } from '../../lib/Rundown' import { PartInstance } from '../../lib/collections/PartInstances' -import { Segment } from '../../lib/collections/Segments' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { RundownPlaylist } from '../../lib/collections/RundownPlaylists' import { literal, getCurrentTime, applyToArray } from '../../lib/lib' import { processAndPrunePieceInstanceTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' @@ -294,7 +294,7 @@ export namespace RundownUtils { showStyleBase: UIShowStyleBase, playlist: RundownPlaylist, rundown: Pick, - segment: Segment, + segment: DBSegment, segmentsBeforeThisInRundownSet: Set, rundownsBeforeThisInPlaylist: RundownId[], rundownsToShowstyles: Map, @@ -366,7 +366,7 @@ export namespace RundownUtils { takeCount: 0, }, } - )[0] as { segment: Segment; partInstances: PartInstanceLimited[] } | undefined + )[0] as { segment: DBSegment; partInstances: PartInstanceLimited[] } | undefined if (segmentInfo && segmentInfo.partInstances.length > 0) { // create local deep copies of the studio outputLayers and sourceLayers so that we can store diff --git a/meteor/client/lib/shelf.ts b/meteor/client/lib/shelf.ts index 19278b6bd1..b9e4f3a702 100644 --- a/meteor/client/lib/shelf.ts +++ b/meteor/client/lib/shelf.ts @@ -6,7 +6,7 @@ import { PartInstance } from '../../lib/collections/PartInstances' import { PieceInstance } from '../../lib/collections/PieceInstances' import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' import { RundownPlaylist } from '../../lib/collections/RundownPlaylists' -import { DBSegment } from '../../lib/collections/Segments' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { processAndPrunePieceInstanceTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' import { getUnfinishedPieceInstancesReactive } from './rundownLayouts' import { UIShowStyleBase } from '../../lib/api/showStyles' diff --git a/meteor/client/ui/ClockView/CameraScreen/Rundown.tsx b/meteor/client/ui/ClockView/CameraScreen/Rundown.tsx index a3ffe68b74..5b4950e8cb 100644 --- a/meteor/client/ui/ClockView/CameraScreen/Rundown.tsx +++ b/meteor/client/ui/ClockView/CameraScreen/Rundown.tsx @@ -3,7 +3,7 @@ import { useSubscription, useTracker } from '../../../lib/ReactMeteorData/ReactM import { Rundown as RundownObj } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { PubSub } from '../../../../lib/api/pubsub' import { Segments } from '../../../collections' -import { Segment } from '../../../../lib/collections/Segments' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { Segment as SegmentComponent } from './Segment' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists' @@ -22,7 +22,7 @@ export function Rundown({ playlist, rundown, rundownIdsBefore }: IProps): JSX.El useSubscription(PubSub.uiShowStyleBase, rundown.showStyleBaseId) - const segments = useTracker(() => Segments.find({ rundownId }).fetch(), [rundownId], [] as Segment[]) + const segments = useTracker(() => Segments.find({ rundownId }).fetch(), [rundownId], [] as DBSegment[]) const showStyleBase = useTracker( () => UIShowStyleBases.findOne(rundown.showStyleBaseId), diff --git a/meteor/client/ui/ClockView/PresenterScreen.tsx b/meteor/client/ui/ClockView/PresenterScreen.tsx index 835ccf3fc7..76b93cd47e 100644 --- a/meteor/client/ui/ClockView/PresenterScreen.tsx +++ b/meteor/client/ui/ClockView/PresenterScreen.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import ClassNames from 'classnames' -import { DBSegment, Segment } from '../../../lib/collections/Segments' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { PartUi } from '../SegmentTimeline/SegmentTimelineContainer' import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' import { Rundown } from '../../../lib/collections/Rundowns' @@ -83,7 +83,7 @@ function getShowStyleBaseIdSegmentPartUi( partInstance: PartInstance, playlist: RundownPlaylist, orderedSegmentsAndParts: { - segments: Segment[] + segments: DBSegment[] parts: Part[] }, pieces: Map, diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx index 9fedb10232..60d0452a6f 100644 --- a/meteor/client/ui/RundownView.tsx +++ b/meteor/client/ui/RundownView.tsx @@ -17,7 +17,7 @@ import Tooltip from 'rc-tooltip' import { NavLink, Route, Prompt } from 'react-router-dom' import { RundownPlaylist } from '../../lib/collections/RundownPlaylists' import { Rundown } from '../../lib/collections/Rundowns' -import { DBSegment, Segment } from '../../lib/collections/Segments' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { StudioRouteSet } from '@sofie-automation/corelib/dist/dataModel/Studio' import { Part } from '../../lib/collections/Parts' import { ContextMenu, MenuItem, ContextMenuTrigger } from '@jstarpl/react-contextmenu' @@ -1152,7 +1152,7 @@ export type MinimalRundown = Pick[] } diff --git a/meteor/client/ui/Shelf/AdLibPanel.tsx b/meteor/client/ui/Shelf/AdLibPanel.tsx index 7e1096c3a6..ab7db43c5b 100644 --- a/meteor/client/ui/Shelf/AdLibPanel.tsx +++ b/meteor/client/ui/Shelf/AdLibPanel.tsx @@ -5,7 +5,7 @@ import { useTracker } from '../../lib/ReactMeteorData/react-meteor-data' import { useTranslation } from 'react-i18next' import { Rundown } from '../../../lib/collections/Rundowns' import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' -import { DBSegment, Segment } from '../../../lib/collections/Segments' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart } from '../../../lib/collections/Parts' import { IAdLibListItem } from './AdLibListItem' import ClassNames from 'classnames' @@ -171,7 +171,7 @@ export function fetchAndFilter(props: IFetchAndFilterProps): AdLibFetchAndFilter ( currentPartInstanceId: PartInstanceId | null, nextPartInstanceId: PartInstanceId | null, - segments: Segment[], + segments: DBSegment[], rundowns: Record ) => { const currentPartInstance = diff --git a/meteor/client/ui/Shelf/MiniRundownPanel.tsx b/meteor/client/ui/Shelf/MiniRundownPanel.tsx index 7d311a7750..f19d1abacd 100644 --- a/meteor/client/ui/Shelf/MiniRundownPanel.tsx +++ b/meteor/client/ui/Shelf/MiniRundownPanel.tsx @@ -12,7 +12,7 @@ import { withTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' import { PartInstance } from '../../../lib/collections/PartInstances' -import { Segment } from '../../../lib/collections/Segments' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { dashboardElementStyle } from './DashboardPanel' import { Meteor } from 'meteor/meteor' import { PartInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' @@ -30,7 +30,7 @@ interface IMiniRundownPanelProps { interface IMiniRundownPanelTrackedProps { currentPartInstance?: PartInstance nextPartInstance?: PartInstance - allSegments?: Segment[] + allSegments?: DBSegment[] } interface IState {} @@ -134,7 +134,7 @@ export const MiniRundownPanel = withTracker { + allSegments?.forEach((segment: DBSegment) => { if (segment.isHidden) return miniRundownSegments.push({ identifier: getSegmentIdentifier(segment), @@ -162,7 +162,7 @@ function getMiniRundownList( return miniRundownSegments } -function getSegmentCssClass(segment: Segment, currentPart?: PartInstance, nextPart?: PartInstance): string { +function getSegmentCssClass(segment: DBSegment, currentPart?: PartInstance, nextPart?: PartInstance): string { if (segment._id === currentPart?.segmentId) { return MiniRundownPanelInner.currentSegmentCssClass } @@ -190,11 +190,11 @@ function getElementStyle(props, isDashboardLayout: boolean) { } } -function getSegmentName(segment: Segment | undefined): string { +function getSegmentName(segment: DBSegment | undefined): string { return segment?.name !== undefined ? segment?.name : '' } -function getSegmentIdentifier(segment: Segment | undefined): string { +function getSegmentIdentifier(segment: DBSegment | undefined): string { return segment?.identifier !== undefined ? segment?.identifier : '' } diff --git a/meteor/client/ui/Shelf/NextInfoPanel.tsx b/meteor/client/ui/Shelf/NextInfoPanel.tsx index 13abf39d79..f738673782 100644 --- a/meteor/client/ui/Shelf/NextInfoPanel.tsx +++ b/meteor/client/ui/Shelf/NextInfoPanel.tsx @@ -11,7 +11,7 @@ import { withTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' import { PartInstance } from '../../../lib/collections/PartInstances' -import { Segment } from '../../../lib/collections/Segments' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { dashboardElementStyle } from './DashboardPanel' import { PartInstances, Segments } from '../../collections' @@ -24,7 +24,7 @@ interface INextInfoPanelProps { interface INextInfoPanelTrackedProps { nextPartInstance?: PartInstance - nextSegment?: Segment + nextSegment?: DBSegment } export class NextInfoPanelInner extends MeteorReactComponent { @@ -62,7 +62,7 @@ export class NextInfoPanelInner extends MeteorReactComponent( (props: INextInfoPanelProps & INextInfoPanelTrackedProps) => { let nextPartInstance: PartInstance | undefined = undefined - let nextSegment: Segment | undefined = undefined + let nextSegment: DBSegment | undefined = undefined if (props.playlist.nextPartInfo) { nextPartInstance = PartInstances.findOne(props.playlist.nextPartInfo.partInstanceId) diff --git a/meteor/client/ui/Shelf/SegmentNamePanel.tsx b/meteor/client/ui/Shelf/SegmentNamePanel.tsx index f031ccffc4..4b36e59c72 100644 --- a/meteor/client/ui/Shelf/SegmentNamePanel.tsx +++ b/meteor/client/ui/Shelf/SegmentNamePanel.tsx @@ -10,7 +10,7 @@ import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' import { dashboardElementStyle } from './DashboardPanel' import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts' import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData' -import { Segment } from '../../../lib/collections/Segments' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { PartInstance } from '../../../lib/collections/PartInstances' import { RundownPlaylistCollectionUtil } from '../../../lib/collections/rundownPlaylistUtil' @@ -70,7 +70,7 @@ function getSegmentName(selectedSegment: 'current' | 'next', playlist: RundownPl if (selectedSegment === 'current') { if (currentPartInstance) { const segment = RundownPlaylistCollectionUtil.getSegments(playlist, { _id: currentPartInstance.segmentId })[0] as - | Segment + | DBSegment | undefined return segment?.name } @@ -81,7 +81,7 @@ function getSegmentName(selectedSegment: 'current' | 'next', playlist: RundownPl })[0] as PartInstance | undefined if (nextPartInstance && nextPartInstance.segmentId !== currentPartInstance.segmentId) { const segment = RundownPlaylistCollectionUtil.getSegments(playlist, { _id: nextPartInstance.segmentId })[0] as - | Segment + | DBSegment | undefined return segment?.name } @@ -93,7 +93,7 @@ function getSegmentName(selectedSegment: 'current' | 'next', playlist: RundownPl const segmentIndex = orderedSegmentsAndParts.segments.findIndex((s) => s._id === currentPartInstance.segmentId) if (segmentIndex === -1) return - const nextSegment = orderedSegmentsAndParts.segments.slice(segmentIndex + 1)[0] as Segment | undefined + const nextSegment = orderedSegmentsAndParts.segments.slice(segmentIndex + 1)[0] as DBSegment | undefined return nextSegment?.name } } diff --git a/meteor/client/ui/Shelf/SegmentTimingPanel.tsx b/meteor/client/ui/Shelf/SegmentTimingPanel.tsx index 660db2808e..39d2f4005c 100644 --- a/meteor/client/ui/Shelf/SegmentTimingPanel.tsx +++ b/meteor/client/ui/Shelf/SegmentTimingPanel.tsx @@ -10,7 +10,7 @@ import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/Reac import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { RundownUtils } from '../../lib/rundown' import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' -import { Segment } from '../../../lib/collections/Segments' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { SegmentDuration } from '../RundownView/RundownTiming/SegmentDuration' import { PartExtended } from '../../../lib/Rundown' import { memoizedIsolatedAutorun } from '../../../lib/memoizedIsolatedAutorun' @@ -34,7 +34,7 @@ interface ISegmentTimingPanelProps { } interface ISegmentTimingPanelTrackedProps { - liveSegment?: Segment + liveSegment?: DBSegment parts?: PartExtended[] pieces?: Map active: boolean diff --git a/meteor/lib/Rundown.ts b/meteor/lib/Rundown.ts index aa77f31ed9..5888a3da61 100644 --- a/meteor/lib/Rundown.ts +++ b/meteor/lib/Rundown.ts @@ -1,7 +1,7 @@ import * as _ from 'underscore' import { Piece } from './collections/Pieces' import { IOutputLayer, ISourceLayer, ITranslatableMessage } from '@sofie-automation/blueprints-integration' -import { DBSegment, Segment } from './collections/Segments' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart } from './collections/Parts' import { PartInstance, wrapPartToTemporaryInstance } from './collections/PartInstances' import { PieceInstance } from './collections/PieceInstances' @@ -260,7 +260,7 @@ export function getSegmentsWithPartInstances( segmentsOptions?: FindOptions, partsOptions?: FindOptions, partInstancesOptions?: FindOptions -): Array<{ segment: Segment; partInstances: PartInstance[] }> { +): Array<{ segment: DBSegment; partInstances: PartInstance[] }> { const { segments, parts: rawParts } = RundownPlaylistCollectionUtil.getSegmentsAndPartsSync( playlist, segmentsQuery, diff --git a/meteor/lib/api/pubsub.ts b/meteor/lib/api/pubsub.ts index 69bd97af2e..96d42d0604 100644 --- a/meteor/lib/api/pubsub.ts +++ b/meteor/lib/api/pubsub.ts @@ -39,7 +39,7 @@ import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataMod import { RundownLayoutBase } from '../collections/RundownLayouts' import { DBRundownPlaylist } from '../collections/RundownPlaylists' import { DBRundown } from '../collections/Rundowns' -import { DBSegment } from '../collections/Segments' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' import { SnapshotItem } from '../collections/Snapshots' diff --git a/meteor/lib/api/triggers/actionFilterChainCompilers.ts b/meteor/lib/api/triggers/actionFilterChainCompilers.ts index 67941c48ca..fef4f90340 100644 --- a/meteor/lib/api/triggers/actionFilterChainCompilers.ts +++ b/meteor/lib/api/triggers/actionFilterChainCompilers.ts @@ -18,7 +18,7 @@ import { SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyle import { assertNever, generateTranslation } from '../../lib' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { DBRundown } from '../../collections/Rundowns' -import { DBSegment } from '../../collections/Segments' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { sortAdlibs } from '../../Rundown' import { ReactivePlaylistActionContext } from './actionFactory' import { FindOptions } from '../../collections/lib' diff --git a/meteor/lib/collections/Segments.ts b/meteor/lib/collections/Segments.ts deleted file mode 100644 index 2f4e87fe8e..0000000000 --- a/meteor/lib/collections/Segments.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' -export * from '@sofie-automation/corelib/dist/dataModel/Segment' - -export type Segment = DBSegment diff --git a/meteor/lib/collections/libCollections.ts b/meteor/lib/collections/libCollections.ts index cca282bff6..a309a6242c 100644 --- a/meteor/lib/collections/libCollections.ts +++ b/meteor/lib/collections/libCollections.ts @@ -16,7 +16,7 @@ import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataM import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' import { DBRundownPlaylist } from './RundownPlaylists' import { DBRundown } from './Rundowns' -import { Segment } from './Segments' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' export const AdLibActions = createSyncReadOnlyMongoCollection(CollectionName.AdLibActions) @@ -44,4 +44,4 @@ export const RundownPlaylists = createSyncReadOnlyMongoCollection(CollectionName.Rundowns) -export const Segments = createSyncReadOnlyMongoCollection(CollectionName.Segments) +export const Segments = createSyncReadOnlyMongoCollection(CollectionName.Segments) diff --git a/meteor/lib/collections/rundownPlaylistUtil.ts b/meteor/lib/collections/rundownPlaylistUtil.ts index 8b6146daf6..e8b4866aec 100644 --- a/meteor/lib/collections/rundownPlaylistUtil.ts +++ b/meteor/lib/collections/rundownPlaylistUtil.ts @@ -17,7 +17,6 @@ import { FindOptions } from './lib' import { PartInstance } from './PartInstances' import { Part } from './Parts' import { RundownPlaylist } from './RundownPlaylists' -import { Segment } from './Segments' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { Piece } from './Pieces' @@ -84,8 +83,8 @@ export class RundownPlaylistCollectionUtil { /** Returns all segments joined with their rundowns in their correct oreder for this RundownPlaylist */ static getRundownsAndSegments( playlist: Pick, - selector?: MongoQuery, - options?: FindOptions + selector?: MongoQuery, + options?: FindOptions ): Array<{ rundown: Pick< Rundown, @@ -97,7 +96,7 @@ export class RundownPlaylistCollectionUtil { | 'showStyleVariantId' | 'endOfRundownIsShowBreak' > - segments: Segment[] + segments: DBSegment[] }> { const rundowns = RundownPlaylistCollectionUtil.getRundownsOrdered(playlist, undefined, { fields: { @@ -129,9 +128,9 @@ export class RundownPlaylistCollectionUtil { /** Returns all segments in their correct order for this RundownPlaylist */ static getSegments( playlist: Pick, - selector?: MongoQuery, - options?: FindOptions - ): Segment[] { + selector?: MongoQuery, + options?: FindOptions + ): DBSegment[] { const rundownIds = RundownPlaylistCollectionUtil.getRundownUnorderedIDs(playlist) const segments = Segments.find( { @@ -176,11 +175,11 @@ export class RundownPlaylistCollectionUtil { /** Synchronous version of getSegmentsAndParts, to be used client-side */ static getSegmentsAndPartsSync( playlist: Pick, - segmentsQuery?: MongoQuery, + segmentsQuery?: MongoQuery, partsQuery?: MongoQuery, segmentsOptions?: Omit, 'projection'>, // We are mangling fields, so block projection partsOptions?: Omit, 'projection'> // We are mangling fields, so block projection - ): { segments: Segment[]; parts: Part[] } { + ): { segments: DBSegment[]; parts: Part[] } { const rundownIds = RundownPlaylistCollectionUtil.getRundownUnorderedIDs(playlist) const segments = Segments.find( { @@ -362,13 +361,13 @@ export class RundownPlaylistCollectionUtil { static _sortParts( parts: Part[], playlist: Pick, - segments: Array> + segments: Array> ): Part[] { return sortPartsInSegments(parts, playlist.rundownIdsInOrder, segments) } static _sortPartsInner

>( parts: P[], - sortedSegments: Array> + sortedSegments: Array> ): P[] { return sortPartsInSortedSegments(parts, sortedSegments) } diff --git a/meteor/lib/main.ts b/meteor/lib/main.ts index ffe2c9423c..273c85133d 100644 --- a/meteor/lib/main.ts +++ b/meteor/lib/main.ts @@ -18,7 +18,6 @@ import './collections/Pieces' import './collections/RundownLayouts' import './collections/RundownPlaylists' import './collections/Rundowns' -import './collections/Segments' import './collections/Snapshots' import './collections/Studios' import './collections/Timeline' diff --git a/meteor/server/collections/rundown.ts b/meteor/server/collections/rundown.ts index 944c8485b6..1df2de09ac 100644 --- a/meteor/server/collections/rundown.ts +++ b/meteor/server/collections/rundown.ts @@ -11,7 +11,7 @@ import { RundownBaselineObj } from '@sofie-automation/corelib/dist/dataModel/Run import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PartInstance } from '../../lib/collections/PartInstances' import { Part } from '../../lib/collections/Parts' -import { Segment } from '../../lib/collections/Segments' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { createAsyncOnlyReadOnlyMongoCollection } from './collection' import { registerIndex } from './indices' @@ -132,7 +132,7 @@ registerIndex(RundownPlaylists, { activationId: 1, }) -export const Segments = createAsyncOnlyReadOnlyMongoCollection(CollectionName.Segments) +export const Segments = createAsyncOnlyReadOnlyMongoCollection(CollectionName.Segments) registerIndex(Segments, { rundownId: 1, _rank: 1, diff --git a/meteor/server/publications/rundown.ts b/meteor/server/publications/rundown.ts index a33739c149..5917e2187b 100644 --- a/meteor/server/publications/rundown.ts +++ b/meteor/server/publications/rundown.ts @@ -5,7 +5,7 @@ import { PubSub } from '../../lib/api/pubsub' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { RundownReadAccess } from '../security/rundown' -import { DBSegment } from '../../lib/collections/Segments' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart } from '../../lib/collections/Parts' import { Piece } from '../../lib/collections/Pieces' import { PieceInstance } from '../../lib/collections/PieceInstances' diff --git a/meteor/server/publications/segmentPartNotesUI/__tests__/generateNotesForSegment.test.ts b/meteor/server/publications/segmentPartNotesUI/__tests__/generateNotesForSegment.test.ts index 9066095563..3aad53787b 100644 --- a/meteor/server/publications/segmentPartNotesUI/__tests__/generateNotesForSegment.test.ts +++ b/meteor/server/publications/segmentPartNotesUI/__tests__/generateNotesForSegment.test.ts @@ -7,7 +7,7 @@ import { SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/ import { clone, getRandomId, literal } from '@sofie-automation/corelib/dist/lib' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { UISegmentPartNote } from '../../../../lib/api/rundownNotifications' -import { Segment } from '../../../../lib/collections/Segments' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { generateTranslation } from '../../../../lib/lib' import { generateNotesForSegment } from '../generateNotesForSegment' import { PartFields, PartInstanceFields, SegmentFields } from '../reactiveContentCache' @@ -16,7 +16,7 @@ describe('generateNotesForSegment', () => { test('no notes', async () => { const playlistId = protectString('playlist0') const nrcsName = 'some nrcs' - const segment: Pick = { + const segment: Pick = { _id: protectString('segment0'), _rank: 1, rundownId: protectString('rundown0'), @@ -32,7 +32,7 @@ describe('generateNotesForSegment', () => { test('orphaned: deleted segment', async () => { const playlistId = protectString('playlist0') const nrcsName = 'some nrcs' - const segment: Pick = { + const segment: Pick = { _id: protectString('segment0'), _rank: 1, rundownId: protectString('rundown0'), @@ -67,7 +67,7 @@ describe('generateNotesForSegment', () => { test('orphaned: hidden segment', async () => { const playlistId = protectString('playlist0') const nrcsName = 'some nrcs' - const segment: Pick = { + const segment: Pick = { _id: protectString('segment0'), _rank: 1, rundownId: protectString('rundown0'), @@ -118,7 +118,7 @@ describe('generateNotesForSegment', () => { }, }) - const segment: Pick = { + const segment: Pick = { _id: protectString('segment0'), _rank: 1, rundownId: protectString('rundown0'), @@ -172,7 +172,7 @@ describe('generateNotesForSegment', () => { const playlistId = protectString('playlist0') const nrcsName = 'some nrcs' - const segment: Pick = { + const segment: Pick = { _id: protectString('segment0'), _rank: 1, rundownId: protectString('rundown0'), @@ -264,7 +264,7 @@ describe('generateNotesForSegment', () => { const playlistId = protectString('playlist0') const nrcsName = 'some nrcs' - const segment: Pick = { + const segment: Pick = { _id: protectString('segment0'), _rank: 1, rundownId: protectString('rundown0'), @@ -295,7 +295,7 @@ describe('generateNotesForSegment', () => { const playlistId = protectString('playlist0') const nrcsName = 'some nrcs' - const segment: Pick = { + const segment: Pick = { _id: protectString('segment0'), _rank: 1, rundownId: protectString('rundown0'), @@ -325,7 +325,7 @@ describe('generateNotesForSegment', () => { const playlistId = protectString('playlist0') const nrcsName = 'some nrcs' - const segment: Pick = { + const segment: Pick = { _id: protectString('segment0'), _rank: 1, rundownId: protectString('rundown0'), @@ -355,7 +355,7 @@ describe('generateNotesForSegment', () => { const playlistId = protectString('playlist0') const nrcsName = 'some nrcs' - const segment: Pick = { + const segment: Pick = { _id: protectString('segment0'), _rank: 1, rundownId: protectString('rundown0'), @@ -407,7 +407,7 @@ describe('generateNotesForSegment', () => { const playlistId = protectString('playlist0') const nrcsName = 'some nrcs' - const segment: Pick = { + const segment: Pick = { _id: protectString('segment0'), _rank: 1, rundownId: protectString('rundown0'), diff --git a/meteor/server/publications/segmentPartNotesUI/generateNotesForSegment.ts b/meteor/server/publications/segmentPartNotesUI/generateNotesForSegment.ts index e477b30aad..64c4000936 100644 --- a/meteor/server/publications/segmentPartNotesUI/generateNotesForSegment.ts +++ b/meteor/server/publications/segmentPartNotesUI/generateNotesForSegment.ts @@ -6,13 +6,13 @@ import { SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/ import { literal } from '@sofie-automation/corelib/dist/lib' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { UISegmentPartNote } from '../../../lib/api/rundownNotifications' -import { Segment } from '../../../lib/collections/Segments' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { generateTranslation } from '../../../lib/lib' import { SegmentFields, PartFields, PartInstanceFields } from './reactiveContentCache' export function generateNotesForSegment( playlistId: RundownPlaylistId, - segment: Pick, + segment: Pick, nrcsName: string, parts: Pick[], partInstances: Pick[] diff --git a/meteor/server/publications/segmentPartNotesUI/publication.ts b/meteor/server/publications/segmentPartNotesUI/publication.ts index 6f989f06d6..029a5bd4b0 100644 --- a/meteor/server/publications/segmentPartNotesUI/publication.ts +++ b/meteor/server/publications/segmentPartNotesUI/publication.ts @@ -6,7 +6,7 @@ import { UISegmentPartNote } from '../../../lib/api/rundownNotifications' import { DBPartInstance } from '../../../lib/collections/PartInstances' import { DBPart } from '../../../lib/collections/Parts' import { Rundown } from '../../../lib/collections/Rundowns' -import { DBSegment } from '../../../lib/collections/Segments' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { groupByToMap, literal, normalizeArrayToMap, protectString } from '../../../lib/lib' import { CustomPublishCollection, diff --git a/meteor/server/security/rundown.ts b/meteor/server/security/rundown.ts index 1520ad5360..054f2e4626 100644 --- a/meteor/server/security/rundown.ts +++ b/meteor/server/security/rundown.ts @@ -4,7 +4,7 @@ import * as _ from 'underscore' import { Credentials, ResolvedCredentials } from './lib/credentials' import { logNotAllowed } from './lib/lib' import { allowAccessToRundown } from './lib/security' -import { DBSegment } from '../../lib/collections/Segments' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { ExpectedMediaItem } from '@sofie-automation/corelib/dist/dataModel/ExpectedMediaItem' import { PeripheralDeviceType, PeripheralDevice } from '../../lib/collections/PeripheralDevices' import { ExpectedPlayoutItem } from '@sofie-automation/corelib/dist/dataModel/ExpectedPlayoutItem' From 6cbef72e90d19c1a4cb3c1f1996c858c36e389e2 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 4 Jul 2023 16:31:23 +0100 Subject: [PATCH 007/479] chore: tidy up Rundown collection rexports --- meteor/__mocks__/defaultCollectionObjects.ts | 2 +- meteor/__mocks__/helpers/database.ts | 2 +- meteor/client/lib/__tests__/rundownTiming.test.ts | 2 +- meteor/client/lib/rundown.ts | 2 +- meteor/client/ui/ClipTrimPanel/ClipTrimDialog.tsx | 2 +- meteor/client/ui/ClockView/PresenterScreen.tsx | 2 +- meteor/client/ui/Prompter/PrompterView.tsx | 2 +- meteor/client/ui/Prompter/prompter.ts | 2 +- meteor/client/ui/RundownList/RundownListItem.tsx | 2 +- meteor/client/ui/RundownList/RundownListItemView.tsx | 2 +- meteor/client/ui/RundownList/RundownPlaylistUi.tsx | 2 +- meteor/client/ui/RundownList/RundownViewLayoutSelection.tsx | 2 +- meteor/client/ui/RundownList/util.ts | 2 +- meteor/client/ui/RundownView.tsx | 2 +- meteor/client/ui/RundownView/RundownDividerHeader.tsx | 2 +- meteor/client/ui/RundownView/RundownNotifier.tsx | 2 +- meteor/client/ui/RundownView/RundownSystemStatus.tsx | 2 +- meteor/client/ui/RundownView/RundownTiming/NextBreakTiming.tsx | 2 +- meteor/client/ui/RundownView/RundownTiming/RundownName.tsx | 2 +- meteor/client/ui/SegmentContainer/withResolvedSegment.ts | 2 +- meteor/client/ui/SegmentTimeline/SegmentNextPreview.tsx | 2 +- .../components/triggeredActions/TriggeredActionsEditor.tsx | 2 +- meteor/client/ui/Shelf/AdLibPanel.tsx | 2 +- meteor/client/ui/Shelf/Shelf.tsx | 2 +- meteor/client/ui/Shelf/SystemStatusPanel.tsx | 2 +- meteor/lib/Rundown.ts | 2 +- meteor/lib/api/pubsub.ts | 2 +- meteor/lib/api/triggers/actionFilterChainCompilers.ts | 2 +- meteor/lib/collections/Rundowns.ts | 1 - meteor/lib/collections/libCollections.ts | 2 +- meteor/lib/main.ts | 1 - meteor/server/api/deviceTriggers/StudioObserver.ts | 2 +- meteor/server/api/ingest/actions.ts | 2 +- meteor/server/api/ingest/genericDevice/actions.ts | 2 +- meteor/server/api/ingest/lib.ts | 2 +- meteor/server/api/ingest/mosDevice/actions.ts | 2 +- meteor/server/api/lib.ts | 2 +- .../server/publications/lib/__tests__/rundownsObserver.test.ts | 2 +- meteor/server/publications/segmentPartNotesUI/publication.ts | 2 +- meteor/server/security/lib/security.ts | 2 +- meteor/server/security/rundownPlaylist.ts | 2 +- 41 files changed, 39 insertions(+), 41 deletions(-) delete mode 100644 meteor/lib/collections/Rundowns.ts diff --git a/meteor/__mocks__/defaultCollectionObjects.ts b/meteor/__mocks__/defaultCollectionObjects.ts index b884cbbf4d..8bb0a2069e 100644 --- a/meteor/__mocks__/defaultCollectionObjects.ts +++ b/meteor/__mocks__/defaultCollectionObjects.ts @@ -1,7 +1,7 @@ import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { clone, getCurrentTime, unprotectString } from '../lib/lib' import { DBRundownPlaylist } from '../lib/collections/RundownPlaylists' -import { DBRundown } from '../lib/collections/Rundowns' +import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart } from '../lib/collections/Parts' import { IBlueprintPieceType, PieceLifespan } from '@sofie-automation/blueprints-integration' diff --git a/meteor/__mocks__/helpers/database.ts b/meteor/__mocks__/helpers/database.ts index 781ad41382..bb5626e98a 100644 --- a/meteor/__mocks__/helpers/database.ts +++ b/meteor/__mocks__/helpers/database.ts @@ -48,7 +48,7 @@ import { Complete, normalizeArray, } from '../../lib/lib' -import { DBRundown } from '../../lib/collections/Rundowns' +import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart } from '../../lib/collections/Parts' import { EmptyPieceTimelineObjectsBlob, Piece } from '../../lib/collections/Pieces' diff --git a/meteor/client/lib/__tests__/rundownTiming.test.ts b/meteor/client/lib/__tests__/rundownTiming.test.ts index 60bbe5f91e..9ad5bf0511 100644 --- a/meteor/client/lib/__tests__/rundownTiming.test.ts +++ b/meteor/client/lib/__tests__/rundownTiming.test.ts @@ -2,7 +2,7 @@ import { RundownPlaylist, DBRundownPlaylist } from '../../../lib/collections/Run import { PartInstance, wrapPartToTemporaryInstance } from '../../../lib/collections/PartInstances' import { DBPart, Part } from '../../../lib/collections/Parts' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' -import { DBRundown } from '../../../lib/collections/Rundowns' +import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { literal, protectString } from '../../../lib/lib' import { RundownTimingCalculator, RundownTimingContext } from '../rundownTiming' import { IBlueprintPieceType, PlaylistTimingType } from '@sofie-automation/blueprints-integration' diff --git a/meteor/client/lib/rundown.ts b/meteor/client/lib/rundown.ts index d0d4a7fa9f..b5997bd920 100644 --- a/meteor/client/lib/rundown.ts +++ b/meteor/client/lib/rundown.ts @@ -34,7 +34,7 @@ import { IAdLibListItem } from '../ui/Shelf/AdLibListItem' import { BucketAdLibItem, BucketAdLibUi } from '../ui/Shelf/RundownViewBuckets' import { FindOptions } from '../../lib/collections/lib' import { getShowHiddenSourceLayers } from './localStorage' -import { Rundown } from '../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' import { calculatePartInstanceExpectedDurationWithPreroll, diff --git a/meteor/client/ui/ClipTrimPanel/ClipTrimDialog.tsx b/meteor/client/ui/ClipTrimPanel/ClipTrimDialog.tsx index 561f483db9..3e0841b010 100644 --- a/meteor/client/ui/ClipTrimPanel/ClipTrimDialog.tsx +++ b/meteor/client/ui/ClipTrimPanel/ClipTrimDialog.tsx @@ -8,7 +8,7 @@ import { MeteorCall } from '../../../lib/api/methods' import { NotificationCenter, Notification, NoticeLevel } from '../../../lib/notifications/notifications' import { protectString, stringifyError } from '../../../lib/lib' import { ClientAPI } from '../../../lib/api/client' -import { Rundown } from '../../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { PieceInstancePiece } from '../../../lib/collections/PieceInstances' import { UIStudio } from '../../../lib/api/studios' import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' diff --git a/meteor/client/ui/ClockView/PresenterScreen.tsx b/meteor/client/ui/ClockView/PresenterScreen.tsx index 76b93cd47e..c44a8a3540 100644 --- a/meteor/client/ui/ClockView/PresenterScreen.tsx +++ b/meteor/client/ui/ClockView/PresenterScreen.tsx @@ -3,7 +3,7 @@ import ClassNames from 'classnames' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { PartUi } from '../SegmentTimeline/SegmentTimelineContainer' import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' -import { Rundown } from '../../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { withTranslation, WithTranslation } from 'react-i18next' import { withTiming, WithTiming } from '../RundownView/RundownTiming/withTiming' import { Translated, withTracker } from '../../lib/ReactMeteorData/ReactMeteorData' diff --git a/meteor/client/ui/Prompter/PrompterView.tsx b/meteor/client/ui/Prompter/PrompterView.tsx index c40ad33496..c7c27e8045 100644 --- a/meteor/client/ui/Prompter/PrompterView.tsx +++ b/meteor/client/ui/Prompter/PrompterView.tsx @@ -18,7 +18,7 @@ import { documentTitle } from '../../lib/DocumentTitleProvider' import { StudioScreenSaver } from '../StudioScreenSaver/StudioScreenSaver' import { RundownTimingProvider } from '../RundownView/RundownTiming/RundownTimingProvider' import { OverUnderTimer } from './OverUnderTimer' -import { Rundown } from '../../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { PartInstanceId, PieceId, RundownPlaylistId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { UIStudios } from '../Collections' import { UIStudio } from '../../../lib/api/studios' diff --git a/meteor/client/ui/Prompter/prompter.ts b/meteor/client/ui/Prompter/prompter.ts index 530bd61766..4c16ed6a98 100644 --- a/meteor/client/ui/Prompter/prompter.ts +++ b/meteor/client/ui/Prompter/prompter.ts @@ -6,7 +6,7 @@ import { Piece } from '../../../lib/collections/Pieces' import { getPieceInstancesForPartInstance, getSegmentsWithPartInstances } from '../../../lib/Rundown' import { FindOptions } from '../../../lib/collections/lib' import { PieceInstance } from '../../../lib/collections/PieceInstances' -import { Rundown } from '../../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { processAndPrunePieceInstanceTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' import { UIShowStyleBases } from '../Collections' import { diff --git a/meteor/client/ui/RundownList/RundownListItem.tsx b/meteor/client/ui/RundownList/RundownListItem.tsx index c09965af64..144f443d32 100644 --- a/meteor/client/ui/RundownList/RundownListItem.tsx +++ b/meteor/client/ui/RundownList/RundownListItem.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from 'react' import classNames from 'classnames' -import { Rundown } from '../../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { getAllowConfigure, getAllowService, getAllowStudio } from '../../lib/localStorage' import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { confirmDeleteRundown, confirmReSyncRundown, getShowStyleBaseLink } from './util' diff --git a/meteor/client/ui/RundownList/RundownListItemView.tsx b/meteor/client/ui/RundownList/RundownListItemView.tsx index cdbccf968f..5a82d23eb4 100644 --- a/meteor/client/ui/RundownList/RundownListItemView.tsx +++ b/meteor/client/ui/RundownList/RundownListItemView.tsx @@ -2,7 +2,7 @@ import Tooltip from 'rc-tooltip' import React from 'react' import { useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' -import { Rundown } from '../../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { getAllowStudio } from '../../lib/localStorage' import { RundownUtils } from '../../lib/rundown' import { iconDragHandle, iconRemove, iconResync } from './icons' diff --git a/meteor/client/ui/RundownList/RundownPlaylistUi.tsx b/meteor/client/ui/RundownList/RundownPlaylistUi.tsx index 1a3050294c..ed9767fc46 100644 --- a/meteor/client/ui/RundownList/RundownPlaylistUi.tsx +++ b/meteor/client/ui/RundownList/RundownPlaylistUi.tsx @@ -7,7 +7,7 @@ import { unprotectString } from '../../../lib/lib' import { ActiveProgressBar } from './ActiveProgressBar' import { RundownListItem } from './RundownListItem' import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' -import { Rundown } from '../../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { Link } from 'react-router-dom' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faFolderOpen } from '@fortawesome/free-solid-svg-icons' diff --git a/meteor/client/ui/RundownList/RundownViewLayoutSelection.tsx b/meteor/client/ui/RundownList/RundownViewLayoutSelection.tsx index 338347c60b..ddd664f627 100644 --- a/meteor/client/ui/RundownList/RundownViewLayoutSelection.tsx +++ b/meteor/client/ui/RundownList/RundownViewLayoutSelection.tsx @@ -3,7 +3,7 @@ import { UIStateStorage } from '../../lib/UIStateStorage' import { Link } from 'react-router-dom' import { SplitDropdown } from '../../lib/SplitDropdown' import { getRundownPlaylistLink, getRundownWithShelfLayoutLink as getRundownWithLayoutLink, getShelfLink } from './util' -import { Rundown } from '../../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { RundownLayoutBase } from '../../../lib/collections/RundownLayouts' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import classNames from 'classnames' diff --git a/meteor/client/ui/RundownList/util.ts b/meteor/client/ui/RundownList/util.ts index 22e8c7adc7..107344008e 100644 --- a/meteor/client/ui/RundownList/util.ts +++ b/meteor/client/ui/RundownList/util.ts @@ -1,4 +1,4 @@ -import { Rundown } from '../../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { unprotectString } from '../../../lib/lib' import { doModalDialog } from '../../lib/ModalDialog' import { doUserAction, UserAction } from '../../../lib/clientUserAction' diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx index 60d0452a6f..cb116db8c0 100644 --- a/meteor/client/ui/RundownView.tsx +++ b/meteor/client/ui/RundownView.tsx @@ -16,7 +16,7 @@ import * as i18next from 'i18next' import Tooltip from 'rc-tooltip' import { NavLink, Route, Prompt } from 'react-router-dom' import { RundownPlaylist } from '../../lib/collections/RundownPlaylists' -import { Rundown } from '../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { StudioRouteSet } from '@sofie-automation/corelib/dist/dataModel/Studio' import { Part } from '../../lib/collections/Parts' diff --git a/meteor/client/ui/RundownView/RundownDividerHeader.tsx b/meteor/client/ui/RundownView/RundownDividerHeader.tsx index f264b00bc7..231bc0ca19 100644 --- a/meteor/client/ui/RundownView/RundownDividerHeader.tsx +++ b/meteor/client/ui/RundownView/RundownDividerHeader.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Rundown } from '../../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData' import Moment from 'react-moment' import { TimingDataResolution, TimingTickResolution, withTiming, WithTiming } from './RundownTiming/withTiming' diff --git a/meteor/client/ui/RundownView/RundownNotifier.tsx b/meteor/client/ui/RundownView/RundownNotifier.tsx index ce2ebebb34..71bb73ee06 100644 --- a/meteor/client/ui/RundownView/RundownNotifier.tsx +++ b/meteor/client/ui/RundownView/RundownNotifier.tsx @@ -16,7 +16,7 @@ import { PeripheralDevice } from '../../../lib/collections/PeripheralDevices' import { getCurrentTime, unprotectString } from '../../../lib/lib' import { PubSub, meteorSubscribe } from '../../../lib/api/pubsub' import { ReactiveVar } from 'meteor/reactive-var' -import { Rundown } from '../../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { doModalDialog } from '../../lib/ModalDialog' import { doUserAction, UserAction } from '../../../lib/clientUserAction' // import { withTranslation, getI18n, getDefaults } from 'react-i18next' diff --git a/meteor/client/ui/RundownView/RundownSystemStatus.tsx b/meteor/client/ui/RundownView/RundownSystemStatus.tsx index f3473b01fe..d1d47227f9 100644 --- a/meteor/client/ui/RundownView/RundownSystemStatus.tsx +++ b/meteor/client/ui/RundownView/RundownSystemStatus.tsx @@ -8,7 +8,7 @@ import { PeripheralDeviceCategory, PeripheralDeviceType, } from '../../../lib/collections/PeripheralDevices' -import { Rundown } from '../../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { Time, getCurrentTime, unprotectString } from '../../../lib/lib' import { withTranslation, WithTranslation } from 'react-i18next' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' diff --git a/meteor/client/ui/RundownView/RundownTiming/NextBreakTiming.tsx b/meteor/client/ui/RundownView/RundownTiming/NextBreakTiming.tsx index 5d158e0265..193ed69555 100644 --- a/meteor/client/ui/RundownView/RundownTiming/NextBreakTiming.tsx +++ b/meteor/client/ui/RundownView/RundownTiming/NextBreakTiming.tsx @@ -1,7 +1,7 @@ import React from 'react' import { WithTranslation, withTranslation } from 'react-i18next' import Moment from 'react-moment' -import { Rundown } from '../../../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { Translated } from '../../../lib/ReactMeteorData/ReactMeteorData' import { WithTiming, withTiming } from './withTiming' import ClassNames from 'classnames' diff --git a/meteor/client/ui/RundownView/RundownTiming/RundownName.tsx b/meteor/client/ui/RundownView/RundownTiming/RundownName.tsx index 42dbb1b76e..71b563ba01 100644 --- a/meteor/client/ui/RundownView/RundownTiming/RundownName.tsx +++ b/meteor/client/ui/RundownView/RundownTiming/RundownName.tsx @@ -5,7 +5,7 @@ import { withTiming, WithTiming } from './withTiming' import ClassNames from 'classnames' import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists' import { LoopingIcon } from '../../../lib/ui/icons/looping' -import { Rundown } from '../../../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { RundownUtils } from '../../../lib/rundown' import { getCurrentTime } from '../../../../lib/lib' import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' diff --git a/meteor/client/ui/SegmentContainer/withResolvedSegment.ts b/meteor/client/ui/SegmentContainer/withResolvedSegment.ts index 73783f38c9..994e4b6a6b 100644 --- a/meteor/client/ui/SegmentContainer/withResolvedSegment.ts +++ b/meteor/client/ui/SegmentContainer/withResolvedSegment.ts @@ -13,7 +13,7 @@ import { import { IContextMenuContext } from '../RundownView' import { equalSets } from '../../../lib/lib' import { RundownUtils } from '../../lib/rundown' -import { Rundown } from '../../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { PartInstance } from '../../../lib/collections/PartInstances' import { Part } from '../../../lib/collections/Parts' import { slowDownReactivity } from '../../lib/reactiveData/reactiveDataHelper' diff --git a/meteor/client/ui/SegmentTimeline/SegmentNextPreview.tsx b/meteor/client/ui/SegmentTimeline/SegmentNextPreview.tsx index 524fb539eb..d03f701e43 100644 --- a/meteor/client/ui/SegmentTimeline/SegmentNextPreview.tsx +++ b/meteor/client/ui/SegmentTimeline/SegmentNextPreview.tsx @@ -5,7 +5,7 @@ // import { withTranslation } from 'react-i18next' // import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' -// import { Rundown } from '../../../lib/collections/Rundowns' +// import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' // import { PartUi, IOutputLayerUi, ISourceLayerUi, PieceUi } from './SegmentTimelineContainer' // import { SourceLayerItemContainer } from './SourceLayerItemContainer' diff --git a/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx b/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx index 69072a3110..ec1053ca23 100644 --- a/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx +++ b/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx @@ -9,7 +9,7 @@ import { TriggeredActionEntry, TRIGGERED_ACTION_ENTRY_DRAG_TYPE } from './Trigge import { literal, unprotectString } from '../../../../../lib/lib' import { TriggersHandler } from '../../../../lib/triggers/TriggersHandler' import { RundownPlaylist } from '../../../../../lib/collections/RundownPlaylists' -import { Rundown } from '../../../../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { Part } from '../../../../../lib/collections/Parts' import { MeteorCall } from '../../../../../lib/api/methods' import { UploadButton } from '../../../../lib/uploadButton' diff --git a/meteor/client/ui/Shelf/AdLibPanel.tsx b/meteor/client/ui/Shelf/AdLibPanel.tsx index ab7db43c5b..7dd10d4f37 100644 --- a/meteor/client/ui/Shelf/AdLibPanel.tsx +++ b/meteor/client/ui/Shelf/AdLibPanel.tsx @@ -3,7 +3,7 @@ import _ from 'underscore' import { Meteor } from 'meteor/meteor' import { useTracker } from '../../lib/ReactMeteorData/react-meteor-data' import { useTranslation } from 'react-i18next' -import { Rundown } from '../../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart } from '../../../lib/collections/Parts' diff --git a/meteor/client/ui/Shelf/Shelf.tsx b/meteor/client/ui/Shelf/Shelf.tsx index 392ce73e92..358f67106e 100644 --- a/meteor/client/ui/Shelf/Shelf.tsx +++ b/meteor/client/ui/Shelf/Shelf.tsx @@ -32,7 +32,7 @@ import { IAdLibListItem } from './AdLibListItem' import ShelfContextMenu from './ShelfContextMenu' import { doUserAction, UserAction } from '../../../lib/clientUserAction' import { MeteorCall } from '../../../lib/api/methods' -import { Rundown } from '../../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' import { ShelfDisplayOptions } from '../../lib/shelf' import { UIShowStyleBase } from '../../../lib/api/showStyles' diff --git a/meteor/client/ui/Shelf/SystemStatusPanel.tsx b/meteor/client/ui/Shelf/SystemStatusPanel.tsx index 101519d33b..1467b0eff5 100644 --- a/meteor/client/ui/Shelf/SystemStatusPanel.tsx +++ b/meteor/client/ui/Shelf/SystemStatusPanel.tsx @@ -11,7 +11,7 @@ import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' import { dashboardElementStyle } from './DashboardPanel' import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts' import { RundownSystemStatus } from '../RundownView/RundownSystemStatus' -import { DBRundown, Rundown } from '../../../lib/collections/Rundowns' +import { DBRundown, Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { UIStudio } from '../../../lib/api/studios' import { RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { Rundowns } from '../../collections' diff --git a/meteor/lib/Rundown.ts b/meteor/lib/Rundown.ts index 5888a3da61..71561319b7 100644 --- a/meteor/lib/Rundown.ts +++ b/meteor/lib/Rundown.ts @@ -14,7 +14,7 @@ import { PieceInstanceWithTimings } from '@sofie-automation/corelib/dist/playout import { invalidateAfter } from '../lib/invalidatingTime' import { getCurrentTime, groupByToMap, ProtectedString, protectString } from './lib' import { RundownPlaylist } from './collections/RundownPlaylists' -import { Rundown } from './collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { isTranslatableMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import { mongoWhereFilter, MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { FindOptions } from './collections/lib' diff --git a/meteor/lib/api/pubsub.ts b/meteor/lib/api/pubsub.ts index 96d42d0604..b3400f42d5 100644 --- a/meteor/lib/api/pubsub.ts +++ b/meteor/lib/api/pubsub.ts @@ -38,7 +38,7 @@ import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataM import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' import { RundownLayoutBase } from '../collections/RundownLayouts' import { DBRundownPlaylist } from '../collections/RundownPlaylists' -import { DBRundown } from '../collections/Rundowns' +import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' diff --git a/meteor/lib/api/triggers/actionFilterChainCompilers.ts b/meteor/lib/api/triggers/actionFilterChainCompilers.ts index fef4f90340..2ca82420ef 100644 --- a/meteor/lib/api/triggers/actionFilterChainCompilers.ts +++ b/meteor/lib/api/triggers/actionFilterChainCompilers.ts @@ -17,7 +17,7 @@ import { DBRundownPlaylist, RundownPlaylist } from '../../collections/RundownPla import { SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { assertNever, generateTranslation } from '../../lib' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' -import { DBRundown } from '../../collections/Rundowns' +import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { sortAdlibs } from '../../Rundown' import { ReactivePlaylistActionContext } from './actionFactory' diff --git a/meteor/lib/collections/Rundowns.ts b/meteor/lib/collections/Rundowns.ts deleted file mode 100644 index 6ec84a9b44..0000000000 --- a/meteor/lib/collections/Rundowns.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '@sofie-automation/corelib/dist/dataModel/Rundown' diff --git a/meteor/lib/collections/libCollections.ts b/meteor/lib/collections/libCollections.ts index a309a6242c..2c896e0db9 100644 --- a/meteor/lib/collections/libCollections.ts +++ b/meteor/lib/collections/libCollections.ts @@ -15,7 +15,7 @@ import { Piece } from './Pieces' import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' import { DBRundownPlaylist } from './RundownPlaylists' -import { DBRundown } from './Rundowns' +import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' export const AdLibActions = createSyncReadOnlyMongoCollection(CollectionName.AdLibActions) diff --git a/meteor/lib/main.ts b/meteor/lib/main.ts index 273c85133d..0df9948956 100644 --- a/meteor/lib/main.ts +++ b/meteor/lib/main.ts @@ -17,7 +17,6 @@ import './collections/PieceInstances' import './collections/Pieces' import './collections/RundownLayouts' import './collections/RundownPlaylists' -import './collections/Rundowns' import './collections/Snapshots' import './collections/Studios' import './collections/Timeline' diff --git a/meteor/server/api/deviceTriggers/StudioObserver.ts b/meteor/server/api/deviceTriggers/StudioObserver.ts index d49dcf43a6..c99c653836 100644 --- a/meteor/server/api/deviceTriggers/StudioObserver.ts +++ b/meteor/server/api/deviceTriggers/StudioObserver.ts @@ -13,7 +13,7 @@ import _ from 'underscore' import { MongoCursor } from '../../../lib/collections/lib' import { DBPartInstance } from '../../../lib/collections/PartInstances' import { DBRundownPlaylist } from '../../../lib/collections/RundownPlaylists' -import { DBRundown } from '../../../lib/collections/Rundowns' +import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { logger } from '../../logging' import { observerChain } from '../../publications/lib/observerChain' diff --git a/meteor/server/api/ingest/actions.ts b/meteor/server/api/ingest/actions.ts index 9e4da2a680..5c1320182b 100644 --- a/meteor/server/api/ingest/actions.ts +++ b/meteor/server/api/ingest/actions.ts @@ -1,7 +1,7 @@ import { getPeripheralDeviceFromRundown } from './lib' import { MOSDeviceActions } from './mosDevice/actions' import { Meteor } from 'meteor/meteor' -import { Rundown } from '../../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { TriggerReloadDataResponse } from '../../../lib/api/userActions' import { GenericDeviceActions } from './genericDevice/actions' import { PeripheralDeviceType } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' diff --git a/meteor/server/api/ingest/genericDevice/actions.ts b/meteor/server/api/ingest/genericDevice/actions.ts index 1e69637b91..fb9dcedea6 100644 --- a/meteor/server/api/ingest/genericDevice/actions.ts +++ b/meteor/server/api/ingest/genericDevice/actions.ts @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor' import { PeripheralDevice } from '../../../../lib/collections/PeripheralDevices' -import { Rundown } from '../../../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { TriggerReloadDataResponse } from '../../../../lib/api/userActions' import { stringifyError } from '../../../../lib/lib' import { logger } from '../../../logging' diff --git a/meteor/server/api/ingest/lib.ts b/meteor/server/api/ingest/lib.ts index c6eaa34639..350d86eaa0 100644 --- a/meteor/server/api/ingest/lib.ts +++ b/meteor/server/api/ingest/lib.ts @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor' import { getHash, getCurrentTime, protectString, stringifyError } from '../../../lib/lib' import { PeripheralDevice, PeripheralDeviceCategory } from '../../../lib/collections/PeripheralDevices' -import { Rundown } from '../../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { logger } from '../../logging' import { PeripheralDeviceContentWriteAccess } from '../../security/peripheralDevice' import { MethodContext } from '../../../lib/api/methods' diff --git a/meteor/server/api/ingest/mosDevice/actions.ts b/meteor/server/api/ingest/mosDevice/actions.ts index 030172a6ef..f2443a0a6b 100644 --- a/meteor/server/api/ingest/mosDevice/actions.ts +++ b/meteor/server/api/ingest/mosDevice/actions.ts @@ -1,6 +1,6 @@ import { MOS } from '@sofie-automation/corelib' import { logger } from '../../../logging' -import { Rundown } from '../../../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { Meteor } from 'meteor/meteor' import { PeripheralDevice } from '../../../../lib/collections/PeripheralDevices' import { Piece } from '../../../../lib/collections/Pieces' diff --git a/meteor/server/api/lib.ts b/meteor/server/api/lib.ts index a2153593ed..2772e0f405 100644 --- a/meteor/server/api/lib.ts +++ b/meteor/server/api/lib.ts @@ -2,7 +2,7 @@ import { RundownId, RundownPlaylistId, StudioId } from '@sofie-automation/coreli import { Meteor } from 'meteor/meteor' import { MethodContext } from '../../lib/api/methods' import { RundownPlaylist } from '../../lib/collections/RundownPlaylists' -import { Rundown } from '../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { RundownContentAccess, RundownPlaylistContentAccess, diff --git a/meteor/server/publications/lib/__tests__/rundownsObserver.test.ts b/meteor/server/publications/lib/__tests__/rundownsObserver.test.ts index 827f4c30cf..3e49b417e5 100644 --- a/meteor/server/publications/lib/__tests__/rundownsObserver.test.ts +++ b/meteor/server/publications/lib/__tests__/rundownsObserver.test.ts @@ -1,6 +1,6 @@ import { RundownId, RundownPlaylistId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { protectString } from '@sofie-automation/corelib/dist/protectedString' -import { Rundown } from '../../../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { Rundowns } from '../../../collections' import { runAllTimers, runTimersUntilNow, testInFiber } from '../../../../__mocks__/helpers/jest' import { MongoMock } from '../../../../__mocks__/mongo' diff --git a/meteor/server/publications/segmentPartNotesUI/publication.ts b/meteor/server/publications/segmentPartNotesUI/publication.ts index 029a5bd4b0..79ca15dce9 100644 --- a/meteor/server/publications/segmentPartNotesUI/publication.ts +++ b/meteor/server/publications/segmentPartNotesUI/publication.ts @@ -5,7 +5,7 @@ import { CustomCollectionName, PubSub } from '../../../lib/api/pubsub' import { UISegmentPartNote } from '../../../lib/api/rundownNotifications' import { DBPartInstance } from '../../../lib/collections/PartInstances' import { DBPart } from '../../../lib/collections/Parts' -import { Rundown } from '../../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { groupByToMap, literal, normalizeArrayToMap, protectString } from '../../../lib/lib' import { diff --git a/meteor/server/security/lib/security.ts b/meteor/server/security/lib/security.ts index fdb5da9594..343d3dafbd 100644 --- a/meteor/server/security/lib/security.ts +++ b/meteor/server/security/lib/security.ts @@ -4,7 +4,7 @@ import { Settings } from '../../../lib/Settings' import { resolveCredentials, ResolvedCredentials, Credentials, isResolvedCredentials } from './credentials' import { allAccess, noAccess, combineAccess, Access } from './access' import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' -import { Rundown } from '../../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { isProtectedString } from '../../../lib/lib' import { DBOrganization } from '../../../lib/collections/Organization' import { PeripheralDevice } from '../../../lib/collections/PeripheralDevices' diff --git a/meteor/server/security/rundownPlaylist.ts b/meteor/server/security/rundownPlaylist.ts index 99cb94064f..53c10d78e0 100644 --- a/meteor/server/security/rundownPlaylist.ts +++ b/meteor/server/security/rundownPlaylist.ts @@ -6,7 +6,7 @@ import { RundownPlaylist } from '../../lib/collections/RundownPlaylists' import { Credentials, ResolvedCredentials, resolveCredentials } from './lib/credentials' import { triggerWriteAccess } from './lib/securityVerify' import { isProtectedString } from '../../lib/lib' -import { Rundown } from '../../lib/collections/Rundowns' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { Settings } from '../../lib/Settings' import { OrganizationId, From 5bf402d9bcd9885660de385c99554a910a43792f Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 4 Jul 2023 16:34:18 +0100 Subject: [PATCH 008/479] chore: tidy up RundownPlaylist collection rexports --- meteor/__mocks__/defaultCollectionObjects.ts | 2 +- meteor/__mocks__/helpers/database.ts | 2 +- .../lib/__tests__/rundownTiming.test.ts | 36 +++++++++---------- meteor/client/lib/rundown.ts | 6 ++-- meteor/client/lib/rundownLayouts.ts | 6 ++-- meteor/client/lib/rundownTiming.ts | 6 ++-- meteor/client/lib/shelf.ts | 8 ++--- .../client/lib/triggers/TriggersHandler.tsx | 4 +-- meteor/client/ui/AfterBroadcastForm.tsx | 4 +-- .../client/ui/ClockView/CameraScreen/Part.tsx | 4 +-- .../ui/ClockView/CameraScreen/Rundown.tsx | 4 +-- .../ui/ClockView/CameraScreen/index.tsx | 4 +-- .../client/ui/ClockView/PresenterScreen.tsx | 12 +++---- meteor/client/ui/Prompter/OverUnderTimer.tsx | 4 +-- meteor/client/ui/Prompter/PrompterView.tsx | 10 +++--- meteor/client/ui/RundownList.tsx | 4 +-- .../ui/RundownList/ActiveProgressBar.tsx | 4 +-- .../ui/RundownList/RundownPlaylistUi.tsx | 4 +-- meteor/client/ui/RundownView.tsx | 20 +++++------ .../ui/RundownView/RundownDividerHeader.tsx | 4 +-- .../ui/RundownView/RundownSystemStatus.tsx | 4 +-- .../RundownTiming/PartCountdown.tsx | 4 +-- .../RundownTiming/PlaylistEndTiming.tsx | 4 +-- .../RundownTiming/PlaylistStartTiming.tsx | 4 +-- .../RundownView/RundownTiming/RundownName.tsx | 4 +-- .../RundownTiming/RundownTimingProvider.tsx | 4 +-- .../ui/RundownView/RundownViewShelf.tsx | 4 +-- .../SegmentContainer/withResolvedSegment.ts | 4 +-- meteor/client/ui/SegmentList/SegmentList.tsx | 4 +-- .../ui/SegmentList/SegmentListHeader.tsx | 4 +-- .../SegmentStoryboard/SegmentStoryboard.tsx | 4 +-- .../ui/SegmentTimeline/Parts/OutputGroup.tsx | 4 +-- .../Parts/SegmentTimelinePart.tsx | 4 +-- .../ui/SegmentTimeline/Parts/SourceLayer.tsx | 4 +-- .../ui/SegmentTimeline/SegmentContextMenu.tsx | 4 +-- .../ui/SegmentTimeline/SegmentNextPreview.tsx | 2 +- .../ui/SegmentTimeline/SegmentTimeline.tsx | 4 +-- .../SegmentTimelinePartHoverPreview.tsx | 4 +-- .../SegmentTimelineSmallPartFlag.tsx | 4 +-- .../SourceLayerItemContainer.tsx | 4 +-- .../TriggeredActionsEditor.tsx | 4 +-- meteor/client/ui/Shelf/AdLibListItem.tsx | 4 +-- meteor/client/ui/Shelf/AdLibListView.tsx | 4 +-- meteor/client/ui/Shelf/AdLibPanel.tsx | 11 +++--- meteor/client/ui/Shelf/BucketPanel.tsx | 4 +-- meteor/client/ui/Shelf/ColoredBoxPanel.tsx | 4 +-- .../client/ui/Shelf/DashboardActionButton.tsx | 4 +-- .../ui/Shelf/DashboardActionButtonGroup.tsx | 4 +-- .../client/ui/Shelf/DashboardPieceButton.tsx | 4 +-- meteor/client/ui/Shelf/EndWordsPanel.tsx | 4 +-- meteor/client/ui/Shelf/ExternalFramePanel.tsx | 4 +-- meteor/client/ui/Shelf/GlobalAdLibPanel.tsx | 4 +-- .../ItemRenderers/ActionItemRenderer.tsx | 4 +-- .../ItemRenderers/ItemRendererFactory.ts | 4 +-- .../ui/Shelf/Inspector/ShelfInspector.tsx | 4 +-- meteor/client/ui/Shelf/MiniRundownPanel.tsx | 4 +-- .../client/ui/Shelf/NextBreakTimingPanel.tsx | 4 +-- meteor/client/ui/Shelf/NextInfoPanel.tsx | 4 +-- meteor/client/ui/Shelf/PartNamePanel.tsx | 4 +-- meteor/client/ui/Shelf/PartTimingPanel.tsx | 4 +-- .../client/ui/Shelf/PieceCountdownPanel.tsx | 4 +-- .../client/ui/Shelf/PlaylistEndTimerPanel.tsx | 4 +-- meteor/client/ui/Shelf/PlaylistNamePanel.tsx | 4 +-- .../ui/Shelf/PlaylistStartTimerPanel.tsx | 4 +-- meteor/client/ui/Shelf/RundownViewBuckets.tsx | 4 +-- meteor/client/ui/Shelf/SegmentNamePanel.tsx | 6 ++-- meteor/client/ui/Shelf/SegmentTimingPanel.tsx | 4 +-- meteor/client/ui/Shelf/Shelf.tsx | 4 +-- .../client/ui/Shelf/ShelfDashboardLayout.tsx | 4 +-- meteor/client/ui/Shelf/ShelfRundownLayout.tsx | 4 +-- meteor/client/ui/Shelf/ShowStylePanel.tsx | 4 +-- meteor/client/ui/Shelf/StudioNamePanel.tsx | 4 +-- meteor/client/ui/Shelf/SystemStatusPanel.tsx | 4 +-- meteor/client/ui/Shelf/TextLabelPanel.tsx | 4 +-- meteor/client/ui/Shelf/TimeOfDayPanel.tsx | 4 +-- .../StudioScreenSaver/StudioScreenSaver.tsx | 8 ++--- meteor/lib/Rundown.ts | 6 ++-- meteor/lib/api/pubsub.ts | 2 +- meteor/lib/api/triggers/actionFactory.ts | 6 ++-- .../triggers/actionFilterChainCompilers.ts | 4 +-- meteor/lib/collections/RundownPlaylists.ts | 5 --- meteor/lib/collections/libCollections.ts | 2 +- meteor/lib/collections/rundownPlaylistUtil.ts | 25 +++++++------ meteor/lib/main.ts | 1 - meteor/server/api/cleanup.ts | 6 ++-- .../api/deviceTriggers/StudioObserver.ts | 2 +- meteor/server/api/evaluations.ts | 4 +-- meteor/server/api/lib.ts | 4 +-- meteor/server/api/rest/v1/index.ts | 18 +++++----- meteor/server/api/snapshot.ts | 6 ++-- meteor/server/api/studio/lib.ts | 4 +-- .../rundown/publication.ts | 6 ++-- .../segmentPartNotesUI/publication.ts | 6 ++-- meteor/server/security/lib/security.ts | 8 ++--- meteor/server/security/rundownPlaylist.ts | 4 +-- meteor/server/security/studio.ts | 6 ++-- meteor/server/webmanifest.ts | 2 +- 97 files changed, 254 insertions(+), 256 deletions(-) delete mode 100644 meteor/lib/collections/RundownPlaylists.ts diff --git a/meteor/__mocks__/defaultCollectionObjects.ts b/meteor/__mocks__/defaultCollectionObjects.ts index 8bb0a2069e..ad4400c876 100644 --- a/meteor/__mocks__/defaultCollectionObjects.ts +++ b/meteor/__mocks__/defaultCollectionObjects.ts @@ -1,6 +1,6 @@ import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { clone, getCurrentTime, unprotectString } from '../lib/lib' -import { DBRundownPlaylist } from '../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart } from '../lib/collections/Parts' diff --git a/meteor/__mocks__/helpers/database.ts b/meteor/__mocks__/helpers/database.ts index bb5626e98a..587a322de3 100644 --- a/meteor/__mocks__/helpers/database.ts +++ b/meteor/__mocks__/helpers/database.ts @@ -52,7 +52,7 @@ import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart } from '../../lib/collections/Parts' import { EmptyPieceTimelineObjectsBlob, Piece } from '../../lib/collections/Pieces' -import { DBRundownPlaylist } from '../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { restartRandomId } from '../random' diff --git a/meteor/client/lib/__tests__/rundownTiming.test.ts b/meteor/client/lib/__tests__/rundownTiming.test.ts index 9ad5bf0511..01592cf562 100644 --- a/meteor/client/lib/__tests__/rundownTiming.test.ts +++ b/meteor/client/lib/__tests__/rundownTiming.test.ts @@ -1,4 +1,4 @@ -import { RundownPlaylist, DBRundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PartInstance, wrapPartToTemporaryInstance } from '../../../lib/collections/PartInstances' import { DBPart, Part } from '../../../lib/collections/Parts' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' @@ -12,7 +12,7 @@ import { CalculateTimingsPiece } from '@sofie-automation/corelib/dist/playout/ti const DEFAULT_DURATION = 0 const DEFAULT_NONZERO_DURATION = 4000 -function makeMockPlaylist(): RundownPlaylist { +function makeMockPlaylist(): DBRundownPlaylist { return literal({ _id: protectString('mock-playlist'), externalId: 'mock-playlist', @@ -75,7 +75,7 @@ function makeMockSegment( }) } -function makeMockRundown(id: string, playlist: RundownPlaylist) { +function makeMockRundown(id: string, playlist: DBRundownPlaylist) { playlist.rundownIdsInOrder.push(protectString(id)) return literal({ _id: protectString(id), @@ -100,7 +100,7 @@ function makeMockRundown(id: string, playlist: RundownPlaylist) { describe('rundown Timing Calculator', () => { it('Provides output for empty playlist', () => { const timing = new RundownTimingCalculator() - const playlist: RundownPlaylist = makeMockPlaylist() + const playlist: DBRundownPlaylist = makeMockPlaylist() const parts: Part[] = [] const segmentsMap: Map = new Map() const partInstancesMap: Map = new Map() @@ -147,7 +147,7 @@ describe('rundown Timing Calculator', () => { it('Calculates time for unplayed playlist with start time and duration', () => { const timing = new RundownTimingCalculator() - const playlist: RundownPlaylist = makeMockPlaylist() + const playlist: DBRundownPlaylist = makeMockPlaylist() playlist.timing = { type: 'forward-time' as any, expectedStart: 0, @@ -249,7 +249,7 @@ describe('rundown Timing Calculator', () => { it('Calculates time for unplayed playlist with end time and duration', () => { const timing = new RundownTimingCalculator() - const playlist: RundownPlaylist = makeMockPlaylist() + const playlist: DBRundownPlaylist = makeMockPlaylist() playlist.timing = { type: 'forward-time' as any, expectedStart: 0, @@ -351,7 +351,7 @@ describe('rundown Timing Calculator', () => { it('Produces timing per rundown with start time and duration', () => { const timing = new RundownTimingCalculator() - const playlist: RundownPlaylist = makeMockPlaylist() + const playlist: DBRundownPlaylist = makeMockPlaylist() playlist.timing = { type: 'forward-time' as any, expectedStart: 0, @@ -457,7 +457,7 @@ describe('rundown Timing Calculator', () => { it('Handles display duration groups', () => { const timing = new RundownTimingCalculator() - const playlist: RundownPlaylist = makeMockPlaylist() + const playlist: DBRundownPlaylist = makeMockPlaylist() playlist.timing = { type: 'forward-time' as any, expectedStart: 0, @@ -584,7 +584,7 @@ describe('rundown Timing Calculator', () => { describe('Non-zero default Part duration', () => { it('Calculates time for unplayed playlist with start time and duration', () => { const timing = new RundownTimingCalculator() - const playlist: RundownPlaylist = makeMockPlaylist() + const playlist: DBRundownPlaylist = makeMockPlaylist() playlist.timing = { type: 'forward-time' as any, expectedStart: 0, @@ -686,7 +686,7 @@ describe('rundown Timing Calculator', () => { it('Handles display duration groups', () => { const timing = new RundownTimingCalculator() - const playlist: RundownPlaylist = makeMockPlaylist() + const playlist: DBRundownPlaylist = makeMockPlaylist() playlist.timing = { type: 'forward-time' as any, expectedStart: 0, @@ -825,7 +825,7 @@ describe('rundown Timing Calculator', () => { it('Handles budget duration', () => { const timing = new RundownTimingCalculator() - const playlist: RundownPlaylist = makeMockPlaylist() + const playlist: DBRundownPlaylist = makeMockPlaylist() playlist.timing = { type: 'forward-time' as any, expectedStart: 0, @@ -945,7 +945,7 @@ describe('rundown Timing Calculator', () => { it('Adds Piece preroll to Part durations', () => { const timing = new RundownTimingCalculator() - const playlist: RundownPlaylist = makeMockPlaylist() + const playlist: DBRundownPlaylist = makeMockPlaylist() playlist.timing = { type: 'forward-time' as any, expectedStart: 0, @@ -1066,7 +1066,7 @@ describe('rundown Timing Calculator', () => { it('Back-time: Can find the next expectedStart rundown anchor when it is in a future segment', () => { const timing = new RundownTimingCalculator() - const playlist: RundownPlaylist = makeMockPlaylist() + const playlist: DBRundownPlaylist = makeMockPlaylist() playlist.timing = { type: 'back-time' as any, expectedDuration: 4000, @@ -1215,7 +1215,7 @@ describe('rundown Timing Calculator', () => { it('Back-time: Can find the next expectedEnd rundown anchor when it is a future segment', () => { const timing = new RundownTimingCalculator() - const playlist: RundownPlaylist = makeMockPlaylist() + const playlist: DBRundownPlaylist = makeMockPlaylist() playlist.timing = { type: 'back-time' as any, expectedDuration: 4000, @@ -1364,7 +1364,7 @@ describe('rundown Timing Calculator', () => { it('Back-time: Can find the next expectedEnd rundown anchor when it is the current segment', () => { const timing = new RundownTimingCalculator() - const playlist: RundownPlaylist = makeMockPlaylist() + const playlist: DBRundownPlaylist = makeMockPlaylist() playlist.timing = { type: 'back-time' as any, expectedDuration: 4000, @@ -1519,7 +1519,7 @@ describe('rundown Timing Calculator', () => { it('Forward-time: Can find the next expectedStart rundown anchor when it is in a future segment', () => { const timing = new RundownTimingCalculator() - const playlist: RundownPlaylist = makeMockPlaylist() + const playlist: DBRundownPlaylist = makeMockPlaylist() playlist.timing = { type: 'forward-time' as any, expectedStart: 0, @@ -1668,7 +1668,7 @@ describe('rundown Timing Calculator', () => { it('Forward-time: Can find the next expectedEnd rundown anchor when it is a future segment', () => { const timing = new RundownTimingCalculator() - const playlist: RundownPlaylist = makeMockPlaylist() + const playlist: DBRundownPlaylist = makeMockPlaylist() playlist.timing = { type: 'forward-time' as any, expectedStart: 0, @@ -1817,7 +1817,7 @@ describe('rundown Timing Calculator', () => { it('Forward-time: Can find the next expectedEnd rundown anchor when it is the current segment', () => { const timing = new RundownTimingCalculator() - const playlist: RundownPlaylist = makeMockPlaylist() + const playlist: DBRundownPlaylist = makeMockPlaylist() playlist.timing = { type: 'forward-time' as any, expectedStart: 0, diff --git a/meteor/client/lib/rundown.ts b/meteor/client/lib/rundown.ts index b5997bd920..5a01ce1819 100644 --- a/meteor/client/lib/rundown.ts +++ b/meteor/client/lib/rundown.ts @@ -25,7 +25,7 @@ import { } from '../../lib/Rundown' import { PartInstance } from '../../lib/collections/PartInstances' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' -import { RundownPlaylist } from '../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { literal, getCurrentTime, applyToArray } from '../../lib/lib' import { processAndPrunePieceInstanceTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' import { createPieceGroupAndCap, PieceTimelineMetadata } from '@sofie-automation/corelib/dist/playout/pieces' @@ -276,7 +276,7 @@ export namespace RundownUtils { * * @export * @param {ShowStyleBase} showStyleBase - * @param {RundownPlaylist} playlist + * @param {DBRundownPlaylist} playlist * @param {DBSegment} segment * @param {Set} segmentsBeforeThisInRundownSet * @param {PartId[]} orderedAllPartIds @@ -292,7 +292,7 @@ export namespace RundownUtils { */ export function getResolvedSegment( showStyleBase: UIShowStyleBase, - playlist: RundownPlaylist, + playlist: DBRundownPlaylist, rundown: Pick, segment: DBSegment, segmentsBeforeThisInRundownSet: Set, diff --git a/meteor/client/lib/rundownLayouts.ts b/meteor/client/lib/rundownLayouts.ts index 6e881f8104..2a3caf8aa7 100644 --- a/meteor/client/lib/rundownLayouts.ts +++ b/meteor/client/lib/rundownLayouts.ts @@ -3,7 +3,7 @@ import { processAndPrunePieceInstanceTimings } from '@sofie-automation/corelib/d import { UIShowStyleBase } from '../../lib/api/showStyles' import { PieceInstance } from '../../lib/collections/PieceInstances' import { RequiresActiveLayers } from '../../lib/collections/RundownLayouts' -import { RundownPlaylist } from '../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { getCurrentTime } from '../../lib/lib' import { invalidateAt } from './../../lib/invalidatingTime' import { memoizedIsolatedAutorun } from '../../lib/memoizedIsolatedAutorun' @@ -13,7 +13,7 @@ import { PartInstances, PieceInstances } from '../collections' * If the conditions of the filter are met, activePieceInstance will include the first piece instance found that matches the filter, otherwise it will be undefined. */ export function getIsFilterActive( - playlist: RundownPlaylist, + playlist: DBRundownPlaylist, showStyleBase: UIShowStyleBase, panel: RequiresActiveLayers ): { active: boolean; activePieceInstance: PieceInstance | undefined } { @@ -51,7 +51,7 @@ export function getIsFilterActive( } export function getUnfinishedPieceInstancesReactive( - playlist: RundownPlaylist, + playlist: DBRundownPlaylist, showStyleBase: UIShowStyleBase ): PieceInstance[] { if (playlist.activationId && playlist.currentPartInfo) { diff --git a/meteor/client/lib/rundownTiming.ts b/meteor/client/lib/rundownTiming.ts index 28f8430925..014bc3b991 100644 --- a/meteor/client/lib/rundownTiming.ts +++ b/meteor/client/lib/rundownTiming.ts @@ -25,7 +25,7 @@ import { wrapPartToTemporaryInstance, } from '../../lib/collections/PartInstances' import { Part } from '../../lib/collections/Parts' -import { DBRundownPlaylist, RundownPlaylist } from '../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { getCurrentTime, objectFromEntries } from '../../lib/lib' import { Settings } from '../../lib/Settings' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' @@ -82,7 +82,7 @@ export class RundownTimingCalculator { * * @param {number} now * @param {boolean} isLowResolution - * @param {(RundownPlaylist | undefined)} playlist + * @param {(DBRundownPlaylist | undefined)} playlist * @param {Rundown[]} rundowns * @param {(Rundown | undefined)} currentRundown * @param {Part[]} parts @@ -94,7 +94,7 @@ export class RundownTimingCalculator { updateDurations( now: number, isLowResolution: boolean, - playlist: RundownPlaylist | undefined, + playlist: DBRundownPlaylist | undefined, rundowns: Rundown[], currentRundown: Rundown | undefined, parts: Part[], diff --git a/meteor/client/lib/shelf.ts b/meteor/client/lib/shelf.ts index b9e4f3a702..c3585f53d8 100644 --- a/meteor/client/lib/shelf.ts +++ b/meteor/client/lib/shelf.ts @@ -5,7 +5,7 @@ import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { PartInstance } from '../../lib/collections/PartInstances' import { PieceInstance } from '../../lib/collections/PieceInstances' import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' -import { RundownPlaylist } from '../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { processAndPrunePieceInstanceTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' import { getUnfinishedPieceInstancesReactive } from './rundownLayouts' @@ -46,7 +46,7 @@ export interface AdlibSegmentUi extends DBSegment { isCompatibleShowStyle: boolean } -export function getNextPiecesReactive(playlist: RundownPlaylist, showsStyleBase: UIShowStyleBase): PieceInstance[] { +export function getNextPiecesReactive(playlist: DBRundownPlaylist, showsStyleBase: UIShowStyleBase): PieceInstance[] { let prospectivePieceInstances: PieceInstance[] = [] if (playlist.activationId && playlist.nextPartInfo) { prospectivePieceInstances = PieceInstances.find({ @@ -86,7 +86,7 @@ export function getNextPiecesReactive(playlist: RundownPlaylist, showsStyleBase: } export function getUnfinishedPieceInstancesGrouped( - playlist: RundownPlaylist, + playlist: DBRundownPlaylist, showStyleBase: UIShowStyleBase ): { unfinishedPieceInstances: PieceInstance[]; unfinishedAdLibIds: PieceId[]; unfinishedTags: string[] } { const unfinishedPieceInstances = getUnfinishedPieceInstancesReactive(playlist, showStyleBase) @@ -109,7 +109,7 @@ export function getUnfinishedPieceInstancesGrouped( } export function getNextPieceInstancesGrouped( - playlist: RundownPlaylist, + playlist: DBRundownPlaylist, showsStyleBase: UIShowStyleBase ): { nextAdLibIds: PieceId[]; nextTags: string[]; nextPieceInstances: PieceInstance[] } { const nextPieceInstances = getNextPiecesReactive(playlist, showsStyleBase) diff --git a/meteor/client/lib/triggers/TriggersHandler.tsx b/meteor/client/lib/triggers/TriggersHandler.tsx index 22b85f7a32..7b829312c7 100644 --- a/meteor/client/lib/triggers/TriggersHandler.tsx +++ b/meteor/client/lib/triggers/TriggersHandler.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next' import Sorensen from '@sofie-automation/sorensen' import { PubSub } from '../../../lib/api/pubsub' import { useSubscription, useTracker } from '../ReactMeteorData/ReactMeteorData' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PlayoutActions, SomeAction, SomeBlueprintTrigger, TriggerType } from '@sofie-automation/blueprints-integration' import { isPreviewableAction, @@ -334,7 +334,7 @@ export const TriggersHandler: React.FC = function TriggersHandler( nextPartInfo: 1, currentPartInfo: 1, }, - }) as Pick | undefined + }) as Pick | undefined if (playlist) { let context = rundownPlaylistContext.get() if (context === null) { diff --git a/meteor/client/ui/AfterBroadcastForm.tsx b/meteor/client/ui/AfterBroadcastForm.tsx index 29332c0f9f..fb666dd173 100644 --- a/meteor/client/ui/AfterBroadcastForm.tsx +++ b/meteor/client/ui/AfterBroadcastForm.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react' -import { RundownPlaylist } from '../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { TFunction, useTranslation } from 'react-i18next' import { EditAttribute } from '../lib/EditAttribute' import { EvaluationBase } from '../../lib/collections/Evaluations' @@ -11,7 +11,7 @@ import { ClientAPI } from '../../lib/api/client' import { hashSingleUseToken } from '../../lib/api/userActions' interface IProps { - playlist: RundownPlaylist + playlist: DBRundownPlaylist } const DEFAULT_STATE = { diff --git a/meteor/client/ui/ClockView/CameraScreen/Part.tsx b/meteor/client/ui/ClockView/CameraScreen/Part.tsx index 0ba445dd66..e6ec451233 100644 --- a/meteor/client/ui/ClockView/CameraScreen/Part.tsx +++ b/meteor/client/ui/ClockView/CameraScreen/Part.tsx @@ -2,7 +2,7 @@ import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' import classNames from 'classnames' import React, { useContext } from 'react' import { AreaZoom } from '.' -import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PieceExtended } from '../../../../lib/Rundown' import { getAllowSpeaking, getAllowVibrating } from '../../../lib/localStorage' import { AutoNextStatus } from '../../RundownView/RundownTiming/AutoNextStatus' @@ -16,7 +16,7 @@ import { Piece } from './Piece' interface IProps { part: PartUi piece: PieceExtended - playlist: RundownPlaylist + playlist: DBRundownPlaylist isLive: boolean isNext: boolean } diff --git a/meteor/client/ui/ClockView/CameraScreen/Rundown.tsx b/meteor/client/ui/ClockView/CameraScreen/Rundown.tsx index 5b4950e8cb..5916e60adc 100644 --- a/meteor/client/ui/ClockView/CameraScreen/Rundown.tsx +++ b/meteor/client/ui/ClockView/CameraScreen/Rundown.tsx @@ -6,13 +6,13 @@ import { Segments } from '../../../collections' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { Segment as SegmentComponent } from './Segment' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' -import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { UIShowStyleBases } from '../../Collections' import { RundownToShowStyleContext, StudioContext } from '.' import { RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' interface IProps { - playlist: RundownPlaylist + playlist: DBRundownPlaylist rundown: RundownObj rundownIdsBefore: RundownId[] } diff --git a/meteor/client/ui/ClockView/CameraScreen/index.tsx b/meteor/client/ui/ClockView/CameraScreen/index.tsx index 809bebc34e..936779baef 100644 --- a/meteor/client/ui/ClockView/CameraScreen/index.tsx +++ b/meteor/client/ui/ClockView/CameraScreen/index.tsx @@ -5,7 +5,7 @@ import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { PubSub } from '../../../../lib/api/pubsub' import { UIStudio } from '../../../../lib/api/studios' -import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PieceExtended } from '../../../../lib/Rundown' import { PartInstances, Rundowns } from '../../../collections' import { useSubscription, useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData' @@ -23,7 +23,7 @@ import { useWakeLock } from './useWakeLock' import { useDebounce } from '../../../lib/lib' interface IProps { - playlist: RundownPlaylist | undefined + playlist: DBRundownPlaylist | undefined studioId: StudioId } diff --git a/meteor/client/ui/ClockView/PresenterScreen.tsx b/meteor/client/ui/ClockView/PresenterScreen.tsx index c44a8a3540..062761ab2e 100644 --- a/meteor/client/ui/ClockView/PresenterScreen.tsx +++ b/meteor/client/ui/ClockView/PresenterScreen.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import ClassNames from 'classnames' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { PartUi } from '../SegmentTimeline/SegmentTimelineContainer' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { withTranslation, WithTranslation } from 'react-i18next' import { withTiming, WithTiming } from '../RundownView/RundownTiming/withTiming' @@ -60,7 +60,7 @@ interface RundownOverviewState { } export interface RundownOverviewTrackedProps { studio: UIStudio | undefined - playlist?: RundownPlaylist + playlist?: DBRundownPlaylist rundowns: Rundown[] segments: Array pieces: Map @@ -81,7 +81,7 @@ export interface RundownOverviewTrackedProps { function getShowStyleBaseIdSegmentPartUi( partInstance: PartInstance, - playlist: RundownPlaylist, + playlist: DBRundownPlaylist, orderedSegmentsAndParts: { segments: DBSegment[] parts: Part[] @@ -165,7 +165,7 @@ function getShowStyleBaseIdSegmentPartUi( export const getPresenterScreenReactive = (props: RundownOverviewProps): RundownOverviewTrackedProps => { const studio = UIStudios.findOne(props.studioId) - let playlist: RundownPlaylist | undefined + let playlist: DBRundownPlaylist | undefined if (props.playlistId) playlist = RundownPlaylists.findOne(props.playlistId, { @@ -312,7 +312,7 @@ export class PresenterScreenBase extends MeteorReactComponent< _id: 1, activationId: 1, }, - }) as Pick | undefined + }) as Pick | undefined if (playlist) { this.subscribe(PubSub.rundowns, [playlist._id], null) @@ -357,7 +357,7 @@ export class PresenterScreenBase extends MeteorReactComponent< nextPartInfo: 1, previousPartInfo: 1, }, - }) as Pick | undefined + }) as Pick | undefined if (playlistR) { const { nextPartInstance, currentPartInstance } = RundownPlaylistCollectionUtil.getSelectedPartInstances(playlistR) diff --git a/meteor/client/ui/Prompter/OverUnderTimer.tsx b/meteor/client/ui/Prompter/OverUnderTimer.tsx index ff2e408ed0..3bac27a3ce 100644 --- a/meteor/client/ui/Prompter/OverUnderTimer.tsx +++ b/meteor/client/ui/Prompter/OverUnderTimer.tsx @@ -1,12 +1,12 @@ import * as React from 'react' import { withTiming, WithTiming } from '../RundownView/RundownTiming/withTiming' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { RundownUtils } from '../../lib/rundown' import ClassNames from 'classnames' import { getPlaylistTimingDiff } from '../../lib/rundownTiming' interface IProps { - rundownPlaylist: RundownPlaylist + rundownPlaylist: DBRundownPlaylist style?: React.CSSProperties | undefined } diff --git a/meteor/client/ui/Prompter/PrompterView.tsx b/meteor/client/ui/Prompter/PrompterView.tsx index c7c27e8045..f9accb048c 100644 --- a/meteor/client/ui/Prompter/PrompterView.tsx +++ b/meteor/client/ui/Prompter/PrompterView.tsx @@ -5,7 +5,7 @@ import ClassNames from 'classnames' import { Meteor } from 'meteor/meteor' import { Route } from 'react-router-dom' import { translateWithTracker, Translated } from '../../lib/ReactMeteorData/ReactMeteorData' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { parse as queryStringParse } from 'query-string' import { Spinner } from '../../lib/Spinner' @@ -77,7 +77,7 @@ interface IProps { } interface ITrackedProps { - rundownPlaylist?: RundownPlaylist + rundownPlaylist?: DBRundownPlaylist studio?: UIStudio } @@ -220,7 +220,7 @@ export class PrompterViewInner extends MeteorReactComponent | undefined + ) as Pick | undefined if (playlist?._id) { this.subscribe(PubSub.rundowns, [playlist._id], null) } @@ -557,7 +557,7 @@ export const PrompterView = translateWithTracker(({ s lastIncorrectPartPlaybackReported: 0, }, } - ) as Omit | undefined + ) as Omit | undefined return literal({ rundownPlaylist, @@ -604,7 +604,7 @@ export const Prompter = translateWithTracker, _id: 1, activationId: 1, }, - }) as Pick | undefined + }) as Pick | undefined if (playlist) { const rundownIDs = RundownPlaylistCollectionUtil.getRundownUnorderedIDs(playlist) this.subscribe(PubSub.segments, { diff --git a/meteor/client/ui/RundownList.tsx b/meteor/client/ui/RundownList.tsx index 27e8bb1fc4..27571d3d06 100644 --- a/meteor/client/ui/RundownList.tsx +++ b/meteor/client/ui/RundownList.tsx @@ -2,7 +2,7 @@ import Tooltip from 'rc-tooltip' import * as React from 'react' import { PubSub } from '../../lib/api/pubsub' import { GENESIS_SYSTEM_VERSION } from '../../lib/collections/CoreSystem' -import { RundownPlaylist } from '../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { getAllowConfigure, getHelpMode } from '../lib/localStorage' import { literal, unprotectString } from '../../lib/lib' import { useSubscription, useTracker } from '../lib/ReactMeteorData/react-meteor-data' @@ -91,7 +91,7 @@ export function RundownList(): JSX.Element { ) const rundownPlaylists = useTracker( () => - RundownPlaylists.find({}, { sort: { created: -1 } }).map((playlist: RundownPlaylist) => { + RundownPlaylists.find({}, { sort: { created: -1 } }).map((playlist: DBRundownPlaylist) => { const rundowns = RundownPlaylistCollectionUtil.getRundownsOrdered(playlist) const unsyncedRundowns = rundowns.filter((rundown) => !!rundown.orphaned) diff --git a/meteor/client/ui/RundownList/ActiveProgressBar.tsx b/meteor/client/ui/RundownList/ActiveProgressBar.tsx index 78f2e2f721..03e4238b22 100644 --- a/meteor/client/ui/RundownList/ActiveProgressBar.tsx +++ b/meteor/client/ui/RundownList/ActiveProgressBar.tsx @@ -1,9 +1,9 @@ import React from 'react' import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { useCurrentTime } from '../../lib/lib' -export function ActiveProgressBar({ rundownPlaylist }: { rundownPlaylist: RundownPlaylist }): JSX.Element | null { +export function ActiveProgressBar({ rundownPlaylist }: { rundownPlaylist: DBRundownPlaylist }): JSX.Element | null { const currentTime = useCurrentTime() const { startedPlayback, timing } = rundownPlaylist diff --git a/meteor/client/ui/RundownList/RundownPlaylistUi.tsx b/meteor/client/ui/RundownList/RundownPlaylistUi.tsx index ed9767fc46..d5292bee79 100644 --- a/meteor/client/ui/RundownList/RundownPlaylistUi.tsx +++ b/meteor/client/ui/RundownList/RundownPlaylistUi.tsx @@ -6,7 +6,7 @@ import { RundownLayoutBase } from '../../../lib/collections/RundownLayouts' import { unprotectString } from '../../../lib/lib' import { ActiveProgressBar } from './ActiveProgressBar' import { RundownListItem } from './RundownListItem' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { Link } from 'react-router-dom' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' @@ -27,7 +27,7 @@ import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTi import { TOOLTIP_DEFAULT_DELAY } from '../../lib/lib' import { RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' -export interface RundownPlaylistUi extends RundownPlaylist { +export interface RundownPlaylistUi extends DBRundownPlaylist { rundowns: Rundown[] unsyncedRundowns: Rundown[] } diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx index cb116db8c0..337273590a 100644 --- a/meteor/client/ui/RundownView.tsx +++ b/meteor/client/ui/RundownView.tsx @@ -15,7 +15,7 @@ import Escape from './../lib/Escape' import * as i18next from 'i18next' import Tooltip from 'rc-tooltip' import { NavLink, Route, Prompt } from 'react-router-dom' -import { RundownPlaylist } from '../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { StudioRouteSet } from '@sofie-automation/corelib/dist/dataModel/Studio' @@ -151,7 +151,7 @@ const HIDE_NOTIFICATIONS_AFTER_MOUNT: number | undefined = 5000 const DEFAULT_SEGMENT_VIEW_MODE = SegmentViewMode.Timeline interface ITimingWarningProps { - playlist: RundownPlaylist + playlist: DBRundownPlaylist inActiveRundownView?: boolean studioMode: boolean oneMinuteBeforeAction: (e: Event) => void @@ -247,7 +247,7 @@ const WarningDisplay = withTranslation()( ) ) interface ITimingDisplayProps { - rundownPlaylist: RundownPlaylist + rundownPlaylist: DBRundownPlaylist currentRundown: Rundown | undefined rundownCount: number layout: RundownLayoutRundownHeader | undefined @@ -323,7 +323,7 @@ const TimingDisplay = withTranslation()( ) interface IRundownHeaderProps { - playlist: RundownPlaylist + playlist: DBRundownPlaylist showStyleBase: UIShowStyleBase showStyleVariant: DBShowStyleVariant currentRundown: Rundown | undefined @@ -1159,7 +1159,7 @@ type MatchedSegment = { interface ITrackedProps { rundownPlaylistId: RundownPlaylistId rundowns: Rundown[] - playlist?: RundownPlaylist + playlist?: DBRundownPlaylist currentRundown?: Rundown matchedSegments: MatchedSegment[] rundownsToShowstyles: Map @@ -1563,7 +1563,7 @@ export const RundownView = translateWithTracker(( _id: 1, studioId: 1, }, - }) as Pick | undefined + }) as Pick | undefined if (!playlist) return this.subscribe(PubSub.uiSegmentPartNotes, playlistId) @@ -1578,7 +1578,7 @@ export const RundownView = translateWithTracker(( _id: 1, activationId: 1, }, - }) as Pick | undefined + }) as Pick | undefined if (!playlist) return const rundowns = RundownPlaylistCollectionUtil.getRundownsUnordered(playlist, undefined, { @@ -1639,7 +1639,7 @@ export const RundownView = translateWithTracker(( nextPartInfo: 1, previousPartInfo: 1, }, - }) as Pick | undefined + }) as Pick | undefined if (playlist) { const rundownIds = RundownPlaylistCollectionUtil.getRundownUnorderedIDs(playlist) // Use Meteor.subscribe so that this subscription doesn't mess with this.subscriptionsReady() @@ -2476,7 +2476,7 @@ export const RundownView = translateWithTracker(( segment: DBSegment, _index: number, rundownAndSegments: MatchedSegment, - rundownPlaylist: RundownPlaylist, + rundownPlaylist: DBRundownPlaylist, studio: UIStudio, showStyleBase: UIShowStyleBase, isLastSegment: boolean, @@ -2808,7 +2808,7 @@ export const RundownView = translateWithTracker(( renderRundownView( studio: UIStudio, - playlist: RundownPlaylist, + playlist: DBRundownPlaylist, showStyleBase: UIShowStyleBase, showStyleVariant: DBShowStyleVariant ) { diff --git a/meteor/client/ui/RundownView/RundownDividerHeader.tsx b/meteor/client/ui/RundownView/RundownDividerHeader.tsx index 231bc0ca19..9d40d4ae5b 100644 --- a/meteor/client/ui/RundownView/RundownDividerHeader.tsx +++ b/meteor/client/ui/RundownView/RundownDividerHeader.tsx @@ -5,12 +5,12 @@ import Moment from 'react-moment' import { TimingDataResolution, TimingTickResolution, withTiming, WithTiming } from './RundownTiming/withTiming' import { RundownUtils } from '../../lib/rundown' import { withTranslation } from 'react-i18next' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' interface IProps { rundown: Pick - playlist: RundownPlaylist + playlist: DBRundownPlaylist } const QUATER_DAY = 6 * 60 * 60 * 1000 diff --git a/meteor/client/ui/RundownView/RundownSystemStatus.tsx b/meteor/client/ui/RundownView/RundownSystemStatus.tsx index d1d47227f9..830e8dc423 100644 --- a/meteor/client/ui/RundownView/RundownSystemStatus.tsx +++ b/meteor/client/ui/RundownView/RundownSystemStatus.tsx @@ -12,7 +12,7 @@ import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { Time, getCurrentTime, unprotectString } from '../../../lib/lib' import { withTranslation, WithTranslation } from 'react-i18next' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PubSub } from '../../../lib/api/pubsub' import { StatusCode } from '@sofie-automation/blueprints-integration' import { UIStudio } from '../../../lib/api/studios' @@ -63,7 +63,7 @@ export const MOSLastUpdateStatus = withTranslation()( interface IProps { studio: UIStudio - playlist: RundownPlaylist + playlist: DBRundownPlaylist rundownIds: RundownId[] firstRundown: Rundown | undefined } diff --git a/meteor/client/ui/RundownView/RundownTiming/PartCountdown.tsx b/meteor/client/ui/RundownView/RundownTiming/PartCountdown.tsx index af6b7901b6..3f57db8690 100644 --- a/meteor/client/ui/RundownView/RundownTiming/PartCountdown.tsx +++ b/meteor/client/ui/RundownView/RundownTiming/PartCountdown.tsx @@ -3,7 +3,7 @@ import Moment from 'react-moment' import { withTiming, WithTiming } from './withTiming' import { unprotectString } from '../../../../lib/lib' import { RundownUtils } from '../../../lib/rundown' -import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' import { PartId } from '@sofie-automation/corelib/dist/dataModel/Ids' @@ -12,7 +12,7 @@ interface IPartCountdownProps { hideOnZero?: boolean label?: ReactNode useWallClock?: boolean - playlist: RundownPlaylist + playlist: DBRundownPlaylist } /** diff --git a/meteor/client/ui/RundownView/RundownTiming/PlaylistEndTiming.tsx b/meteor/client/ui/RundownView/RundownTiming/PlaylistEndTiming.tsx index 132af1aba9..2d2406e14d 100644 --- a/meteor/client/ui/RundownView/RundownTiming/PlaylistEndTiming.tsx +++ b/meteor/client/ui/RundownView/RundownTiming/PlaylistEndTiming.tsx @@ -6,11 +6,11 @@ import { Translated } from '../../../lib/ReactMeteorData/ReactMeteorData' import { RundownUtils } from '../../../lib/rundown' import { withTiming, WithTiming } from './withTiming' import ClassNames from 'classnames' -import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { getPlaylistTimingDiff } from '../../../lib/rundownTiming' interface IEndTimingProps { - rundownPlaylist: RundownPlaylist + rundownPlaylist: DBRundownPlaylist loop?: boolean expectedStart?: number expectedDuration?: number diff --git a/meteor/client/ui/RundownView/RundownTiming/PlaylistStartTiming.tsx b/meteor/client/ui/RundownView/RundownTiming/PlaylistStartTiming.tsx index c7026451c2..8b7e1be7bc 100644 --- a/meteor/client/ui/RundownView/RundownTiming/PlaylistStartTiming.tsx +++ b/meteor/client/ui/RundownView/RundownTiming/PlaylistStartTiming.tsx @@ -3,14 +3,14 @@ import { WithTranslation, withTranslation } from 'react-i18next' import Moment from 'react-moment' import { Translated } from '../../../lib/ReactMeteorData/ReactMeteorData' import { withTiming, WithTiming } from './withTiming' -import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { RundownUtils } from '../../../lib/rundown' import { getCurrentTime } from '../../../../lib/lib' import ClassNames from 'classnames' import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' interface IEndTimingProps { - rundownPlaylist: RundownPlaylist + rundownPlaylist: DBRundownPlaylist hidePlannedStart?: boolean hideDiff?: boolean plannedStartText?: string diff --git a/meteor/client/ui/RundownView/RundownTiming/RundownName.tsx b/meteor/client/ui/RundownView/RundownTiming/RundownName.tsx index 71b563ba01..2a0cca0ecd 100644 --- a/meteor/client/ui/RundownView/RundownTiming/RundownName.tsx +++ b/meteor/client/ui/RundownView/RundownTiming/RundownName.tsx @@ -3,7 +3,7 @@ import { WithTranslation, withTranslation } from 'react-i18next' import { Translated } from '../../../lib/ReactMeteorData/ReactMeteorData' import { withTiming, WithTiming } from './withTiming' import ClassNames from 'classnames' -import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { LoopingIcon } from '../../../lib/ui/icons/looping' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { RundownUtils } from '../../../lib/rundown' @@ -11,7 +11,7 @@ import { getCurrentTime } from '../../../../lib/lib' import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' interface IRundownNameProps { - rundownPlaylist: RundownPlaylist + rundownPlaylist: DBRundownPlaylist currentRundown?: Rundown rundownCount: number hideDiff?: boolean diff --git a/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx b/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx index 5f9d1aa180..e355fc9720 100644 --- a/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx +++ b/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx @@ -5,7 +5,7 @@ import { withTracker } from '../../../lib/ReactMeteorData/react-meteor-data' import { Part } from '../../../../lib/collections/Parts' import { getCurrentTime } from '../../../../lib/lib' import { MeteorReactComponent } from '../../../lib/MeteorReactComponent' -import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PartInstance } from '../../../../lib/collections/PartInstances' import { RundownTiming, TimeEventArgs } from './RundownTiming' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' @@ -30,7 +30,7 @@ const CURRENT_TIME_GRANULARITY = 1000 / 60 */ interface IRundownTimingProviderProps { /** Rundown Playlist that is to be used for generating the timing information. */ - playlist?: RundownPlaylist + playlist?: DBRundownPlaylist /** Interval for high-resolution timing events. If undefined, it will fall back * onto TIMING_DEFAULT_REFRESH_INTERVAL. diff --git a/meteor/client/ui/RundownView/RundownViewShelf.tsx b/meteor/client/ui/RundownView/RundownViewShelf.tsx index f7a5af8811..d6d7657015 100644 --- a/meteor/client/ui/RundownView/RundownViewShelf.tsx +++ b/meteor/client/ui/RundownView/RundownViewShelf.tsx @@ -4,7 +4,7 @@ import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/Reac import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { SegmentUi } from '../SegmentTimeline/SegmentTimelineContainer' import { unprotectString } from '../../../lib/lib' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { OutputLayers, SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DashboardPieceButton } from '../Shelf/DashboardPieceButton' import { IBlueprintActionTriggerMode, ISourceLayer } from '@sofie-automation/blueprints-integration' @@ -35,7 +35,7 @@ import { PartInstanceId, PieceId } from '@sofie-automation/corelib/dist/dataMode interface IRundownViewShelfProps { studio: UIStudio segment: SegmentUi - playlist: RundownPlaylist + playlist: DBRundownPlaylist showStyleBase: UIShowStyleBase adLibSegmentUi: AdlibSegmentUi hotkeyGroup: string diff --git a/meteor/client/ui/SegmentContainer/withResolvedSegment.ts b/meteor/client/ui/SegmentContainer/withResolvedSegment.ts index 994e4b6a6b..1f38f7e2f5 100644 --- a/meteor/client/ui/SegmentContainer/withResolvedSegment.ts +++ b/meteor/client/ui/SegmentContainer/withResolvedSegment.ts @@ -1,7 +1,7 @@ import * as React from 'react' import * as _ from 'underscore' import { ISourceLayer, NoteSeverity, PieceLifespan } from '@sofie-automation/blueprints-integration' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { withTracker } from '../../lib/ReactMeteorData/react-meteor-data' import { IOutputLayerExtended, @@ -75,7 +75,7 @@ export interface IProps { rundownsToShowstyles: Map studio: UIStudio showStyleBase: UIShowStyleBase - playlist: RundownPlaylist + playlist: DBRundownPlaylist rundown: MinimalRundown timeScale: number onPieceDoubleClick?: (item: PieceUi, e: React.MouseEvent) => void diff --git a/meteor/client/ui/SegmentList/SegmentList.tsx b/meteor/client/ui/SegmentList/SegmentList.tsx index f7b64d3004..2d35dc4344 100644 --- a/meteor/client/ui/SegmentList/SegmentList.tsx +++ b/meteor/client/ui/SegmentList/SegmentList.tsx @@ -1,6 +1,6 @@ import React, { ReactNode, useLayoutEffect, useMemo, useRef, useState } from 'react' import classNames from 'classnames' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { UIStateStorage } from '../../lib/UIStateStorage' import { PartUi, PieceUi, SegmentNoteCounts, SegmentUi } from '../SegmentContainer/withResolvedSegment' import { IContextMenuContext } from '../RundownView' @@ -31,7 +31,7 @@ interface IProps { key: string segment: SegmentUi - playlist: RundownPlaylist + playlist: DBRundownPlaylist studio: UIStudio parts: Array pieces: Map diff --git a/meteor/client/ui/SegmentList/SegmentListHeader.tsx b/meteor/client/ui/SegmentList/SegmentListHeader.tsx index 86dd606339..49afdd0abd 100644 --- a/meteor/client/ui/SegmentList/SegmentListHeader.tsx +++ b/meteor/client/ui/SegmentList/SegmentListHeader.tsx @@ -11,7 +11,7 @@ import { PartCountdown } from '../RundownView/RundownTiming/PartCountdown' import { SegmentDuration } from '../RundownView/RundownTiming/SegmentDuration' import { PartId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { useTranslation } from 'react-i18next' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { IContextMenuContext } from '../RundownView' import { NoteSeverity } from '@sofie-automation/blueprints-integration' import { CriticalIconSmall, WarningIconSmall } from '../../lib/ui/icons/notifications' @@ -44,7 +44,7 @@ export function SegmentListHeader({ isDetached: boolean isDetachedStick: boolean segment: SegmentUi - playlist: RundownPlaylist + playlist: DBRundownPlaylist studio: UIStudio parts: Array pieces: Map diff --git a/meteor/client/ui/SegmentStoryboard/SegmentStoryboard.tsx b/meteor/client/ui/SegmentStoryboard/SegmentStoryboard.tsx index ee8ad61470..006d3a1ba0 100644 --- a/meteor/client/ui/SegmentStoryboard/SegmentStoryboard.tsx +++ b/meteor/client/ui/SegmentStoryboard/SegmentStoryboard.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' import { NoteSeverity } from '@sofie-automation/blueprints-integration' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { IContextMenuContext } from '../RundownView' import { IOutputLayerUi, PartUi, PieceUi, SegmentNoteCounts, SegmentUi } from '../SegmentContainer/withResolvedSegment' import { ContextMenuTrigger } from '@jstarpl/react-contextmenu' @@ -41,7 +41,7 @@ interface IProps { id: string key: string segment: SegmentUi - playlist: RundownPlaylist + playlist: DBRundownPlaylist studio: UIStudio parts: Array pieces: Map diff --git a/meteor/client/ui/SegmentTimeline/Parts/OutputGroup.tsx b/meteor/client/ui/SegmentTimeline/Parts/OutputGroup.tsx index 4468ab07c6..ad58d38884 100644 --- a/meteor/client/ui/SegmentTimeline/Parts/OutputGroup.tsx +++ b/meteor/client/ui/SegmentTimeline/Parts/OutputGroup.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { ISourceLayerExtended } from '../../../../lib/Rundown' import { IContextMenuContext } from '../../RundownView' import { IOutputLayerUi, PartUi, PieceUi, SegmentUi } from '../SegmentTimelineContainer' @@ -15,7 +15,7 @@ import { CalculateTimingsPiece } from '@sofie-automation/corelib/dist/playout/ti interface IOutputGroupProps { layer: IOutputLayerUi sourceLayers: ISourceLayerExtended[] - playlist: RundownPlaylist + playlist: DBRundownPlaylist studio: UIStudio segment: SegmentUi part: PartUi diff --git a/meteor/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx b/meteor/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx index e8b9b73b6e..871a373126 100644 --- a/meteor/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx +++ b/meteor/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx @@ -3,7 +3,7 @@ import _ from 'underscore' import { withTranslation, WithTranslation, TFunction } from 'react-i18next' import ClassNames from 'classnames' -import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { SegmentUi, PartUi, IOutputLayerUi, PieceUi, LIVE_LINE_TIME_PADDING } from '../SegmentTimelineContainer' import { TimingDataResolution, @@ -47,7 +47,7 @@ export const BREAKPOINT_TOO_SMALL_FOR_DISPLAY = 6 interface IProps { segment: SegmentUi - playlist: RundownPlaylist + playlist: DBRundownPlaylist studio: UIStudio part: PartUi pieces: CalculateTimingsPiece[] diff --git a/meteor/client/ui/SegmentTimeline/Parts/SourceLayer.tsx b/meteor/client/ui/SegmentTimeline/Parts/SourceLayer.tsx index 3cdf8257fe..16ae2b0c29 100644 --- a/meteor/client/ui/SegmentTimeline/Parts/SourceLayer.tsx +++ b/meteor/client/ui/SegmentTimeline/Parts/SourceLayer.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useState } from 'react' import _ from 'underscore' -import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { literal, unprotectString } from '../../../../lib/lib' import { getElementDocumentOffset, OffsetPosition } from '../../../utils/positions' import { IContextMenuContext } from '../../RundownView' @@ -15,7 +15,7 @@ import { CalculateTimingsPiece } from '@sofie-automation/corelib/dist/playout/ti export interface ISourceLayerPropsBase { key: string outputLayer: IOutputLayerUi - playlist: RundownPlaylist + playlist: DBRundownPlaylist studio: UIStudio segment: SegmentUi part: PartUi diff --git a/meteor/client/ui/SegmentTimeline/SegmentContextMenu.tsx b/meteor/client/ui/SegmentTimeline/SegmentContextMenu.tsx index 26614bdc92..80c84bee21 100644 --- a/meteor/client/ui/SegmentTimeline/SegmentContextMenu.tsx +++ b/meteor/client/ui/SegmentTimeline/SegmentContextMenu.tsx @@ -3,7 +3,7 @@ import Escape from './../../lib/Escape' import { withTranslation } from 'react-i18next' import { ContextMenu, MenuItem } from '@jstarpl/react-contextmenu' import { Part } from '../../../lib/collections/Parts' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData' import { RundownUtils } from '../../lib/rundown' import { IContextMenuContext } from '../RundownView' @@ -13,7 +13,7 @@ import { SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' interface IProps { onSetNext: (part: Part | undefined, e: any, offset?: number, take?: boolean) => void onSetNextSegment: (segmentId: SegmentId | null, e: any) => void - playlist?: RundownPlaylist + playlist?: DBRundownPlaylist studioMode: boolean contextMenuContext: IContextMenuContext | null enablePlayFromAnywhere: boolean diff --git a/meteor/client/ui/SegmentTimeline/SegmentNextPreview.tsx b/meteor/client/ui/SegmentTimeline/SegmentNextPreview.tsx index d03f701e43..030e8fa0cc 100644 --- a/meteor/client/ui/SegmentTimeline/SegmentNextPreview.tsx +++ b/meteor/client/ui/SegmentTimeline/SegmentNextPreview.tsx @@ -4,7 +4,7 @@ // import * as _ from 'underscore' // import { withTranslation } from 'react-i18next' -// import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +// import { RundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' // import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' // import { PartUi, IOutputLayerUi, ISourceLayerUi, PieceUi } from './SegmentTimelineContainer' diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx index ad5aa647ec..350eb4d8ae 100644 --- a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx +++ b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx @@ -5,7 +5,7 @@ import { WithTranslation, withTranslation } from 'react-i18next' import ClassNames from 'classnames' import { ContextMenuTrigger } from '@jstarpl/react-contextmenu' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { SegmentUi, PartUi, IOutputLayerUi, PieceUi } from './SegmentTimelineContainer' import { TimelineGrid } from './TimelineGrid' import { SegmentTimelinePart, SegmentTimelinePartClass } from './Parts/SegmentTimelinePart' @@ -56,7 +56,7 @@ interface IProps { id: string key: string segment: SegmentUi - playlist: RundownPlaylist + playlist: DBRundownPlaylist followLiveSegments: boolean studio: UIStudio parts: Array diff --git a/meteor/client/ui/SegmentTimeline/SmallParts/SegmentTimelinePartHoverPreview.tsx b/meteor/client/ui/SegmentTimeline/SmallParts/SegmentTimelinePartHoverPreview.tsx index 8e4f2d996f..f1bea5594a 100644 --- a/meteor/client/ui/SegmentTimeline/SmallParts/SegmentTimelinePartHoverPreview.tsx +++ b/meteor/client/ui/SegmentTimeline/SmallParts/SegmentTimelinePartHoverPreview.tsx @@ -1,6 +1,6 @@ import React, { useState, useLayoutEffect } from 'react' import { useTranslation } from 'react-i18next' -import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { unprotectString } from '../../../../lib/lib' import { RundownUtils } from '../../../lib/rundown' import { PartUi, SegmentUi } from '../SegmentTimelineContainer' @@ -33,7 +33,7 @@ export const SegmentTimelinePartHoverPreview = ({ followingPart: PartUi | undefined segment: SegmentUi - playlist: RundownPlaylist + playlist: DBRundownPlaylist studio: UIStudio collapsedOutputs: { [key: string]: boolean diff --git a/meteor/client/ui/SegmentTimeline/SmallParts/SegmentTimelineSmallPartFlag.tsx b/meteor/client/ui/SegmentTimeline/SmallParts/SegmentTimelineSmallPartFlag.tsx index 9ab2f1e567..fcb1488ada 100644 --- a/meteor/client/ui/SegmentTimeline/SmallParts/SegmentTimelineSmallPartFlag.tsx +++ b/meteor/client/ui/SegmentTimeline/SmallParts/SegmentTimelineSmallPartFlag.tsx @@ -4,7 +4,7 @@ import { ISourceLayer } from '@sofie-automation/blueprints-integration' import { SegmentTimelineSmallPartFlagIcon } from './SegmentTimelineSmallPartFlagIcon' import { protectString, unprotectString } from '../../../../lib/lib' import { PartUi, SegmentUi } from '../SegmentTimelineContainer' -import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { SegmentTimelinePartHoverPreview } from './SegmentTimelinePartHoverPreview' import RundownViewEventBus, { RundownViewEvents } from '../../../../lib/api/triggers/RundownViewEventBus' import { UIStudio } from '../../../../lib/api/studios' @@ -25,7 +25,7 @@ export const SegmentTimelineSmallPartFlag = withTiming< timeToPixelRatio: number segment: SegmentUi - playlist: RundownPlaylist + playlist: DBRundownPlaylist studio: UIStudio collapsedOutputs: { [key: string]: boolean diff --git a/meteor/client/ui/SegmentTimeline/SourceLayerItemContainer.tsx b/meteor/client/ui/SegmentTimeline/SourceLayerItemContainer.tsx index 7e2497adae..4030b906bd 100644 --- a/meteor/client/ui/SegmentTimeline/SourceLayerItemContainer.tsx +++ b/meteor/client/ui/SegmentTimeline/SourceLayerItemContainer.tsx @@ -1,11 +1,11 @@ import * as React from 'react' import { ISourceLayerItemProps, SourceLayerItem } from './SourceLayerItem' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { withMediaObjectStatus } from './withMediaObjectStatus' import { UIStudio } from '../../../lib/api/studios' interface IPropsHeader extends ISourceLayerItemProps { - playlist: RundownPlaylist + playlist: DBRundownPlaylist studio: UIStudio } diff --git a/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx b/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx index ec1053ca23..5a95674937 100644 --- a/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx +++ b/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx @@ -8,7 +8,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { TriggeredActionEntry, TRIGGERED_ACTION_ENTRY_DRAG_TYPE } from './TriggeredActionEntry' import { literal, unprotectString } from '../../../../../lib/lib' import { TriggersHandler } from '../../../../lib/triggers/TriggersHandler' -import { RundownPlaylist } from '../../../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { Part } from '../../../../../lib/collections/Parts' import { MeteorCall } from '../../../../../lib/api/methods' @@ -33,7 +33,7 @@ import { SourceLayers, OutputLayers } from '@sofie-automation/corelib/dist/dataM import { RundownPlaylistCollectionUtil } from '../../../../../lib/collections/rundownPlaylistUtil' export interface PreviewContext { - rundownPlaylist: RundownPlaylist | null + rundownPlaylist: DBRundownPlaylist | null currentRundownId: RundownId | null currentSegmentPartIds: PartId[] nextSegmentPartIds: PartId[] diff --git a/meteor/client/ui/Shelf/AdLibListItem.tsx b/meteor/client/ui/Shelf/AdLibListItem.tsx index 5426cc6230..f530fdef49 100644 --- a/meteor/client/ui/Shelf/AdLibListItem.tsx +++ b/meteor/client/ui/Shelf/AdLibListItem.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import ClassNames from 'classnames' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { ISourceLayer, IOutputLayer, IBlueprintActionTriggerMode } from '@sofie-automation/blueprints-integration' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { unprotectString } from '../../../lib/lib' import renderItem from './Renderers/ItemRendererFactory' import { withMediaObjectStatus } from '../SegmentTimeline/withMediaObjectStatus' @@ -28,7 +28,7 @@ interface IListViewItemProps { disabled?: boolean onSelectAdLib?: (aSLine: IAdLibListItem) => void onToggleAdLib?: (aSLine: IAdLibListItem, queue: boolean, context: any, mode?: IBlueprintActionTriggerMode) => void - playlist: RundownPlaylist + playlist: DBRundownPlaylist } export const AdLibListItem = withMediaObjectStatus()( diff --git a/meteor/client/ui/Shelf/AdLibListView.tsx b/meteor/client/ui/Shelf/AdLibListView.tsx index ebda2fb2e8..a2bcf8313f 100644 --- a/meteor/client/ui/Shelf/AdLibListView.tsx +++ b/meteor/client/ui/Shelf/AdLibListView.tsx @@ -5,7 +5,7 @@ import { RundownUtils } from '../../lib/rundown' import { AdLibListItem, IAdLibListItem } from './AdLibListItem' import { AdLibPieceUi, AdlibSegmentUi } from '../../lib/shelf' import { RundownLayoutFilter, RundownLayoutFilterBase } from '../../../lib/collections/RundownLayouts' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { BucketAdLibActionUi, BucketAdLibUi } from './RundownViewBuckets' import { PieceUi } from '../SegmentContainer/withResolvedSegment' import { IBlueprintActionTriggerMode } from '@sofie-automation/blueprints-integration' @@ -24,7 +24,7 @@ interface IListViewPropsHeader { noSegments: boolean filter: RundownLayoutFilter | undefined rundownAdLibs?: Array - playlist: RundownPlaylist + playlist: DBRundownPlaylist studio: UIStudio } diff --git a/meteor/client/ui/Shelf/AdLibPanel.tsx b/meteor/client/ui/Shelf/AdLibPanel.tsx index 7dd10d4f37..1330c0f60f 100644 --- a/meteor/client/ui/Shelf/AdLibPanel.tsx +++ b/meteor/client/ui/Shelf/AdLibPanel.tsx @@ -4,7 +4,7 @@ import { Meteor } from 'meteor/meteor' import { useTracker } from '../../lib/ReactMeteorData/react-meteor-data' import { useTranslation } from 'react-i18next' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart } from '../../../lib/collections/Parts' import { IAdLibListItem } from './AdLibListItem' @@ -63,7 +63,7 @@ import { RundownPlaylistCollectionUtil } from '../../../lib/collections/rundownP export interface IAdLibPanelProps { // liveSegment: Segment | undefined visible: boolean - playlist: RundownPlaylist + playlist: DBRundownPlaylist studio: UIStudio showStyleBase: UIShowStyleBase studioMode: boolean @@ -128,7 +128,10 @@ function actionToAdLibPieceUi( } interface IFetchAndFilterProps { - playlist: Pick + playlist: Pick< + DBRundownPlaylist, + '_id' | 'currentPartInfo' | 'nextPartInfo' | 'previousPartInfo' | 'rundownIdsInOrder' + > showStyleBase: Pick filter?: RundownLayoutFilterBase includeGlobalAdLibs?: boolean @@ -541,7 +544,7 @@ export function AdLibPanel({ () => fetchAndFilter({ playlist: playlist as Pick< - RundownPlaylist, + DBRundownPlaylist, '_id' | 'studioId' | 'currentPartInfo' | 'nextPartInfo' | 'previousPartInfo' | 'rundownIdsInOrder' >, showStyleBase: showStyleBase as Pick, diff --git a/meteor/client/ui/Shelf/BucketPanel.tsx b/meteor/client/ui/Shelf/BucketPanel.tsx index ff0c34b0ff..6317d0ac2b 100644 --- a/meteor/client/ui/Shelf/BucketPanel.tsx +++ b/meteor/client/ui/Shelf/BucketPanel.tsx @@ -32,7 +32,7 @@ import { IDashboardPanelTrackedProps } from './DashboardPanel' import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' import { Bucket } from '../../../lib/collections/Buckets' import { Events as MOSEvents } from '../../lib/data/mos/plugin-support' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { MeteorCall } from '../../../lib/api/methods' import { DragDropItemTypes } from '../DragDropItemTypes' import { BucketPieceButton, IBucketPieceDropResult } from './BucketPieceButton' @@ -231,7 +231,7 @@ export function actionToAdLibPieceUi( export interface IBucketPanelProps { bucket: Bucket - playlist: RundownPlaylist + playlist: DBRundownPlaylist showStyleBase: UIShowStyleBase shouldQueue: boolean editableName?: boolean diff --git a/meteor/client/ui/Shelf/ColoredBoxPanel.tsx b/meteor/client/ui/Shelf/ColoredBoxPanel.tsx index d26dfa0523..784e66ef44 100644 --- a/meteor/client/ui/Shelf/ColoredBoxPanel.tsx +++ b/meteor/client/ui/Shelf/ColoredBoxPanel.tsx @@ -5,7 +5,7 @@ import { RundownLayoutColoredBox, } from '../../../lib/collections/RundownLayouts' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { dashboardElementStyle } from './DashboardPanel' import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts' import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData' @@ -15,7 +15,7 @@ interface IColoredBoxPanelProps { visible?: boolean layout: RundownLayoutBase panel: RundownLayoutColoredBox - playlist: RundownPlaylist + playlist: DBRundownPlaylist } interface IState {} diff --git a/meteor/client/ui/Shelf/DashboardActionButton.tsx b/meteor/client/ui/Shelf/DashboardActionButton.tsx index 17ccfc1c13..643ac41ce8 100644 --- a/meteor/client/ui/Shelf/DashboardActionButton.tsx +++ b/meteor/client/ui/Shelf/DashboardActionButton.tsx @@ -2,11 +2,11 @@ import * as React from 'react' import ClassNames from 'classnames' import { ActionButtonType, DashboardLayoutActionButton } from '../../../lib/collections/RundownLayouts' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' export interface IDashboardButtonProps { button: DashboardLayoutActionButton - playlist: RundownPlaylist + playlist: DBRundownPlaylist studioMode: boolean onButtonDown: (button: DashboardLayoutActionButton, e: React.SyntheticEvent) => void diff --git a/meteor/client/ui/Shelf/DashboardActionButtonGroup.tsx b/meteor/client/ui/Shelf/DashboardActionButtonGroup.tsx index f22749fefc..a7ccc25511 100644 --- a/meteor/client/ui/Shelf/DashboardActionButtonGroup.tsx +++ b/meteor/client/ui/Shelf/DashboardActionButtonGroup.tsx @@ -5,7 +5,7 @@ import { DashboardActionButton } from './DashboardActionButton' import { doUserAction, UserAction } from '../../../lib/clientUserAction' import { withTranslation } from 'react-i18next' import { Translated } from '../../lib/ReactMeteorData/react-meteor-data' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { MeteorCall } from '../../../lib/api/methods' import { doModalDialog } from '../../lib/ModalDialog' import { NoticeLevel, Notification, NotificationCenter } from '../../../lib/notifications/notifications' @@ -17,7 +17,7 @@ import { hashSingleUseToken } from '../../../lib/api/userActions' export interface IDashboardButtonGroupProps { buttons: DashboardLayoutActionButton[] studioMode: boolean - playlist: RundownPlaylist + playlist: DBRundownPlaylist onChangeQueueAdLib?: (isQueue: boolean, e: any) => void } diff --git a/meteor/client/ui/Shelf/DashboardPieceButton.tsx b/meteor/client/ui/Shelf/DashboardPieceButton.tsx index 9fac4071df..b7d13c16f1 100644 --- a/meteor/client/ui/Shelf/DashboardPieceButton.tsx +++ b/meteor/client/ui/Shelf/DashboardPieceButton.tsx @@ -11,7 +11,7 @@ import { NoraContent, IBlueprintPieceType, } from '@sofie-automation/blueprints-integration' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { IAdLibListItem } from './AdLibListItem' import SplitInputIcon from '../PieceIcons/Renderers/SplitInputIcon' import { PieceDisplayStyle } from '../../../lib/collections/RundownLayouts' @@ -35,7 +35,7 @@ export interface IDashboardButtonProps { outputLayer?: IOutputLayer onToggleAdLib: (aSLine: IAdLibListItem, queue: boolean, context: React.SyntheticEvent) => void onSelectAdLib: (aSLine: IAdLibListItem, context: React.SyntheticEvent) => void - playlist: RundownPlaylist + playlist: DBRundownPlaylist isOnAir?: boolean isNext?: boolean widthScale?: number diff --git a/meteor/client/ui/Shelf/EndWordsPanel.tsx b/meteor/client/ui/Shelf/EndWordsPanel.tsx index bbefc6d163..9284eed13e 100644 --- a/meteor/client/ui/Shelf/EndWordsPanel.tsx +++ b/meteor/client/ui/Shelf/EndWordsPanel.tsx @@ -10,7 +10,7 @@ import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts' import { dashboardElementStyle } from './DashboardPanel' import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PieceInstance } from '../../../lib/collections/PieceInstances' import { ScriptContent } from '@sofie-automation/blueprints-integration' import { getUnfinishedPieceInstancesReactive } from '../../lib/rundownLayouts' @@ -22,7 +22,7 @@ interface IEndsWordsPanelProps { visible?: boolean layout: RundownLayoutBase panel: RundownLayoutEndWords - playlist: RundownPlaylist + playlist: DBRundownPlaylist showStyleBase: UIShowStyleBase } diff --git a/meteor/client/ui/Shelf/ExternalFramePanel.tsx b/meteor/client/ui/Shelf/ExternalFramePanel.tsx index 08cf2f4400..057fb9dfe7 100644 --- a/meteor/client/ui/Shelf/ExternalFramePanel.tsx +++ b/meteor/client/ui/Shelf/ExternalFramePanel.tsx @@ -10,7 +10,7 @@ import { import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts' import { dashboardElementStyle } from './DashboardPanel' import { assertNever, getRandomString, literal, protectString } from '../../../lib/lib' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PartInstance } from '../../../lib/collections/PartInstances' import { parseMosPluginMessageXml, MosPluginMessage } from '../../lib/parsers/mos/mosXml2Js' import { @@ -39,7 +39,7 @@ interface IProps { layout: RundownLayoutBase panel: RundownLayoutExternalFrame visible: boolean - playlist: RundownPlaylist + playlist: DBRundownPlaylist } enum SofieExternalMessageType { diff --git a/meteor/client/ui/Shelf/GlobalAdLibPanel.tsx b/meteor/client/ui/Shelf/GlobalAdLibPanel.tsx index 4241ea8785..1f7be24c55 100644 --- a/meteor/client/ui/Shelf/GlobalAdLibPanel.tsx +++ b/meteor/client/ui/Shelf/GlobalAdLibPanel.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { IAdLibListItem } from './AdLibListItem' import { AdLibPanel } from './AdLibPanel' import { PieceUi } from '../SegmentTimeline/SegmentTimelineContainer' @@ -17,7 +17,7 @@ import { UIShowStyleBase } from '../../../lib/api/showStyles' import { UIStudio } from '../../../lib/api/studios' interface IProps { - playlist: RundownPlaylist + playlist: DBRundownPlaylist showStyleBase: UIShowStyleBase studio: UIStudio visible: boolean diff --git a/meteor/client/ui/Shelf/Inspector/ItemRenderers/ActionItemRenderer.tsx b/meteor/client/ui/Shelf/Inspector/ItemRenderers/ActionItemRenderer.tsx index f3fc6f88d1..beb3bc7636 100644 --- a/meteor/client/ui/Shelf/Inspector/ItemRenderers/ActionItemRenderer.tsx +++ b/meteor/client/ui/Shelf/Inspector/ItemRenderers/ActionItemRenderer.tsx @@ -18,7 +18,7 @@ import { ProtectedString } from '../../../../../lib/lib' import { doUserAction, UserAction } from '../../../../../lib/clientUserAction' import { MeteorCall } from '../../../../../lib/api/methods' import { BucketAdLibItem, BucketAdLibActionUi } from '../../RundownViewBuckets' -import { RundownPlaylist } from '../../../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { actionToAdLibPieceUi } from '../../BucketPanel' import RundownViewEventBus, { RundownViewEvents } from '../../../../../lib/api/triggers/RundownViewEventBus' import { IAdLibListItem } from '../../AdLibListItem' @@ -36,7 +36,7 @@ export interface IProps { piece: PieceUi | IAdLibListItem | BucketAdLibActionUi showStyleBase: UIShowStyleBase studio: UIStudio - rundownPlaylist: RundownPlaylist + rundownPlaylist: DBRundownPlaylist onSelectPiece: (piece: BucketAdLibItem | AdLibPieceUi | PieceUi | undefined) => void } diff --git a/meteor/client/ui/Shelf/Inspector/ItemRenderers/ItemRendererFactory.ts b/meteor/client/ui/Shelf/Inspector/ItemRenderers/ItemRendererFactory.ts index dd00a8420a..806ad5e367 100644 --- a/meteor/client/ui/Shelf/Inspector/ItemRenderers/ItemRendererFactory.ts +++ b/meteor/client/ui/Shelf/Inspector/ItemRenderers/ItemRendererFactory.ts @@ -5,7 +5,7 @@ import ActionItemRenderer, { isActionItem } from './ActionItemRenderer' import { PieceUi } from '../../../SegmentTimeline/SegmentTimelineContainer' import { BucketAdLibItem } from '../../RundownViewBuckets' -import { RundownPlaylist } from '../../../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { IAdLibListItem } from '../../AdLibListItem' import { AdLibPieceUi } from '../../../../lib/shelf' import { UIShowStyleBase } from '../../../../../lib/api/showStyles' @@ -15,7 +15,7 @@ export default function renderItem( piece: BucketAdLibItem | IAdLibListItem | PieceUi, showStyleBase: UIShowStyleBase, studio: UIStudio, - rundownPlaylist: RundownPlaylist, + rundownPlaylist: DBRundownPlaylist, onSelectPiece: (piece: BucketAdLibItem | IAdLibListItem | PieceUi | undefined) => void ): JSX.Element { if (!piece['isAction'] && isNoraItem(piece as AdLibPieceUi | PieceUi)) { diff --git a/meteor/client/ui/Shelf/Inspector/ShelfInspector.tsx b/meteor/client/ui/Shelf/Inspector/ShelfInspector.tsx index 7979451118..69ca8b58ec 100644 --- a/meteor/client/ui/Shelf/Inspector/ShelfInspector.tsx +++ b/meteor/client/ui/Shelf/Inspector/ShelfInspector.tsx @@ -5,7 +5,7 @@ import { PieceUi } from '../../SegmentTimeline/SegmentTimelineContainer' import { ContextMenuTrigger } from '@jstarpl/react-contextmenu' import { contextMenuHoldToDisplayTime } from '../../../lib/lib' import { BucketAdLibItem } from '../RundownViewBuckets' -import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { IAdLibListItem } from '../AdLibListItem' import { UIShowStyleBase } from '../../../../lib/api/showStyles' import { UIStudio } from '../../../../lib/api/studios' @@ -16,7 +16,7 @@ interface IShelfInspectorProps { selected: BucketAdLibItem | IAdLibListItem | PieceUi | undefined showStyleBase: UIShowStyleBase studio: UIStudio - rundownPlaylist: RundownPlaylist + rundownPlaylist: DBRundownPlaylist onSelectPiece: (piece: BucketAdLibItem | IAdLibListItem | PieceUi | undefined) => void } diff --git a/meteor/client/ui/Shelf/MiniRundownPanel.tsx b/meteor/client/ui/Shelf/MiniRundownPanel.tsx index f19d1abacd..146b93dd55 100644 --- a/meteor/client/ui/Shelf/MiniRundownPanel.tsx +++ b/meteor/client/ui/Shelf/MiniRundownPanel.tsx @@ -10,7 +10,7 @@ import { import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts' import { withTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PartInstance } from '../../../lib/collections/PartInstances' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { dashboardElementStyle } from './DashboardPanel' @@ -24,7 +24,7 @@ interface IMiniRundownPanelProps { visible?: boolean layout: RundownLayoutBase panel: RundownLayoutMiniRundown - playlist: RundownPlaylist + playlist: DBRundownPlaylist } interface IMiniRundownPanelTrackedProps { diff --git a/meteor/client/ui/Shelf/NextBreakTimingPanel.tsx b/meteor/client/ui/Shelf/NextBreakTimingPanel.tsx index 982dd89f55..1cd37770dc 100644 --- a/meteor/client/ui/Shelf/NextBreakTimingPanel.tsx +++ b/meteor/client/ui/Shelf/NextBreakTimingPanel.tsx @@ -9,7 +9,7 @@ import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts' import { dashboardElementStyle } from './DashboardPanel' import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { withTranslation } from 'react-i18next' import { NextBreakTiming } from '../RundownView/RundownTiming/NextBreakTiming' @@ -17,7 +17,7 @@ interface INextBreakTimingPanelProps { visible?: boolean layout: RundownLayoutBase panel: RundownLayoutNextBreakTiming - playlist: RundownPlaylist + playlist: DBRundownPlaylist } export class NextBreakTimingPanelInner extends MeteorReactComponent> { diff --git a/meteor/client/ui/Shelf/NextInfoPanel.tsx b/meteor/client/ui/Shelf/NextInfoPanel.tsx index f738673782..7cbc4b3fbe 100644 --- a/meteor/client/ui/Shelf/NextInfoPanel.tsx +++ b/meteor/client/ui/Shelf/NextInfoPanel.tsx @@ -9,7 +9,7 @@ import { import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts' import { withTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PartInstance } from '../../../lib/collections/PartInstances' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { dashboardElementStyle } from './DashboardPanel' @@ -19,7 +19,7 @@ interface INextInfoPanelProps { visible?: boolean layout: RundownLayoutBase panel: RundownLayoutNextInfo - playlist: RundownPlaylist + playlist: DBRundownPlaylist } interface INextInfoPanelTrackedProps { diff --git a/meteor/client/ui/Shelf/PartNamePanel.tsx b/meteor/client/ui/Shelf/PartNamePanel.tsx index eccd3087c3..b2dcbc4932 100644 --- a/meteor/client/ui/Shelf/PartNamePanel.tsx +++ b/meteor/client/ui/Shelf/PartNamePanel.tsx @@ -7,7 +7,7 @@ import { RundownLayoutPartName, } from '../../../lib/collections/RundownLayouts' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { dashboardElementStyle } from './DashboardPanel' import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts' import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData' @@ -22,7 +22,7 @@ interface IPartNamePanelProps { visible?: boolean layout: RundownLayoutBase panel: RundownLayoutPartName - playlist: RundownPlaylist + playlist: DBRundownPlaylist showStyleBase: UIShowStyleBase } diff --git a/meteor/client/ui/Shelf/PartTimingPanel.tsx b/meteor/client/ui/Shelf/PartTimingPanel.tsx index 8020e995d7..4b08a4b973 100644 --- a/meteor/client/ui/Shelf/PartTimingPanel.tsx +++ b/meteor/client/ui/Shelf/PartTimingPanel.tsx @@ -7,7 +7,7 @@ import { } from '../../../lib/collections/RundownLayouts' import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PartInstance } from '../../../lib/collections/PartInstances' import { dashboardElementStyle } from './DashboardPanel' import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts' @@ -22,7 +22,7 @@ interface IPartTimingPanelProps { visible?: boolean layout: RundownLayoutBase panel: RundownLayoutPartTiming - playlist: RundownPlaylist + playlist: DBRundownPlaylist showStyleBase: UIShowStyleBase } diff --git a/meteor/client/ui/Shelf/PieceCountdownPanel.tsx b/meteor/client/ui/Shelf/PieceCountdownPanel.tsx index e9b6813b44..70af881ca3 100644 --- a/meteor/client/ui/Shelf/PieceCountdownPanel.tsx +++ b/meteor/client/ui/Shelf/PieceCountdownPanel.tsx @@ -12,7 +12,7 @@ import { withTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { RundownUtils } from '../../lib/rundown' import { RundownTiming, TimingEvent } from '../RundownView/RundownTiming/RundownTiming' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PieceInstance } from '../../../lib/collections/PieceInstances' import { VTContent } from '@sofie-automation/blueprints-integration' import { getUnfinishedPieceInstancesReactive } from '../../lib/rundownLayouts' @@ -21,7 +21,7 @@ interface IPieceCountdownPanelProps { visible?: boolean layout: RundownLayoutBase panel: RundownLayoutPieceCountdown - playlist: RundownPlaylist + playlist: DBRundownPlaylist showStyleBase: UIShowStyleBase } diff --git a/meteor/client/ui/Shelf/PlaylistEndTimerPanel.tsx b/meteor/client/ui/Shelf/PlaylistEndTimerPanel.tsx index f40746cde3..05c48ce586 100644 --- a/meteor/client/ui/Shelf/PlaylistEndTimerPanel.tsx +++ b/meteor/client/ui/Shelf/PlaylistEndTimerPanel.tsx @@ -9,7 +9,7 @@ import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts' import { dashboardElementStyle } from './DashboardPanel' import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { withTranslation } from 'react-i18next' import { PlaylistEndTiming } from '../RundownView/RundownTiming/PlaylistEndTiming' import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' @@ -18,7 +18,7 @@ interface IPlaylistEndTimerPanelProps { visible?: boolean layout: RundownLayoutBase panel: RundownLayoutPlaylistEndTimer - playlist: RundownPlaylist + playlist: DBRundownPlaylist } interface IState {} diff --git a/meteor/client/ui/Shelf/PlaylistNamePanel.tsx b/meteor/client/ui/Shelf/PlaylistNamePanel.tsx index 28b168b3bb..8547ac1bee 100644 --- a/meteor/client/ui/Shelf/PlaylistNamePanel.tsx +++ b/meteor/client/ui/Shelf/PlaylistNamePanel.tsx @@ -6,7 +6,7 @@ import { RundownLayoutPlaylistName, } from '../../../lib/collections/RundownLayouts' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { dashboardElementStyle } from './DashboardPanel' import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts' import { withTracker } from '../../lib/ReactMeteorData/ReactMeteorData' @@ -20,7 +20,7 @@ interface IPlaylistNamePanelProps { visible?: boolean layout: RundownLayoutBase panel: RundownLayoutPlaylistName - playlist: RundownPlaylist + playlist: DBRundownPlaylist } interface IState {} diff --git a/meteor/client/ui/Shelf/PlaylistStartTimerPanel.tsx b/meteor/client/ui/Shelf/PlaylistStartTimerPanel.tsx index ff774ddbd4..30670cb2a1 100644 --- a/meteor/client/ui/Shelf/PlaylistStartTimerPanel.tsx +++ b/meteor/client/ui/Shelf/PlaylistStartTimerPanel.tsx @@ -9,14 +9,14 @@ import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts' import { dashboardElementStyle } from './DashboardPanel' import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { withTranslation } from 'react-i18next' import { PlaylistStartTiming } from '../RundownView/RundownTiming/PlaylistStartTiming' interface IPlaylistStartTimerPanelProps { layout: RundownLayoutBase panel: RundownLayoutPlaylistStartTimer - playlist: RundownPlaylist + playlist: DBRundownPlaylist } interface IState {} diff --git a/meteor/client/ui/Shelf/RundownViewBuckets.tsx b/meteor/client/ui/Shelf/RundownViewBuckets.tsx index 1e399506a7..e8e42536d8 100644 --- a/meteor/client/ui/Shelf/RundownViewBuckets.tsx +++ b/meteor/client/ui/Shelf/RundownViewBuckets.tsx @@ -12,7 +12,7 @@ import { withTranslation } from 'react-i18next' import { faBars } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { unprotectString, partial, literal, ProtectedString } from '../../../lib/lib' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { getElementDocumentOffset } from '../../utils/positions' import { UIStateStorage } from '../../lib/UIStateStorage' import { doModalDialog, ModalDialogQueueItem } from '../../lib/ModalDialog' @@ -77,7 +77,7 @@ export function isAdLib(item: BucketAdLibItem): item is BucketAdLibUi { interface IBucketsProps { buckets: Bucket[] | undefined - playlist: RundownPlaylist + playlist: DBRundownPlaylist showStyleBase: UIShowStyleBase shouldQueue: boolean fullViewport: boolean diff --git a/meteor/client/ui/Shelf/SegmentNamePanel.tsx b/meteor/client/ui/Shelf/SegmentNamePanel.tsx index 4b36e59c72..d592c4e2af 100644 --- a/meteor/client/ui/Shelf/SegmentNamePanel.tsx +++ b/meteor/client/ui/Shelf/SegmentNamePanel.tsx @@ -6,7 +6,7 @@ import { RundownLayoutSegmentName, } from '../../../lib/collections/RundownLayouts' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { dashboardElementStyle } from './DashboardPanel' import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts' import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData' @@ -18,7 +18,7 @@ interface ISegmentNamePanelProps { visible?: boolean layout: RundownLayoutBase panel: RundownLayoutSegmentName - playlist: RundownPlaylist + playlist: DBRundownPlaylist } interface IState {} @@ -58,7 +58,7 @@ class SegmentNamePanelInner extends MeteorReactComponent< } } -function getSegmentName(selectedSegment: 'current' | 'next', playlist: RundownPlaylist): string | undefined { +function getSegmentName(selectedSegment: 'current' | 'next', playlist: DBRundownPlaylist): string | undefined { const currentPartInstance = playlist.currentPartInfo ? (RundownPlaylistCollectionUtil.getActivePartInstances(playlist, { _id: playlist.currentPartInfo.partInstanceId, diff --git a/meteor/client/ui/Shelf/SegmentTimingPanel.tsx b/meteor/client/ui/Shelf/SegmentTimingPanel.tsx index 39d2f4005c..6e9b6c2575 100644 --- a/meteor/client/ui/Shelf/SegmentTimingPanel.tsx +++ b/meteor/client/ui/Shelf/SegmentTimingPanel.tsx @@ -9,7 +9,7 @@ import { import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { RundownUtils } from '../../lib/rundown' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { SegmentDuration } from '../RundownView/RundownTiming/SegmentDuration' import { PartExtended } from '../../../lib/Rundown' @@ -29,7 +29,7 @@ interface ISegmentTimingPanelProps { visible?: boolean layout: RundownLayoutBase panel: RundownLayoutSegmentTiming - playlist: RundownPlaylist + playlist: DBRundownPlaylist showStyleBase: UIShowStyleBase } diff --git a/meteor/client/ui/Shelf/Shelf.tsx b/meteor/client/ui/Shelf/Shelf.tsx index 358f67106e..a5d8a3c859 100644 --- a/meteor/client/ui/Shelf/Shelf.tsx +++ b/meteor/client/ui/Shelf/Shelf.tsx @@ -8,7 +8,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData' import { PieceUi } from '../SegmentTimeline/SegmentTimelineContainer' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { getElementDocumentOffset } from '../../utils/positions' import { RundownLayoutFilter, RundownLayoutShelfBase } from '../../../lib/collections/RundownLayouts' import { UIStateStorage } from '../../lib/UIStateStorage' @@ -47,7 +47,7 @@ export enum ShelfTabs { export interface IShelfProps extends React.ComponentPropsWithRef { isExpanded: boolean buckets: Array - playlist: RundownPlaylist + playlist: DBRundownPlaylist currentRundown: Rundown studio: UIStudio showStyleBase: UIShowStyleBase diff --git a/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx b/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx index 76bf23c265..30e1b2fa55 100644 --- a/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx +++ b/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx @@ -5,7 +5,7 @@ import { TimelineDashboardPanel } from './TimelineDashboardPanel' import { DashboardPanel } from './DashboardPanel' import { ExternalFramePanel } from './ExternalFramePanel' import { DashboardActionButtonGroup } from './DashboardActionButtonGroup' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { AdLibRegionPanel } from './AdLibRegionPanel' import { PieceCountdownPanel } from './PieceCountdownPanel' import { BucketAdLibItem } from './RundownViewBuckets' @@ -34,7 +34,7 @@ import { UIStudio } from '../../../lib/api/studios' export interface IShelfDashboardLayoutProps { rundownLayout: DashboardLayout - playlist: RundownPlaylist + playlist: DBRundownPlaylist // buckets: Bucket[] | undefined showStyleBase: UIShowStyleBase showStyleVariant: DBShowStyleVariant diff --git a/meteor/client/ui/Shelf/ShelfRundownLayout.tsx b/meteor/client/ui/Shelf/ShelfRundownLayout.tsx index 4148b8051d..2570f6193b 100644 --- a/meteor/client/ui/Shelf/ShelfRundownLayout.tsx +++ b/meteor/client/ui/Shelf/ShelfRundownLayout.tsx @@ -9,7 +9,7 @@ import { AdLibPanel } from './AdLibPanel' import { AdLibPieceUi } from '../../lib/shelf' import { GlobalAdLibPanel } from './GlobalAdLibPanel' import { HotkeyHelpPanel } from './HotkeyHelpPanel' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PieceUi } from '../SegmentTimeline/SegmentTimelineContainer' import { useTranslation } from 'react-i18next' import { BucketAdLibItem } from './RundownViewBuckets' @@ -19,7 +19,7 @@ import { UIStudio } from '../../../lib/api/studios' export interface IShelfRundownLayoutProps { rundownLayout: RundownLayout | undefined - playlist: RundownPlaylist + playlist: DBRundownPlaylist showStyleBase: UIShowStyleBase studioMode: boolean studio: UIStudio diff --git a/meteor/client/ui/Shelf/ShowStylePanel.tsx b/meteor/client/ui/Shelf/ShowStylePanel.tsx index c7570245e4..973a1b1ec6 100644 --- a/meteor/client/ui/Shelf/ShowStylePanel.tsx +++ b/meteor/client/ui/Shelf/ShowStylePanel.tsx @@ -6,7 +6,7 @@ import { } from '../../../lib/collections/RundownLayouts' import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { dashboardElementStyle } from './DashboardPanel' import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' @@ -17,7 +17,7 @@ interface IShowStylePanelProps { visible?: boolean layout: RundownLayoutBase panel: RundownLayoutShowStyleDisplay - playlist: RundownPlaylist + playlist: DBRundownPlaylist showStyleBase: DBShowStyleBase showStyleVariant: DBShowStyleVariant } diff --git a/meteor/client/ui/Shelf/StudioNamePanel.tsx b/meteor/client/ui/Shelf/StudioNamePanel.tsx index e7cd956c97..90f9f252b6 100644 --- a/meteor/client/ui/Shelf/StudioNamePanel.tsx +++ b/meteor/client/ui/Shelf/StudioNamePanel.tsx @@ -5,7 +5,7 @@ import { RundownLayoutBase, RundownLayoutStudioName, } from '../../../lib/collections/RundownLayouts' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { dashboardElementStyle } from './DashboardPanel' import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts' import { useTranslation } from 'react-i18next' @@ -15,7 +15,7 @@ interface IStudioNamePanelProps { visible?: boolean layout: RundownLayoutBase panel: RundownLayoutStudioName - playlist: RundownPlaylist + playlist: DBRundownPlaylist studio: UIStudio } diff --git a/meteor/client/ui/Shelf/SystemStatusPanel.tsx b/meteor/client/ui/Shelf/SystemStatusPanel.tsx index 1467b0eff5..6f15113500 100644 --- a/meteor/client/ui/Shelf/SystemStatusPanel.tsx +++ b/meteor/client/ui/Shelf/SystemStatusPanel.tsx @@ -7,7 +7,7 @@ import { } from '../../../lib/collections/RundownLayouts' import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { dashboardElementStyle } from './DashboardPanel' import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts' import { RundownSystemStatus } from '../RundownView/RundownSystemStatus' @@ -22,7 +22,7 @@ interface ISystemStatusPanelProps { visible?: boolean layout: RundownLayoutBase panel: RundownLayoutSytemStatus - playlist: RundownPlaylist + playlist: DBRundownPlaylist } interface IState {} diff --git a/meteor/client/ui/Shelf/TextLabelPanel.tsx b/meteor/client/ui/Shelf/TextLabelPanel.tsx index e98fed2643..fcd3ffa34f 100644 --- a/meteor/client/ui/Shelf/TextLabelPanel.tsx +++ b/meteor/client/ui/Shelf/TextLabelPanel.tsx @@ -6,7 +6,7 @@ import { RundownLayoutTextLabel, } from '../../../lib/collections/RundownLayouts' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { dashboardElementStyle } from './DashboardPanel' import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts' @@ -14,7 +14,7 @@ interface ITextLabelPanelProps { visible?: boolean layout: RundownLayoutBase panel: RundownLayoutTextLabel - playlist: RundownPlaylist + playlist: DBRundownPlaylist } interface IState {} diff --git a/meteor/client/ui/Shelf/TimeOfDayPanel.tsx b/meteor/client/ui/Shelf/TimeOfDayPanel.tsx index 668ed8189c..d96dfbe9df 100644 --- a/meteor/client/ui/Shelf/TimeOfDayPanel.tsx +++ b/meteor/client/ui/Shelf/TimeOfDayPanel.tsx @@ -6,7 +6,7 @@ import { } from '../../../lib/collections/RundownLayouts' import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { dashboardElementStyle } from './DashboardPanel' import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts' import { withTranslation } from 'react-i18next' @@ -16,7 +16,7 @@ interface ITimeOfDayPanelProps { visible?: boolean layout: RundownLayoutBase panel: RundownLayoutTimeOfDay - playlist: RundownPlaylist + playlist: DBRundownPlaylist } interface IState {} diff --git a/meteor/client/ui/StudioScreenSaver/StudioScreenSaver.tsx b/meteor/client/ui/StudioScreenSaver/StudioScreenSaver.tsx index 26cedbc36d..7c062c7ebc 100644 --- a/meteor/client/ui/StudioScreenSaver/StudioScreenSaver.tsx +++ b/meteor/client/ui/StudioScreenSaver/StudioScreenSaver.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { translateWithTracker, Translated } from '../../lib/ReactMeteorData/ReactMeteorData' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { getCurrentTime } from '../../../lib/lib' import { invalidateAfter } from '../../../lib/invalidatingTime' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' @@ -23,7 +23,7 @@ interface IProps { interface ITrackedProps { studio: Pick | undefined - rundownPlaylist: Pick | undefined + rundownPlaylist: Pick | undefined } interface IState { @@ -42,7 +42,7 @@ interface IState { interface FindNextPlaylistResult { studio: Pick | undefined - rundownPlaylist: Pick | undefined + rundownPlaylist: Pick | undefined } export const findNextPlaylist = (props: IProps): FindNextPlaylistResult => { invalidateAfter(5000) @@ -66,7 +66,7 @@ export const findNextPlaylist = (props: IProps): FindNextPlaylistResult => { studioId: 1, }, } - ).fetch() as Pick[] + ).fetch() as Pick[] ) .sort(PlaylistTiming.sortTimings) .find((rundownPlaylist) => { diff --git a/meteor/lib/Rundown.ts b/meteor/lib/Rundown.ts index 71561319b7..8adef77178 100644 --- a/meteor/lib/Rundown.ts +++ b/meteor/lib/Rundown.ts @@ -13,7 +13,7 @@ import { import { PieceInstanceWithTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' import { invalidateAfter } from '../lib/invalidatingTime' import { getCurrentTime, groupByToMap, ProtectedString, protectString } from './lib' -import { RundownPlaylist } from './collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { isTranslatableMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import { mongoWhereFilter, MongoQuery } from '@sofie-automation/corelib/dist/mongo' @@ -243,7 +243,7 @@ export function getPieceInstancesForPartInstance( * to limit the data, in correct order. * * @export - * @param {RundownPlaylist} playlist + * @param {DBRundownPlaylist} playlist * @param {(MongoQuery)} [segmentsQuery] * @param {(MongoQuery)} [partsQuery] * @param {MongoQuery} [partInstancesQuery] @@ -253,7 +253,7 @@ export function getPieceInstancesForPartInstance( * @return {*} {Array<{ segment: Segment; partInstances: PartInstance[] }>} */ export function getSegmentsWithPartInstances( - playlist: RundownPlaylist, + playlist: DBRundownPlaylist, segmentsQuery?: MongoQuery, partsQuery?: MongoQuery, partInstancesQuery?: MongoQuery, diff --git a/meteor/lib/api/pubsub.ts b/meteor/lib/api/pubsub.ts index b3400f42d5..167a969d69 100644 --- a/meteor/lib/api/pubsub.ts +++ b/meteor/lib/api/pubsub.ts @@ -37,7 +37,7 @@ import { Piece } from '../collections/Pieces' import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' import { RundownLayoutBase } from '../collections/RundownLayouts' -import { DBRundownPlaylist } from '../collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' diff --git a/meteor/lib/api/triggers/actionFactory.ts b/meteor/lib/api/triggers/actionFactory.ts index e91e3bbeee..7a40a1a7d7 100644 --- a/meteor/lib/api/triggers/actionFactory.ts +++ b/meteor/lib/api/triggers/actionFactory.ts @@ -14,7 +14,7 @@ import { Meteor } from 'meteor/meteor' import { Tracker } from 'meteor/tracker' import { MeteorCall } from '../methods' import { PartInstance } from '../../collections/PartInstances' -import { RundownPlaylist } from '../../collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { DBShowStyleBase, SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { assertNever, DummyReactiveVar } from '../../lib' @@ -43,7 +43,7 @@ type XOR = T | U extends object ? (Without & U) | (Without & T export interface ReactivePlaylistActionContext { rundownPlaylistId: ReactiveVar rundownPlaylist: ReactiveVar< - Pick + Pick > currentRundownId: ReactiveVar @@ -55,7 +55,7 @@ export interface ReactivePlaylistActionContext { } interface PlainPlaylistContext { - rundownPlaylist: RundownPlaylist + rundownPlaylist: DBRundownPlaylist currentRundownId: RundownId | null currentSegmentPartIds: PartId[] nextSegmentPartIds: PartId[] diff --git a/meteor/lib/api/triggers/actionFilterChainCompilers.ts b/meteor/lib/api/triggers/actionFilterChainCompilers.ts index 2ca82420ef..4ed45d9f97 100644 --- a/meteor/lib/api/triggers/actionFilterChainCompilers.ts +++ b/meteor/lib/api/triggers/actionFilterChainCompilers.ts @@ -13,7 +13,7 @@ import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { DBPart } from '../../collections/Parts' import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' -import { DBRundownPlaylist, RundownPlaylist } from '../../collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { assertNever, generateTranslation } from '../../lib' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' @@ -764,7 +764,7 @@ export function compileAdLibFilter( export function rundownPlaylistFilter( studioId: StudioId, filterChain: IRundownPlaylistFilterLink[] -): RundownPlaylist | undefined { +): DBRundownPlaylist | undefined { const selector: MongoQuery = { $and: [ { diff --git a/meteor/lib/collections/RundownPlaylists.ts b/meteor/lib/collections/RundownPlaylists.ts deleted file mode 100644 index d87b6c305e..0000000000 --- a/meteor/lib/collections/RundownPlaylists.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -export { DBRundownPlaylist } - -/** Note: Use RundownPlaylist instead */ -export type RundownPlaylist = DBRundownPlaylist diff --git a/meteor/lib/collections/libCollections.ts b/meteor/lib/collections/libCollections.ts index 2c896e0db9..5b075537c1 100644 --- a/meteor/lib/collections/libCollections.ts +++ b/meteor/lib/collections/libCollections.ts @@ -14,7 +14,7 @@ import { PieceInstance } from './PieceInstances' import { Piece } from './Pieces' import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' -import { DBRundownPlaylist } from './RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' diff --git a/meteor/lib/collections/rundownPlaylistUtil.ts b/meteor/lib/collections/rundownPlaylistUtil.ts index e8b4866aec..6be86a405b 100644 --- a/meteor/lib/collections/rundownPlaylistUtil.ts +++ b/meteor/lib/collections/rundownPlaylistUtil.ts @@ -16,7 +16,6 @@ import { Rundowns, Segments, Parts, PartInstances, Pieces } from './libCollectio import { FindOptions } from './lib' import { PartInstance } from './PartInstances' import { Part } from './Parts' -import { RundownPlaylist } from './RundownPlaylists' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { Piece } from './Pieces' @@ -27,7 +26,7 @@ import { Piece } from './Pieces' export class RundownPlaylistCollectionUtil { /** Returns an array of all Rundowns in the RundownPlaylist, sorted in playout order */ static getRundownsOrdered( - playlist: Pick, + playlist: Pick, selector?: MongoQuery, options?: FindOptions ): Rundown[] { @@ -41,7 +40,7 @@ export class RundownPlaylistCollectionUtil { } /** Returns an array of all Rundowns in the RundownPlaylist, in no predictable order */ static getRundownsUnordered( - playlist: Pick, + playlist: Pick, selector?: MongoQuery, options?: FindOptions ): Rundown[] { @@ -59,13 +58,13 @@ export class RundownPlaylistCollectionUtil { ).fetch() } /** Returns an array with the id:s of all Rundowns in the RundownPlaylist, sorted in playout order */ - static getRundownOrderedIDs(playlist: Pick): RundownId[] { + static getRundownOrderedIDs(playlist: Pick): RundownId[] { const allIds = RundownPlaylistCollectionUtil.getRundownUnorderedIDs(playlist) return sortRundownIDsInPlaylist(playlist.rundownIdsInOrder, allIds) } /** Returns an array with the id:s of all Rundowns in the RundownPlaylist, in no predictable order */ - static getRundownUnorderedIDs(playlist: Pick): RundownId[] { + static getRundownUnorderedIDs(playlist: Pick): RundownId[] { const rundowns = Rundowns.find( { playlistId: playlist._id, @@ -82,7 +81,7 @@ export class RundownPlaylistCollectionUtil { /** Returns all segments joined with their rundowns in their correct oreder for this RundownPlaylist */ static getRundownsAndSegments( - playlist: Pick, + playlist: Pick, selector?: MongoQuery, options?: FindOptions ): Array<{ @@ -127,7 +126,7 @@ export class RundownPlaylistCollectionUtil { } /** Returns all segments in their correct order for this RundownPlaylist */ static getSegments( - playlist: Pick, + playlist: Pick, selector?: MongoQuery, options?: FindOptions ): DBSegment[] { @@ -150,7 +149,7 @@ export class RundownPlaylistCollectionUtil { return RundownPlaylistCollectionUtil._sortSegments(segments, playlist) } static getUnorderedParts( - playlist: Pick, + playlist: Pick, selector?: MongoQuery, options?: FindOptions ): Part[] { @@ -174,7 +173,7 @@ export class RundownPlaylistCollectionUtil { } /** Synchronous version of getSegmentsAndParts, to be used client-side */ static getSegmentsAndPartsSync( - playlist: Pick, + playlist: Pick, segmentsQuery?: MongoQuery, partsQuery?: MongoQuery, segmentsOptions?: Omit, 'projection'>, // We are mangling fields, so block projection @@ -239,7 +238,7 @@ export class RundownPlaylistCollectionUtil { } } static getSelectedPartInstances( - playlist: Pick, + playlist: Pick, rundownIds0?: RundownId[] ): { currentPartInstance: PartInstance | undefined @@ -273,7 +272,7 @@ export class RundownPlaylistCollectionUtil { } static getAllPartInstances( - playlist: Pick, + playlist: Pick, selector?: MongoQuery, options?: FindOptions ): PartInstance[] { @@ -292,7 +291,7 @@ export class RundownPlaylistCollectionUtil { } /** Return a list of PartInstances, omitting the reset ones (ie only the ones that are relevant) */ static getActivePartInstances( - playlist: Pick, + playlist: Pick, selector?: MongoQuery, options?: FindOptions ): PartInstance[] { @@ -303,7 +302,7 @@ export class RundownPlaylistCollectionUtil { return RundownPlaylistCollectionUtil.getAllPartInstances(playlist, newSelector, options) } static getActivePartInstancesMap( - playlist: Pick, + playlist: Pick, selector?: MongoQuery, options?: FindOptions ): Record { diff --git a/meteor/lib/main.ts b/meteor/lib/main.ts index 0df9948956..caebd04821 100644 --- a/meteor/lib/main.ts +++ b/meteor/lib/main.ts @@ -16,7 +16,6 @@ import './collections/PeripheralDevices' import './collections/PieceInstances' import './collections/Pieces' import './collections/RundownLayouts' -import './collections/RundownPlaylists' import './collections/Snapshots' import './collections/Studios' import './collections/Timeline' diff --git a/meteor/server/api/cleanup.ts b/meteor/server/api/cleanup.ts index c6224f2293..64caf38c8c 100644 --- a/meteor/server/api/cleanup.ts +++ b/meteor/server/api/cleanup.ts @@ -1,7 +1,7 @@ import { ProtectedString, getCurrentTime } from '../../lib/lib' import { CollectionCleanupResult } from '../../lib/api/system' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' -import { RundownPlaylist } from '../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { getActiveRundownPlaylistsInStudioFromDb, getExpiredRemovedPackageInfos, @@ -454,7 +454,9 @@ async function isAllowedToRunCleanup(): Promise { const studios = await Studios.findFetchAsync({}, { fields: { _id: 1 } }) for (const studio of studios) { - const activePlaylist: RundownPlaylist | undefined = await getActiveRundownPlaylistsInStudioFromDb(studio._id)[0] + const activePlaylist: DBRundownPlaylist | undefined = await getActiveRundownPlaylistsInStudioFromDb( + studio._id + )[0] if (activePlaylist) { return `There is an active RundownPlaylist: "${activePlaylist.name}" in studio "${studio.name}" (${activePlaylist._id}, ${studio._id})` } diff --git a/meteor/server/api/deviceTriggers/StudioObserver.ts b/meteor/server/api/deviceTriggers/StudioObserver.ts index c99c653836..bbb3215657 100644 --- a/meteor/server/api/deviceTriggers/StudioObserver.ts +++ b/meteor/server/api/deviceTriggers/StudioObserver.ts @@ -12,7 +12,7 @@ import { Meteor } from 'meteor/meteor' import _ from 'underscore' import { MongoCursor } from '../../../lib/collections/lib' import { DBPartInstance } from '../../../lib/collections/PartInstances' -import { DBRundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { logger } from '../../logging' diff --git a/meteor/server/api/evaluations.ts b/meteor/server/api/evaluations.ts index 5d378cf2bb..7088198584 100644 --- a/meteor/server/api/evaluations.ts +++ b/meteor/server/api/evaluations.ts @@ -6,7 +6,7 @@ import * as _ from 'underscore' import { fetchStudioLight } from '../optimizations' import { sendSlackMessageToWebhook } from './integration/slack' import { OrganizationId, UserId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { RundownPlaylist } from '../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { Evaluations, RundownPlaylists } from '../collections' export async function saveEvaluation( @@ -66,7 +66,7 @@ export async function saveEvaluation( _id: 1, name: 1, }, - })) as Pick + })) as Pick const hostUrl = getSofieHostUrl() diff --git a/meteor/server/api/lib.ts b/meteor/server/api/lib.ts index 2772e0f405..b75b57e97d 100644 --- a/meteor/server/api/lib.ts +++ b/meteor/server/api/lib.ts @@ -1,7 +1,7 @@ import { RundownId, RundownPlaylistId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { Meteor } from 'meteor/meteor' import { MethodContext } from '../../lib/api/methods' -import { RundownPlaylist } from '../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { RundownContentAccess, @@ -15,7 +15,7 @@ import { * It is identical to RundownPlaylistContentAccess, except for confirming access is allowed */ export interface VerifiedRundownPlaylistContentAccess extends RundownPlaylistContentAccess { - playlist: RundownPlaylist + playlist: DBRundownPlaylist studioId: StudioId } /** diff --git a/meteor/server/api/rest/v1/index.ts b/meteor/server/api/rest/v1/index.ts index b156a37e05..018aedc145 100644 --- a/meteor/server/api/rest/v1/index.ts +++ b/meteor/server/api/rest/v1/index.ts @@ -85,7 +85,7 @@ import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' -import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { executePeripheralDeviceFunction } from '../../peripheralDevice/executeFunction' import { makeMeteorConnectionFromKoa } from '../koa' @@ -131,7 +131,7 @@ class ServerRestAPI implements RestAPI { _event: string ): Promise>> { const rundownPlaylists = (await RundownPlaylists.findFetchAsync({}, { projection: { _id: 1 } })) as Array< - Pick + Pick > return ClientAPI.responseSuccess( rundownPlaylists.map((rundownPlaylist) => ({ id: unprotectString(rundownPlaylist._id) })) @@ -787,7 +787,7 @@ class ServerRestAPI implements RestAPI { activationId: 1, }, } - )) as Array> + )) as Array> if (playlists.some((playlist) => playlist.activationId !== undefined)) { throw new Meteor.Error( 412, @@ -829,7 +829,7 @@ class ServerRestAPI implements RestAPI { activationId: 1, }, } - )) as Array> + )) as Array> if (playlists.some((playlist) => playlist.activationId !== undefined)) { throw new Meteor.Error( 412, @@ -916,7 +916,7 @@ class ServerRestAPI implements RestAPI { activationId: 1, }, } - )) as Array> + )) as Array> if (playlists.some((playlist) => playlist.activationId !== undefined)) { throw new Meteor.Error( 412, @@ -949,7 +949,7 @@ class ServerRestAPI implements RestAPI { activationId: 1, }, } - )) as Array> + )) as Array> if (playlists.some((playlist) => playlist.activationId !== undefined)) { throw new Meteor.Error( 412, @@ -1025,7 +1025,7 @@ class ServerRestAPI implements RestAPI { activationId: 1, }, } - )) as Array> + )) as Array> if (playlists.some((p) => p.activationId !== undefined)) { throw new Meteor.Error(412, `Studio ${studioId} cannot be updated, it is in use in an active Playlist`) } @@ -1062,7 +1062,7 @@ class ServerRestAPI implements RestAPI { activationId: 1, }, } - )) as Array> + )) as Array> if (playlists.some((p) => p.activationId !== undefined)) { throw new Meteor.Error(412, `Studio ${studioId} cannot be deleted, it is in use in an active Playlist`) } @@ -1077,7 +1077,7 @@ class ServerRestAPI implements RestAPI { _id: 1, }, } - )) as Array> + )) as Array> const promises = rundownPlaylists.map(async (rundownPlaylist) => ServerClientAPI.runUserActionInLogForPlaylistOnWorker( diff --git a/meteor/server/api/snapshot.ts b/meteor/server/api/snapshot.ts index 9b994f7bd8..e74fff4260 100644 --- a/meteor/server/api/snapshot.ts +++ b/meteor/server/api/snapshot.ts @@ -43,7 +43,7 @@ import { Blueprint } from '../../lib/collections/Blueprints' import { IngestRundown, VTContent } from '@sofie-automation/blueprints-integration' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { importIngestRundown } from './ingest/http' -import { RundownPlaylist } from '../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { RundownLayoutBase } from '../../lib/collections/RundownLayouts' import { DBTriggeredActions } from '../../lib/collections/TriggeredActions' import { Settings } from '../../lib/Settings' @@ -324,7 +324,7 @@ function getPiecesMediaObjects(pieces: PieceGeneric[]): string[] { } async function createRundownPlaylistSnapshot( - playlist: ReadonlyDeep, + playlist: ReadonlyDeep, full = false ): Promise { /** Max count of one type of items to include in the snapshot */ @@ -633,7 +633,7 @@ export async function storeRundownPlaylistSnapshot( return storeSnaphot(s, access.organizationId, reason) } export async function internalStoreRundownPlaylistSnapshot( - playlist: RundownPlaylist, + playlist: DBRundownPlaylist, reason: string, full?: boolean ): Promise { diff --git a/meteor/server/api/studio/lib.ts b/meteor/server/api/studio/lib.ts index 9a9f01a62f..a727d32e63 100644 --- a/meteor/server/api/studio/lib.ts +++ b/meteor/server/api/studio/lib.ts @@ -1,14 +1,14 @@ import { RundownPlaylistId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PackageInfoDB } from '@sofie-automation/corelib/dist/dataModel/PackageInfos' import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { getCurrentTime, protectString } from '../../../lib/lib' import { ExpectedPackages, PackageInfos, PeripheralDevices, RundownPlaylists } from '../../collections' export async function getActiveRundownPlaylistsInStudioFromDb( studioId: StudioId, excludeRundownPlaylistId?: RundownPlaylistId -): Promise { +): Promise { return RundownPlaylists.findFetchAsync({ studioId: studioId, activationId: { $exists: true }, diff --git a/meteor/server/publications/pieceContentStatusUI/rundown/publication.ts b/meteor/server/publications/pieceContentStatusUI/rundown/publication.ts index 09e22e909b..1c44d35bf8 100644 --- a/meteor/server/publications/pieceContentStatusUI/rundown/publication.ts +++ b/meteor/server/publications/pieceContentStatusUI/rundown/publication.ts @@ -14,7 +14,7 @@ import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mo import { ReadonlyDeep } from 'type-fest' import { CustomCollectionName, PubSub } from '../../../../lib/api/pubsub' import { UIPieceContentStatus } from '../../../../lib/api/rundownNotifications' -import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { MediaObjects, PackageContainerPackageStatuses, @@ -92,7 +92,7 @@ interface UIPieceContentStatusesUpdateProps extends IContentStatusesUpdatePropsB type RundownPlaylistFields = '_id' | 'studioId' const rundownPlaylistFieldSpecifier = literal< - MongoFieldSpecifierOnesStrict> + MongoFieldSpecifierOnesStrict> >({ _id: 1, studioId: 1, @@ -116,7 +116,7 @@ async function setupUIPieceContentStatusesPublicationObservers( const playlist = (await RundownPlaylists.findOneAsync(args.rundownPlaylistId, { projection: rundownPlaylistFieldSpecifier, - })) as Pick | undefined + })) as Pick | undefined if (!playlist) throw new Error(`RundownPlaylist "${args.rundownPlaylistId}" not found!`) const rundownsObserver = new RundownsObserver(playlist.studioId, playlist._id, (rundownIds) => { diff --git a/meteor/server/publications/segmentPartNotesUI/publication.ts b/meteor/server/publications/segmentPartNotesUI/publication.ts index 79ca15dce9..64f95307a1 100644 --- a/meteor/server/publications/segmentPartNotesUI/publication.ts +++ b/meteor/server/publications/segmentPartNotesUI/publication.ts @@ -29,7 +29,7 @@ import { } from './reactiveContentCache' import { RundownsObserver } from '../lib/rundownsObserver' import { RundownContentObserver } from './rundownContentObserver' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { generateNotesForSegment } from './generateNotesForSegment' import { RundownPlaylists } from '../../collections' import { check, Match } from 'meteor/check' @@ -51,7 +51,7 @@ interface UISegmentPartNotesUpdateProps { type RundownPlaylistFields = '_id' | 'studioId' const rundownPlaylistFieldSpecifier = literal< - MongoFieldSpecifierOnesStrict> + MongoFieldSpecifierOnesStrict> >({ _id: 1, studioId: 1, @@ -63,7 +63,7 @@ async function setupUISegmentPartNotesPublicationObservers( ): Promise { const playlist = (await RundownPlaylists.findOneAsync(args.playlistId, { projection: rundownPlaylistFieldSpecifier, - })) as Pick | undefined + })) as Pick | undefined if (!playlist) throw new Error(`RundownPlaylist "${args.playlistId}" not found!`) const rundownsObserver = new RundownsObserver(playlist.studioId, playlist._id, (rundownIds) => { diff --git a/meteor/server/security/lib/security.ts b/meteor/server/security/lib/security.ts index 343d3dafbd..d55476afa1 100644 --- a/meteor/server/security/lib/security.ts +++ b/meteor/server/security/lib/security.ts @@ -3,7 +3,7 @@ import { MongoQueryKey } from '@sofie-automation/corelib/dist/mongo' import { Settings } from '../../../lib/Settings' import { resolveCredentials, ResolvedCredentials, Credentials, isResolvedCredentials } from './credentials' import { allAccess, noAccess, combineAccess, Access } from './access' -import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { isProtectedString } from '../../../lib/lib' import { DBOrganization } from '../../../lib/collections/Organization' @@ -163,7 +163,7 @@ export async function allowAccessToStudio( export async function allowAccessToRundownPlaylist( cred0: Credentials | ResolvedCredentials, playlistId: RundownPlaylistId -): Promise> { +): Promise> { if (!Settings.enableUserAccounts) return allAccess(null, 'No security') if (!playlistId) return noAccess('playlistId not set') const cred = await resolveCredentials(cred0) @@ -324,9 +324,9 @@ namespace AccessRules { } else return noAccess(`User is not in the same organization as the studio ${studio._id}`) } export async function accessRundownPlaylist( - playlist: RundownPlaylist, + playlist: DBRundownPlaylist, cred: ResolvedCredentials - ): Promise> { + ): Promise> { const studio = await fetchStudioLight(playlist.studioId) if (!studio) return noAccess(`Studio of playlist "${playlist._id}" not found`) return { ...accessStudio(studio, cred), document: playlist } diff --git a/meteor/server/security/rundownPlaylist.ts b/meteor/server/security/rundownPlaylist.ts index 53c10d78e0..7740b65f22 100644 --- a/meteor/server/security/rundownPlaylist.ts +++ b/meteor/server/security/rundownPlaylist.ts @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor' import { check } from '../../lib/check' import { logNotAllowed } from './lib/lib' import { allowAccessToRundownPlaylist } from './lib/security' -import { RundownPlaylist } from '../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { Credentials, ResolvedCredentials, resolveCredentials } from './lib/credentials' import { triggerWriteAccess } from './lib/securityVerify' import { isProtectedString } from '../../lib/lib' @@ -50,7 +50,7 @@ export interface RundownPlaylistContentAccess { userId: UserId | null organizationId: OrganizationId | null studioId: StudioId | null - playlist: RundownPlaylist | null + playlist: DBRundownPlaylist | null cred: ResolvedCredentials | Credentials } diff --git a/meteor/server/security/studio.ts b/meteor/server/security/studio.ts index 390d6cb88d..4928d6a11a 100644 --- a/meteor/server/security/studio.ts +++ b/meteor/server/security/studio.ts @@ -4,7 +4,7 @@ import { MongoQueryKey } from '@sofie-automation/corelib/dist/mongo' import { logNotAllowed } from './lib/lib' import { ExternalMessageQueueObj } from '../../lib/collections/ExternalMessageQueue' import { Credentials, ResolvedCredentials, resolveCredentials } from './lib/credentials' -import { RundownPlaylist } from '../../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { Settings } from '../../lib/Settings' import { triggerWriteAccess } from './lib/securityVerify' import { isProtectedString } from '../../lib/lib' @@ -63,8 +63,8 @@ export namespace StudioContentWriteAccess { export async function rundownPlaylist( cred0: Credentials, - existingPlaylist: RundownPlaylist | RundownPlaylistId - ): Promise { + existingPlaylist: DBRundownPlaylist | RundownPlaylistId + ): Promise { triggerWriteAccess() if (existingPlaylist && isProtectedString(existingPlaylist)) { const playlistId = existingPlaylist diff --git a/meteor/server/webmanifest.ts b/meteor/server/webmanifest.ts index c4a59aa5f7..86067ef5fb 100644 --- a/meteor/server/webmanifest.ts +++ b/meteor/server/webmanifest.ts @@ -12,7 +12,7 @@ import { getLocale, Translations } from './lib' import { generateTranslation } from '../lib/lib' import { ITranslatableMessage } from '@sofie-automation/blueprints-integration' import { interpollateTranslation } from '@sofie-automation/corelib/dist/TranslatableMessage' -import { DBRundownPlaylist } from '../lib/collections/RundownPlaylists' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { getCoreSystemAsync } from './coreSystem/collection' import Koa from 'koa' import KoaRouter from '@koa/router' From 108119da690a1792838ba5927e4bbd5a6fdcaa88 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 4 Jul 2023 16:36:45 +0100 Subject: [PATCH 009/479] chore: tidy up Piece collection rexports --- meteor/__mocks__/defaultCollectionObjects.ts | 2 +- meteor/__mocks__/helpers/database.ts | 2 +- meteor/client/lib/__tests__/rundown.test.ts | 2 +- meteor/client/lib/reactiveData/reactiveData.ts | 2 +- meteor/client/ui/FloatingInspectors/VTFloatingInspector.tsx | 2 +- meteor/client/ui/PieceIcons/PieceName.tsx | 2 +- meteor/client/ui/PieceIcons/Renderers/SplitInputIcon.tsx | 2 +- meteor/client/ui/Prompter/prompter.ts | 2 +- meteor/client/ui/RundownView/RundownNotifier.tsx | 2 +- .../SegmentContainer/getReactivePieceNoteCountsForSegment.tsx | 2 +- meteor/client/ui/Shelf/DashboardPieceButtonSplitPreview.tsx | 2 +- .../ui/Shelf/Inspector/ItemRenderers/ActionItemRenderer.tsx | 2 +- .../ui/Shelf/Inspector/ItemRenderers/DefaultItemRenderer.tsx | 2 +- .../client/ui/Shelf/Inspector/ItemRenderers/InspectorTitle.tsx | 2 +- .../client/ui/Shelf/Inspector/ItemRenderers/NoraItemEditor.tsx | 2 +- meteor/lib/Rundown.ts | 2 +- meteor/lib/api/pubsub.ts | 2 +- meteor/lib/collections/Pieces.ts | 1 - meteor/lib/collections/libCollections.ts | 2 +- meteor/lib/collections/rundownPlaylistUtil.ts | 2 +- meteor/lib/main.ts | 1 - meteor/lib/mediaObjects.ts | 2 +- meteor/server/api/__tests__/peripheralDevice.test.ts | 2 +- meteor/server/api/ingest/mosDevice/actions.ts | 2 +- meteor/server/api/snapshot.ts | 2 +- meteor/server/publications/rundown.ts | 2 +- 26 files changed, 24 insertions(+), 26 deletions(-) delete mode 100644 meteor/lib/collections/Pieces.ts diff --git a/meteor/__mocks__/defaultCollectionObjects.ts b/meteor/__mocks__/defaultCollectionObjects.ts index ad4400c876..f542fc67a5 100644 --- a/meteor/__mocks__/defaultCollectionObjects.ts +++ b/meteor/__mocks__/defaultCollectionObjects.ts @@ -5,7 +5,7 @@ import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart } from '../lib/collections/Parts' import { IBlueprintPieceType, PieceLifespan } from '@sofie-automation/blueprints-integration' -import { Piece, EmptyPieceTimelineObjectsBlob } from '../lib/collections/Pieces' +import { Piece, EmptyPieceTimelineObjectsBlob } from '@sofie-automation/corelib/dist/dataModel/Piece' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { getRundownId } from '../server/api/ingest/lib' import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' diff --git a/meteor/__mocks__/helpers/database.ts b/meteor/__mocks__/helpers/database.ts index 587a322de3..956158b497 100644 --- a/meteor/__mocks__/helpers/database.ts +++ b/meteor/__mocks__/helpers/database.ts @@ -51,7 +51,7 @@ import { import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart } from '../../lib/collections/Parts' -import { EmptyPieceTimelineObjectsBlob, Piece } from '../../lib/collections/Pieces' +import { EmptyPieceTimelineObjectsBlob, Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' diff --git a/meteor/client/lib/__tests__/rundown.test.ts b/meteor/client/lib/__tests__/rundown.test.ts index e0de86a68a..75eadef2d9 100644 --- a/meteor/client/lib/__tests__/rundown.test.ts +++ b/meteor/client/lib/__tests__/rundown.test.ts @@ -7,7 +7,7 @@ import { convertToUIShowStyleBase, } from '../../../__mocks__/helpers/database' import { RundownUtils } from '../rundown' -import { Piece } from '../../../lib/collections/Pieces' +import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { defaultPartInstance, defaultPiece, defaultPieceInstance } from '../../../__mocks__/defaultCollectionObjects' import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { PieceLifespan } from '@sofie-automation/blueprints-integration' diff --git a/meteor/client/lib/reactiveData/reactiveData.ts b/meteor/client/lib/reactiveData/reactiveData.ts index 2037b1bd4c..34c9846386 100644 --- a/meteor/client/lib/reactiveData/reactiveData.ts +++ b/meteor/client/lib/reactiveData/reactiveData.ts @@ -1,6 +1,6 @@ import { Tracker } from 'meteor/tracker' import { ReactiveVar } from 'meteor/reactive-var' -import { Piece } from '../../../lib/collections/Pieces' +import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { PeripheralDevice } from '../../../lib/collections/PeripheralDevices' import { getCurrentTime } from '../../../lib/lib' import { FindOptions } from '../../../lib/collections/lib' diff --git a/meteor/client/ui/FloatingInspectors/VTFloatingInspector.tsx b/meteor/client/ui/FloatingInspectors/VTFloatingInspector.tsx index 291403e55e..e20c27c691 100644 --- a/meteor/client/ui/FloatingInspectors/VTFloatingInspector.tsx +++ b/meteor/client/ui/FloatingInspectors/VTFloatingInspector.tsx @@ -6,7 +6,7 @@ import { FloatingInspector } from '../FloatingInspector' import { NoticeLevel } from '../../../lib/notifications/notifications' import { VTContent } from '@sofie-automation/blueprints-integration' import { IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' -import { PieceStatusCode } from '../../../lib/collections/Pieces' +import { PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' import { VideoPreviewPlayer } from '../../lib/VideoPreviewPlayer' import classNames from 'classnames' import { UIStudio } from '../../../lib/api/studios' diff --git a/meteor/client/ui/PieceIcons/PieceName.tsx b/meteor/client/ui/PieceIcons/PieceName.tsx index e6fc8f36e1..32f7edefd4 100644 --- a/meteor/client/ui/PieceIcons/PieceName.tsx +++ b/meteor/client/ui/PieceIcons/PieceName.tsx @@ -5,7 +5,7 @@ import { EvsContent, SourceLayerType } from '@sofie-automation/blueprints-integr import { PubSub } from '../../../lib/api/pubsub' import { IPropsHeader } from './PieceIcon' import { findPieceInstanceToShow } from './utils' -import { PieceGeneric } from '../../../lib/collections/Pieces' +import { PieceGeneric } from '@sofie-automation/corelib/dist/dataModel/Piece' import { RundownPlaylistActivationId } from '@sofie-automation/corelib/dist/dataModel/Ids' interface INamePropsHeader extends IPropsHeader { diff --git a/meteor/client/ui/PieceIcons/Renderers/SplitInputIcon.tsx b/meteor/client/ui/PieceIcons/Renderers/SplitInputIcon.tsx index 91b9c4be32..c4b7a32e78 100644 --- a/meteor/client/ui/PieceIcons/Renderers/SplitInputIcon.tsx +++ b/meteor/client/ui/PieceIcons/Renderers/SplitInputIcon.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { PieceGeneric } from '../../../../lib/collections/Pieces' +import { PieceGeneric } from '@sofie-automation/corelib/dist/dataModel/Piece' import { SplitsContent, SourceLayerType } from '@sofie-automation/blueprints-integration' import { RundownUtils } from '../../../lib/rundown' import classNames from 'classnames' diff --git a/meteor/client/ui/Prompter/prompter.ts b/meteor/client/ui/Prompter/prompter.ts index 4c16ed6a98..7466c2be99 100644 --- a/meteor/client/ui/Prompter/prompter.ts +++ b/meteor/client/ui/Prompter/prompter.ts @@ -2,7 +2,7 @@ import { check } from '../../../lib/check' import * as _ from 'underscore' import { ScriptContent, SourceLayerType } from '@sofie-automation/blueprints-integration' import { normalizeArrayToMap, protectString } from '../../../lib/lib' -import { Piece } from '../../../lib/collections/Pieces' +import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { getPieceInstancesForPartInstance, getSegmentsWithPartInstances } from '../../../lib/Rundown' import { FindOptions } from '../../../lib/collections/lib' import { PieceInstance } from '../../../lib/collections/PieceInstances' diff --git a/meteor/client/ui/RundownView/RundownNotifier.tsx b/meteor/client/ui/RundownView/RundownNotifier.tsx index 71bb73ee06..a9cc968acc 100644 --- a/meteor/client/ui/RundownView/RundownNotifier.tsx +++ b/meteor/client/ui/RundownView/RundownNotifier.tsx @@ -21,7 +21,7 @@ import { doModalDialog } from '../../lib/ModalDialog' import { doUserAction, UserAction } from '../../../lib/clientUserAction' // import { withTranslation, getI18n, getDefaults } from 'react-i18next' import { i18nTranslator as t } from '../i18n' -import { PieceStatusCode } from '../../../lib/collections/Pieces' +import { PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' import { PeripheralDevicesAPI } from '../../lib/clientAPI' import { handleRundownReloadResponse } from '../RundownView' import { MeteorCall } from '../../../lib/api/methods' diff --git a/meteor/client/ui/SegmentContainer/getReactivePieceNoteCountsForSegment.tsx b/meteor/client/ui/SegmentContainer/getReactivePieceNoteCountsForSegment.tsx index e456112332..4b6ccbb5a0 100644 --- a/meteor/client/ui/SegmentContainer/getReactivePieceNoteCountsForSegment.tsx +++ b/meteor/client/ui/SegmentContainer/getReactivePieceNoteCountsForSegment.tsx @@ -2,7 +2,7 @@ import { NoteSeverity } from '@sofie-automation/blueprints-integration' import { assertNever, literal } from '@sofie-automation/corelib/dist/lib' import { MongoFieldSpecifierOnes } from '@sofie-automation/corelib/dist/mongo' import { UIPieceContentStatus, UISegmentPartNote } from '../../../lib/api/rundownNotifications' -import { PieceStatusCode } from '../../../lib/collections/Pieces' +import { PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' import { getIgnorePieceContentStatus } from '../../lib/localStorage' import { UIPieceContentStatuses, UISegmentPartNotes } from '../Collections' import { SegmentNoteCounts, SegmentUi } from './withResolvedSegment' diff --git a/meteor/client/ui/Shelf/DashboardPieceButtonSplitPreview.tsx b/meteor/client/ui/Shelf/DashboardPieceButtonSplitPreview.tsx index a723fa28f0..57f7a6e2da 100644 --- a/meteor/client/ui/Shelf/DashboardPieceButtonSplitPreview.tsx +++ b/meteor/client/ui/Shelf/DashboardPieceButtonSplitPreview.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { SplitsContent } from '@sofie-automation/blueprints-integration' -import { PieceGeneric } from '../../../lib/collections/Pieces' +import { PieceGeneric } from '@sofie-automation/corelib/dist/dataModel/Piece' import { getSplitPreview } from '../../lib/ui/splitPreview' import { RenderSplitPreview } from '../../lib/SplitPreviewBox' diff --git a/meteor/client/ui/Shelf/Inspector/ItemRenderers/ActionItemRenderer.tsx b/meteor/client/ui/Shelf/Inspector/ItemRenderers/ActionItemRenderer.tsx index beb3bc7636..ad9f6b30c3 100644 --- a/meteor/client/ui/Shelf/Inspector/ItemRenderers/ActionItemRenderer.tsx +++ b/meteor/client/ui/Shelf/Inspector/ItemRenderers/ActionItemRenderer.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import * as _ from 'underscore' import { PieceUi } from '../../../SegmentTimeline/SegmentTimelineContainer' import { RundownUtils } from '../../../../lib/rundown' -import { Piece } from '../../../../../lib/collections/Pieces' +import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { ConfigManifestEntry as BlueprintConfigManifestEntry, IBlueprintActionTriggerMode, diff --git a/meteor/client/ui/Shelf/Inspector/ItemRenderers/DefaultItemRenderer.tsx b/meteor/client/ui/Shelf/Inspector/ItemRenderers/DefaultItemRenderer.tsx index 4fd03ab96f..6ecc22dde1 100644 --- a/meteor/client/ui/Shelf/Inspector/ItemRenderers/DefaultItemRenderer.tsx +++ b/meteor/client/ui/Shelf/Inspector/ItemRenderers/DefaultItemRenderer.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { PieceUi } from '../../../SegmentTimeline/SegmentTimelineContainer' import { IAdLibListItem } from '../../AdLibListItem' import { RundownUtils } from '../../../../lib/rundown' -import { Piece } from '../../../../../lib/collections/Pieces' +import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import InspectorTitle from './InspectorTitle' import { BucketAdLibUi } from '../../RundownViewBuckets' import { AdLibPieceUi } from '../../../../lib/shelf' diff --git a/meteor/client/ui/Shelf/Inspector/ItemRenderers/InspectorTitle.tsx b/meteor/client/ui/Shelf/Inspector/ItemRenderers/InspectorTitle.tsx index 725951ba73..6dc6987579 100644 --- a/meteor/client/ui/Shelf/Inspector/ItemRenderers/InspectorTitle.tsx +++ b/meteor/client/ui/Shelf/Inspector/ItemRenderers/InspectorTitle.tsx @@ -3,7 +3,7 @@ import ClassNames from 'classnames' import { PieceUi } from '../../../SegmentTimeline/SegmentTimelineContainer' import { BucketAdLibUi, BucketAdLibActionUi } from '../../RundownViewBuckets' import { RundownUtils } from '../../../../lib/rundown' -import { Piece } from '../../../../../lib/collections/Pieces' +import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { withMediaObjectStatus } from '../../../SegmentTimeline/withMediaObjectStatus' import { IAdLibListItem } from '../../AdLibListItem' import { AdLibPieceUi } from '../../../../lib/shelf' diff --git a/meteor/client/ui/Shelf/Inspector/ItemRenderers/NoraItemEditor.tsx b/meteor/client/ui/Shelf/Inspector/ItemRenderers/NoraItemEditor.tsx index 6c9f9d7d20..e1543cf423 100644 --- a/meteor/client/ui/Shelf/Inspector/ItemRenderers/NoraItemEditor.tsx +++ b/meteor/client/ui/Shelf/Inspector/ItemRenderers/NoraItemEditor.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { createMosObjectXmlStringNoraBluePrintPiece } from '../../../../lib/data/nora/browser-plugin-data' import { parseMosPluginMessageXml, MosPluginMessage } from '../../../../lib/parsers/mos/mosXml2Js' -import { PieceGeneric } from '../../../../../lib/collections/Pieces' +import { PieceGeneric } from '@sofie-automation/corelib/dist/dataModel/Piece' import { createMosAppInfoXmlString } from '../../../../lib/data/mos/plugin-support' //TODO: figure out what the origin should be diff --git a/meteor/lib/Rundown.ts b/meteor/lib/Rundown.ts index 8adef77178..13af44e3aa 100644 --- a/meteor/lib/Rundown.ts +++ b/meteor/lib/Rundown.ts @@ -1,5 +1,5 @@ import * as _ from 'underscore' -import { Piece } from './collections/Pieces' +import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { IOutputLayer, ISourceLayer, ITranslatableMessage } from '@sofie-automation/blueprints-integration' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart } from './collections/Parts' diff --git a/meteor/lib/api/pubsub.ts b/meteor/lib/api/pubsub.ts index 167a969d69..acb71a522d 100644 --- a/meteor/lib/api/pubsub.ts +++ b/meteor/lib/api/pubsub.ts @@ -33,7 +33,7 @@ import { DBPart } from '../collections/Parts' import { PeripheralDeviceCommand } from '../collections/PeripheralDeviceCommands' import { PeripheralDevice } from '../collections/PeripheralDevices' import { PieceInstance } from '../collections/PieceInstances' -import { Piece } from '../collections/Pieces' +import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' import { RundownLayoutBase } from '../collections/RundownLayouts' diff --git a/meteor/lib/collections/Pieces.ts b/meteor/lib/collections/Pieces.ts deleted file mode 100644 index 519e34e309..0000000000 --- a/meteor/lib/collections/Pieces.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '@sofie-automation/corelib/dist/dataModel/Piece' diff --git a/meteor/lib/collections/libCollections.ts b/meteor/lib/collections/libCollections.ts index 5b075537c1..9123b9b84f 100644 --- a/meteor/lib/collections/libCollections.ts +++ b/meteor/lib/collections/libCollections.ts @@ -11,7 +11,7 @@ import { DBOrganization } from './Organization' import { PartInstance } from './PartInstances' import { Part } from './Parts' import { PieceInstance } from './PieceInstances' -import { Piece } from './Pieces' +import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' diff --git a/meteor/lib/collections/rundownPlaylistUtil.ts b/meteor/lib/collections/rundownPlaylistUtil.ts index 6be86a405b..7cf98f43f1 100644 --- a/meteor/lib/collections/rundownPlaylistUtil.ts +++ b/meteor/lib/collections/rundownPlaylistUtil.ts @@ -17,7 +17,7 @@ import { FindOptions } from './lib' import { PartInstance } from './PartInstances' import { Part } from './Parts' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' -import { Piece } from './Pieces' +import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' /** * Direct database accessors for the RundownPlaylist diff --git a/meteor/lib/main.ts b/meteor/lib/main.ts index caebd04821..67c17b00ae 100644 --- a/meteor/lib/main.ts +++ b/meteor/lib/main.ts @@ -14,7 +14,6 @@ import './collections/Parts' import './collections/PeripheralDeviceCommands' import './collections/PeripheralDevices' import './collections/PieceInstances' -import './collections/Pieces' import './collections/RundownLayouts' import './collections/Snapshots' import './collections/Studios' diff --git a/meteor/lib/mediaObjects.ts b/meteor/lib/mediaObjects.ts index 9390e01021..fedd7c9aa6 100644 --- a/meteor/lib/mediaObjects.ts +++ b/meteor/lib/mediaObjects.ts @@ -1,5 +1,5 @@ import { PackageInfo } from '@sofie-automation/blueprints-integration' -import { PieceStatusCode } from './collections/Pieces' +import { PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' import { ITranslatableMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' export interface ScanInfoForPackages { diff --git a/meteor/server/api/__tests__/peripheralDevice.test.ts b/meteor/server/api/__tests__/peripheralDevice.test.ts index 3116073132..4ccf099f78 100644 --- a/meteor/server/api/__tests__/peripheralDevice.test.ts +++ b/meteor/server/api/__tests__/peripheralDevice.test.ts @@ -5,7 +5,7 @@ import { PeripheralDeviceCategory, PeripheralDeviceType, } from '../../../lib/collections/PeripheralDevices' -import { EmptyPieceTimelineObjectsBlob } from '../../../lib/collections/Pieces' +import { EmptyPieceTimelineObjectsBlob } from '@sofie-automation/corelib/dist/dataModel/Piece' import { getCurrentTime, literal, diff --git a/meteor/server/api/ingest/mosDevice/actions.ts b/meteor/server/api/ingest/mosDevice/actions.ts index f2443a0a6b..74547c1ff1 100644 --- a/meteor/server/api/ingest/mosDevice/actions.ts +++ b/meteor/server/api/ingest/mosDevice/actions.ts @@ -3,7 +3,7 @@ import { logger } from '../../../logging' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { Meteor } from 'meteor/meteor' import { PeripheralDevice } from '../../../../lib/collections/PeripheralDevices' -import { Piece } from '../../../../lib/collections/Pieces' +import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { IngestPart } from '@sofie-automation/blueprints-integration' import { parseMosString } from './lib' import { stringifyError } from '../../../../lib/lib' diff --git a/meteor/server/api/snapshot.ts b/meteor/server/api/snapshot.ts index e74fff4260..68bb91d754 100644 --- a/meteor/server/api/snapshot.ts +++ b/meteor/server/api/snapshot.ts @@ -14,7 +14,7 @@ import { SnapshotRundownPlaylist, } from '../../lib/collections/Snapshots' import { UserActionsLogItem } from '../../lib/collections/UserActionsLog' -import { PieceGeneric } from '../../lib/collections/Pieces' +import { PieceGeneric } from '@sofie-automation/corelib/dist/dataModel/Piece' import { MediaObject } from '@sofie-automation/shared-lib/dist/core/model/MediaObjects' import { getCurrentTime, diff --git a/meteor/server/publications/rundown.ts b/meteor/server/publications/rundown.ts index 5917e2187b..a7e47d55cc 100644 --- a/meteor/server/publications/rundown.ts +++ b/meteor/server/publications/rundown.ts @@ -7,7 +7,7 @@ import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { RundownReadAccess } from '../security/rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart } from '../../lib/collections/Parts' -import { Piece } from '../../lib/collections/Pieces' +import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { PieceInstance } from '../../lib/collections/PieceInstances' import { DBPartInstance, PartInstance } from '../../lib/collections/PartInstances' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' From 382ac2e5346005fb5cee97b7be9cfb92c184b1f6 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 4 Jul 2023 16:37:50 +0100 Subject: [PATCH 010/479] chore: tidy up PieceInstance collection rexports --- meteor/__mocks__/defaultCollectionObjects.ts | 2 +- meteor/client/lib/rundown.ts | 2 +- meteor/client/lib/rundownLayouts.ts | 2 +- meteor/client/lib/shelf.ts | 2 +- meteor/client/ui/ClipTrimPanel/ClipTrimDialog.tsx | 2 +- .../FloatingInspectorTimeInformationRow.tsx | 2 +- meteor/client/ui/FloatingInspectors/L3rdFloatingInspector.tsx | 2 +- meteor/client/ui/PieceIcons/PieceIcon.tsx | 2 +- meteor/client/ui/Prompter/prompter.ts | 2 +- meteor/client/ui/Shelf/AdLibRegionPanel.tsx | 2 +- meteor/client/ui/Shelf/EndWordsPanel.tsx | 2 +- meteor/client/ui/Shelf/PieceCountdownPanel.tsx | 2 +- meteor/client/ui/Shelf/Renderers/L3rdListItemRenderer.tsx | 2 +- meteor/lib/Rundown.ts | 2 +- meteor/lib/api/pubsub.ts | 2 +- meteor/lib/collections/PieceInstances.ts | 1 - meteor/lib/collections/libCollections.ts | 2 +- meteor/lib/main.ts | 1 - meteor/server/__tests__/cronjobs.test.ts | 2 +- meteor/server/publications/rundown.ts | 2 +- 20 files changed, 18 insertions(+), 20 deletions(-) delete mode 100644 meteor/lib/collections/PieceInstances.ts diff --git a/meteor/__mocks__/defaultCollectionObjects.ts b/meteor/__mocks__/defaultCollectionObjects.ts index f542fc67a5..3ce882418b 100644 --- a/meteor/__mocks__/defaultCollectionObjects.ts +++ b/meteor/__mocks__/defaultCollectionObjects.ts @@ -10,7 +10,7 @@ import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { getRundownId } from '../server/api/ingest/lib' import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { PartInstance } from '../lib/collections/PartInstances' -import { PieceInstance } from '../lib/collections/PieceInstances' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { PartId, PartInstanceId, diff --git a/meteor/client/lib/rundown.ts b/meteor/client/lib/rundown.ts index 5a01ce1819..503f86f4ce 100644 --- a/meteor/client/lib/rundown.ts +++ b/meteor/client/lib/rundown.ts @@ -29,7 +29,7 @@ import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/Rund import { literal, getCurrentTime, applyToArray } from '../../lib/lib' import { processAndPrunePieceInstanceTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' import { createPieceGroupAndCap, PieceTimelineMetadata } from '@sofie-automation/corelib/dist/playout/pieces' -import { PieceInstance } from '../../lib/collections/PieceInstances' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { IAdLibListItem } from '../ui/Shelf/AdLibListItem' import { BucketAdLibItem, BucketAdLibUi } from '../ui/Shelf/RundownViewBuckets' import { FindOptions } from '../../lib/collections/lib' diff --git a/meteor/client/lib/rundownLayouts.ts b/meteor/client/lib/rundownLayouts.ts index 2a3caf8aa7..2acb76c507 100644 --- a/meteor/client/lib/rundownLayouts.ts +++ b/meteor/client/lib/rundownLayouts.ts @@ -1,7 +1,7 @@ import { PartInstanceId, RundownPlaylistActivationId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { processAndPrunePieceInstanceTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' import { UIShowStyleBase } from '../../lib/api/showStyles' -import { PieceInstance } from '../../lib/collections/PieceInstances' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { RequiresActiveLayers } from '../../lib/collections/RundownLayouts' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { getCurrentTime } from '../../lib/lib' diff --git a/meteor/client/lib/shelf.ts b/meteor/client/lib/shelf.ts index c3585f53d8..20db188267 100644 --- a/meteor/client/lib/shelf.ts +++ b/meteor/client/lib/shelf.ts @@ -3,7 +3,7 @@ import _ from 'underscore' import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { PartInstance } from '../../lib/collections/PartInstances' -import { PieceInstance } from '../../lib/collections/PieceInstances' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' diff --git a/meteor/client/ui/ClipTrimPanel/ClipTrimDialog.tsx b/meteor/client/ui/ClipTrimPanel/ClipTrimDialog.tsx index 3e0841b010..7024a4a6da 100644 --- a/meteor/client/ui/ClipTrimPanel/ClipTrimDialog.tsx +++ b/meteor/client/ui/ClipTrimPanel/ClipTrimDialog.tsx @@ -9,7 +9,7 @@ import { NotificationCenter, Notification, NoticeLevel } from '../../../lib/noti import { protectString, stringifyError } from '../../../lib/lib' import { ClientAPI } from '../../../lib/api/client' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { PieceInstancePiece } from '../../../lib/collections/PieceInstances' +import { PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { UIStudio } from '../../../lib/api/studios' import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' diff --git a/meteor/client/ui/FloatingInspectors/FloatingInspectorHelpers/FloatingInspectorTimeInformationRow.tsx b/meteor/client/ui/FloatingInspectors/FloatingInspectorHelpers/FloatingInspectorTimeInformationRow.tsx index 7a7de99023..fab6948209 100644 --- a/meteor/client/ui/FloatingInspectors/FloatingInspectorHelpers/FloatingInspectorTimeInformationRow.tsx +++ b/meteor/client/ui/FloatingInspectors/FloatingInspectorHelpers/FloatingInspectorTimeInformationRow.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { PieceInstancePiece } from '../../../../lib/collections/PieceInstances' +import { PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { RundownUtils } from '../../../lib/rundown' import { PieceLifespan } from '@sofie-automation/blueprints-integration' import { TFunction, useTranslation } from 'react-i18next' diff --git a/meteor/client/ui/FloatingInspectors/L3rdFloatingInspector.tsx b/meteor/client/ui/FloatingInspectors/L3rdFloatingInspector.tsx index 08b57c7dd8..db3d72090b 100644 --- a/meteor/client/ui/FloatingInspectors/L3rdFloatingInspector.tsx +++ b/meteor/client/ui/FloatingInspectors/L3rdFloatingInspector.tsx @@ -6,7 +6,7 @@ import { GraphicsContent, NoraContent } from '@sofie-automation/blueprints-integ import { NoraFloatingInspector } from './NoraFloatingInspector' import { FloatingInspector } from '../FloatingInspector' import { Time } from '../../../lib/lib' -import { PieceInstancePiece } from '../../../lib/collections/PieceInstances' +import { PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { FloatingInspectorTimeInformationRow } from './FloatingInspectorHelpers/FloatingInspectorTimeInformationRow' import { IFloatingInspectorPosition, useInspectorPosition } from './IFloatingInspectorPosition' diff --git a/meteor/client/ui/PieceIcons/PieceIcon.tsx b/meteor/client/ui/PieceIcons/PieceIcon.tsx index 0e56670cf6..860e24f085 100644 --- a/meteor/client/ui/PieceIcons/PieceIcon.tsx +++ b/meteor/client/ui/PieceIcons/PieceIcon.tsx @@ -15,7 +15,7 @@ import LiveSpeakInputIcon from './Renderers/LiveSpeakInputIcon' import GraphicsInputIcon from './Renderers/GraphicsInputIcon' import UnknownInputIcon from './Renderers/UnknownInputIcon' import { PubSub } from '../../../lib/api/pubsub' -import { PieceInstance } from '../../../lib/collections/PieceInstances' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { findPieceInstanceToShow, findPieceInstanceToShowFromInstances } from './utils' import LocalInputIcon from './Renderers/LocalInputIcon' import { diff --git a/meteor/client/ui/Prompter/prompter.ts b/meteor/client/ui/Prompter/prompter.ts index 7466c2be99..e168d44b83 100644 --- a/meteor/client/ui/Prompter/prompter.ts +++ b/meteor/client/ui/Prompter/prompter.ts @@ -5,7 +5,7 @@ import { normalizeArrayToMap, protectString } from '../../../lib/lib' import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { getPieceInstancesForPartInstance, getSegmentsWithPartInstances } from '../../../lib/Rundown' import { FindOptions } from '../../../lib/collections/lib' -import { PieceInstance } from '../../../lib/collections/PieceInstances' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { processAndPrunePieceInstanceTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' import { UIShowStyleBases } from '../Collections' diff --git a/meteor/client/ui/Shelf/AdLibRegionPanel.tsx b/meteor/client/ui/Shelf/AdLibRegionPanel.tsx index 7eb8e38bc4..f084674c5f 100644 --- a/meteor/client/ui/Shelf/AdLibRegionPanel.tsx +++ b/meteor/client/ui/Shelf/AdLibRegionPanel.tsx @@ -24,7 +24,7 @@ import { isAdLibNext, isAdLibOnAir, } from '../../lib/shelf' -import { PieceInstance } from '../../../lib/collections/PieceInstances' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { PieceUi } from '../SegmentTimeline/SegmentTimelineContainer' import { withMediaObjectStatus } from '../SegmentTimeline/withMediaObjectStatus' import { ISourceLayer } from '@sofie-automation/blueprints-integration' diff --git a/meteor/client/ui/Shelf/EndWordsPanel.tsx b/meteor/client/ui/Shelf/EndWordsPanel.tsx index 9284eed13e..483f510d6b 100644 --- a/meteor/client/ui/Shelf/EndWordsPanel.tsx +++ b/meteor/client/ui/Shelf/EndWordsPanel.tsx @@ -11,7 +11,7 @@ import { dashboardElementStyle } from './DashboardPanel' import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { PieceInstance } from '../../../lib/collections/PieceInstances' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { ScriptContent } from '@sofie-automation/blueprints-integration' import { getUnfinishedPieceInstancesReactive } from '../../lib/rundownLayouts' import { getScriptPreview } from '../../lib/ui/scriptPreview' diff --git a/meteor/client/ui/Shelf/PieceCountdownPanel.tsx b/meteor/client/ui/Shelf/PieceCountdownPanel.tsx index 70af881ca3..465e769d26 100644 --- a/meteor/client/ui/Shelf/PieceCountdownPanel.tsx +++ b/meteor/client/ui/Shelf/PieceCountdownPanel.tsx @@ -13,7 +13,7 @@ import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { RundownUtils } from '../../lib/rundown' import { RundownTiming, TimingEvent } from '../RundownView/RundownTiming/RundownTiming' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { PieceInstance } from '../../../lib/collections/PieceInstances' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { VTContent } from '@sofie-automation/blueprints-integration' import { getUnfinishedPieceInstancesReactive } from '../../lib/rundownLayouts' import { UIShowStyleBase } from '../../../lib/api/showStyles' diff --git a/meteor/client/ui/Shelf/Renderers/L3rdListItemRenderer.tsx b/meteor/client/ui/Shelf/Renderers/L3rdListItemRenderer.tsx index 539c421807..2aea952cfe 100644 --- a/meteor/client/ui/Shelf/Renderers/L3rdListItemRenderer.tsx +++ b/meteor/client/ui/Shelf/Renderers/L3rdListItemRenderer.tsx @@ -9,7 +9,7 @@ import { getElementWidth } from '../../../utils/dimensions' import { StyledTimecode } from '../../../lib/StyledTimecode' import { assertNever, protectString } from '../../../../lib/lib' import { L3rdFloatingInspector } from '../../FloatingInspectors/L3rdFloatingInspector' -import { PieceInstancePiece } from '../../../../lib/collections/PieceInstances' +import { PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { AdLibPieceUi } from '../../../lib/shelf' import { ActionAdLibHotkeyPreview } from '../../../lib/triggers/ActionAdLibHotkeyPreview' diff --git a/meteor/lib/Rundown.ts b/meteor/lib/Rundown.ts index 13af44e3aa..730b64fc22 100644 --- a/meteor/lib/Rundown.ts +++ b/meteor/lib/Rundown.ts @@ -4,7 +4,7 @@ import { IOutputLayer, ISourceLayer, ITranslatableMessage } from '@sofie-automat import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart } from './collections/Parts' import { PartInstance, wrapPartToTemporaryInstance } from './collections/PartInstances' -import { PieceInstance } from './collections/PieceInstances' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { getPieceInstancesForPart, buildPiecesStartingInThisPartQuery, diff --git a/meteor/lib/api/pubsub.ts b/meteor/lib/api/pubsub.ts index acb71a522d..4c81a2b569 100644 --- a/meteor/lib/api/pubsub.ts +++ b/meteor/lib/api/pubsub.ts @@ -32,7 +32,7 @@ import { PartInstance } from '../collections/PartInstances' import { DBPart } from '../collections/Parts' import { PeripheralDeviceCommand } from '../collections/PeripheralDeviceCommands' import { PeripheralDevice } from '../collections/PeripheralDevices' -import { PieceInstance } from '../collections/PieceInstances' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' diff --git a/meteor/lib/collections/PieceInstances.ts b/meteor/lib/collections/PieceInstances.ts deleted file mode 100644 index 177f2a2266..0000000000 --- a/meteor/lib/collections/PieceInstances.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '@sofie-automation/corelib/dist/dataModel/PieceInstance' diff --git a/meteor/lib/collections/libCollections.ts b/meteor/lib/collections/libCollections.ts index 9123b9b84f..f7b78bffdc 100644 --- a/meteor/lib/collections/libCollections.ts +++ b/meteor/lib/collections/libCollections.ts @@ -10,7 +10,7 @@ import { createSyncMongoCollection, createSyncReadOnlyMongoCollection } from './ import { DBOrganization } from './Organization' import { PartInstance } from './PartInstances' import { Part } from './Parts' -import { PieceInstance } from './PieceInstances' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' diff --git a/meteor/lib/main.ts b/meteor/lib/main.ts index 67c17b00ae..200a6aa458 100644 --- a/meteor/lib/main.ts +++ b/meteor/lib/main.ts @@ -13,7 +13,6 @@ import './collections/PartInstances' import './collections/Parts' import './collections/PeripheralDeviceCommands' import './collections/PeripheralDevices' -import './collections/PieceInstances' import './collections/RundownLayouts' import './collections/Snapshots' import './collections/Studios' diff --git a/meteor/server/__tests__/cronjobs.test.ts b/meteor/server/__tests__/cronjobs.test.ts index dc6b5d2af0..abe2a23c96 100644 --- a/meteor/server/__tests__/cronjobs.test.ts +++ b/meteor/server/__tests__/cronjobs.test.ts @@ -10,7 +10,7 @@ import { SYSTEM_ID } from '../../lib/collections/CoreSystem' import * as lib from '../../lib/lib' import { DBPart } from '../../lib/collections/Parts' import { PartInstance } from '../../lib/collections/PartInstances' -import { PieceInstance } from '../../lib/collections/PieceInstances' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { Meteor } from 'meteor/meteor' import { EmptyPieceTimelineObjectsBlob } from '@sofie-automation/corelib/dist/dataModel/Piece' import { diff --git a/meteor/server/publications/rundown.ts b/meteor/server/publications/rundown.ts index a7e47d55cc..9a23bec49d 100644 --- a/meteor/server/publications/rundown.ts +++ b/meteor/server/publications/rundown.ts @@ -8,7 +8,7 @@ import { RundownReadAccess } from '../security/rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart } from '../../lib/collections/Parts' import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' -import { PieceInstance } from '../../lib/collections/PieceInstances' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { DBPartInstance, PartInstance } from '../../lib/collections/PartInstances' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' import { NoSecurityReadAccess } from '../security/noSecurity' From fcc1fae3e660ac87b2382dfc4a552c0c277ab65e Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 4 Jul 2023 16:38:46 +0100 Subject: [PATCH 011/479] chore: tidy up Part collection rexports --- .../lib/__tests__/rundownTiming.test.ts | 34 +++++++++---------- .../client/ui/ClockView/PresenterScreen.tsx | 4 +-- .../InvalidFloatingInspector.tsx | 2 +- meteor/client/ui/RundownView.tsx | 4 +-- .../RundownTiming/RundownTimingProvider.tsx | 6 ++-- .../SegmentContainer/withResolvedSegment.ts | 4 +-- .../SegmentStoryboard/SegmentStoryboard.tsx | 2 +- .../Parts/InvalidPartCover.tsx | 2 +- .../Parts/SegmentTimelinePart.tsx | 9 +++-- .../ui/SegmentTimeline/SegmentContextMenu.tsx | 8 ++--- .../ui/SegmentTimeline/SegmentTimeline.tsx | 2 +- .../TriggeredActionsEditor.tsx | 6 ++-- meteor/client/ui/Shelf/AdLibPanel.tsx | 2 +- meteor/client/ui/Shelf/SegmentTimingPanel.tsx | 4 +-- meteor/lib/Rundown.ts | 2 +- meteor/lib/api/pubsub.ts | 2 +- .../triggers/actionFilterChainCompilers.ts | 2 +- meteor/lib/collections/PartInstances.ts | 2 +- meteor/lib/collections/Parts.ts | 5 --- meteor/lib/collections/libCollections.ts | 4 +-- meteor/lib/collections/rundownPlaylistUtil.ts | 13 ++++--- meteor/lib/main.ts | 1 - meteor/server/__tests__/cronjobs.test.ts | 2 +- meteor/server/collections/rundown.ts | 4 +-- meteor/server/publications/rundown.ts | 2 +- .../segmentPartNotesUI/publication.ts | 2 +- 26 files changed, 64 insertions(+), 66 deletions(-) delete mode 100644 meteor/lib/collections/Parts.ts diff --git a/meteor/client/lib/__tests__/rundownTiming.test.ts b/meteor/client/lib/__tests__/rundownTiming.test.ts index 01592cf562..be199a4f7d 100644 --- a/meteor/client/lib/__tests__/rundownTiming.test.ts +++ b/meteor/client/lib/__tests__/rundownTiming.test.ts @@ -1,6 +1,6 @@ import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PartInstance, wrapPartToTemporaryInstance } from '../../../lib/collections/PartInstances' -import { DBPart, Part } from '../../../lib/collections/Parts' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { literal, protectString } from '../../../lib/lib' @@ -42,7 +42,7 @@ function makeMockPart( displayDurationGroup?: string expectedDuration?: number } -): Part { +): DBPart { return literal({ _id: protectString(id), externalId: id, @@ -101,7 +101,7 @@ describe('rundown Timing Calculator', () => { it('Provides output for empty playlist', () => { const timing = new RundownTimingCalculator() const playlist: DBRundownPlaylist = makeMockPlaylist() - const parts: Part[] = [] + const parts: DBPart[] = [] const segmentsMap: Map = new Map() const partInstancesMap: Map = new Map() const result = timing.updateDurations( @@ -159,7 +159,7 @@ describe('rundown Timing Calculator', () => { const segmentsMap: Map = new Map() segmentsMap.set(protectString(segmentId1), makeMockSegment(segmentId1, 0, rundownId)) segmentsMap.set(protectString(segmentId2), makeMockSegment(segmentId2, 0, rundownId)) - const parts: Part[] = [] + const parts: DBPart[] = [] parts.push(makeMockPart('part1', 0, rundownId, segmentId1, { expectedDuration: 1000 })) parts.push(makeMockPart('part2', 0, rundownId, segmentId1, { expectedDuration: 1000 })) parts.push(makeMockPart('part3', 0, rundownId, segmentId2, { expectedDuration: 1000 })) @@ -261,7 +261,7 @@ describe('rundown Timing Calculator', () => { const segmentsMap: Map = new Map() segmentsMap.set(protectString(segmentId1), makeMockSegment(segmentId1, 0, rundownId)) segmentsMap.set(protectString(segmentId2), makeMockSegment(segmentId2, 0, rundownId)) - const parts: Part[] = [] + const parts: DBPart[] = [] parts.push(makeMockPart('part1', 0, rundownId, segmentId1, { expectedDuration: 1000 })) parts.push(makeMockPart('part2', 0, rundownId, segmentId1, { expectedDuration: 1000 })) parts.push(makeMockPart('part3', 0, rundownId, segmentId2, { expectedDuration: 1000 })) @@ -364,7 +364,7 @@ describe('rundown Timing Calculator', () => { const segmentsMap: Map = new Map() segmentsMap.set(protectString(segmentId1), makeMockSegment(segmentId1, 0, rundownId1)) segmentsMap.set(protectString(segmentId2), makeMockSegment(segmentId2, 0, rundownId2)) - const parts: Part[] = [] + const parts: DBPart[] = [] parts.push(makeMockPart('part1', 0, rundownId1, segmentId1, { expectedDuration: 1000 })) parts.push(makeMockPart('part2', 0, rundownId1, segmentId1, { expectedDuration: 1000 })) parts.push(makeMockPart('part3', 0, rundownId2, segmentId2, { expectedDuration: 1000 })) @@ -469,7 +469,7 @@ describe('rundown Timing Calculator', () => { const segmentsMap: Map = new Map() segmentsMap.set(protectString(segmentId1), makeMockSegment(segmentId1, 0, rundownId1)) segmentsMap.set(protectString(segmentId2), makeMockSegment(segmentId2, 0, rundownId1)) - const parts: Part[] = [] + const parts: DBPart[] = [] parts.push( makeMockPart('part1', 0, rundownId1, segmentId1, { expectedDuration: 1000, @@ -596,7 +596,7 @@ describe('rundown Timing Calculator', () => { const segmentsMap: Map = new Map() segmentsMap.set(protectString(segmentId1), makeMockSegment(segmentId1, 0, rundownId)) segmentsMap.set(protectString(segmentId2), makeMockSegment(segmentId2, 0, rundownId)) - const parts: Part[] = [] + const parts: DBPart[] = [] parts.push(makeMockPart('part1', 0, rundownId, segmentId1, { expectedDuration: 1000 })) parts.push(makeMockPart('part2', 0, rundownId, segmentId1, { expectedDuration: 1000 })) parts.push(makeMockPart('part3', 0, rundownId, segmentId2, { expectedDuration: 1000 })) @@ -698,7 +698,7 @@ describe('rundown Timing Calculator', () => { const segmentsMap: Map = new Map() segmentsMap.set(protectString(segmentId1), makeMockSegment(segmentId1, 0, rundownId1)) segmentsMap.set(protectString(segmentId2), makeMockSegment(segmentId2, 0, rundownId1)) - const parts: Part[] = [] + const parts: DBPart[] = [] parts.push( makeMockPart('part1', 0, rundownId1, segmentId1, { expectedDuration: 1000, @@ -837,7 +837,7 @@ describe('rundown Timing Calculator', () => { const segmentsMap: Map = new Map() segmentsMap.set(protectString(segmentId1), makeMockSegment(segmentId1, 0, rundownId1)) segmentsMap.set(protectString(segmentId2), makeMockSegment(segmentId2, 0, rundownId1)) - const parts: Part[] = [] + const parts: DBPart[] = [] parts.push( makeMockPart('part1', 0, rundownId1, segmentId1, { budgetDuration: 2000, @@ -957,7 +957,7 @@ describe('rundown Timing Calculator', () => { const segmentsMap: Map = new Map() segmentsMap.set(protectString(segmentId1), makeMockSegment(segmentId1, 0, rundownId)) segmentsMap.set(protectString(segmentId2), makeMockSegment(segmentId2, 0, rundownId)) - const parts: Part[] = [] + const parts: DBPart[] = [] parts.push(makeMockPart('part1', 0, rundownId, segmentId1, { expectedDuration: 1000 })) parts.push(makeMockPart('part2', 0, rundownId, segmentId1, { expectedDuration: 1000 })) parts.push(makeMockPart('part3', 0, rundownId, segmentId2, { expectedDuration: 1000 })) @@ -1084,7 +1084,7 @@ describe('rundown Timing Calculator', () => { protectString(segmentId2), makeMockSegment(segmentId2, 0, rundownId1, { expectedStart: 2000, expectedEnd: 4000 }) ) - const parts: Part[] = [] + const parts: DBPart[] = [] parts.push( makeMockPart('part1', 0, rundownId1, segmentId1, { expectedDuration: 1000, @@ -1233,7 +1233,7 @@ describe('rundown Timing Calculator', () => { protectString(segmentId2), makeMockSegment(segmentId2, 0, rundownId1, { expectedStart: 2000, expectedEnd: 4000 }) ) - const parts: Part[] = [] + const parts: DBPart[] = [] parts.push( makeMockPart('part1', 0, rundownId1, segmentId1, { expectedDuration: 1000, @@ -1382,7 +1382,7 @@ describe('rundown Timing Calculator', () => { protectString(segmentId2), makeMockSegment(segmentId2, 0, rundownId1, { expectedStart: 2000, expectedEnd: 3000 }) ) - const parts: Part[] = [] + const parts: DBPart[] = [] parts.push( makeMockPart('part1', 0, rundownId1, segmentId1, { expectedDuration: 1000, @@ -1537,7 +1537,7 @@ describe('rundown Timing Calculator', () => { protectString(segmentId2), makeMockSegment(segmentId2, 0, rundownId1, { expectedStart: 2000, expectedEnd: 4000 }) ) - const parts: Part[] = [] + const parts: DBPart[] = [] parts.push( makeMockPart('part1', 0, rundownId1, segmentId1, { expectedDuration: 1000, @@ -1686,7 +1686,7 @@ describe('rundown Timing Calculator', () => { protectString(segmentId2), makeMockSegment(segmentId2, 0, rundownId1, { expectedStart: 2000, expectedEnd: 4000 }) ) - const parts: Part[] = [] + const parts: DBPart[] = [] parts.push( makeMockPart('part1', 0, rundownId1, segmentId1, { expectedDuration: 1000, @@ -1835,7 +1835,7 @@ describe('rundown Timing Calculator', () => { protectString(segmentId2), makeMockSegment(segmentId2, 0, rundownId1, { expectedStart: 2000, expectedEnd: 3000 }) ) - const parts: Part[] = [] + const parts: DBPart[] = [] parts.push( makeMockPart('part1', 0, rundownId1, segmentId1, { expectedDuration: 1000, diff --git a/meteor/client/ui/ClockView/PresenterScreen.tsx b/meteor/client/ui/ClockView/PresenterScreen.tsx index 062761ab2e..2d507f6b8b 100644 --- a/meteor/client/ui/ClockView/PresenterScreen.tsx +++ b/meteor/client/ui/ClockView/PresenterScreen.tsx @@ -16,7 +16,7 @@ import { PieceNameContainer } from '../PieceIcons/PieceName' import { Timediff } from './Timediff' import { RundownUtils } from '../../lib/rundown' import { PieceLifespan } from '@sofie-automation/blueprints-integration' -import { Part } from '../../../lib/collections/Parts' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { PieceCountdownContainer } from '../PieceIcons/PieceCountdown' import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' import { DashboardLayout, RundownLayoutBase, RundownLayoutPresenterView } from '../../../lib/collections/RundownLayouts' @@ -84,7 +84,7 @@ function getShowStyleBaseIdSegmentPartUi( playlist: DBRundownPlaylist, orderedSegmentsAndParts: { segments: DBSegment[] - parts: Part[] + parts: DBPart[] }, pieces: Map, rundownsToShowstyles: Map, diff --git a/meteor/client/ui/FloatingInspectors/InvalidFloatingInspector.tsx b/meteor/client/ui/FloatingInspectors/InvalidFloatingInspector.tsx index 1a8a515d82..8550a79891 100644 --- a/meteor/client/ui/FloatingInspectors/InvalidFloatingInspector.tsx +++ b/meteor/client/ui/FloatingInspectors/InvalidFloatingInspector.tsx @@ -2,7 +2,7 @@ import React, { useRef } from 'react' import { useTranslation } from 'react-i18next' import { FloatingInspector } from '../FloatingInspector' -import { DBPart } from '../../../lib/collections/Parts' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { NoteSeverity } from '@sofie-automation/blueprints-integration' import { CriticalIconSmall, WarningIconSmall } from '../../lib/ui/icons/notifications' import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx index 337273590a..4e1d9d2584 100644 --- a/meteor/client/ui/RundownView.tsx +++ b/meteor/client/ui/RundownView.tsx @@ -19,7 +19,7 @@ import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/Rund import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { StudioRouteSet } from '@sofie-automation/corelib/dist/dataModel/Studio' -import { Part } from '../../lib/collections/Parts' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { ContextMenu, MenuItem, ContextMenuTrigger } from '@jstarpl/react-contextmenu' import { RundownTimingProvider } from './RundownView/RundownTiming/RundownTimingProvider' import { withTiming, WithTiming } from './RundownView/RundownTiming/withTiming' @@ -2034,7 +2034,7 @@ export const RundownView = translateWithTracker(( }) } - onSetNext = (part: Part | undefined, e: any, offset?: number, take?: boolean) => { + onSetNext = (part: DBPart | undefined, e: any, offset?: number, take?: boolean) => { const { t } = this.props if (this.state.studioMode && part && part._id && this.props.playlist) { const playlistId = this.props.playlist._id diff --git a/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx b/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx index e355fc9720..4b086ecd50 100644 --- a/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx +++ b/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx @@ -2,7 +2,7 @@ import React, { PropsWithChildren } from 'react' import { Meteor } from 'meteor/meteor' import * as PropTypes from 'prop-types' import { withTracker } from '../../../lib/ReactMeteorData/react-meteor-data' -import { Part } from '../../../../lib/collections/Parts' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { getCurrentTime } from '../../../../lib/lib' import { MeteorReactComponent } from '../../../lib/MeteorReactComponent' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' @@ -47,7 +47,7 @@ interface IRundownTimingProviderState {} interface IRundownTimingProviderTrackedProps { rundowns: Array currentRundown: Rundown | undefined - parts: Array + parts: Array partInstancesMap: Map pieces: Map segmentEntryPartInstances: PartInstance[] @@ -67,7 +67,7 @@ export const RundownTimingProvider = withTracker< IRundownTimingProviderTrackedProps >((props) => { let rundowns: Array = [] - let parts: Array = [] + let parts: Array = [] let segments: Array = [] const partInstancesMap = new Map() let pieces: Map = new Map() diff --git a/meteor/client/ui/SegmentContainer/withResolvedSegment.ts b/meteor/client/ui/SegmentContainer/withResolvedSegment.ts index 1f38f7e2f5..baf80d5323 100644 --- a/meteor/client/ui/SegmentContainer/withResolvedSegment.ts +++ b/meteor/client/ui/SegmentContainer/withResolvedSegment.ts @@ -15,7 +15,7 @@ import { equalSets } from '../../../lib/lib' import { RundownUtils } from '../../lib/rundown' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { PartInstance } from '../../../lib/collections/PartInstances' -import { Part } from '../../../lib/collections/Parts' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { slowDownReactivity } from '../../lib/reactiveData/reactiveDataHelper' import { memoizedIsolatedAutorun } from '../../../lib/memoizedIsolatedAutorun' import { getIsFilterActive } from '../../lib/rundownLayouts' @@ -180,7 +180,7 @@ export function withResolvedSegment( { fields: { _id: 1 }, } - ).parts as Pick[] + ).parts as Pick[] ).map((part) => part._id), 'playlist.getAllOrderedParts', props.playlist._id diff --git a/meteor/client/ui/SegmentStoryboard/SegmentStoryboard.tsx b/meteor/client/ui/SegmentStoryboard/SegmentStoryboard.tsx index 006d3a1ba0..b3342b28d7 100644 --- a/meteor/client/ui/SegmentStoryboard/SegmentStoryboard.tsx +++ b/meteor/client/ui/SegmentStoryboard/SegmentStoryboard.tsx @@ -8,7 +8,7 @@ import { CriticalIconSmall, WarningIconSmall } from '../../lib/ui/icons/notifica import { SegmentDuration } from '../RundownView/RundownTiming/SegmentDuration' import { PartCountdown } from '../RundownView/RundownTiming/PartCountdown' import { contextMenuHoldToDisplayTime, useCombinedRefs } from '../../lib/lib' -import { isPartPlayable } from '../../../lib/collections/Parts' +import { isPartPlayable } from '@sofie-automation/corelib/dist/dataModel/Part' import { useTranslation } from 'react-i18next' import { UIStateStorage } from '../../lib/UIStateStorage' import { literal, unprotectString } from '../../../lib/lib' diff --git a/meteor/client/ui/SegmentTimeline/Parts/InvalidPartCover.tsx b/meteor/client/ui/SegmentTimeline/Parts/InvalidPartCover.tsx index fe4203df12..c7433b48ef 100644 --- a/meteor/client/ui/SegmentTimeline/Parts/InvalidPartCover.tsx +++ b/meteor/client/ui/SegmentTimeline/Parts/InvalidPartCover.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react' -import { DBPart } from '../../../../lib/collections/Parts' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { InvalidFloatingInspector } from '../../FloatingInspectors/InvalidFloatingInspector' interface IProps { diff --git a/meteor/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx b/meteor/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx index 871a373126..99a8e7df73 100644 --- a/meteor/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx +++ b/meteor/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx @@ -28,7 +28,7 @@ import RundownViewEventBus, { import { LoopingIcon } from '../../../lib/ui/icons/looping' import { SegmentEnd } from '../../../lib/ui/icons/segment' import { getShowHiddenSourceLayers } from '../../../lib/localStorage' -import { Part } from '../../../../lib/collections/Parts' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { RundownTimingContext } from '../../../lib/rundownTiming' import { OutputGroup } from './OutputGroup' import { InvalidPartCover } from './InvalidPartCover' @@ -544,7 +544,12 @@ export class SegmentTimelinePartClass extends React.Component { + private renderEndOfSegment = ( + t: TFunction, + innerPart: DBPart, + isEndOfShow: boolean, + isEndOfLoopingShow?: boolean + ) => { const isNext = this.state.isLive && ((!this.props.isLastSegment && !this.props.isLastInSegment) || !!this.props.playlist.nextPartInfo) && diff --git a/meteor/client/ui/SegmentTimeline/SegmentContextMenu.tsx b/meteor/client/ui/SegmentTimeline/SegmentContextMenu.tsx index 80c84bee21..92cdf74fc0 100644 --- a/meteor/client/ui/SegmentTimeline/SegmentContextMenu.tsx +++ b/meteor/client/ui/SegmentTimeline/SegmentContextMenu.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import Escape from './../../lib/Escape' import { withTranslation } from 'react-i18next' import { ContextMenu, MenuItem } from '@jstarpl/react-contextmenu' -import { Part } from '../../../lib/collections/Parts' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData' import { RundownUtils } from '../../lib/rundown' @@ -11,7 +11,7 @@ import { PartUi, SegmentUi } from './SegmentTimelineContainer' import { SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' interface IProps { - onSetNext: (part: Part | undefined, e: any, offset?: number, take?: boolean) => void + onSetNext: (part: DBPart | undefined, e: any, offset?: number, take?: boolean) => void onSetNextSegment: (segmentId: SegmentId | null, e: any) => void playlist?: DBRundownPlaylist studioMode: boolean @@ -116,12 +116,12 @@ export const SegmentContextMenu = withTranslation()( } } - onSetAsNextFromHere = (part: Part, e) => { + onSetAsNextFromHere = (part: DBPart, e) => { const offset = this.getTimePosition() this.props.onSetNext(part, e, offset || 0) } - onPlayFromHere = (part: Part, e) => { + onPlayFromHere = (part: DBPart, e) => { const offset = this.getTimePosition() this.props.onSetNext(part, e, offset || 0, true) } diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx index 350eb4d8ae..987da50713 100644 --- a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx +++ b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx @@ -25,7 +25,7 @@ import { showPointerLockCursor, hidePointerLockCursor } from '../../lib/PointerL import { Settings } from '../../../lib/Settings' import { IContextMenuContext } from '../RundownView' import { literal, protectString, unprotectString } from '../../../lib/lib' -import { isPartPlayable } from '../../../lib/collections/Parts' +import { isPartPlayable } from '@sofie-automation/corelib/dist/dataModel/Part' import { contextMenuHoldToDisplayTime } from '../../lib/lib' import { WarningIconSmall, CriticalIconSmall } from '../../lib/ui/icons/notifications' import RundownViewEventBus, { RundownViewEvents, HighlightEvent } from '../../../lib/api/triggers/RundownViewEventBus' diff --git a/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx b/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx index 5a95674937..fe5f9e4053 100644 --- a/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx +++ b/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx @@ -10,7 +10,7 @@ import { literal, unprotectString } from '../../../../../lib/lib' import { TriggersHandler } from '../../../../lib/triggers/TriggersHandler' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { Part } from '../../../../../lib/collections/Parts' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { MeteorCall } from '../../../../../lib/api/methods' import { UploadButton } from '../../../../lib/uploadButton' import { ErrorBoundary } from '../../../../lib/ErrorBoundary' @@ -210,8 +210,8 @@ export const TriggeredActionsEditor: React.FC = function TriggeredAction const previewContext = useTracker( () => { - let thisCurrentPart: Part | null = null - let thisNextPart: Part | null = null + let thisCurrentPart: DBPart | null = null + let thisNextPart: DBPart | null = null let thisCurrentSegmentPartIds: PartId[] = [] let thisNextSegmentPartIds: PartId[] = [] if (rundownPlaylist) { diff --git a/meteor/client/ui/Shelf/AdLibPanel.tsx b/meteor/client/ui/Shelf/AdLibPanel.tsx index 1330c0f60f..640ddd22c3 100644 --- a/meteor/client/ui/Shelf/AdLibPanel.tsx +++ b/meteor/client/ui/Shelf/AdLibPanel.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' -import { DBPart } from '../../../lib/collections/Parts' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { IAdLibListItem } from './AdLibListItem' import ClassNames from 'classnames' diff --git a/meteor/client/ui/Shelf/SegmentTimingPanel.tsx b/meteor/client/ui/Shelf/SegmentTimingPanel.tsx index 6e9b6c2575..b6be98c3a9 100644 --- a/meteor/client/ui/Shelf/SegmentTimingPanel.tsx +++ b/meteor/client/ui/Shelf/SegmentTimingPanel.tsx @@ -15,7 +15,7 @@ import { SegmentDuration } from '../RundownView/RundownTiming/SegmentDuration' import { PartExtended } from '../../../lib/Rundown' import { memoizedIsolatedAutorun } from '../../../lib/memoizedIsolatedAutorun' import { slowDownReactivity } from '../../lib/reactiveData/reactiveDataHelper' -import { Part } from '../../../lib/collections/Parts' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { PartInstance } from '../../../lib/collections/PartInstances' import { dashboardElementStyle } from './DashboardPanel' import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts' @@ -115,7 +115,7 @@ export const SegmentTimingPanel = translateWithTracker< { fields: { _id: 1 }, } - ).parts as Pick[] + ).parts as Pick[] ).map((part) => part._id), 'playlist.getAllOrderedParts', props.playlist._id diff --git a/meteor/lib/Rundown.ts b/meteor/lib/Rundown.ts index 730b64fc22..7a277cafb4 100644 --- a/meteor/lib/Rundown.ts +++ b/meteor/lib/Rundown.ts @@ -2,7 +2,7 @@ import * as _ from 'underscore' import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { IOutputLayer, ISourceLayer, ITranslatableMessage } from '@sofie-automation/blueprints-integration' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' -import { DBPart } from './collections/Parts' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { PartInstance, wrapPartToTemporaryInstance } from './collections/PartInstances' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { diff --git a/meteor/lib/api/pubsub.ts b/meteor/lib/api/pubsub.ts index 4c81a2b569..7e32f040dd 100644 --- a/meteor/lib/api/pubsub.ts +++ b/meteor/lib/api/pubsub.ts @@ -29,7 +29,7 @@ import { MediaWorkFlowStep } from '@sofie-automation/shared-lib/dist/core/model/ import { DBOrganization } from '../collections/Organization' import { PackageContainerStatusDB } from '@sofie-automation/corelib/dist/dataModel/PackageContainerStatus' import { PartInstance } from '../collections/PartInstances' -import { DBPart } from '../collections/Parts' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { PeripheralDeviceCommand } from '../collections/PeripheralDeviceCommands' import { PeripheralDevice } from '../collections/PeripheralDevices' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' diff --git a/meteor/lib/api/triggers/actionFilterChainCompilers.ts b/meteor/lib/api/triggers/actionFilterChainCompilers.ts index 4ed45d9f97..6af03bb1dc 100644 --- a/meteor/lib/api/triggers/actionFilterChainCompilers.ts +++ b/meteor/lib/api/triggers/actionFilterChainCompilers.ts @@ -10,7 +10,7 @@ import { } from '@sofie-automation/blueprints-integration' import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' -import { DBPart } from '../../collections/Parts' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' diff --git a/meteor/lib/collections/PartInstances.ts b/meteor/lib/collections/PartInstances.ts index f88d6c60f6..02339b7610 100644 --- a/meteor/lib/collections/PartInstances.ts +++ b/meteor/lib/collections/PartInstances.ts @@ -1,5 +1,5 @@ import { protectString, unprotectString } from '../lib' -import { DBPart } from './Parts' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { PartId, RundownPlaylistActivationId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' diff --git a/meteor/lib/collections/Parts.ts b/meteor/lib/collections/Parts.ts deleted file mode 100644 index b8c270d158..0000000000 --- a/meteor/lib/collections/Parts.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' -export * from '@sofie-automation/corelib/dist/dataModel/Part' - -/** Note: Use Part instead */ -export type Part = DBPart diff --git a/meteor/lib/collections/libCollections.ts b/meteor/lib/collections/libCollections.ts index f7b78bffdc..9192074ad2 100644 --- a/meteor/lib/collections/libCollections.ts +++ b/meteor/lib/collections/libCollections.ts @@ -9,7 +9,7 @@ import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { createSyncMongoCollection, createSyncReadOnlyMongoCollection } from './lib' import { DBOrganization } from './Organization' import { PartInstance } from './PartInstances' -import { Part } from './Parts' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' @@ -30,7 +30,7 @@ export const Pieces = createSyncReadOnlyMongoCollection(CollectionName.Pi export const PartInstances = createSyncReadOnlyMongoCollection(CollectionName.PartInstances) -export const Parts = createSyncReadOnlyMongoCollection(CollectionName.Parts) +export const Parts = createSyncReadOnlyMongoCollection(CollectionName.Parts) export const RundownBaselineAdLibActions = createSyncReadOnlyMongoCollection( CollectionName.RundownBaselineAdLibActions diff --git a/meteor/lib/collections/rundownPlaylistUtil.ts b/meteor/lib/collections/rundownPlaylistUtil.ts index 7cf98f43f1..7edb062387 100644 --- a/meteor/lib/collections/rundownPlaylistUtil.ts +++ b/meteor/lib/collections/rundownPlaylistUtil.ts @@ -15,7 +15,6 @@ import _ from 'underscore' import { Rundowns, Segments, Parts, PartInstances, Pieces } from './libCollections' import { FindOptions } from './lib' import { PartInstance } from './PartInstances' -import { Part } from './Parts' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' @@ -150,9 +149,9 @@ export class RundownPlaylistCollectionUtil { } static getUnorderedParts( playlist: Pick, - selector?: MongoQuery, - options?: FindOptions - ): Part[] { + selector?: MongoQuery, + options?: FindOptions + ): DBPart[] { const rundownIds = RundownPlaylistCollectionUtil.getRundownUnorderedIDs(playlist) const parts = Parts.find( { @@ -178,7 +177,7 @@ export class RundownPlaylistCollectionUtil { partsQuery?: MongoQuery, segmentsOptions?: Omit, 'projection'>, // We are mangling fields, so block projection partsOptions?: Omit, 'projection'> // We are mangling fields, so block projection - ): { segments: DBSegment[]; parts: Part[] } { + ): { segments: DBSegment[]; parts: DBPart[] } { const rundownIds = RundownPlaylistCollectionUtil.getRundownUnorderedIDs(playlist) const segments = Segments.find( { @@ -358,10 +357,10 @@ export class RundownPlaylistCollectionUtil { return Array.from(rundownsMap.values()) } static _sortParts( - parts: Part[], + parts: DBPart[], playlist: Pick, segments: Array> - ): Part[] { + ): DBPart[] { return sortPartsInSegments(parts, playlist.rundownIdsInOrder, segments) } static _sortPartsInner

>( diff --git a/meteor/lib/main.ts b/meteor/lib/main.ts index 200a6aa458..d8ccd2c910 100644 --- a/meteor/lib/main.ts +++ b/meteor/lib/main.ts @@ -10,7 +10,6 @@ import './collections/ExpectedPackages' import './collections/ExternalMessageQueue' import './collections/Organization' import './collections/PartInstances' -import './collections/Parts' import './collections/PeripheralDeviceCommands' import './collections/PeripheralDevices' import './collections/RundownLayouts' diff --git a/meteor/server/__tests__/cronjobs.test.ts b/meteor/server/__tests__/cronjobs.test.ts index abe2a23c96..edbeca899f 100644 --- a/meteor/server/__tests__/cronjobs.test.ts +++ b/meteor/server/__tests__/cronjobs.test.ts @@ -8,7 +8,7 @@ import { IBlueprintPieceType, PieceLifespan, StatusCode, TSR } from '@sofie-auto import { PeripheralDeviceType, PeripheralDeviceCategory } from '../../lib/collections/PeripheralDevices' import { SYSTEM_ID } from '../../lib/collections/CoreSystem' import * as lib from '../../lib/lib' -import { DBPart } from '../../lib/collections/Parts' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { PartInstance } from '../../lib/collections/PartInstances' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { Meteor } from 'meteor/meteor' diff --git a/meteor/server/collections/rundown.ts b/meteor/server/collections/rundown.ts index 1df2de09ac..6562898ffc 100644 --- a/meteor/server/collections/rundown.ts +++ b/meteor/server/collections/rundown.ts @@ -10,7 +10,7 @@ import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataMod import { RundownBaselineObj } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineObj' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PartInstance } from '../../lib/collections/PartInstances' -import { Part } from '../../lib/collections/Parts' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { createAsyncOnlyReadOnlyMongoCollection } from './collection' import { registerIndex } from './indices' @@ -60,7 +60,7 @@ registerIndex(PartInstances, { reset: 1, }) -export const Parts = createAsyncOnlyReadOnlyMongoCollection(CollectionName.Parts) +export const Parts = createAsyncOnlyReadOnlyMongoCollection(CollectionName.Parts) registerIndex(Parts, { rundownId: 1, segmentId: 1, diff --git a/meteor/server/publications/rundown.ts b/meteor/server/publications/rundown.ts index 9a23bec49d..3284d5f1b9 100644 --- a/meteor/server/publications/rundown.ts +++ b/meteor/server/publications/rundown.ts @@ -6,7 +6,7 @@ import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { RundownReadAccess } from '../security/rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' -import { DBPart } from '../../lib/collections/Parts' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { DBPartInstance, PartInstance } from '../../lib/collections/PartInstances' diff --git a/meteor/server/publications/segmentPartNotesUI/publication.ts b/meteor/server/publications/segmentPartNotesUI/publication.ts index 64f95307a1..65c295403f 100644 --- a/meteor/server/publications/segmentPartNotesUI/publication.ts +++ b/meteor/server/publications/segmentPartNotesUI/publication.ts @@ -4,7 +4,7 @@ import { ReadonlyDeep } from 'type-fest' import { CustomCollectionName, PubSub } from '../../../lib/api/pubsub' import { UISegmentPartNote } from '../../../lib/api/rundownNotifications' import { DBPartInstance } from '../../../lib/collections/PartInstances' -import { DBPart } from '../../../lib/collections/Parts' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { groupByToMap, literal, normalizeArrayToMap, protectString } from '../../../lib/lib' From f60ba7952b2b3fc560c7f5ca74dfe8459e747e3a Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 4 Jul 2023 16:40:55 +0100 Subject: [PATCH 012/479] chore: tidy up PartInstance collection rexports --- meteor/__mocks__/defaultCollectionObjects.ts | 2 +- meteor/__mocks__/helpers/database.ts | 2 +- meteor/client/lib/rundownTiming.ts | 8 ++++---- meteor/client/ui/Shelf/BucketPanel.tsx | 4 ++-- meteor/lib/collections/PartInstances.ts | 1 - meteor/server/api/deviceTriggers/StudioObserver.ts | 2 +- meteor/server/publications/rundown.ts | 3 ++- .../server/publications/segmentPartNotesUI/publication.ts | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/meteor/__mocks__/defaultCollectionObjects.ts b/meteor/__mocks__/defaultCollectionObjects.ts index 3ce882418b..857182a9dc 100644 --- a/meteor/__mocks__/defaultCollectionObjects.ts +++ b/meteor/__mocks__/defaultCollectionObjects.ts @@ -3,7 +3,7 @@ import { clone, getCurrentTime, unprotectString } from '../lib/lib' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' -import { DBPart } from '../lib/collections/Parts' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { IBlueprintPieceType, PieceLifespan } from '@sofie-automation/blueprints-integration' import { Piece, EmptyPieceTimelineObjectsBlob } from '@sofie-automation/corelib/dist/dataModel/Piece' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' diff --git a/meteor/__mocks__/helpers/database.ts b/meteor/__mocks__/helpers/database.ts index 956158b497..f65badb99d 100644 --- a/meteor/__mocks__/helpers/database.ts +++ b/meteor/__mocks__/helpers/database.ts @@ -50,7 +50,7 @@ import { } from '../../lib/lib' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' -import { DBPart } from '../../lib/collections/Parts' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { EmptyPieceTimelineObjectsBlob, Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' diff --git a/meteor/client/lib/rundownTiming.ts b/meteor/client/lib/rundownTiming.ts index 014bc3b991..72d1a70efd 100644 --- a/meteor/client/lib/rundownTiming.ts +++ b/meteor/client/lib/rundownTiming.ts @@ -24,7 +24,7 @@ import { PartInstance, wrapPartToTemporaryInstance, } from '../../lib/collections/PartInstances' -import { Part } from '../../lib/collections/Parts' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { getCurrentTime, objectFromEntries } from '../../lib/lib' import { Settings } from '../../lib/Settings' @@ -85,7 +85,7 @@ export class RundownTimingCalculator { * @param {(DBRundownPlaylist | undefined)} playlist * @param {Rundown[]} rundowns * @param {(Rundown | undefined)} currentRundown - * @param {Part[]} parts + * @param {DBPart[]} parts * @param {Map} partInstancesMap * @param {number} [defaultDuration] * @return {*} {RundownTimingContext} @@ -97,7 +97,7 @@ export class RundownTimingCalculator { playlist: DBRundownPlaylist | undefined, rundowns: Rundown[], currentRundown: Rundown | undefined, - parts: Part[], + parts: DBPart[], partInstancesMap: Map, pieces: Map, segmentsMap: Map, @@ -658,7 +658,7 @@ export class RundownTimingCalculator { this.temporaryPartInstances.clear() } - private getPartInstanceOrGetCachedTemp(partInstancesMap: Map, part: Part): PartInstance { + private getPartInstanceOrGetCachedTemp(partInstancesMap: Map, part: DBPart): PartInstance { const origPartId = part._id const partInstance = partInstancesMap.get(origPartId) if (partInstance !== undefined) { diff --git a/meteor/client/ui/Shelf/BucketPanel.tsx b/meteor/client/ui/Shelf/BucketPanel.tsx index 6317d0ac2b..d069bb0fb9 100644 --- a/meteor/client/ui/Shelf/BucketPanel.tsx +++ b/meteor/client/ui/Shelf/BucketPanel.tsx @@ -38,7 +38,7 @@ import { DragDropItemTypes } from '../DragDropItemTypes' import { BucketPieceButton, IBucketPieceDropResult } from './BucketPieceButton' import { ContextMenuTrigger } from '@jstarpl/react-contextmenu' import update from 'immutability-helper' -import { PartInstance, DBPartInstance } from '../../../lib/collections/PartInstances' +import { PartInstance } from '../../../lib/collections/PartInstances' import { BucketAdLibAction } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibAction' import { RundownUtils } from '../../lib/rundown' import { BucketAdLibItem, BucketAdLibActionUi, isAdLibAction, isAdLib, BucketAdLibUi } from './RundownViewBuckets' @@ -273,7 +273,7 @@ export const BucketPanel = translateWithTracker, I const selectedPart = props.playlist.currentPartInfo?.partInstanceId || props.playlist.nextPartInfo?.partInstanceId if (selectedPart) { const part = PartInstances.findOne(selectedPart, { - fields: literal>({ + fields: literal>({ rundownId: 1, //@ts-expect-error deep property 'part._id': 1, diff --git a/meteor/lib/collections/PartInstances.ts b/meteor/lib/collections/PartInstances.ts index 02339b7610..34a38519cd 100644 --- a/meteor/lib/collections/PartInstances.ts +++ b/meteor/lib/collections/PartInstances.ts @@ -3,7 +3,6 @@ import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { PartId, RundownPlaylistActivationId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' -export * from '@sofie-automation/corelib/dist/dataModel/PartInstance' export interface PartInstance extends DBPartInstance { isTemporary: boolean diff --git a/meteor/server/api/deviceTriggers/StudioObserver.ts b/meteor/server/api/deviceTriggers/StudioObserver.ts index bbb3215657..c733ff248d 100644 --- a/meteor/server/api/deviceTriggers/StudioObserver.ts +++ b/meteor/server/api/deviceTriggers/StudioObserver.ts @@ -11,7 +11,7 @@ import EventEmitter from 'events' import { Meteor } from 'meteor/meteor' import _ from 'underscore' import { MongoCursor } from '../../../lib/collections/lib' -import { DBPartInstance } from '../../../lib/collections/PartInstances' +import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' diff --git a/meteor/server/publications/rundown.ts b/meteor/server/publications/rundown.ts index 3284d5f1b9..df239e7973 100644 --- a/meteor/server/publications/rundown.ts +++ b/meteor/server/publications/rundown.ts @@ -9,7 +9,7 @@ import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { DBPartInstance, PartInstance } from '../../lib/collections/PartInstances' +import { PartInstance } from '../../lib/collections/PartInstances' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' import { NoSecurityReadAccess } from '../security/noSecurity' import { OrganizationReadAccess } from '../security/organization' @@ -45,6 +45,7 @@ import { } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ExpectedMediaItem } from '@sofie-automation/corelib/dist/dataModel/ExpectedMediaItem' import { ExpectedPlayoutItem } from '@sofie-automation/corelib/dist/dataModel/ExpectedPlayoutItem' +import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' meteorPublish(PubSub.rundownsForDevice, async function (deviceId, token: string | undefined) { check(deviceId, String) diff --git a/meteor/server/publications/segmentPartNotesUI/publication.ts b/meteor/server/publications/segmentPartNotesUI/publication.ts index 65c295403f..2c7efa2c12 100644 --- a/meteor/server/publications/segmentPartNotesUI/publication.ts +++ b/meteor/server/publications/segmentPartNotesUI/publication.ts @@ -3,7 +3,7 @@ import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mo import { ReadonlyDeep } from 'type-fest' import { CustomCollectionName, PubSub } from '../../../lib/api/pubsub' import { UISegmentPartNote } from '../../../lib/api/rundownNotifications' -import { DBPartInstance } from '../../../lib/collections/PartInstances' +import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' From cb19f3356aedac3151c807152e68f746beaeb81e Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 4 Jul 2023 16:42:46 +0100 Subject: [PATCH 013/479] chore: tidy up Blueprint collection rexports --- meteor/__mocks__/helpers/database.ts | 2 +- meteor/client/ui/Settings/BlueprintSettings.tsx | 2 +- meteor/client/ui/Settings/SettingsMenu.tsx | 2 +- meteor/lib/api/pubsub.ts | 2 +- meteor/lib/collections/Blueprints.ts | 2 -- meteor/lib/main.ts | 1 - meteor/server/api/blueprints/__tests__/api.test.ts | 2 +- meteor/server/api/blueprints/__tests__/lib.ts | 2 +- meteor/server/api/blueprints/api.ts | 2 +- meteor/server/api/blueprints/cache.ts | 2 +- meteor/server/api/rundown.ts | 2 +- meteor/server/api/snapshot.ts | 2 +- meteor/server/coreSystem/checkDatabaseVersions.ts | 2 +- meteor/server/publications/organization.ts | 2 +- meteor/server/security/organization.ts | 2 +- 15 files changed, 13 insertions(+), 16 deletions(-) delete mode 100644 meteor/lib/collections/Blueprints.ts diff --git a/meteor/__mocks__/helpers/database.ts b/meteor/__mocks__/helpers/database.ts index f65badb99d..e780a7fa30 100644 --- a/meteor/__mocks__/helpers/database.ts +++ b/meteor/__mocks__/helpers/database.ts @@ -35,7 +35,7 @@ import { } from '@sofie-automation/blueprints-integration' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' -import { Blueprint } from '../../lib/collections/Blueprints' +import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { ICoreSystem, SYSTEM_ID, stripVersion } from '../../lib/collections/CoreSystem' import { internalUploadBlueprint } from '../../server/api/blueprints/api' import { diff --git a/meteor/client/ui/Settings/BlueprintSettings.tsx b/meteor/client/ui/Settings/BlueprintSettings.tsx index 217395ea64..561b4bcd91 100644 --- a/meteor/client/ui/Settings/BlueprintSettings.tsx +++ b/meteor/client/ui/Settings/BlueprintSettings.tsx @@ -4,7 +4,7 @@ import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/reac import { Spinner } from '../../lib/Spinner' import { doModalDialog } from '../../lib/ModalDialog' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' -import { Blueprint } from '../../../lib/collections/Blueprints' +import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import Moment from 'react-moment' import { Link } from 'react-router-dom' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' diff --git a/meteor/client/ui/Settings/SettingsMenu.tsx b/meteor/client/ui/Settings/SettingsMenu.tsx index d882d00c87..afabd802d8 100644 --- a/meteor/client/ui/Settings/SettingsMenu.tsx +++ b/meteor/client/ui/Settings/SettingsMenu.tsx @@ -13,7 +13,7 @@ import { faPlus, faTrash, faExclamationTriangle, faCaretRight, faCaretDown } fro import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' -import { Blueprint } from '../../../lib/collections/Blueprints' +import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { PubSub, meteorSubscribe } from '../../../lib/api/pubsub' import { MeteorCall } from '../../../lib/api/methods' import { Settings as MeteorSettings } from '../../../lib/Settings' diff --git a/meteor/lib/api/pubsub.ts b/meteor/lib/api/pubsub.ts index 7e32f040dd..6b2cf5d116 100644 --- a/meteor/lib/api/pubsub.ts +++ b/meteor/lib/api/pubsub.ts @@ -13,7 +13,7 @@ import { DBTimelineDatastoreEntry } from '@sofie-automation/corelib/dist/dataMod import { Meteor } from 'meteor/meteor' import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' -import { Blueprint } from '../collections/Blueprints' +import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { BucketAdLibAction } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibAction' import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' import { Bucket } from '../collections/Buckets' diff --git a/meteor/lib/collections/Blueprints.ts b/meteor/lib/collections/Blueprints.ts deleted file mode 100644 index 47c66463fe..0000000000 --- a/meteor/lib/collections/Blueprints.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' -export { Blueprint } diff --git a/meteor/lib/main.ts b/meteor/lib/main.ts index d8ccd2c910..92a090a7f3 100644 --- a/meteor/lib/main.ts +++ b/meteor/lib/main.ts @@ -2,7 +2,6 @@ * This file is the entry-point for both server side and client side code */ -import './collections/Blueprints' import './collections/Buckets' import './collections/CoreSystem' import './collections/Evaluations' diff --git a/meteor/server/api/blueprints/__tests__/api.test.ts b/meteor/server/api/blueprints/__tests__/api.test.ts index 1aff6221ba..cf949eeb0f 100644 --- a/meteor/server/api/blueprints/__tests__/api.test.ts +++ b/meteor/server/api/blueprints/__tests__/api.test.ts @@ -2,7 +2,7 @@ import * as _ from 'underscore' import { setupDefaultStudioEnvironment, packageBlueprint } from '../../../../__mocks__/helpers/database' import { testInFiber } from '../../../../__mocks__/helpers/jest' import { literal, getRandomId, protectString } from '../../../../lib/lib' -import { Blueprint } from '../../../../lib/collections/Blueprints' +import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { BlueprintManifestType } from '@sofie-automation/blueprints-integration' import { SYSTEM_ID, ICoreSystem } from '../../../../lib/collections/CoreSystem' import { insertBlueprint, uploadBlueprint } from '../api' diff --git a/meteor/server/api/blueprints/__tests__/lib.ts b/meteor/server/api/blueprints/__tests__/lib.ts index c8ad003ece..f8ebb094be 100644 --- a/meteor/server/api/blueprints/__tests__/lib.ts +++ b/meteor/server/api/blueprints/__tests__/lib.ts @@ -1,6 +1,6 @@ import { BlueprintManifestType, SomeBlueprintManifest } from '@sofie-automation/blueprints-integration' import { getRandomId, literal, protectString } from '../../../../lib/lib' -import { Blueprint } from '../../../../lib/collections/Blueprints' +import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { JSONBlobStringify } from '@sofie-automation/shared-lib/dist/lib/JSONBlob' export function generateFakeBlueprint( diff --git a/meteor/server/api/blueprints/api.ts b/meteor/server/api/blueprints/api.ts index 71f0bcdd39..f2b6064f70 100644 --- a/meteor/server/api/blueprints/api.ts +++ b/meteor/server/api/blueprints/api.ts @@ -4,7 +4,7 @@ import { promises as fsp } from 'fs' import { getCurrentTime, unprotectString, getRandomId } from '../../../lib/lib' import { logger } from '../../logging' import { Meteor } from 'meteor/meteor' -import { Blueprint } from '../../../lib/collections/Blueprints' +import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { BlueprintManifestType, IShowStyleConfigPreset, diff --git a/meteor/server/api/blueprints/cache.ts b/meteor/server/api/blueprints/cache.ts index 019859a804..b1d96ace53 100644 --- a/meteor/server/api/blueprints/cache.ts +++ b/meteor/server/api/blueprints/cache.ts @@ -1,7 +1,7 @@ import * as _ from 'underscore' import { VM, VMScript } from 'vm2' import { logger } from '../../logging' -import { Blueprint } from '../../../lib/collections/Blueprints' +import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { SomeBlueprintManifest } from '@sofie-automation/blueprints-integration' import { stringifyError } from '@sofie-automation/corelib/dist/lib' diff --git a/meteor/server/api/rundown.ts b/meteor/server/api/rundown.ts index 5430620f9c..1848066610 100644 --- a/meteor/server/api/rundown.ts +++ b/meteor/server/api/rundown.ts @@ -13,7 +13,7 @@ import { StudioContentWriteAccess } from '../security/studio' import { runIngestOperation } from './ingest/lib' import { IngestJobs } from '@sofie-automation/corelib/dist/worker/ingest' import { VerifiedRundownContentAccess, VerifiedRundownPlaylistContentAccess } from './lib' -import { Blueprint } from '../../lib/collections/Blueprints' +import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { Blueprints, Rundowns, ShowStyleBases, ShowStyleVariants, Studios } from '../collections' diff --git a/meteor/server/api/snapshot.ts b/meteor/server/api/snapshot.ts index 68bb91d754..c8f2253bf9 100644 --- a/meteor/server/api/snapshot.ts +++ b/meteor/server/api/snapshot.ts @@ -39,7 +39,7 @@ import { ICoreSystem, parseVersion } from '../../lib/collections/CoreSystem' import { CURRENT_SYSTEM_VERSION } from '../migration/currentSystemVersion' import { isVersionSupported } from '../migration/databaseMigration' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' -import { Blueprint } from '../../lib/collections/Blueprints' +import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { IngestRundown, VTContent } from '@sofie-automation/blueprints-integration' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { importIngestRundown } from './ingest/http' diff --git a/meteor/server/coreSystem/checkDatabaseVersions.ts b/meteor/server/coreSystem/checkDatabaseVersions.ts index b4bf362402..c75173729c 100644 --- a/meteor/server/coreSystem/checkDatabaseVersions.ts +++ b/meteor/server/coreSystem/checkDatabaseVersions.ts @@ -1,7 +1,7 @@ import { StatusCode } from '@sofie-automation/blueprints-integration' import { BlueprintId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' -import { Blueprint } from '../../lib/collections/Blueprints' +import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { Blueprints, ShowStyleBases, Studios } from '../collections' import { parseVersion, diff --git a/meteor/server/publications/organization.ts b/meteor/server/publications/organization.ts index 1548658f3c..6bc5127a00 100644 --- a/meteor/server/publications/organization.ts +++ b/meteor/server/publications/organization.ts @@ -1,6 +1,6 @@ import { meteorPublish, AutoFillSelector } from './lib' import { PubSub } from '../../lib/api/pubsub' -import { Blueprint } from '../../lib/collections/Blueprints' +import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { Evaluation } from '../../lib/collections/Evaluations' import { SnapshotItem } from '../../lib/collections/Snapshots' import { UserActionsLogItem } from '../../lib/collections/UserActionsLog' diff --git a/meteor/server/security/organization.ts b/meteor/server/security/organization.ts index 71a0ebda93..45d13853fc 100644 --- a/meteor/server/security/organization.ts +++ b/meteor/server/security/organization.ts @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor' import { SnapshotItem } from '../../lib/collections/Snapshots' -import { Blueprint } from '../../lib/collections/Blueprints' +import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { logNotAllowed } from './lib/lib' import { MongoQueryKey } from '@sofie-automation/corelib/dist/mongo' import { allowAccessToOrganization } from './lib/security' From 9f9e610a543ab76165de3b4c76ffd429fa024507 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 4 Jul 2023 16:43:28 +0100 Subject: [PATCH 014/479] chore: tidy up ExternalMessageQueue collection rexports --- meteor/client/ui/Status/ExternalMessages.tsx | 2 +- meteor/lib/api/pubsub.ts | 2 +- meteor/lib/collections/ExternalMessageQueue.ts | 1 - meteor/lib/main.ts | 1 - meteor/server/api/__tests__/externalMessageQueue.test.ts | 2 +- meteor/server/publications/studio.ts | 2 +- meteor/server/security/studio.ts | 2 +- 7 files changed, 5 insertions(+), 7 deletions(-) delete mode 100644 meteor/lib/collections/ExternalMessageQueue.ts diff --git a/meteor/client/ui/Status/ExternalMessages.tsx b/meteor/client/ui/Status/ExternalMessages.tsx index a2fcaddd67..ff7d18dd2e 100644 --- a/meteor/client/ui/Status/ExternalMessages.tsx +++ b/meteor/client/ui/Status/ExternalMessages.tsx @@ -6,7 +6,7 @@ import { MomentFromNow } from '../../lib/Moment' import { getAllowConfigure } from '../../lib/localStorage' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import * as _ from 'underscore' -import { ExternalMessageQueueObj } from '../../../lib/collections/ExternalMessageQueue' +import { ExternalMessageQueueObj } from '@sofie-automation/corelib/dist/dataModel/ExternalMessageQueue' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { makeTableOfObject } from '../../lib/utilComponents' import ClassNames from 'classnames' diff --git a/meteor/lib/api/pubsub.ts b/meteor/lib/api/pubsub.ts index 6b2cf5d116..1b781aeae1 100644 --- a/meteor/lib/api/pubsub.ts +++ b/meteor/lib/api/pubsub.ts @@ -23,7 +23,7 @@ import { ExpectedMediaItem } from '@sofie-automation/corelib/dist/dataModel/Expe import { ExpectedPackageDB } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' import { ExpectedPackageWorkStatus } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackageWorkStatuses' import { ExpectedPlayoutItem } from '@sofie-automation/corelib/dist/dataModel/ExpectedPlayoutItem' -import { ExternalMessageQueueObj } from '../collections/ExternalMessageQueue' +import { ExternalMessageQueueObj } from '@sofie-automation/corelib/dist/dataModel/ExternalMessageQueue' import { MediaWorkFlow } from '@sofie-automation/shared-lib/dist/core/model/MediaWorkFlows' import { MediaWorkFlowStep } from '@sofie-automation/shared-lib/dist/core/model/MediaWorkFlowSteps' import { DBOrganization } from '../collections/Organization' diff --git a/meteor/lib/collections/ExternalMessageQueue.ts b/meteor/lib/collections/ExternalMessageQueue.ts deleted file mode 100644 index c8d19369e0..0000000000 --- a/meteor/lib/collections/ExternalMessageQueue.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '@sofie-automation/corelib/dist/dataModel/ExternalMessageQueue' diff --git a/meteor/lib/main.ts b/meteor/lib/main.ts index 92a090a7f3..151d31c293 100644 --- a/meteor/lib/main.ts +++ b/meteor/lib/main.ts @@ -6,7 +6,6 @@ import './collections/Buckets' import './collections/CoreSystem' import './collections/Evaluations' import './collections/ExpectedPackages' -import './collections/ExternalMessageQueue' import './collections/Organization' import './collections/PartInstances' import './collections/PeripheralDeviceCommands' diff --git a/meteor/server/api/__tests__/externalMessageQueue.test.ts b/meteor/server/api/__tests__/externalMessageQueue.test.ts index 949b4bbf64..8abb284a9a 100644 --- a/meteor/server/api/__tests__/externalMessageQueue.test.ts +++ b/meteor/server/api/__tests__/externalMessageQueue.test.ts @@ -1,5 +1,5 @@ import '../../../__mocks__/_extendJest' -import { ExternalMessageQueueObj } from '../../../lib/collections/ExternalMessageQueue' +import { ExternalMessageQueueObj } from '@sofie-automation/corelib/dist/dataModel/ExternalMessageQueue' import { ExternalMessageQueue, RundownPlaylists, Rundowns } from '../../collections' import { IBlueprintExternalMessageQueueType, PlaylistTimingType } from '@sofie-automation/blueprints-integration' import { testInFiber } from '../../../__mocks__/helpers/jest' diff --git a/meteor/server/publications/studio.ts b/meteor/server/publications/studio.ts index e4ecc0b742..561d9a8182 100644 --- a/meteor/server/publications/studio.ts +++ b/meteor/server/publications/studio.ts @@ -4,7 +4,7 @@ import { meteorPublish, AutoFillSelector } from './lib' import { CustomCollectionName, PubSub } from '../../lib/api/pubsub' import { getActiveRoutes, getRoutedMappings } from '../../lib/collections/Studios' import { PeripheralDeviceReadAccess } from '../security/peripheralDevice' -import { ExternalMessageQueueObj } from '../../lib/collections/ExternalMessageQueue' +import { ExternalMessageQueueObj } from '@sofie-automation/corelib/dist/dataModel/ExternalMessageQueue' import { StudioReadAccess } from '../security/studio' import { OrganizationReadAccess } from '../security/organization' import { NoSecurityReadAccess } from '../security/noSecurity' diff --git a/meteor/server/security/studio.ts b/meteor/server/security/studio.ts index 4928d6a11a..f05b325fdb 100644 --- a/meteor/server/security/studio.ts +++ b/meteor/server/security/studio.ts @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor' import { allowAccessToStudio } from './lib/security' import { MongoQueryKey } from '@sofie-automation/corelib/dist/mongo' import { logNotAllowed } from './lib/lib' -import { ExternalMessageQueueObj } from '../../lib/collections/ExternalMessageQueue' +import { ExternalMessageQueueObj } from '@sofie-automation/corelib/dist/dataModel/ExternalMessageQueue' import { Credentials, ResolvedCredentials, resolveCredentials } from './lib/credentials' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { Settings } from '../../lib/Settings' From c8429265ad6a3b5ce9b568177129c4a8048b0b9e Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 4 Jul 2023 16:45:22 +0100 Subject: [PATCH 015/479] chore: tidy up PeripheralDevice collection rexports --- meteor/__mocks__/helpers/database.ts | 2 +- meteor/client/collections/index.ts | 2 +- meteor/client/lib/clientAPI.ts | 2 +- meteor/client/lib/reactiveData/reactiveData.ts | 2 +- meteor/client/ui/RundownView.tsx | 2 +- meteor/client/ui/RundownView/RundownNotifier.tsx | 2 +- meteor/client/ui/RundownView/RundownSystemStatus.tsx | 2 +- meteor/client/ui/Settings/DeviceSettings.tsx | 2 +- meteor/client/ui/Settings/SettingsMenu.tsx | 2 +- meteor/client/ui/Settings/Studio/Devices/SelectDevices.tsx | 2 +- meteor/client/ui/Settings/StudioSettings.tsx | 2 +- .../ui/Settings/components/ConfigManifestOAuthFlow.tsx | 2 +- .../Settings/components/GenericDeviceSettingsComponent.tsx | 2 +- meteor/client/ui/Status/SystemStatus.tsx | 2 +- meteor/client/ui/Status/package-status/index.tsx | 2 +- meteor/lib/api/pubsub.ts | 4 ++-- meteor/lib/collections/PeripheralDeviceCommands.ts | 1 - meteor/lib/main.ts | 2 -- meteor/server/__tests__/cronjobs.test.ts | 5 ++++- meteor/server/api/__tests__/client.test.ts | 4 ++-- meteor/server/api/__tests__/peripheralDevice.test.ts | 2 +- meteor/server/api/__tests__/userActions/system.test.ts | 2 +- .../server/api/blueprints/__tests__/migrationContext.test.ts | 2 +- meteor/server/api/blueprints/migrationContext.ts | 5 ++++- meteor/server/api/ingest/genericDevice/actions.ts | 2 +- meteor/server/api/ingest/lib.ts | 2 +- meteor/server/api/ingest/mosDevice/__tests__/actions.test.ts | 4 ++-- meteor/server/api/ingest/mosDevice/actions.ts | 2 +- meteor/server/api/ingest/rundownInput.ts | 2 +- meteor/server/api/integration/mediaWorkFlows.ts | 2 +- meteor/server/api/mediaManager.ts | 2 +- meteor/server/api/packageManager.ts | 2 +- meteor/server/api/peripheralDevice.ts | 2 +- meteor/server/api/snapshot.ts | 4 ++-- meteor/server/collections/index.ts | 2 +- meteor/server/cronjobs.ts | 2 +- meteor/server/publications/lib.ts | 2 +- meteor/server/publications/peripheralDevice.ts | 2 +- meteor/server/publications/peripheralDeviceForDevice.ts | 2 +- meteor/server/security/lib/credentials.ts | 2 +- meteor/server/security/lib/security.ts | 2 +- meteor/server/security/peripheralDevice.ts | 2 +- meteor/server/security/rundown.ts | 2 +- meteor/server/systemStatus/systemStatus.ts | 2 +- 44 files changed, 52 insertions(+), 49 deletions(-) delete mode 100644 meteor/lib/collections/PeripheralDeviceCommands.ts diff --git a/meteor/__mocks__/helpers/database.ts b/meteor/__mocks__/helpers/database.ts index e780a7fa30..eec258e839 100644 --- a/meteor/__mocks__/helpers/database.ts +++ b/meteor/__mocks__/helpers/database.ts @@ -5,7 +5,7 @@ import { PeripheralDeviceCategory, PERIPHERAL_SUBTYPE_PROCESS, PeripheralDeviceSubType, -} from '../../lib/collections/PeripheralDevices' +} from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { PieceLifespan, diff --git a/meteor/client/collections/index.ts b/meteor/client/collections/index.ts index 8a3871f2b3..7158421b30 100644 --- a/meteor/client/collections/index.ts +++ b/meteor/client/collections/index.ts @@ -26,7 +26,7 @@ import { createSyncReadOnlyMongoCollection, wrapMongoCollection, } from '../../lib/collections/lib' -import { PeripheralDevice } from '../../lib/collections/PeripheralDevices' +import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { RundownLayoutBase } from '../../lib/collections/RundownLayouts' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' diff --git a/meteor/client/lib/clientAPI.ts b/meteor/client/lib/clientAPI.ts index 6887bc7ff1..92c511f36f 100644 --- a/meteor/client/lib/clientAPI.ts +++ b/meteor/client/lib/clientAPI.ts @@ -1,4 +1,4 @@ -import { PeripheralDevice } from '../../lib/collections/PeripheralDevices' +import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { MeteorCall } from '../../lib/api/methods' import { PeripheralDeviceId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { eventContextForLog } from '../../lib/clientUserAction' diff --git a/meteor/client/lib/reactiveData/reactiveData.ts b/meteor/client/lib/reactiveData/reactiveData.ts index 34c9846386..edc161f8cf 100644 --- a/meteor/client/lib/reactiveData/reactiveData.ts +++ b/meteor/client/lib/reactiveData/reactiveData.ts @@ -1,7 +1,7 @@ import { Tracker } from 'meteor/tracker' import { ReactiveVar } from 'meteor/reactive-var' import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' -import { PeripheralDevice } from '../../../lib/collections/PeripheralDevices' +import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { getCurrentTime } from '../../../lib/lib' import { FindOptions } from '../../../lib/collections/lib' import { RundownPlaylistId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx index 4e1d9d2584..ec775721b6 100644 --- a/meteor/client/ui/RundownView.tsx +++ b/meteor/client/ui/RundownView.tsx @@ -58,7 +58,7 @@ import { NotificationCenterPanel } from '../lib/notifications/NotificationCenter import { NotificationCenter, NoticeLevel, Notification } from '../../lib/notifications/notifications' import { SupportPopUp } from './SupportPopUp' import { KeyboardFocusIndicator } from '../lib/KeyboardFocusIndicator' -import { PeripheralDevice, PeripheralDeviceType } from '../../lib/collections/PeripheralDevices' +import { PeripheralDevice, PeripheralDeviceType } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { doUserAction, UserAction } from '../../lib/clientUserAction' import { hashSingleUseToken, ReloadRundownPlaylistResponse, TriggerReloadDataResponse } from '../../lib/api/userActions' import { ClipTrimDialog } from './ClipTrimPanel/ClipTrimDialog' diff --git a/meteor/client/ui/RundownView/RundownNotifier.tsx b/meteor/client/ui/RundownView/RundownNotifier.tsx index a9cc968acc..9a708761f9 100644 --- a/meteor/client/ui/RundownView/RundownNotifier.tsx +++ b/meteor/client/ui/RundownView/RundownNotifier.tsx @@ -12,7 +12,7 @@ import { } from '../../../lib/notifications/notifications' import { WithManagedTracker } from '../../lib/reactiveData/reactiveDataHelper' import { reactiveData } from '../../lib/reactiveData/reactiveData' -import { PeripheralDevice } from '../../../lib/collections/PeripheralDevices' +import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { getCurrentTime, unprotectString } from '../../../lib/lib' import { PubSub, meteorSubscribe } from '../../../lib/api/pubsub' import { ReactiveVar } from 'meteor/reactive-var' diff --git a/meteor/client/ui/RundownView/RundownSystemStatus.tsx b/meteor/client/ui/RundownView/RundownSystemStatus.tsx index 830e8dc423..c975f1f078 100644 --- a/meteor/client/ui/RundownView/RundownSystemStatus.tsx +++ b/meteor/client/ui/RundownView/RundownSystemStatus.tsx @@ -7,7 +7,7 @@ import { PeripheralDevice, PeripheralDeviceCategory, PeripheralDeviceType, -} from '../../../lib/collections/PeripheralDevices' +} from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { Time, getCurrentTime, unprotectString } from '../../../lib/lib' import { withTranslation, WithTranslation } from 'react-i18next' diff --git a/meteor/client/ui/Settings/DeviceSettings.tsx b/meteor/client/ui/Settings/DeviceSettings.tsx index 3dce2d5b15..0f7e784b90 100644 --- a/meteor/client/ui/Settings/DeviceSettings.tsx +++ b/meteor/client/ui/Settings/DeviceSettings.tsx @@ -4,7 +4,7 @@ import { PeripheralDeviceType, PERIPHERAL_SUBTYPE_PROCESS, PeripheralDeviceCategory, -} from '../../../lib/collections/PeripheralDevices' +} from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { EditAttribute } from '../../lib/EditAttribute' import { doModalDialog } from '../../lib/ModalDialog' import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/react-meteor-data' diff --git a/meteor/client/ui/Settings/SettingsMenu.tsx b/meteor/client/ui/Settings/SettingsMenu.tsx index afabd802d8..b047376e27 100644 --- a/meteor/client/ui/Settings/SettingsMenu.tsx +++ b/meteor/client/ui/Settings/SettingsMenu.tsx @@ -5,7 +5,7 @@ import { doModalDialog } from '../../lib/ModalDialog' import { NavLink, useLocation } from 'react-router-dom' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' -import { PeripheralDevice, PERIPHERAL_SUBTYPE_PROCESS } from '../../../lib/collections/PeripheralDevices' +import { PeripheralDevice, PERIPHERAL_SUBTYPE_PROCESS } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { NotificationCenter, Notification, NoticeLevel } from '../../../lib/notifications/notifications' diff --git a/meteor/client/ui/Settings/Studio/Devices/SelectDevices.tsx b/meteor/client/ui/Settings/Studio/Devices/SelectDevices.tsx index d47a1824e2..93af00dccd 100644 --- a/meteor/client/ui/Settings/Studio/Devices/SelectDevices.tsx +++ b/meteor/client/ui/Settings/Studio/Devices/SelectDevices.tsx @@ -3,7 +3,7 @@ import Tooltip from 'rc-tooltip' import { doModalDialog } from '../../../../lib/ModalDialog' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faExclamationTriangle, faTrash, faPlus } from '@fortawesome/free-solid-svg-icons' -import { PeripheralDevice, PeripheralDeviceType } from '../../../../../lib/collections/PeripheralDevices' +import { PeripheralDevice, PeripheralDeviceType } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { Link } from 'react-router-dom' import { MomentFromNow } from '../../../../lib/Moment' import { useTranslation } from 'react-i18next' diff --git a/meteor/client/ui/Settings/StudioSettings.tsx b/meteor/client/ui/Settings/StudioSettings.tsx index 746e6048a7..3e0ae25bd0 100644 --- a/meteor/client/ui/Settings/StudioSettings.tsx +++ b/meteor/client/ui/Settings/StudioSettings.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from 'react' import { useTracker } from '../../lib/ReactMeteorData/react-meteor-data' import { Spinner } from '../../lib/Spinner' -import { PeripheralDeviceType } from '../../../lib/collections/PeripheralDevices' +import { PeripheralDeviceType } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { StudioRoutings } from './Studio/Routings' import { StudioDevices } from './Studio/Devices' import { MappingsSettingsManifest, MappingsSettingsManifests, StudioMappings } from './Studio/Mappings' diff --git a/meteor/client/ui/Settings/components/ConfigManifestOAuthFlow.tsx b/meteor/client/ui/Settings/components/ConfigManifestOAuthFlow.tsx index 85bdbf7da7..9428063713 100644 --- a/meteor/client/ui/Settings/components/ConfigManifestOAuthFlow.tsx +++ b/meteor/client/ui/Settings/components/ConfigManifestOAuthFlow.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { withTranslation } from 'react-i18next' -import { PeripheralDevice } from '../../../../lib/collections/PeripheralDevices' +import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { Translated } from '../../../lib/ReactMeteorData/react-meteor-data' import { IngestDeviceSettings } from '@sofie-automation/corelib/dist/dataModel/PeripheralDeviceSettings/ingestDevice' import { NotificationCenter, Notification, NoticeLevel } from '../../../../lib/notifications/notifications' diff --git a/meteor/client/ui/Settings/components/GenericDeviceSettingsComponent.tsx b/meteor/client/ui/Settings/components/GenericDeviceSettingsComponent.tsx index 4e43faff24..74cb4ec24f 100644 --- a/meteor/client/ui/Settings/components/GenericDeviceSettingsComponent.tsx +++ b/meteor/client/ui/Settings/components/GenericDeviceSettingsComponent.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { PeripheralDevice, PeripheralDeviceType } from '../../../../lib/collections/PeripheralDevices' +import { PeripheralDevice, PeripheralDeviceType } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { DeviceItem } from '../../Status/SystemStatus' import { ConfigManifestOAuthFlowComponent } from './ConfigManifestOAuthFlow' import { protectString, unprotectString } from '../../../../lib/lib' diff --git a/meteor/client/ui/Status/SystemStatus.tsx b/meteor/client/ui/Status/SystemStatus.tsx index 7642433657..b06226fb79 100644 --- a/meteor/client/ui/Status/SystemStatus.tsx +++ b/meteor/client/ui/Status/SystemStatus.tsx @@ -4,7 +4,7 @@ import { PeripheralDevice, PeripheralDeviceType, PERIPHERAL_SUBTYPE_PROCESS, -} from '../../../lib/collections/PeripheralDevices' +} from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import * as reacti18next from 'react-i18next' import * as i18next from 'i18next' import Moment from 'react-moment' diff --git a/meteor/client/ui/Status/package-status/index.tsx b/meteor/client/ui/Status/package-status/index.tsx index b95e7d87db..bdfc920519 100644 --- a/meteor/client/ui/Status/package-status/index.tsx +++ b/meteor/client/ui/Status/package-status/index.tsx @@ -19,7 +19,7 @@ import { PeripheralDevices, } from '../../../collections' import { PeripheralDeviceId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { PeripheralDevice } from '../../../../lib/collections/PeripheralDevices' +import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' export const ExpectedPackagesStatus: React.FC<{}> = function ExpectedPackagesStatus(_props: {}) { const { t } = useTranslation() diff --git a/meteor/lib/api/pubsub.ts b/meteor/lib/api/pubsub.ts index 1b781aeae1..171c50ef48 100644 --- a/meteor/lib/api/pubsub.ts +++ b/meteor/lib/api/pubsub.ts @@ -30,8 +30,8 @@ import { DBOrganization } from '../collections/Organization' import { PackageContainerStatusDB } from '@sofie-automation/corelib/dist/dataModel/PackageContainerStatus' import { PartInstance } from '../collections/PartInstances' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' -import { PeripheralDeviceCommand } from '../collections/PeripheralDeviceCommands' -import { PeripheralDevice } from '../collections/PeripheralDevices' +import { PeripheralDeviceCommand } from '@sofie-automation/corelib/dist/dataModel/PeripheralDeviceCommand' +import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' diff --git a/meteor/lib/collections/PeripheralDeviceCommands.ts b/meteor/lib/collections/PeripheralDeviceCommands.ts deleted file mode 100644 index 9c62a09664..0000000000 --- a/meteor/lib/collections/PeripheralDeviceCommands.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '@sofie-automation/corelib/dist/dataModel/PeripheralDeviceCommand' diff --git a/meteor/lib/main.ts b/meteor/lib/main.ts index 151d31c293..9dee266e55 100644 --- a/meteor/lib/main.ts +++ b/meteor/lib/main.ts @@ -8,8 +8,6 @@ import './collections/Evaluations' import './collections/ExpectedPackages' import './collections/Organization' import './collections/PartInstances' -import './collections/PeripheralDeviceCommands' -import './collections/PeripheralDevices' import './collections/RundownLayouts' import './collections/Snapshots' import './collections/Studios' diff --git a/meteor/server/__tests__/cronjobs.test.ts b/meteor/server/__tests__/cronjobs.test.ts index edbeca899f..231142076b 100644 --- a/meteor/server/__tests__/cronjobs.test.ts +++ b/meteor/server/__tests__/cronjobs.test.ts @@ -5,7 +5,10 @@ import { logger } from '../logging' import { getRandomId, getRandomString, protectString } from '../../lib/lib' import { SnapshotType } from '../../lib/collections/Snapshots' import { IBlueprintPieceType, PieceLifespan, StatusCode, TSR } from '@sofie-automation/blueprints-integration' -import { PeripheralDeviceType, PeripheralDeviceCategory } from '../../lib/collections/PeripheralDevices' +import { + PeripheralDeviceType, + PeripheralDeviceCategory, +} from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { SYSTEM_ID } from '../../lib/collections/CoreSystem' import * as lib from '../../lib/lib' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' diff --git a/meteor/server/api/__tests__/client.test.ts b/meteor/server/api/__tests__/client.test.ts index 506da4c0d1..00002af836 100644 --- a/meteor/server/api/__tests__/client.test.ts +++ b/meteor/server/api/__tests__/client.test.ts @@ -3,14 +3,14 @@ import { MeteorMock } from '../../../__mocks__/meteor' import { UserActionsLogItem } from '../../../lib/collections/UserActionsLog' import { ClientAPIMethods } from '../../../lib/api/client' import { protectString, makePromise, LogLevel } from '../../../lib/lib' -import { PeripheralDeviceCommand } from '../../../lib/collections/PeripheralDeviceCommands' +import { PeripheralDeviceCommand } from '@sofie-automation/corelib/dist/dataModel/PeripheralDeviceCommand' import { setLogLevel } from '../../logging' import { testInFiber, beforeAllInFiber } from '../../../__mocks__/helpers/jest' import { PeripheralDeviceCategory, PeripheralDeviceType, PERIPHERAL_SUBTYPE_PROCESS, -} from '../../../lib/collections/PeripheralDevices' +} from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { setupMockPeripheralDevice, setupMockStudio } from '../../../__mocks__/helpers/database' import { MeteorCall } from '../../../lib/api/methods' import { PeripheralDeviceId } from '@sofie-automation/corelib/dist/dataModel/Ids' diff --git a/meteor/server/api/__tests__/peripheralDevice.test.ts b/meteor/server/api/__tests__/peripheralDevice.test.ts index 4ccf099f78..986ba0118d 100644 --- a/meteor/server/api/__tests__/peripheralDevice.test.ts +++ b/meteor/server/api/__tests__/peripheralDevice.test.ts @@ -4,7 +4,7 @@ import { PeripheralDevice, PeripheralDeviceCategory, PeripheralDeviceType, -} from '../../../lib/collections/PeripheralDevices' +} from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { EmptyPieceTimelineObjectsBlob } from '@sofie-automation/corelib/dist/dataModel/Piece' import { getCurrentTime, diff --git a/meteor/server/api/__tests__/userActions/system.test.ts b/meteor/server/api/__tests__/userActions/system.test.ts index 56cf606c76..0d1c9c147f 100644 --- a/meteor/server/api/__tests__/userActions/system.test.ts +++ b/meteor/server/api/__tests__/userActions/system.test.ts @@ -4,7 +4,7 @@ import { PeripheralDeviceCategory, PeripheralDeviceType, PERIPHERAL_SUBTYPE_PROCESS, -} from '../../../../lib/collections/PeripheralDevices' +} from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { getCurrentTime, protectString } from '../../../../lib/lib' import { DefaultEnvironment, diff --git a/meteor/server/api/blueprints/__tests__/migrationContext.test.ts b/meteor/server/api/blueprints/__tests__/migrationContext.test.ts index 9fa9e303a8..3f8935b77d 100644 --- a/meteor/server/api/blueprints/__tests__/migrationContext.test.ts +++ b/meteor/server/api/blueprints/__tests__/migrationContext.test.ts @@ -6,7 +6,7 @@ import { PeripheralDeviceCategory, PeripheralDeviceType, PERIPHERAL_SUBTYPE_PROCESS, -} from '../../../../lib/collections/PeripheralDevices' +} from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { literal, getRandomId, protectString, unprotectString } from '../../../../lib/lib' import { LookaheadMode, diff --git a/meteor/server/api/blueprints/migrationContext.ts b/meteor/server/api/blueprints/migrationContext.ts index dcd23d4940..9c49c00aaf 100644 --- a/meteor/server/api/blueprints/migrationContext.ts +++ b/meteor/server/api/blueprints/migrationContext.ts @@ -32,7 +32,10 @@ import { import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' import { check } from '../../../lib/check' -import { PERIPHERAL_SUBTYPE_PROCESS, PeripheralDeviceType } from '../../../lib/collections/PeripheralDevices' +import { + PERIPHERAL_SUBTYPE_PROCESS, + PeripheralDeviceType, +} from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { TriggeredActionsObj } from '../../../lib/collections/TriggeredActions' import { Match } from 'meteor/check' import { MongoModifier } from '@sofie-automation/corelib/dist/mongo' diff --git a/meteor/server/api/ingest/genericDevice/actions.ts b/meteor/server/api/ingest/genericDevice/actions.ts index fb9dcedea6..9e276f0514 100644 --- a/meteor/server/api/ingest/genericDevice/actions.ts +++ b/meteor/server/api/ingest/genericDevice/actions.ts @@ -1,5 +1,5 @@ import { Meteor } from 'meteor/meteor' -import { PeripheralDevice } from '../../../../lib/collections/PeripheralDevices' +import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { TriggerReloadDataResponse } from '../../../../lib/api/userActions' import { stringifyError } from '../../../../lib/lib' diff --git a/meteor/server/api/ingest/lib.ts b/meteor/server/api/ingest/lib.ts index 350d86eaa0..d717106f13 100644 --- a/meteor/server/api/ingest/lib.ts +++ b/meteor/server/api/ingest/lib.ts @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor' import { getHash, getCurrentTime, protectString, stringifyError } from '../../../lib/lib' -import { PeripheralDevice, PeripheralDeviceCategory } from '../../../lib/collections/PeripheralDevices' +import { PeripheralDevice, PeripheralDeviceCategory } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { logger } from '../../logging' import { PeripheralDeviceContentWriteAccess } from '../../security/peripheralDevice' diff --git a/meteor/server/api/ingest/mosDevice/__tests__/actions.test.ts b/meteor/server/api/ingest/mosDevice/__tests__/actions.test.ts index 2825de11e8..5d3c2d0157 100644 --- a/meteor/server/api/ingest/mosDevice/__tests__/actions.test.ts +++ b/meteor/server/api/ingest/mosDevice/__tests__/actions.test.ts @@ -4,9 +4,9 @@ import { Meteor } from 'meteor/meteor' import * as MOS from '@mos-connection/helper' import { setupDefaultStudioEnvironment } from '../../../../../__mocks__/helpers/database' import { testInFiber } from '../../../../../__mocks__/helpers/jest' -import { PeripheralDevice } from '../../../../../lib/collections/PeripheralDevices' +import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { MOSDeviceActions } from '../actions' -import { PeripheralDeviceCommand } from '../../../../../lib/collections/PeripheralDeviceCommands' +import { PeripheralDeviceCommand } from '@sofie-automation/corelib/dist/dataModel/PeripheralDeviceCommand' import { TriggerReloadDataResponse } from '../../../../../lib/api/userActions' import { deferAsync, getRandomId, getRandomString, literal, stringifyError } from '@sofie-automation/corelib/dist/lib' import { PeripheralDeviceCommandId, RundownId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' diff --git a/meteor/server/api/ingest/mosDevice/actions.ts b/meteor/server/api/ingest/mosDevice/actions.ts index 74547c1ff1..cc67731549 100644 --- a/meteor/server/api/ingest/mosDevice/actions.ts +++ b/meteor/server/api/ingest/mosDevice/actions.ts @@ -2,7 +2,7 @@ import { MOS } from '@sofie-automation/corelib' import { logger } from '../../../logging' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { Meteor } from 'meteor/meteor' -import { PeripheralDevice } from '../../../../lib/collections/PeripheralDevices' +import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { IngestPart } from '@sofie-automation/blueprints-integration' import { parseMosString } from './lib' diff --git a/meteor/server/api/ingest/rundownInput.ts b/meteor/server/api/ingest/rundownInput.ts index 72db80ee6e..fadbf40016 100644 --- a/meteor/server/api/ingest/rundownInput.ts +++ b/meteor/server/api/ingest/rundownInput.ts @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor' import { check } from '../../../lib/check' -import { PeripheralDevice } from '../../../lib/collections/PeripheralDevices' +import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { IngestDataCache, MediaObjects, Parts, Rundowns, Segments } from '../../collections' import { lazyIgnore, literal } from '../../../lib/lib' import { IngestRundown, IngestSegment, IngestPart, IngestPlaylist } from '@sofie-automation/blueprints-integration' diff --git a/meteor/server/api/integration/mediaWorkFlows.ts b/meteor/server/api/integration/mediaWorkFlows.ts index bf3c60d2a3..408f8e5f21 100644 --- a/meteor/server/api/integration/mediaWorkFlows.ts +++ b/meteor/server/api/integration/mediaWorkFlows.ts @@ -7,7 +7,7 @@ import { MediaWorkFlowRevision, MediaWorkFlowStepRevision, } from '@sofie-automation/shared-lib/dist/peripheralDevice/mediaManager' -import { PeripheralDeviceType } from '../../../lib/collections/PeripheralDevices' +import { PeripheralDeviceType } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { MethodContext } from '../../../lib/api/methods' import { checkAccessAndGetPeripheralDevice } from '../ingest/lib' import { MediaWorkFlowId, MediaWorkFlowStepId, PeripheralDeviceId } from '@sofie-automation/corelib/dist/dataModel/Ids' diff --git a/meteor/server/api/mediaManager.ts b/meteor/server/api/mediaManager.ts index 0723c93598..3370b85944 100644 --- a/meteor/server/api/mediaManager.ts +++ b/meteor/server/api/mediaManager.ts @@ -1,5 +1,5 @@ import { MediaWorkFlow } from '@sofie-automation/shared-lib/dist/core/model/MediaWorkFlows' -import { PeripheralDevice } from '../../lib/collections/PeripheralDevices' +import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { MediaWorkFlowContentAccess } from '../security/peripheralDevice' import { BasicAccessContext } from '../security/organization' import { MediaWorkFlows, PeripheralDevices } from '../collections' diff --git a/meteor/server/api/packageManager.ts b/meteor/server/api/packageManager.ts index 285b1e3971..0988826393 100644 --- a/meteor/server/api/packageManager.ts +++ b/meteor/server/api/packageManager.ts @@ -2,7 +2,7 @@ import { PeripheralDeviceCategory, PeripheralDeviceType, PERIPHERAL_SUBTYPE_PROCESS, -} from '../../lib/collections/PeripheralDevices' +} from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { PeripheralDeviceContentWriteAccess } from '../security/peripheralDevice' import { StudioContentAccess } from '../security/studio' import { PeripheralDevices } from '../collections' diff --git a/meteor/server/api/peripheralDevice.ts b/meteor/server/api/peripheralDevice.ts index 0f65187e3c..69bf3499ff 100644 --- a/meteor/server/api/peripheralDevice.ts +++ b/meteor/server/api/peripheralDevice.ts @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor' import { check, Match } from '../../lib/check' import * as _ from 'underscore' -import { PeripheralDeviceType, PeripheralDevice } from '../../lib/collections/PeripheralDevices' +import { PeripheralDeviceType, PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { PeripheralDeviceCommands, PeripheralDevices, Rundowns, Studios, UserActionsLog } from '../collections' import { getCurrentTime, protectString, stringifyObjects, literal, unprotectString } from '../../lib/lib' import { logger } from '../logging' diff --git a/meteor/server/api/snapshot.ts b/meteor/server/api/snapshot.ts index c8f2253bf9..8a74f0870c 100644 --- a/meteor/server/api/snapshot.ts +++ b/meteor/server/api/snapshot.ts @@ -29,10 +29,10 @@ import { unprotectString, } from '../../lib/lib' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' -import { PeripheralDevice, PERIPHERAL_SUBTYPE_PROCESS } from '../../lib/collections/PeripheralDevices' +import { PeripheralDevice, PERIPHERAL_SUBTYPE_PROCESS } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { logger } from '../logging' import { TimelineComplete } from '@sofie-automation/corelib/dist/dataModel/Timeline' -import { PeripheralDeviceCommand } from '../../lib/collections/PeripheralDeviceCommands' +import { PeripheralDeviceCommand } from '@sofie-automation/corelib/dist/dataModel/PeripheralDeviceCommand' import { registerClassToMeteorMethods } from '../methods' import { NewSnapshotAPI, SnapshotAPIMethods } from '../../lib/api/shapshot' import { ICoreSystem, parseVersion } from '../../lib/collections/CoreSystem' diff --git a/meteor/server/collections/index.ts b/meteor/server/collections/index.ts index 4c6df5b3a5..4bc6899c06 100644 --- a/meteor/server/collections/index.ts +++ b/meteor/server/collections/index.ts @@ -13,7 +13,7 @@ import { Meteor } from 'meteor/meteor' import { ICoreSystem } from '../../lib/collections/CoreSystem' import { Evaluation } from '../../lib/collections/Evaluations' import { DBOrganization } from '../../lib/collections/Organization' -import { PeripheralDevice } from '../../lib/collections/PeripheralDevices' +import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { RundownLayoutBase } from '../../lib/collections/RundownLayouts' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' diff --git a/meteor/server/cronjobs.ts b/meteor/server/cronjobs.ts index ad8e74986b..4343270cb6 100644 --- a/meteor/server/cronjobs.ts +++ b/meteor/server/cronjobs.ts @@ -1,5 +1,5 @@ import { PeripheralDevices, RundownPlaylists } from './collections' -import { PeripheralDeviceType } from '../lib/collections/PeripheralDevices' +import { PeripheralDeviceType } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { getCurrentTime, stringifyError } from '../lib/lib' import { logger } from './logging' import { Meteor } from 'meteor/meteor' diff --git a/meteor/server/publications/lib.ts b/meteor/server/publications/lib.ts index 16a48c5e16..c74d2d0f91 100644 --- a/meteor/server/publications/lib.ts +++ b/meteor/server/publications/lib.ts @@ -4,7 +4,7 @@ import { extractFunctionSignature } from '../lib' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { ResolvedCredentials, resolveCredentials } from '../security/lib/credentials' import { Settings } from '../../lib/Settings' -import { PeripheralDevice } from '../../lib/collections/PeripheralDevices' +import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { MongoCursor } from '../../lib/collections/lib' import { OrganizationId, diff --git a/meteor/server/publications/peripheralDevice.ts b/meteor/server/publications/peripheralDevice.ts index 07c442a164..07bae89f36 100644 --- a/meteor/server/publications/peripheralDevice.ts +++ b/meteor/server/publications/peripheralDevice.ts @@ -3,7 +3,7 @@ import { check } from '../../lib/check' import { meteorPublish, AutoFillSelector } from './lib' import { PubSub } from '../../lib/api/pubsub' import { PeripheralDeviceReadAccess } from '../security/peripheralDevice' -import { PeripheralDevice } from '../../lib/collections/PeripheralDevices' +import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { OrganizationReadAccess } from '../security/organization' import { StudioReadAccess } from '../security/studio' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' diff --git a/meteor/server/publications/peripheralDeviceForDevice.ts b/meteor/server/publications/peripheralDeviceForDevice.ts index 6cd83897df..20b6af2214 100644 --- a/meteor/server/publications/peripheralDeviceForDevice.ts +++ b/meteor/server/publications/peripheralDeviceForDevice.ts @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor' import { CustomCollectionName, PubSub } from '../../lib/api/pubsub' import { PeripheralDeviceReadAccess } from '../security/peripheralDevice' -import { PeripheralDevice, PeripheralDeviceCategory } from '../../lib/collections/PeripheralDevices' +import { PeripheralDevice, PeripheralDeviceCategory } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { PeripheralDeviceId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PeripheralDevices, Studios } from '../collections' import { TriggerUpdate, meteorCustomPublish, setUpOptimizedObserverArray } from '../lib/customPublication' diff --git a/meteor/server/security/lib/credentials.ts b/meteor/server/security/lib/credentials.ts index dfc3c65145..c7a1dc66eb 100644 --- a/meteor/server/security/lib/credentials.ts +++ b/meteor/server/security/lib/credentials.ts @@ -1,5 +1,5 @@ import { User } from '../../../lib/collections/Users' -import { PeripheralDevice } from '../../../lib/collections/PeripheralDevices' +import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { cacheResult, isProtectedString, clearCacheResult } from '../../../lib/lib' import { LIMIT_CACHE_TIME } from './security' import { profiler } from '../../api/profiler' diff --git a/meteor/server/security/lib/security.ts b/meteor/server/security/lib/security.ts index d55476afa1..ce2af553ab 100644 --- a/meteor/server/security/lib/security.ts +++ b/meteor/server/security/lib/security.ts @@ -7,7 +7,7 @@ import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/Rund import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { isProtectedString } from '../../../lib/lib' import { DBOrganization } from '../../../lib/collections/Organization' -import { PeripheralDevice } from '../../../lib/collections/PeripheralDevices' +import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' import { profiler } from '../../api/profiler' import { fetchShowStyleBasesLight, fetchStudioLight, ShowStyleBaseLight } from '../../optimizations' diff --git a/meteor/server/security/peripheralDevice.ts b/meteor/server/security/peripheralDevice.ts index c818732796..dd93aaf6da 100644 --- a/meteor/server/security/peripheralDevice.ts +++ b/meteor/server/security/peripheralDevice.ts @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor' import { check } from '../../lib/check' -import { PeripheralDevice } from '../../lib/collections/PeripheralDevices' +import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { isProtectedString } from '../../lib/lib' import { logNotAllowed } from './lib/lib' import { MediaWorkFlow } from '@sofie-automation/shared-lib/dist/core/model/MediaWorkFlows' diff --git a/meteor/server/security/rundown.ts b/meteor/server/security/rundown.ts index 054f2e4626..b7e0e1066a 100644 --- a/meteor/server/security/rundown.ts +++ b/meteor/server/security/rundown.ts @@ -6,7 +6,7 @@ import { logNotAllowed } from './lib/lib' import { allowAccessToRundown } from './lib/security' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { ExpectedMediaItem } from '@sofie-automation/corelib/dist/dataModel/ExpectedMediaItem' -import { PeripheralDeviceType, PeripheralDevice } from '../../lib/collections/PeripheralDevices' +import { PeripheralDeviceType, PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { ExpectedPlayoutItem } from '@sofie-automation/corelib/dist/dataModel/ExpectedPlayoutItem' import { Settings } from '../../lib/Settings' import { RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' diff --git a/meteor/server/systemStatus/systemStatus.ts b/meteor/server/systemStatus/systemStatus.ts index ff9b627013..c329dfa622 100644 --- a/meteor/server/systemStatus/systemStatus.ts +++ b/meteor/server/systemStatus/systemStatus.ts @@ -1,5 +1,5 @@ import { Meteor } from 'meteor/meteor' -import { PeripheralDevice, PERIPHERAL_SUBTYPE_PROCESS } from '../../lib/collections/PeripheralDevices' +import { PeripheralDevice, PERIPHERAL_SUBTYPE_PROCESS } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { getCurrentTime, Time, getRandomId, literal } from '../../lib/lib' import { parseVersion, From 1f327579d34c9eedac2b59d5365e0040d975e58c Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 7 Jul 2023 13:53:25 +0100 Subject: [PATCH 016/479] chore: decouple some mongodb types from blueprint types --- .../corelib/src/dataModel/PartInstance.ts | 28 +++++++---- .../corelib/src/dataModel/PieceInstance.ts | 47 ++++++++++++------- packages/corelib/src/dataModel/Rundown.ts | 23 +++++++-- packages/corelib/src/dataModel/Segment.ts | 22 +++++++-- packages/job-worker/src/playout/snapshot.ts | 2 +- .../src/playout/timeline/rundown.ts | 4 +- 6 files changed, 90 insertions(+), 36 deletions(-) diff --git a/packages/corelib/src/dataModel/PartInstance.ts b/packages/corelib/src/dataModel/PartInstance.ts index a293acc340..543b2ca404 100644 --- a/packages/corelib/src/dataModel/PartInstance.ts +++ b/packages/corelib/src/dataModel/PartInstance.ts @@ -1,15 +1,9 @@ -import { IBlueprintPartInstance, IBlueprintPartInstanceTimings, Time } from '@sofie-automation/blueprints-integration' +import { PartEndState, Time } from '@sofie-automation/blueprints-integration' import { PartCalculatedTimings } from '../playout/timings' -import { ProtectedStringProperties } from '../protectedString' import { PartInstanceId, RundownId, RundownPlaylistActivationId, SegmentId, SegmentPlayoutId } from './Ids' import { DBPart } from './Part' -export interface InternalIBlueprintPartInstance - extends ProtectedStringProperties, '_id' | 'segmentId'> { - part: ProtectedStringProperties -} - -export interface DBPartInstance extends InternalIBlueprintPartInstance { +export interface DBPartInstance { _id: PartInstanceId rundownId: RundownId segmentId: SegmentId @@ -18,6 +12,8 @@ export interface DBPartInstance extends InternalIBlueprintPartInstance { playlistActivationId: RundownPlaylistActivationId /** The id of the segment playout. This is unique for each session, and each time the segment is entered */ segmentPlayoutId: SegmentPlayoutId + /** If the playlist was in rehearsal mode when the PartInstance was created */ + rehearsal: boolean /** Whether this instance has been finished with and reset (to restore the original part as the primary version) */ reset?: boolean @@ -35,9 +31,18 @@ export interface DBPartInstance extends InternalIBlueprintPartInstance { /** Once taken, we should have timings for how the part interacts with the one before */ partPlayoutTimings?: PartCalculatedTimings + + /** The end state of the previous part, to allow for bits of this to part to be based on what the previous did/was */ + previousPartEndState?: PartEndState + + /** Whether the PartInstance is an orphan (the Part referenced does not exist). Indicates the reason it is orphaned */ + orphaned?: 'adlib-part' | 'deleted' + + /** If taking out of the current part is blocked, this is the time it is blocked until */ + blockTakeUntil?: number } -export interface PartInstanceTimings extends IBlueprintPartInstanceTimings { +export interface PartInstanceTimings { /** The playback offset that was set for the last take */ playOffset?: Time /** @@ -63,4 +68,9 @@ export interface PartInstanceTimings extends IBlueprintPartInstanceTimings { * This gets set when the plannedStartedPlayback of the following part is set */ plannedStoppedPlayback?: Time + + /** Point in time the Part started playing (ie the time of the playout) */ + reportedStartedPlayback?: Time + /** Point in time the Part stopped playing (ie the time of the playout) */ + reportedStoppedPlayback?: Time } diff --git a/packages/corelib/src/dataModel/PieceInstance.ts b/packages/corelib/src/dataModel/PieceInstance.ts index 4f1058e310..adaa5b1b74 100644 --- a/packages/corelib/src/dataModel/PieceInstance.ts +++ b/packages/corelib/src/dataModel/PieceInstance.ts @@ -1,5 +1,5 @@ -import { IBlueprintPieceInstance, IBlueprintResolvedPieceInstance } from '@sofie-automation/blueprints-integration' -import { ProtectedStringProperties, protectString } from '../protectedString' +import { IBlueprintResolvedPieceInstance, Time } from '@sofie-automation/blueprints-integration' +import { protectString } from '../protectedString' import { PieceInstanceInfiniteId, RundownPlaylistActivationId, @@ -13,19 +13,29 @@ import { omit } from '../lib' export type PieceInstancePiece = Omit -export interface PieceInstanceInfinite - extends ProtectedStringProperties['infinite'], 'infinitePieceId'> { +export interface PieceInstanceInfinite { + infinitePieceId: PieceId + /** When the instance was a copy made from hold */ + fromHold?: boolean + + /** Whether this was 'copied' from the previous PartInstance or Part */ + fromPreviousPart: boolean + /** Whether this was 'copied' from the previous PartInstance via the playhead, rather than from a Part */ + fromPreviousPlayhead?: boolean + /** A random id for this instance of this infinite */ infiniteInstanceId: PieceInstanceInfiniteId /** The index of this PieceInstance within the instance of the infinite (as defined by `infiniteInstanceId`) */ infiniteInstanceIndex: number } -export interface PieceInstance - extends ProtectedStringProperties< - Omit, - '_id' | 'adLibSourceId' | 'partInstanceId' - > { +export interface PieceInstance { + _id: PieceInstanceId + /** The rundown this piece belongs to */ + rundownId: RundownId + /** The part instace this piece belongs to */ + partInstanceId: PartInstanceId + /** Whether this PieceInstance is a temprorary wrapping of a Piece */ readonly isTemporary?: boolean @@ -35,12 +45,6 @@ export interface PieceInstance /** Whether this instance has been finished with and reset (to restore the original piece as the primary version) */ reset?: boolean - _id: PieceInstanceId - /** The rundown this piece belongs to */ - rundownId: RundownId - /** The part instace this piece belongs to */ - partInstanceId: PartInstanceId - piece: PieceInstancePiece /** A flag to signal a given Piece has been deactivated manually */ @@ -52,6 +56,9 @@ export interface PieceInstance /** Only set when this pieceInstance is an infinite. It contains info about the infinite */ infinite?: PieceInstanceInfinite + /** If this piece has been insterted during run of rundown (such as adLibs), then this is set to the timestamp it was inserted */ + dynamicallyInserted?: Time + /** This is set when the duration needs to be overriden from some user action */ userDuration?: | { @@ -63,8 +70,14 @@ export interface PieceInstance endRelativeToNow: number } - plannedStartedPlayback?: number - plannedStoppedPlayback?: number + /** The time the system started playback of this part, undefined if not yet played back (milliseconds since epoch) */ + reportedStartedPlayback?: Time + /** Whether the piece has stopped playback (the most recent time it was played), undefined if not yet played back or is currently playing. + * This is set from a callback from the playout gateway (milliseconds since epoch) + */ + reportedStoppedPlayback?: Time + plannedStartedPlayback?: Time + plannedStoppedPlayback?: Time } export interface ResolvedPieceInstance diff --git a/packages/corelib/src/dataModel/Rundown.ts b/packages/corelib/src/dataModel/Rundown.ts index 026262796b..41fab73264 100644 --- a/packages/corelib/src/dataModel/Rundown.ts +++ b/packages/corelib/src/dataModel/Rundown.ts @@ -1,5 +1,4 @@ -import { IBlueprintRundownDB, Time } from '@sofie-automation/blueprints-integration' -import { ProtectedStringProperties } from '../protectedString' +import { RundownPlaylistTiming, Time } from '@sofie-automation/blueprints-integration' import { RundownId, OrganizationId, @@ -21,8 +20,7 @@ export interface RundownImportVersions { } /** This is a very uncomplete mock-up of the Rundown object */ -export interface Rundown - extends ProtectedStringProperties { +export interface Rundown { _id: RundownId /** ID of the organization that owns the rundown */ organizationId: OrganizationId | null @@ -42,6 +40,10 @@ export interface Rundown importVersions: RundownImportVersions status?: string + + /** Air-status, comes from NCS, examples: "READY" | "NOT READY" */ + airStatus?: string + // There should be something like a Owner user here somewhere? /** Is the rundown in an unsynced (has been unpublished from ENPS) state? */ @@ -53,6 +55,19 @@ export interface Rundown /** Holds notes (warnings / errors) thrown by the blueprints during creation, or appended after */ notes?: Array + externalId: string + /** Rundown slug - user-presentable name */ + name: string + + /** Rundown description: Longer user-presentable description of the rundown */ + description?: string + + /** Rundown timing information */ + timing: RundownPlaylistTiming + + /** Arbitrary data storage for plugins */ + metaData?: unknown + /** External id of the Rundown Playlist to put this rundown in */ playlistExternalId?: string /** Whether the end of the rundown marks a commercial break */ diff --git a/packages/corelib/src/dataModel/Segment.ts b/packages/corelib/src/dataModel/Segment.ts index ae8c09b267..6a86302feb 100644 --- a/packages/corelib/src/dataModel/Segment.ts +++ b/packages/corelib/src/dataModel/Segment.ts @@ -1,6 +1,5 @@ -import { IBlueprintSegmentDB } from '@sofie-automation/blueprints-integration' +import { SegmentDisplayMode, SegmentTimingInfo } from '@sofie-automation/blueprints-integration' import { MongoFieldSpecifierOnes } from '../mongo' -import { ProtectedStringProperties } from '../protectedString' import { SegmentId, RundownId } from './Ids' import { SegmentNote } from './Notes' @@ -15,7 +14,7 @@ export enum SegmentOrphanedReason { export const orphanedHiddenSegmentPropertiesToPreserve: MongoFieldSpecifierOnes = {} /** A "Title" in NRK Lingo / "Stories" in ENPS Lingo. */ -export interface DBSegment extends ProtectedStringProperties { +export interface DBSegment { _id: SegmentId /** Position inside rundown */ _rank: number @@ -26,6 +25,23 @@ export interface DBSegment extends ProtectedStringProperties, previousPartInfo: SelectedPartInstanceTimelineInfo, - currentInfinitePieceIds: Set, + currentInfinitePieceIds: Set, timingContext: RundownTimelineTimingContext, currentPartInstanceTimings: PartCalculatedTimings ): Array { From f99e2eea5256ed3fcb0fdf73731d9f0ff6c2d3e2 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 10 Jul 2023 09:45:01 +0100 Subject: [PATCH 017/479] Revert "fix: replace `MeteorPromiseApply` with `Meteor.applyAsync`" This reverts commit 714ea9ac4e7b806ee30006e31117bc07f24019b9. --- meteor/__mocks__/meteor.ts | 49 ++------------------------------ meteor/lib/__tests__/lib.test.ts | 24 ++++++++++++++++ meteor/lib/api/methods.ts | 5 ++-- meteor/lib/lib.ts | 20 +++++++++++++ meteor/lib/typings/meteor.d.ts | 43 ---------------------------- 5 files changed, 50 insertions(+), 91 deletions(-) delete mode 100644 meteor/lib/typings/meteor.d.ts diff --git a/meteor/__mocks__/meteor.ts b/meteor/__mocks__/meteor.ts index 7dcdce5030..9eef515b57 100644 --- a/meteor/__mocks__/meteor.ts +++ b/meteor/__mocks__/meteor.ts @@ -1,7 +1,5 @@ -import { getRandomString, stringifyError } from '@sofie-automation/corelib/dist/lib' -import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { stringifyError } from '@sofie-automation/corelib/dist/lib' import * as _ from 'underscore' -import type { MethodContext } from '../lib/api/methods' import { Fiber } from './Fibers' import { MongoMock } from './mongo' @@ -119,26 +117,11 @@ export namespace MeteorMock { export function userId(): string | undefined { return mockUser ? mockUser._id : undefined } - function getMethodContext(): MethodContext { + function getMethodContext() { return { - userId: protectString(mockUser?._id) ?? null, + userId: mockUser ? mockUser._id : undefined, connection: { - id: getRandomString(), clientAddress: '1.1.1.1', - close: () => { - /* no-op */ - }, - onClose: () => { - /* no-op */ - }, - httpHeaders: {}, - }, - isSimulation: false, - setUserId: () => { - /* no-op */ - }, - unblock: () => { - /* no-op */ }, } } @@ -211,17 +194,6 @@ export namespace MeteorMock { return waitForPromiseLocal(Promise.resolve(fcn.call(getMethodContext(), ...args))) } } - export async function callAsync(methodName: string, ...args: any[]): Promise { - const fcn: Function = mockMethods[methodName] - if (!fcn) { - console.log(methodName) - console.log(mockMethods) - console.log(new Error(1).stack) - throw new Error(404, `Method '${methodName}' not found`) - } - - return waitForPromiseLocal(Promise.resolve(fcn.call(getMethodContext(), ...args))) - } export function apply( methodName: string, args: any[], @@ -238,21 +210,6 @@ export namespace MeteorMock { // but it'll do for now: call(methodName, ...args, asyncCallback) } - export function applyAsync( - methodName: string, - args: any[], - _options?: { - wait?: boolean - onResultReceived?: Function - returnStubValue?: boolean - throwStubExceptions?: boolean - } - ): any { - // ? - // This is a bad mock, since it doesn't support any of the options.. - // but it'll do for now: - return callAsync(methodName, ...args) - } export function absoluteUrl(path?: string): string { return path + '' // todo } diff --git a/meteor/lib/__tests__/lib.test.ts b/meteor/lib/__tests__/lib.test.ts index 03b53c2453..cf75a51335 100644 --- a/meteor/lib/__tests__/lib.test.ts +++ b/meteor/lib/__tests__/lib.test.ts @@ -15,8 +15,11 @@ import { equivalentArrays, LogLevel, makePromise, + MeteorPromiseApply, } from '../lib' import { MeteorMock } from '../../__mocks__/meteor' +import { MeteorDebugMethods } from '../../server/methods' +import { Settings } from '../Settings' // require('../../../../../server/api/ingest/mosDevice/api.ts') // include in order to create the Meteor methods needed @@ -25,6 +28,27 @@ describe('lib/lib', () => { MeteorMock.mockSetServerEnvironment() }) + testInFiber('MeteorPromiseApply', async () => { + // set up method: + Settings.enableUserAccounts = false + MeteorDebugMethods({ + myMethod: async (value1: string, value2: string) => { + // Do an async operation, to ensure that asynchronous operations work: + const v = await new Promise((resolve) => { + setTimeout(() => { + resolve(value1 + value2) + }, 10) + }) + return v + }, + }) + const pValue: any = MeteorPromiseApply('myMethod', ['myValue', 'AAA']).catch((e) => { + throw e + }) + expect(pValue).toHaveProperty('then') // be a promise + const value = await pValue + expect(value).toEqual('myValueAAA') + }) testInFiber('getCurrentTime', () => { systemTime.diff = 5439 MeteorMock.mockSetClientEnvironment() diff --git a/meteor/lib/api/methods.ts b/meteor/lib/api/methods.ts index 57e221d7d6..f6aa1ced79 100644 --- a/meteor/lib/api/methods.ts +++ b/meteor/lib/api/methods.ts @@ -1,4 +1,5 @@ import * as _ from 'underscore' +import { MeteorPromiseApply } from '../lib' import { NewBlueprintAPI, BlueprintAPIMethods } from './blueprint' import { NewClientAPI, ClientAPIMethods } from './client' import { NewExternalMessageQueueAPI, ExternalMessageQueueAPIMethods } from './ExternalMessageQueue' @@ -73,11 +74,11 @@ function makeMethods( _.each(methods, (serverMethodName: any, methodName: string) => { if (listOfMethodsThatShouldNotRetry?.includes(methodName)) { resultingMethods[methodName] = async (...args) => - Meteor.applyAsync(serverMethodName, args, { + MeteorPromiseApply(serverMethodName, args, { noRetry: true, }) } else { - resultingMethods[methodName] = async (...args) => Meteor.applyAsync(serverMethodName, args) + resultingMethods[methodName] = async (...args) => MeteorPromiseApply(serverMethodName, args) } }) return resultingMethods diff --git a/meteor/lib/lib.ts b/meteor/lib/lib.ts index b740f61c61..e02b93acf5 100644 --- a/meteor/lib/lib.ts +++ b/meteor/lib/lib.ts @@ -20,6 +20,26 @@ type PromisifyFunction = T extends (...args: any) => any ? (...args: Parameters) => Promise> | ReturnType : T +/** + * Convenience method to convert a Meteor.apply() into a Promise + * @param callName {string} Method name + * @param args {Array} An array of arguments for the method call + * @param options (Optional) An object with options for the call. See Meteor documentation. + * @returns {Promise} A promise containing the result of the called method. + */ +export async function MeteorPromiseApply( + callName: Parameters[0], + args: Parameters[1], + options?: Parameters[2] +): Promise { + return new Promise((resolve, reject) => { + Meteor.apply(callName, args, options, (err, res) => { + if (err) reject(err) + else resolve(res) + }) + }) +} + // The diff is currently only used client-side const systemTime = { hasBeenSet: false, diff --git a/meteor/lib/typings/meteor.d.ts b/meteor/lib/typings/meteor.d.ts deleted file mode 100644 index 287ec3d077..0000000000 --- a/meteor/lib/typings/meteor.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -declare module 'meteor/meteor' { - export namespace Meteor { - /** - * Pending https://github.com/meteor/meteor/pull/12645 - */ - - interface MethodApplyOptions { - /** - * (Client only) If true, don't send this method until all previous method calls have completed, and don't send any subsequent method calls until this one is completed. - */ - wait?: boolean | undefined - /** - * (Client only) This callback is invoked with the error or result of the method (just like `asyncCallback`) as soon as the error or result is available. The local cache may not yet reflect the writes performed by the method. - */ - onResultReceived?: ((error: global_Error | Meteor.Error | undefined, result?: Result) => void) | undefined - /** - * (Client only) if true, don't send this method again on reload, simply call the callback an error with the error code 'invocation-failed'. - */ - noRetry?: boolean | undefined - /** - * (Client only) If true then in cases where we would have otherwise discarded the stub's return value and returned undefined, instead we go ahead and return it. Specifically, this is any time other than when (a) we are already inside a stub or (b) we are in Node and no callback was provided. Currently we require this flag to be explicitly passed to reduce the likelihood that stub return values will be confused with server return values; we may improve this in future. - */ - returnStubValue?: boolean | undefined - /** - * (Client only) If true, exceptions thrown by method stubs will be thrown instead of logged, and the method will not be invoked on the server. - */ - throwStubExceptions?: boolean | undefined - } - - /** - * Invokes a method with an async stub, passing any number of arguments. - * @param name Name of method to invoke - * @param args Method arguments - * @param options Optional execution options - * @param asyncCallback Optional callback - */ - function applyAsync( - name: string, - args: ReadonlyArray, - options?: MethodApplyOptions - ): Promise - } -} From b4b5dbcbf3c21cea8001b7ca28438306fcf4b4d7 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 10 Jul 2023 09:47:28 +0100 Subject: [PATCH 018/479] chore: update system version --- meteor/server/migration/currentSystemVersion.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meteor/server/migration/currentSystemVersion.ts b/meteor/server/migration/currentSystemVersion.ts index 1733ac8f98..ed34372725 100644 --- a/meteor/server/migration/currentSystemVersion.ts +++ b/meteor/server/migration/currentSystemVersion.ts @@ -49,4 +49,4 @@ */ // Note: Only set this to release versions, (ie X.Y.Z), not pre-releases (ie X.Y.Z-0-pre-release) -export const CURRENT_SYSTEM_VERSION = '1.50.0' +export const CURRENT_SYSTEM_VERSION = '1.51.0' From 8eaedd22e8efb9750f00ff472301c6b3f2d0f0af Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 11 Jul 2023 16:34:41 +0100 Subject: [PATCH 019/479] feat: refactor server-core-integration subscription handling to reduce duplication --- .../live-status-gateway/src/coreHandler.ts | 12 ++- packages/live-status-gateway/src/wsHandler.ts | 4 +- .../mos-gateway/src/CoreMosDeviceHandler.ts | 18 +--- packages/mos-gateway/src/coreHandler.ts | 9 +- packages/playout-gateway/src/coreHandler.ts | 24 ++--- packages/server-core-integration/src/index.ts | 1 + .../src/lib/CoreConnectionChild.ts | 97 ++++++++----------- .../src/lib/coreConnection.ts | 95 ++++++++---------- .../src/lib/subscriptions.ts | 95 ++++++++++++++++++ 9 files changed, 203 insertions(+), 152 deletions(-) create mode 100644 packages/server-core-integration/src/lib/subscriptions.ts diff --git a/packages/live-status-gateway/src/coreHandler.ts b/packages/live-status-gateway/src/coreHandler.ts index 9a8fb2d55f..b62b208f13 100644 --- a/packages/live-status-gateway/src/coreHandler.ts +++ b/packages/live-status-gateway/src/coreHandler.ts @@ -1,4 +1,10 @@ -import { CoreConnection, CoreOptions, DDPConnectorOptions, Observer } from '@sofie-automation/server-core-integration' +import { + CoreConnection, + CoreOptions, + DDPConnectorOptions, + Observer, + SubscriptionId, +} from '@sofie-automation/server-core-integration' import { DeviceConfig } from './connector' import { Logger } from 'winston' import { Process } from './process' @@ -91,14 +97,14 @@ export class CoreHandler { await this.updateCoreStatus() } - async setupSubscription(collection: string, ...params: any[]): Promise { + async setupSubscription(collection: string, ...params: any[]): Promise { this.logger.info(`Core: Set up subscription for '${collection}'`) const subscriptionId = await this.core.autoSubscribe(collection, ...params) this.logger.info(`Core: Subscription for '${collection}' set up with id ${subscriptionId}`) return subscriptionId } - unsubscribe(subscriptionId: string): void { + unsubscribe(subscriptionId: SubscriptionId): void { this.logger.info(`Core: Unsubscribing id '${subscriptionId}'`) this.core.unsubscribe(subscriptionId) } diff --git a/packages/live-status-gateway/src/wsHandler.ts b/packages/live-status-gateway/src/wsHandler.ts index 9aa3bd1865..504cbb3a15 100644 --- a/packages/live-status-gateway/src/wsHandler.ts +++ b/packages/live-status-gateway/src/wsHandler.ts @@ -1,5 +1,5 @@ import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { Observer } from '@sofie-automation/server-core-integration' +import { Observer, SubscriptionId } from '@sofie-automation/server-core-integration' import { Logger } from 'winston' import { WebSocket } from 'ws' import { CoreHandler } from './coreHandler' @@ -59,7 +59,7 @@ export abstract class CollectionBase { protected _subscribers: Set = new Set() protected _observers: Set> = new Set() protected _collectionData: T | undefined - protected _subscriptionId: string | undefined + protected _subscriptionId: SubscriptionId | undefined protected _dbObserver: Observer | undefined constructor( diff --git a/packages/mos-gateway/src/CoreMosDeviceHandler.ts b/packages/mos-gateway/src/CoreMosDeviceHandler.ts index 36b827c7d6..d93bae446c 100644 --- a/packages/mos-gateway/src/CoreMosDeviceHandler.ts +++ b/packages/mos-gateway/src/CoreMosDeviceHandler.ts @@ -70,7 +70,6 @@ export class CoreMosDeviceHandler { public _mosDevice: IMOSDevice private _coreParentHandler: CoreHandler private _mosHandler: MosHandler - private _subscriptions: Array = [] private _pendingStoryItemChanges: Array = [] private _pendingChangeTimeout: number = 60 * 1000 @@ -123,14 +122,9 @@ export class CoreMosDeviceHandler { this._mosDevice.idPrimary + ' ..' ) - this._subscriptions = [] - Promise.all([this.core.autoSubscribe('peripheralDeviceCommands', this.core.deviceId)]) - .then((subs) => { - this._subscriptions = this._subscriptions.concat(subs) - }) - .catch((e) => { - this._coreParentHandler.logger.error(e) - }) + Promise.all([this.core.autoSubscribe('peripheralDeviceCommands', this.core.deviceId)]).catch((e) => { + this._coreParentHandler.logger.error(e) + }) this._coreParentHandler.logger.info('CoreMos: Setting up observers..') @@ -433,14 +427,12 @@ export class CoreMosDeviceHandler { async dispose(): Promise { this._observers.forEach((obs) => obs.stop()) - for (const subId of this._subscriptions) { - this.core.unsubscribe(subId) - } - await this.core.setStatus({ statusCode: StatusCode.BAD, messages: ['Uninitialized'], }) + + await this.core.destroy() } killProcess(): void { this._coreParentHandler.killProcess() diff --git a/packages/mos-gateway/src/coreHandler.ts b/packages/mos-gateway/src/coreHandler.ts index df766245d4..17908dcf10 100644 --- a/packages/mos-gateway/src/coreHandler.ts +++ b/packages/mos-gateway/src/coreHandler.ts @@ -33,7 +33,6 @@ export class CoreHandler { private _deviceOptions: DeviceConfig private _coreMosHandlers: Array = [] private _onConnected?: () => any - private _subscriptions: Array = [] private _isInitialized = false private _executedFunctions: { [id: string]: boolean } = {} private _coreConfig?: CoreConfig @@ -93,10 +92,6 @@ export class CoreHandler { throw Error('core is undefined!') } - for (const subId of this._subscriptions) { - this.core.unsubscribe(subId) - } - await this.core.setStatus({ statusCode: StatusCode.FATAL, messages: ['Shutting down'], @@ -193,13 +188,11 @@ export class CoreHandler { } this.logger.info('Core: Setting up subscriptions for ' + this.core.deviceId + '..') - const subs = await Promise.all([ + await Promise.all([ this.core.autoSubscribe('peripheralDeviceForDevice', this.core.deviceId), this.core.autoSubscribe('peripheralDeviceCommands', this.core.deviceId), ]) - this._subscriptions = this._subscriptions.concat(subs) - this.setupObserverForPeripheralDeviceCommands(this) } executeFunction(cmd: PeripheralDeviceCommand, fcnObject: CoreHandler | CoreMosDeviceHandler): void { diff --git a/packages/playout-gateway/src/coreHandler.ts b/packages/playout-gateway/src/coreHandler.ts index 08e66e26c6..16dc4a2a75 100644 --- a/packages/playout-gateway/src/coreHandler.ts +++ b/packages/playout-gateway/src/coreHandler.ts @@ -11,6 +11,7 @@ import { StudioId, unprotectString, Observer, + SubscriptionId, } from '@sofie-automation/server-core-integration' import { MediaObject, DeviceOptionsAny, ActionExecutionResult } from 'timeline-state-resolver' import * as _ from 'underscore' @@ -56,8 +57,8 @@ export class CoreHandler { private _certificates?: Buffer[] private _studioId: StudioId | undefined - private _timelineSubscription: string | null = null - private _expectedItemsSubscription: string | null = null + private _timelineSubscription: SubscriptionId | null = null + private _expectedItemsSubscription: SubscriptionId | null = null private _statusInitialized = false private _statusDestroyed = false @@ -213,7 +214,7 @@ export class CoreHandler { .autoSubscribe('timeline', { studioId: studioId, }) - .then((subscriptionId: string | null) => { + .then((subscriptionId) => { this._timelineSubscription = subscriptionId }) .catch((err: any) => { @@ -229,7 +230,7 @@ export class CoreHandler { .autoSubscribe('expectedPlayoutItems', { studioId: studioId, }) - .then((subscriptionId: string | null) => { + .then((subscriptionId) => { this._expectedItemsSubscription = subscriptionId }) .catch((err: any) => { @@ -454,7 +455,6 @@ export class CoreTSRDeviceHandler { public _device!: BaseRemoteDeviceIntegration private _coreParentHandler: CoreHandler private _tsrHandler: TSRHandler - private _subscriptions: Array = [] private _hasGottenStatusChange = false private _deviceStatus: PeripheralDeviceAPI.PeripheralDeviceStatusObject = { statusCode: StatusCode.BAD, @@ -511,10 +511,8 @@ export class CoreTSRDeviceHandler { this._coreParentHandler.logger.info( 'CoreTSRDevice: Setting up subscriptions for ' + this.core.deviceId + ' for device ' + deviceId + ' ..' ) - this._subscriptions = [] try { - const sub = await this.core.autoSubscribe('peripheralDeviceCommands', this.core.deviceId) - this._subscriptions.push(sub) + await this.core.autoSubscribe('peripheralDeviceCommands', this.core.deviceId) } catch (e) { this._coreParentHandler.logger.error(e) } @@ -583,19 +581,15 @@ export class CoreTSRDeviceHandler { } async dispose(): Promise { - this._observers.forEach((obs) => { - obs.stop() - }) - - for (const subId of this._subscriptions) { - this.core.unsubscribe(subId) - } + this._observers.forEach((obs) => obs.stop()) await this._tsrHandler.tsr.removeDevice(this._deviceId) await this.core.setStatus({ statusCode: StatusCode.BAD, messages: ['Uninitialized'], }) + + await this.core.destroy() } killProcess(): void { this._coreParentHandler.killProcess() diff --git a/packages/server-core-integration/src/index.ts b/packages/server-core-integration/src/index.ts index 7546adc1a9..264351a660 100644 --- a/packages/server-core-integration/src/index.ts +++ b/packages/server-core-integration/src/index.ts @@ -3,6 +3,7 @@ export * from './lib/configManifest' export * from './lib/ddpClient' export * from './lib/methods' export * from './lib/process' +export { SubscriptionId } from './lib/subscriptions' // Re-export some util from shared-lib export * from '@sofie-automation/shared-lib/dist/lib/lib' diff --git a/packages/server-core-integration/src/lib/CoreConnectionChild.ts b/packages/server-core-integration/src/lib/CoreConnectionChild.ts index d969d893ac..d985fe02f1 100644 --- a/packages/server-core-integration/src/lib/CoreConnectionChild.ts +++ b/packages/server-core-integration/src/lib/CoreConnectionChild.ts @@ -1,5 +1,4 @@ import { EventEmitter } from 'eventemitter3' -import * as _ from 'underscore' import { PeripheralDeviceStatusObject, PeripheralDeviceInitOptions, @@ -14,6 +13,7 @@ import { PeripheralDeviceForDevice } from '@sofie-automation/shared-lib/dist/cor import { ProtectedString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { CoreConnection, Collection, CoreOptions } from './coreConnection' import { CorePinger } from './ping' +import { SubscriptionId, SubscriptionsHelper } from './subscriptions' export interface ChildCoreOptions { deviceId: PeripheralDeviceId @@ -39,13 +39,8 @@ export class CoreConnectionChild extends EventEmitter private _parentOptions!: CoreOptions private _coreOptions: ChildCoreOptions private _methodQueue!: ConnectionMethodsQueue + private _subscriptions!: SubscriptionsHelper - private _autoSubscriptions: { - [subscriptionId: string]: { - publicationName: string - params: Array - } - } = {} private _pinger: CorePinger private _destroyed = false @@ -74,7 +69,7 @@ export class CoreConnectionChild extends EventEmitter async init(parent: CoreConnection, parentOptions: CoreOptions): Promise { this._destroyed = false - parent.on('connected', this._renewAutoSubscriptions) + parent.on('connected', () => this._subscriptions.renewAutoSubscriptions()) parent.on('connectionChanged', this.doTriggerPing) this._parent = parent @@ -84,14 +79,21 @@ export class CoreConnectionChild extends EventEmitter deviceId: this._coreOptions.deviceId, deviceToken: parentOptions.deviceToken, }) + this._subscriptions = new SubscriptionsHelper( + this._emitError.bind(this), + this._parent.ddp, + parentOptions.deviceToken + ) return this._sendInit() } async destroy(): Promise { this._destroyed = true + this._subscriptions.unsubscribeAll() + if (this._parent) { - this._parent.off('connected', this._renewAutoSubscriptions) + this._parent.off('connected', () => this._subscriptions.renewAutoSubscriptions()) this._parent.off('connectionChanged', this.doTriggerPing) this._parent.removeChild(this) @@ -156,50 +158,39 @@ export class CoreConnectionChild extends EventEmitter return this._parent.getCollection(collectionName) } - async subscribe(publicationName: string, ...params: Array): Promise { - return this.resubscribe(undefined, publicationName, ...params) - } - private async resubscribe( - existingSubscriptionId: string | undefined, - publicationName: string, - ...params: Array - ): Promise { - return new Promise((resolve, reject) => { - if (!this.ddp.ddpClient) { - reject('subscribe: DDP client is not initialized') - return - } - try { - const subscriptionId = this.ddp.ddpClient.subscribe( - publicationName, // name of Meteor Publish function to subscribe to - params.concat([this._parentOptions.deviceToken]), // parameters used by the Publish function - () => { - // TODO - I think this callback has an error parameter? - - // callback when the subscription is complete - resolve(subscriptionId) - }, - existingSubscriptionId - ) - } catch (e) { - reject(e) - } - }) + // /** + // * Subscribe to a DDP publication + // * Upon reconnecting to Sofie, this publication will be terminated + // */ + // async subscribeOnce(publicationName: string, ...params: Array): Promise { + // if (!this._subscriptions) throw new Error('Connection is not ready to handle subscriptions') + + // return this._subscriptions.subscribeOnce(publicationName, ...params) + // } + /** + * Subscribe to a DDP publication + * Upon reconnecting to Sofie, this publication will be restarted + */ + async autoSubscribe(publicationName: string, ...params: Array): Promise { + if (!this._subscriptions) throw new Error('Connection is not ready to handle subscriptions') + + return this._subscriptions.autoSubscribe(publicationName, ...params) } /** - * Like a subscribe, but automatically renews it upon reconnection + * Unsubscribe from subscription to a DDP publication */ - async autoSubscribe(publicationName: string, ...params: Array): Promise { - const subscriptionId = await this.subscribe(publicationName, ...params) - this._autoSubscriptions[subscriptionId] = { - publicationName: publicationName, - params: params, - } - return subscriptionId + unsubscribe(subscriptionId: SubscriptionId): void { + if (!this._subscriptions) throw new Error('Connection is not ready to handle subscriptions') + + this._subscriptions.unsubscribe(subscriptionId) } - unsubscribe(subscriptionId: string): void { - this.ddp.ddpClient?.unsubscribe(subscriptionId) - delete this._autoSubscriptions[subscriptionId] + /** + * Unsubscribe from all subscriptions to DDP publications + */ + unsubscribeAll(): void { + if (!this._subscriptions) throw new Error('Connection is not ready to handle subscriptions') + + this._subscriptions.unsubscribeAll() } observe(collectionName: string): Observer { if (!this._parent) throw new Error('Connection has been destroyed') @@ -241,12 +232,4 @@ export class CoreConnectionChild extends EventEmitter return this.coreMethods.initialize(options) } - - private _renewAutoSubscriptions = () => { - _.each(this._autoSubscriptions, (sub, subId) => { - this.resubscribe(subId, sub.publicationName, ...sub.params).catch((e) => - this._emitError('renewSubscr ' + sub.publicationName + ': ' + e) - ) - }) - } } diff --git a/packages/server-core-integration/src/lib/coreConnection.ts b/packages/server-core-integration/src/lib/coreConnection.ts index 115dd6729e..c3979ba7c1 100644 --- a/packages/server-core-integration/src/lib/coreConnection.ts +++ b/packages/server-core-integration/src/lib/coreConnection.ts @@ -21,6 +21,7 @@ import { PeripheralDeviceForDevice } from '@sofie-automation/shared-lib/dist/cor import { ProtectedString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { ChildCoreOptions, CoreConnectionChild } from './CoreConnectionChild' import { CorePinger } from './ping' +import { SubscriptionId, SubscriptionsHelper } from './subscriptions' // eslint-disable-next-line @typescript-eslint/no-var-requires const PkgInfo = require('../../package.json') @@ -74,18 +75,13 @@ export type CoreConnectionEvents = { export class CoreConnection extends EventEmitter { private _ddp: DDPConnector | undefined private _methodQueue: ConnectionMethodsQueue | undefined + private _subscriptions: SubscriptionsHelper | undefined private _children: Array = [] private _coreOptions: CoreOptions private _timeSync: TimeSync | null = null private _watchDog?: WatchDog private _watchDogPingResponse = '' private _connected = false - private _autoSubscriptions: { - [subscriptionId: string]: { - publicationName: string - params: Array - } - } = {} private _sentConnectionId = '' private _pinger: CorePinger private _destroyed = false @@ -114,7 +110,11 @@ export class CoreConnection extends EventEmitter { } async init(ddpOptions0?: DDPConnectorOptions): Promise { this._destroyed = false - this.on('connected', () => this._renewAutoSubscriptions()) + this.on('connected', () => { + if (this._subscriptions) { + this._subscriptions.renewAutoSubscriptions() + } + }) const ddpOptions = ddpOptions0 || { host: '127.0.0.1', @@ -125,6 +125,11 @@ export class CoreConnection extends EventEmitter { if (!_.has(ddpOptions, 'autoReconnectTimer')) ddpOptions.autoReconnectTimer = 1000 this._ddp = new DDPConnector(ddpOptions) this._methodQueue = new ConnectionMethodsQueue(this._ddp, this._coreOptions) + this._subscriptions = new SubscriptionsHelper( + this._emitError.bind(this), + this._ddp, + this._coreOptions.deviceToken + ) this._ddp.on('error', (err) => { this._emitError('ddpError: ' + (_.isObject(err) && err.message) || err.toString()) @@ -302,50 +307,39 @@ export class CoreConnection extends EventEmitter { } return c } - async subscribe(publicationName: string, ...params: Array): Promise { - return this.resubscribe(undefined, publicationName, ...params) - } - private async resubscribe( - existingSubscriptionId: string | undefined, - publicationName: string, - ...params: Array - ): Promise { - return new Promise((resolve, reject) => { - if (!this.ddp.ddpClient) { - reject('subscribe: DDP client is not initialized') - return - } - try { - const subscriptionId = this.ddp.ddpClient.subscribe( - publicationName, // name of Meteor Publish function to subscribe to - params.concat([this._coreOptions.deviceToken]), // parameters used by the Publish function - () => { - // TODO - I think this callback has an error parameter? - - // callback when the subscription is complete - resolve(subscriptionId) - }, - existingSubscriptionId - ) - } catch (e) { - reject(e) - } - }) + // /** + // * Subscribe to a DDP publication + // * Upon reconnecting to Sofie, this publication will be terminated + // */ + // async subscribeOnce(publicationName: string, ...params: Array): Promise { + // if (!this._subscriptions) throw new Error('Connection is not ready to handle subscriptions') + + // return this._subscriptions.subscribeOnce(publicationName, ...params) + // } + /** + * Subscribe to a DDP publication + * Upon reconnecting to Sofie, this publication will be restarted + */ + async autoSubscribe(publicationName: string, ...params: Array): Promise { + if (!this._subscriptions) throw new Error('Connection is not ready to handle subscriptions') + + return this._subscriptions.autoSubscribe(publicationName, ...params) } /** - * Like a subscribe, but automatically renews it upon reconnection + * Unsubscribe from subscroption to a DDP publication */ - async autoSubscribe(publicationName: string, ...params: Array): Promise { - const subscriptionId = await this.subscribe(publicationName, ...params) - this._autoSubscriptions[subscriptionId] = { - publicationName: publicationName, - params: params, - } - return subscriptionId + unsubscribe(subscriptionId: SubscriptionId): void { + if (!this._subscriptions) throw new Error('Connection is not ready to handle subscriptions') + + this._subscriptions.unsubscribe(subscriptionId) } - unsubscribe(subscriptionId: string): void { - this.ddp.ddpClient?.unsubscribe(subscriptionId) - delete this._autoSubscriptions[subscriptionId] + /** + * Unsubscribe from all subscriptions to DDP publications + */ + unsubscribeAll(): void { + if (!this._subscriptions) throw new Error('Connection is not ready to handle subscriptions') + + this._subscriptions.unsubscribeAll() } observe(collectionName: string): Observer { if (!this.ddp.ddpClient) { @@ -447,11 +441,4 @@ export class CoreConnection extends EventEmitter { return }) } - private _renewAutoSubscriptions() { - _.each(this._autoSubscriptions, (sub, subId) => { - this.resubscribe(subId, sub.publicationName, ...sub.params).catch((e) => - this._emitError('renewSubscr ' + sub.publicationName + ': ' + e) - ) - }) - } } diff --git a/packages/server-core-integration/src/lib/subscriptions.ts b/packages/server-core-integration/src/lib/subscriptions.ts new file mode 100644 index 0000000000..6723d8cfc5 --- /dev/null +++ b/packages/server-core-integration/src/lib/subscriptions.ts @@ -0,0 +1,95 @@ +import { ProtectedString, protectString, unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' +import type { DDPConnector } from './ddpConnector' + +export type SubscriptionId = ProtectedString<'SubscriptionId'> + +export class SubscriptionsHelper { + readonly #ddp: DDPConnector + readonly #deviceToken: string + + readonly #autoSubscriptions = new Map< + SubscriptionId, + { + publicationName: string + params: Array + } + >() + readonly #otherSubscriptions = new Set() + + constructor(private readonly emitError: (err: string) => void, ddp: DDPConnector, deviceToken: string) { + this.#ddp = ddp + this.#deviceToken = deviceToken + } + + public async subscribeOnce(publicationName: string, ...params: Array): Promise { + const subscriptionId = await this.subscribeWithId(undefined, publicationName, ...params) + this.#otherSubscriptions.add(subscriptionId) + return subscriptionId + } + + private async subscribeWithId( + existingSubscriptionId: string | undefined, + publicationName: string, + ...params: Array + ): Promise { + return new Promise((resolve, reject) => { + if (!this.#ddp.ddpClient) { + reject('subscribe: DDP client is not initialized') + return + } + try { + const subscriptionId = this.#ddp.ddpClient.subscribe( + publicationName, // name of Meteor Publish function to subscribe to + params.concat([this.#deviceToken]), // parameters used by the Publish function + () => { + // TODO - I think this callback has an error parameter? + + // callback when the subscription is complete + resolve(protectString(subscriptionId)) + }, + existingSubscriptionId + ) + } catch (e) { + reject(e) + } + }) + } + + async autoSubscribe(publicationName: string, ...params: Array): Promise { + const subscriptionId = await this.subscribeOnce(publicationName, ...params) + this.#autoSubscriptions.set(subscriptionId, { + publicationName: publicationName, + params: params, + }) + return subscriptionId + } + + public unsubscribe(subscriptionId: SubscriptionId): void { + this.#ddp.ddpClient?.unsubscribe(unprotectString(subscriptionId)) + this.#autoSubscriptions.delete(subscriptionId) + this.#otherSubscriptions.delete(subscriptionId) + } + + public renewAutoSubscriptions(): void { + // Forget the other presumed dead subscriptions + this.#otherSubscriptions.clear() + + for (const [subId, sub] of this.#autoSubscriptions.entries()) { + this.subscribeWithId(unprotectString(subId), sub.publicationName, ...sub.params).catch((e) => + this.emitError('renewSubscr ' + sub.publicationName + ': ' + e) + ) + } + } + + public unsubscribeAll(): void { + for (const subId of this.#otherSubscriptions) { + this.unsubscribe(subId) + } + this.#otherSubscriptions.clear() + + for (const subId of this.#autoSubscriptions.keys()) { + this.unsubscribe(subId) + } + this.#autoSubscriptions.clear() + } +} From 65f543ad59d218dd5336bb293fa6621c3703f8a4 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Mon, 17 Jul 2023 16:16:02 +0200 Subject: [PATCH 020/479] feat: Media Status WIP --- meteor/client/ui/MediaStatus/MediaStatus.scss | 0 meteor/client/ui/MediaStatus/MediaStatus.tsx | 9 +++++++++ 2 files changed, 9 insertions(+) create mode 100644 meteor/client/ui/MediaStatus/MediaStatus.scss create mode 100644 meteor/client/ui/MediaStatus/MediaStatus.tsx diff --git a/meteor/client/ui/MediaStatus/MediaStatus.scss b/meteor/client/ui/MediaStatus/MediaStatus.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/meteor/client/ui/MediaStatus/MediaStatus.tsx b/meteor/client/ui/MediaStatus/MediaStatus.tsx new file mode 100644 index 0000000000..ed6b32281c --- /dev/null +++ b/meteor/client/ui/MediaStatus/MediaStatus.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +interface IProps { + className?: string +} + +export function MediaStatus({ className }: IProps): JSX.Element { + return

+} From fa3747ec1cc7965ecc12670022844513fea212e2 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Tue, 18 Jul 2023 17:32:01 +0200 Subject: [PATCH 021/479] feat: Media Status WIP --- meteor/client/ui/MediaStatus/MediaStatus.tsx | 208 +++++++++++++++++- meteor/client/ui/Status.tsx | 4 +- meteor/client/ui/Status/MediaStatus.tsx | 6 + meteor/package.json | 2 +- packages/blueprints-integration/package.json | 2 +- packages/corelib/package.json | 2 +- packages/documentation/package.json | 2 +- packages/job-worker/package.json | 2 +- packages/live-status-gateway/package.json | 2 +- packages/mos-gateway/package.json | 2 +- packages/openapi/package.json | 2 +- packages/playout-gateway/package.json | 2 +- packages/server-core-integration/package.json | 2 +- packages/shared-lib/package.json | 2 +- 14 files changed, 224 insertions(+), 16 deletions(-) create mode 100644 meteor/client/ui/Status/MediaStatus.tsx diff --git a/meteor/client/ui/MediaStatus/MediaStatus.tsx b/meteor/client/ui/MediaStatus/MediaStatus.tsx index ed6b32281c..970c6aab35 100644 --- a/meteor/client/ui/MediaStatus/MediaStatus.tsx +++ b/meteor/client/ui/MediaStatus/MediaStatus.tsx @@ -1,9 +1,209 @@ -import React from 'react' +import React, { useMemo } from 'react' +import { useSubscription, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' +import { PubSub } from '../../../lib/api/pubsub' +import { getSegmentsWithPartInstances } from '../../../lib/Rundown' +import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { + AdLibActions, + AdLibPieces, + PieceInstances, + Pieces, + RundownBaselineAdLibActions, + RundownBaselineAdLibPieces, + RundownPlaylists, + Rundowns, +} from '../../collections' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' -interface IProps { +export function MediaStatus({ + className, + playlistIds, +}: { className?: string -} + playlistIds: RundownPlaylistId[] +}): JSX.Element { + const playlistsWithContent = useTracker( + () => + RundownPlaylists.find( + { + _id: { + $in: playlistIds, + }, + }, + { + projection: { + assignedAbSessions: 0, + currentPartInfo: 0, + holdState: 0, + lastIncorrectPartPlaybackReported: 0, + lastTakeTime: 0, + metaData: 0, + modified: 0, + nextPartInfo: 0, + nextSegmentId: 0, + nextTimeOffset: 0, + previousPartInfo: 0, + previousPersistentState: 0, + resetTime: 0, + rundownsStartedPlayback: 0, + trackedAbSessions: 0, + }, + } + ) + .fetch() + .sort(sortRundownPlaylists) + .map((playlist) => ({ + playlist, + segments: getSegmentsWithPartInstances( + playlist, + undefined, + undefined, + undefined, + { + projection: { + displayAs: 0, + externalId: 0, + externalModified: 0, + metaData: 0, + notes: 0, + segmentTiming: 0, + showShelf: 0, + }, + }, + undefined, + { + projection: { + timings: 0, + partPlayoutTimings: 0, + }, + } + ), + })), + [], + [] + ) + + const rundowns = useTracker( + () => + Rundowns.find({ + playlistId: { + $in: playlistIds, + }, + }).fetch(), + [playlistIds], + [] + ) + + const rundownIds = useMemo(() => rundowns.map((rundown) => rundown._id), [rundowns]) + const showStyleBaseIds = useMemo( + () => Array.from(new Set(rundowns.map((rundown) => rundown.showStyleBaseId))), + [rundowns] + ) + + // TODO Rework this. All we need is a list of PartIds and RundownIds. Fetch the individual content types + // in separate trackers so that there's less revalidation, if one of the object changes + const pieces = useTracker( + () => + playlistsWithContent.map(({ segments, playlist }, index) => ({ + playlist: playlist, + playlistRank: index, + partInstances: segments + .map((segment, segmentIndex) => + segment.partInstances.map((partInstance, partInstanceIndex) => ({ + partId: partInstance.part._id, + partInstanceRank: partInstanceIndex, + segmentRank: segmentIndex, + pieceInstances: PieceInstances.find({ + playlistActivationId: playlist.activationId, + partInstanceId: partInstance._id, + reset: { + $ne: true, + }, + adLibSourceId: { + $exists: false, + }, + }) + .fetch() + .filter( + (pieceInstance) => + pieceInstance.piece.expectedPackages && pieceInstance.piece.expectedPackages.length > 0 + ), + pieces: Pieces.find({ + startPartId: partInstance.part._id, + expectedPackages: { + $exists: true, + }, + }) + .fetch() + .filter((piece) => piece.expectedPackages && piece.expectedPackages.length > 0), + adlibs: AdLibPieces.find({ + partId: partInstance.part._id, + }) + .fetch() + .filter((piece) => piece.expectedPackages && piece.expectedPackages.length > 0), + adlibActions: AdLibActions.find({ + partId: partInstance.part._id, + }) + .fetch() + .filter((adlibAction) => adlibAction.expectedPackages && adlibAction.expectedPackages.length > 0), + })) + ) + .flat(), + adlibs: RundownBaselineAdLibPieces.find({ + rundownId: { + $in: [], // TODO somehow get the list of rundowns for this playlist, + }, + }), + adlibActions: RundownBaselineAdLibActions.find({ + rundownId: { + $in: [], // TODO somehow get the list of rundowns for this playlist, + }, + }), + })), + [playlistsWithContent], + [] + ) + + useSubscription(PubSub.rundownPlaylists, { + _id: { + $in: playlistIds, + }, + }) + useSubscription(PubSub.rundowns, playlistIds, null) + useSubscription(PubSub.segments, { + rundownId: { + $in: rundownIds, + }, + }) + useSubscription(PubSub.parts, rundownIds) + useSubscription(PubSub.partInstancesSimple, { + rundownId: { + $in: rundownIds, + }, + reset: { + $ne: true, + }, + }) + useSubscription(PubSub.pieceInstancesSimple, { + rundownId: { + $in: rundownIds, + }, + }) + useSubscription(PubSub.pieces, { + startRundownId: { + $in: rundownIds, + }, + }) + useSubscription(PubSub.showStyleBases, { + _id: { + $in: showStyleBaseIds, + }, + }) -export function MediaStatus({ className }: IProps): JSX.Element { return
} + +function sortRundownPlaylists(a: DBRundownPlaylist, b: DBRundownPlaylist): number { + return unprotectString(a._id).localeCompare(unprotectString(b._id)) +} diff --git a/meteor/client/ui/Status.tsx b/meteor/client/ui/Status.tsx index 49f98cdbb2..0ab87ac272 100644 --- a/meteor/client/ui/Status.tsx +++ b/meteor/client/ui/Status.tsx @@ -11,6 +11,7 @@ import { EvaluationView } from './Status/Evaluations' import { MeteorReactComponent } from '../lib/MeteorReactComponent' import { PubSub } from '../../lib/api/pubsub' import { ExpectedPackagesStatus } from './Status/package-status' +import { MediaStatus } from './Status/MediaStatus' interface IStatusMenuProps { match?: any @@ -105,7 +106,8 @@ class Status extends MeteorReactComponent> { {/* */} - + + diff --git a/meteor/client/ui/Status/MediaStatus.tsx b/meteor/client/ui/Status/MediaStatus.tsx new file mode 100644 index 0000000000..fb4e61f971 --- /dev/null +++ b/meteor/client/ui/Status/MediaStatus.tsx @@ -0,0 +1,6 @@ +import React from 'react' +import { MediaStatus as MediaStatusComponent } from '../MediaStatus/MediaStatus' + +export function MediaStatus(): JSX.Element | null { + return +} diff --git a/meteor/package.json b/meteor/package.json index 677ab48e01..567f4b50df 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -206,4 +206,4 @@ "@sofie-automation/shared-lib": "portal:../packages/shared-lib" }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} diff --git a/packages/blueprints-integration/package.json b/packages/blueprints-integration/package.json index d0d3a98393..3c2ab8b965 100644 --- a/packages/blueprints-integration/package.json +++ b/packages/blueprints-integration/package.json @@ -51,4 +51,4 @@ ] }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} diff --git a/packages/corelib/package.json b/packages/corelib/package.json index 9e752988ac..675306e632 100644 --- a/packages/corelib/package.json +++ b/packages/corelib/package.json @@ -65,4 +65,4 @@ ] }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} diff --git a/packages/documentation/package.json b/packages/documentation/package.json index f22bca28e0..0664ee210b 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -38,4 +38,4 @@ "last 1 safari version" ] } -} \ No newline at end of file +} diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index 0fcb7deb2d..597344cb41 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -68,4 +68,4 @@ ] }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} diff --git a/packages/live-status-gateway/package.json b/packages/live-status-gateway/package.json index 002020d6cb..9bb26856a6 100644 --- a/packages/live-status-gateway/package.json +++ b/packages/live-status-gateway/package.json @@ -76,4 +76,4 @@ "yarn lint:raw" ] } -} \ No newline at end of file +} diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index 27711cdeda..ec84b1ce7f 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -83,4 +83,4 @@ ] }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} diff --git a/packages/openapi/package.json b/packages/openapi/package.json index a2576877b0..e2072b6d70 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -52,4 +52,4 @@ "yarn lint:raw" ] } -} \ No newline at end of file +} diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index ff074f588c..09568af009 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -74,4 +74,4 @@ ] }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} diff --git a/packages/server-core-integration/package.json b/packages/server-core-integration/package.json index 4ba40a8e6e..7e2c135dc0 100644 --- a/packages/server-core-integration/package.json +++ b/packages/server-core-integration/package.json @@ -87,4 +87,4 @@ ] }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 58bbdd5942..b1129113e1 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -53,4 +53,4 @@ ] }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} From 4468458be41fe975c687ef44cad1a6e12f562b6b Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 19 Jul 2023 17:42:17 +0200 Subject: [PATCH 022/479] feat: MediaStatus WIP --- .../lib/ReactMeteorData/ReactMeteorData.tsx | 26 +- meteor/client/ui/MediaStatus/MediaStatus.tsx | 600 ++++++++++++++++-- meteor/client/ui/Status/MediaStatus.tsx | 9 +- 3 files changed, 563 insertions(+), 72 deletions(-) diff --git a/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx b/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx index 6a36e486dd..55ff4385d3 100644 --- a/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx +++ b/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx @@ -352,7 +352,31 @@ export function useSubscription(sub: K, ...args: Pa subscription.stop() }, 1000) } - }, [stringifyObjects(args)]) + }, [sub, stringifyObjects(args)]) + + return ready +} + +export function useSubscriptions( + sub: K, + argsArray: Parameters[] +): boolean { + const [ready, setReady] = useState(false) + + useEffect(() => { + const subscriptions = Tracker.nonreactive(() => argsArray.map((args) => meteorSubscribe(sub, ...args))) + const isReadyComp = Tracker.nonreactive(() => + Tracker.autorun(() => setReady(subscriptions.reduce((memo, subscription) => memo && subscription.ready(), true))) + ) + return () => { + isReadyComp.stop() + setTimeout(() => { + for (const subscription of subscriptions) { + subscription.stop() + } + }, 1000) + } + }, [sub, stringifyObjects(argsArray)]) return ready } diff --git a/meteor/client/ui/MediaStatus/MediaStatus.tsx b/meteor/client/ui/MediaStatus/MediaStatus.tsx index 970c6aab35..156c318584 100644 --- a/meteor/client/ui/MediaStatus/MediaStatus.tsx +++ b/meteor/client/ui/MediaStatus/MediaStatus.tsx @@ -1,8 +1,18 @@ import React, { useMemo } from 'react' -import { useSubscription, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' +import { useSubscription, useSubscriptions, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { PubSub } from '../../../lib/api/pubsub' import { getSegmentsWithPartInstances } from '../../../lib/Rundown' -import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { + AdLibActionId, + PartId, + PartInstanceId, + PieceId, + PieceInstanceId, + RundownBaselineAdLibActionId, + RundownId, + RundownPlaylistId, + ShowStyleBaseId, +} from '@sofie-automation/corelib/dist/dataModel/Ids' import { AdLibActions, AdLibPieces, @@ -14,7 +24,16 @@ import { Rundowns, } from '../../collections' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' +import { ProtectedString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' +import { ExpectedPackage } from '@sofie-automation/shared-lib/dist/package-manager/package' +import { PartInvalidReason } from '@sofie-automation/corelib/dist/dataModel/Part' +import { IBlueprintActionManifestDisplayContent, SourceLayerType } from '@sofie-automation/blueprints-integration' +import { PieceContentStatusObj } from '../../../lib/mediaObjects' +import { Piece, PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' +import { literal } from '@sofie-automation/corelib/dist/lib' +import { UIPieceContentStatuses, UIShowStyleBases } from '../Collections' +import { isTranslatableMessage, translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' +import { i18nTranslator } from '../i18n' export function MediaStatus({ className, @@ -23,6 +42,43 @@ export function MediaStatus({ className?: string playlistIds: RundownPlaylistId[] }): JSX.Element { + const { rundownIds, partIds, partInstanceIds, partInstanceMeta, partMeta, rundownMeta, showStyleBaseIds } = + useRundownPlaylists(playlistIds) + + const pieceInstanceItems = usePieceInstanceItems(partInstanceIds, partInstanceMeta) + const pieceItems = usePieceItems(partIds, partMeta) + const adlibItems = useAdLibItems(partIds, partMeta) + const adlibActionItems = useAdLibActionItems(partIds, partMeta) + const rundownAdlibItems = useRundownAdLibItems(rundownIds, rundownMeta) + const rundownAdlibActionItems = useRundownAdLibActionItems(rundownIds, rundownMeta) + + useMediaStatusSubscriptions(playlistIds, rundownIds, showStyleBaseIds) + + const combinedList = useCombinedItems( + pieceInstanceItems, + pieceItems, + adlibItems, + adlibActionItems, + rundownAdlibItems, + rundownAdlibActionItems + ) + + return ( +
    + {combinedList.map((listItem) => ( +
  • + {listItem.name}, {listItem.playlistName}, {listItem.sourceLayerName}, {listItem.sourceLayerType} +
  • + ))} +
+ ) +} + +function useCombinedItems(...items: (MediaStatusListItem | undefined)[][]): MediaStatusListItem[] { + return useMemo(() => items.flat().filter(Boolean) as MediaStatusListItem[], items) +} + +function useRundownPlaylists(playlistIds: RundownPlaylistId[]) { const playlistsWithContent = useTracker( () => RundownPlaylists.find( @@ -95,76 +151,104 @@ export function MediaStatus({ [] ) + const { partInstanceIds, partIds, partInstanceMeta, partMeta } = useMemo(() => { + const partInstanceIds: PartInstanceId[] = [] + const partIds: PartId[] = [] + const partInstanceMeta = new Map() + const partMeta = new Map() + playlistsWithContent.forEach(({ playlist, segments }, playlistRank) => { + segments.forEach(({ segment, partInstances }, segmentRank) => { + partInstances.forEach((partInstance, partInstanceRank) => { + const rundown = rundowns.find((rundown) => rundown._id === segment.rundownId) + if (partInstance.isTemporary) { + partIds.push(partInstance.part._id) + partMeta.set(partInstance.part._id, { + playlistId: playlist._id, + playlistName: playlist.name, + rundownName: rundown?.name, + showStyleBaseId: rundown?.showStyleBaseId, + playlistRank, + segmentRank, + segmentIdentifier: segment.identifier, + invalid: partInstance.part.invalid, + invalidReason: partInstance.part.invalidReason, + identifier: partInstance.part.identifier, + rank: partInstanceRank, + }) + return + } + + partInstanceIds.push(partInstance._id) + partInstanceMeta.set(partInstance._id, { + playlistId: playlist._id, + playlistName: playlist.name, + rundownName: rundown?.name, + showStyleBaseId: rundown?.showStyleBaseId, + playlistRank, + segmentRank, + segmentIdentifier: segment.identifier, + invalid: partInstance.part.invalid, + invalidReason: partInstance.part.invalidReason, + identifier: partInstance.part.identifier, + rank: partInstanceRank, + }) + }) + }) + }) + + return { + partInstanceIds, + partIds, + partInstanceMeta, + partMeta, + } + }, [playlistsWithContent]) + const rundownIds = useMemo(() => rundowns.map((rundown) => rundown._id), [rundowns]) const showStyleBaseIds = useMemo( () => Array.from(new Set(rundowns.map((rundown) => rundown.showStyleBaseId))), [rundowns] ) - // TODO Rework this. All we need is a list of PartIds and RundownIds. Fetch the individual content types - // in separate trackers so that there's less revalidation, if one of the object changes - const pieces = useTracker( - () => - playlistsWithContent.map(({ segments, playlist }, index) => ({ - playlist: playlist, - playlistRank: index, - partInstances: segments - .map((segment, segmentIndex) => - segment.partInstances.map((partInstance, partInstanceIndex) => ({ - partId: partInstance.part._id, - partInstanceRank: partInstanceIndex, - segmentRank: segmentIndex, - pieceInstances: PieceInstances.find({ - playlistActivationId: playlist.activationId, - partInstanceId: partInstance._id, - reset: { - $ne: true, - }, - adLibSourceId: { - $exists: false, - }, - }) - .fetch() - .filter( - (pieceInstance) => - pieceInstance.piece.expectedPackages && pieceInstance.piece.expectedPackages.length > 0 - ), - pieces: Pieces.find({ - startPartId: partInstance.part._id, - expectedPackages: { - $exists: true, - }, - }) - .fetch() - .filter((piece) => piece.expectedPackages && piece.expectedPackages.length > 0), - adlibs: AdLibPieces.find({ - partId: partInstance.part._id, - }) - .fetch() - .filter((piece) => piece.expectedPackages && piece.expectedPackages.length > 0), - adlibActions: AdLibActions.find({ - partId: partInstance.part._id, - }) - .fetch() - .filter((adlibAction) => adlibAction.expectedPackages && adlibAction.expectedPackages.length > 0), - })) - ) - .flat(), - adlibs: RundownBaselineAdLibPieces.find({ - rundownId: { - $in: [], // TODO somehow get the list of rundowns for this playlist, - }, - }), - adlibActions: RundownBaselineAdLibActions.find({ - rundownId: { - $in: [], // TODO somehow get the list of rundowns for this playlist, - }, - }), - })), - [playlistsWithContent], - [] - ) + const rundownMeta = useMemo(() => { + const rundownMeta = new Map() + rundowns.forEach((rundown) => { + const playlistIndex = playlistsWithContent.findIndex( + (playlistWithContent) => playlistWithContent.playlist._id === rundown.playlistId + ) + if (playlistIndex < 0) return + const playlist = playlistsWithContent[playlistIndex].playlist + const rank = playlist.rundownIdsInOrder.indexOf(rundown._id) + + rundownMeta.set(rundown._id, { + playlistId: rundown.playlistId, + playlistName: playlist.name, + playlistRank: playlistIndex, + rundownName: rundown.name, + rank, + showStyleBaseId: rundown.showStyleBaseId, + }) + }) + + return rundownMeta + }, [rundowns, playlistsWithContent]) + return { + rundownIds, + partIds, + partInstanceIds, + partInstanceMeta, + partMeta, + rundownMeta, + showStyleBaseIds, + } +} + +function useMediaStatusSubscriptions( + playlistIds: RundownPlaylistId[], + rundownIds: RundownId[], + showStyleBaseIds: ShowStyleBaseId[] +) { useSubscription(PubSub.rundownPlaylists, { _id: { $in: playlistIds, @@ -195,15 +279,391 @@ export function MediaStatus({ $in: rundownIds, }, }) - useSubscription(PubSub.showStyleBases, { - _id: { - $in: showStyleBaseIds, + useSubscription(PubSub.adLibActions, { + rundownId: { + $in: rundownIds, }, }) + useSubscription(PubSub.adLibPieces, { + rundownId: { + $in: rundownIds, + }, + }) + useSubscription(PubSub.rundownBaselineAdLibActions, { + rundownId: { + $in: rundownIds, + }, + }) + useSubscription(PubSub.rundownBaselineAdLibPieces, { + rundownId: { + $in: rundownIds, + }, + }) + const uiShowStyleBaseSubArguments = useMemo( + () => showStyleBaseIds.map((showStyleBaseId) => [showStyleBaseId] as [ShowStyleBaseId]), + [showStyleBaseIds] + ) + useSubscriptions(PubSub.uiShowStyleBase, uiShowStyleBaseSubArguments) + const uiPieceContentStatusesSubArguments = useMemo( + () => playlistIds.map((playlistIds) => [playlistIds] as [RundownPlaylistId]), + [playlistIds] + ) + useSubscriptions(PubSub.uiPieceContentStatuses, uiPieceContentStatusesSubArguments) +} + +function useAdLibActionItems(partIds: PartId[], partMeta: Map) { + const adlibActions = useTracker( + () => + AdLibActions.find({ + partId: { + $in: partIds, + }, + expectedPackages: { + $exists: true, + }, + }) + .fetch() + .filter(onlyWithExpectedPackages), + [partIds], + [] + ) + const adlibActionItems = useTracker( + () => + adlibActions + .map((adlibAction) => { + const meta = partMeta.get(adlibAction.partId) + if (!meta) return + + const adLibActionDisplay = adlibAction.display as Partial + + return getListItemFromPieceAndPartMeta( + adlibAction._id, + { + name: isTranslatableMessage(adlibAction.display.label) + ? translateMessage(adlibAction.display.label, i18nTranslator) + : adlibAction.display.label, + sourceLayerId: adLibActionDisplay.sourceLayerId, + _rank: adlibAction.display._rank, + content: adLibActionDisplay?.content, + }, + meta + ) + }) + .filter(Boolean), + [adlibActions, partMeta], + [] + ) + + return adlibActionItems +} + +function useAdLibItems(partIds: PartId[], partMeta: Map) { + const adlibs = useTracker( + () => + AdLibPieces.find({ + partId: { + $in: partIds, + }, + expectedPackages: { + $exists: true, + }, + }) + .fetch() + .filter(onlyWithExpectedPackages), + [partIds], + [] + ) + const adlibItems = useTracker( + () => + adlibs.map((adlib) => { + if (!adlib.partId) return // this will never happen, since all AdLibPieces in this array will have it set + const meta = partMeta.get(adlib.partId) + + if (!meta) return + return getListItemFromPieceAndPartMeta(adlib._id, adlib, meta) + }), + [adlibs, partMeta], + [] + ) + + return adlibItems +} + +function useRundownAdLibActionItems(rundownIds: RundownId[], rundownMeta: Map) { + const rundownAdlibActions = useTracker( + () => + RundownBaselineAdLibActions.find({ + rundownId: { + $in: rundownIds, + }, + expectedPackages: { + $exists: true, + }, + }) + .fetch() + .filter(onlyWithExpectedPackages), + [rundownIds], + [] + ) + const rundownAdlibActionItems = useTracker( + () => + rundownAdlibActions.map((adlibAction) => { + const meta = rundownMeta.get(adlibAction.rundownId) + if (!meta) return + + const adLibActionDisplay = adlibAction.display as Partial + + return getListItemFromRundownPieceAndRundownMeta( + adlibAction._id, + { + name: isTranslatableMessage(adlibAction.display.label) + ? translateMessage(adlibAction.display.label, i18nTranslator) + : adlibAction.display.label, + sourceLayerId: adLibActionDisplay.sourceLayerId, + _rank: adlibAction.display._rank, + content: adLibActionDisplay?.content, + }, + meta + ) + }), + [rundownAdlibActions, rundownMeta], + [] + ) + + return rundownAdlibActionItems +} + +function useRundownAdLibItems(rundownIds: RundownId[], rundownMeta: Map) { + const rundownAdlibs = useTracker( + () => + RundownBaselineAdLibPieces.find({ + rundownId: { + $in: rundownIds, + }, + expectedPackages: { + $exists: true, + }, + }) + .fetch() + .filter(onlyWithExpectedPackages), + [rundownIds], + [] + ) + const rundownAdlibItems = useTracker( + () => + rundownAdlibs.map((adlib) => { + const meta = rundownMeta.get(adlib.rundownId) + if (!meta) return + + return getListItemFromRundownPieceAndRundownMeta(adlib._id, adlib, meta) + }), + [rundownAdlibs, rundownMeta], + [] + ) + + return rundownAdlibItems +} - return
+function usePieceItems(partIds: PartId[], partMeta: Map) { + const pieces = useTracker( + () => + Pieces.find({ + startPartId: { + $in: partIds, + }, + expectedPackages: { + $exists: true, + }, + }) + .fetch() + .filter(onlyWithExpectedPackages), + [partIds], + [] + ) + const pieceItems = useTracker( + () => + pieces.map((piece) => { + const meta = partMeta.get(piece.startPartId) + + if (!meta) return + return getListItemFromPieceAndPartMeta(piece._id, piece, meta) + }), + [pieces, partMeta], + [] + ) + return pieceItems +} + +function usePieceInstanceItems(partInstanceIds: PartInstanceId[], partInstanceMeta: Map) { + const pieceInstances = useTracker( + () => + PieceInstances.find({ + partInstanceId: { + $in: partInstanceIds, + }, + reset: { + $ne: true, + }, + adLibSourceId: { + $exists: false, + }, + }) + .fetch() + .filter((pieceInstance) => onlyWithExpectedPackages(pieceInstance.piece)), + [partInstanceIds], + [] + ) + const pieceInstanceItems = useTracker( + () => + pieceInstances.map((pieceInstance) => { + const meta = partInstanceMeta.get(pieceInstance.partInstanceId) + + if (!meta) return + return getListItemFromPieceAndPartMeta(pieceInstance._id, pieceInstance.piece, meta) + }), + [pieceInstances, partInstanceMeta], + [] + ) + + return pieceInstanceItems } function sortRundownPlaylists(a: DBRundownPlaylist, b: DBRundownPlaylist): number { return unprotectString(a._id).localeCompare(unprotectString(b._id)) } + +interface PartMeta { + playlistId: RundownPlaylistId + playlistName: string + playlistRank: number + rundownName: string | undefined + showStyleBaseId: ShowStyleBaseId | undefined + segmentRank: number + segmentIdentifier: string | undefined + invalid: boolean | undefined + invalidReason: PartInvalidReason | undefined + rank: number + identifier: string | undefined +} + +interface RundownMeta { + playlistId: RundownPlaylistId + playlistName: string + playlistRank: number + rundownName: string | undefined + showStyleBaseId: ShowStyleBaseId | undefined + rank: number +} + +interface MediaStatusListItem { + _id: ProtectedString + sourceLayerType: SourceLayerType | undefined + sourceLayerName: string | undefined + partIdentifier: string | undefined + segmentIdentifier: string | undefined + name: string + duration: number | undefined + playlistName: string + playlistRank: number + rundownName: string | undefined + segmentRank: number | undefined + partRank: number | undefined + rank: number + status: PieceStatusCode + pieceContentStatus: PieceContentStatusObj | undefined +} + +function onlyWithExpectedPackages(obj: { expectedPackages?: ExpectedPackage.Any[] }) { + return obj.expectedPackages && obj.expectedPackages.length > 0 +} + +function getListItemFromRundownPieceAndRundownMeta( + objId: SomePieceId, + piece: Pick & Partial> & { _rank?: number | undefined }, + meta: RundownMeta +): MediaStatusListItem { + const showStyleBase = meta.showStyleBaseId && UIShowStyleBases.findOne(meta.showStyleBaseId) + const sourceLayer = piece.sourceLayerId !== undefined ? showStyleBase?.sourceLayers?.[piece.sourceLayerId] : undefined + + const partIdentifier = undefined + const segmentIdentifier = undefined + const partRank = meta.rank + const segmentRank = undefined + const playlistName = meta.playlistName + const playlistRank = meta.playlistRank + const rundownName = meta.rundownName + + const uiPieceContentStatus = UIPieceContentStatuses.findOne(objId) + + const name = piece.name + const rank = piece._rank ?? 0 // if no rank (Pieces), should go to the top when "natural"-order sorting + const sourceLayerType = sourceLayer?.type + const sourceLayerName = sourceLayer?.name + const duration = piece.content?.sourceDuration + const status = uiPieceContentStatus?.status.status ?? PieceStatusCode.UNKNOWN + + return literal({ + _id: objId, + name, + sourceLayerName, + sourceLayerType, + partIdentifier, + segmentIdentifier, + segmentRank, + partRank, + playlistName, + rundownName, + duration, + playlistRank, + rank, + status, + pieceContentStatus: uiPieceContentStatus?.status, + }) +} + +/** This is a reactive function, depending on UIShowStyleBases and UIPieceContentStatuses */ +function getListItemFromPieceAndPartMeta( + objId: SomePieceId, + piece: Pick & Partial> & { _rank?: number | undefined }, + meta: PartMeta +): MediaStatusListItem { + const showStyleBase = meta.showStyleBaseId && UIShowStyleBases.findOne(meta.showStyleBaseId) + const sourceLayer = piece.sourceLayerId !== undefined ? showStyleBase?.sourceLayers?.[piece.sourceLayerId] : undefined + + const partIdentifier = meta.identifier + const segmentIdentifier = meta.segmentIdentifier + const partRank = meta.rank + const segmentRank = meta.segmentRank + const playlistName = meta.playlistName + const playlistRank = meta.playlistRank + const rundownName = meta.rundownName + + const uiPieceContentStatus = UIPieceContentStatuses.findOne(objId) + + const name = piece.name + const rank = piece._rank ?? 0 // if no rank (Pieces), should go to the top when "natural"-order sorting + const sourceLayerType = sourceLayer?.type + const sourceLayerName = sourceLayer?.name + const duration = piece.content?.sourceDuration + const status = uiPieceContentStatus?.status.status ?? PieceStatusCode.UNKNOWN + + return literal({ + _id: objId, + name, + sourceLayerName, + sourceLayerType, + partIdentifier, + segmentIdentifier, + segmentRank, + partRank, + playlistName, + rundownName, + duration, + playlistRank, + rank, + status, + pieceContentStatus: uiPieceContentStatus?.status, + }) +} + +type SomePieceId = PieceId | AdLibActionId | RundownBaselineAdLibActionId | PieceInstanceId diff --git a/meteor/client/ui/Status/MediaStatus.tsx b/meteor/client/ui/Status/MediaStatus.tsx index fb4e61f971..81e546682c 100644 --- a/meteor/client/ui/Status/MediaStatus.tsx +++ b/meteor/client/ui/Status/MediaStatus.tsx @@ -1,6 +1,13 @@ import React from 'react' import { MediaStatus as MediaStatusComponent } from '../MediaStatus/MediaStatus' +import { useSubscription, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' +import { RundownPlaylists } from '../../collections' +import { PubSub } from '../../../lib/api/pubsub' export function MediaStatus(): JSX.Element | null { - return + const playlistIds = useTracker(() => RundownPlaylists.find().map((playlist) => playlist._id), [], []) + + useSubscription(PubSub.rundownPlaylists, {}) + + return } From e8acba2d43f7da396fb2d15fdb90e19fe9872fb6 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 19 Jul 2023 17:50:25 +0200 Subject: [PATCH 023/479] feat(MediaStatus): WIP --- meteor/client/ui/MediaStatus/MediaStatus.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/meteor/client/ui/MediaStatus/MediaStatus.tsx b/meteor/client/ui/MediaStatus/MediaStatus.tsx index 156c318584..85d03932f1 100644 --- a/meteor/client/ui/MediaStatus/MediaStatus.tsx +++ b/meteor/client/ui/MediaStatus/MediaStatus.tsx @@ -67,7 +67,8 @@ export function MediaStatus({
    {combinedList.map((listItem) => (
  • - {listItem.name}, {listItem.playlistName}, {listItem.sourceLayerName}, {listItem.sourceLayerType} + {listItem.name}, {listItem.playlistName}, {listItem.sourceLayerName}, {listItem.sourceLayerType},{' '} + {listItem.status}
  • ))}
@@ -136,7 +137,7 @@ function useRundownPlaylists(playlistIds: RundownPlaylistId[]) { } ), })), - [], + [playlistIds], [] ) @@ -255,6 +256,11 @@ function useMediaStatusSubscriptions( }, }) useSubscription(PubSub.rundowns, playlistIds, null) + const uiShowStyleBaseSubArguments = useMemo( + () => showStyleBaseIds.map((showStyleBaseId) => [showStyleBaseId] as [ShowStyleBaseId]), + [showStyleBaseIds] + ) + useSubscriptions(PubSub.uiShowStyleBase, uiShowStyleBaseSubArguments) useSubscription(PubSub.segments, { rundownId: { $in: rundownIds, @@ -299,11 +305,6 @@ function useMediaStatusSubscriptions( $in: rundownIds, }, }) - const uiShowStyleBaseSubArguments = useMemo( - () => showStyleBaseIds.map((showStyleBaseId) => [showStyleBaseId] as [ShowStyleBaseId]), - [showStyleBaseIds] - ) - useSubscriptions(PubSub.uiShowStyleBase, uiShowStyleBaseSubArguments) const uiPieceContentStatusesSubArguments = useMemo( () => playlistIds.map((playlistIds) => [playlistIds] as [RundownPlaylistId]), [playlistIds] From 4d081ab8c3252067d87a6e5f40b4309076edd99a Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Mon, 24 Jul 2023 18:17:32 +0200 Subject: [PATCH 024/479] feat(MediaStatusList): finish CSS --- meteor/client/lib/ui/icons/notifications.tsx | 26 ++- meteor/client/lib/ui/icons/sorting.tsx | 44 +++++ meteor/client/ui/MediaStatus/MediaStatus.tsx | 121 +++++++----- .../ui/MediaStatus/MediaStatusIndicator.tsx | 29 +++ .../client/ui/MediaStatus/SortOrderButton.tsx | 38 ++++ .../SegmentTimeline/withMediaObjectStatus.tsx | 2 + meteor/client/ui/Status.tsx | 2 +- meteor/client/ui/Status/MediaStatus.tsx | 13 -- .../Status/media-status/MediaStatusList.scss | 51 +++++ .../media-status/MediaStatusListHeader.scss | 37 ++++ .../media-status/MediaStatusListHeader.tsx | 66 +++++++ .../media-status/MediaStatusListItem.scss | 94 +++++++++ .../media-status/MediaStatusListItem.tsx | 68 +++++++ .../client/ui/Status/media-status/index.tsx | 180 ++++++++++++++++++ meteor/lib/lib.ts | 9 + meteor/lib/mediaObjects.ts | 2 + .../checkPieceContentStatus.ts | 25 +++ 17 files changed, 749 insertions(+), 58 deletions(-) create mode 100644 meteor/client/lib/ui/icons/sorting.tsx create mode 100644 meteor/client/ui/MediaStatus/MediaStatusIndicator.tsx create mode 100644 meteor/client/ui/MediaStatus/SortOrderButton.tsx delete mode 100644 meteor/client/ui/Status/MediaStatus.tsx create mode 100644 meteor/client/ui/Status/media-status/MediaStatusList.scss create mode 100644 meteor/client/ui/Status/media-status/MediaStatusListHeader.scss create mode 100644 meteor/client/ui/Status/media-status/MediaStatusListHeader.tsx create mode 100644 meteor/client/ui/Status/media-status/MediaStatusListItem.scss create mode 100644 meteor/client/ui/Status/media-status/MediaStatusListItem.tsx create mode 100644 meteor/client/ui/Status/media-status/index.tsx diff --git a/meteor/client/lib/ui/icons/notifications.tsx b/meteor/client/lib/ui/icons/notifications.tsx index db4ca75874..0ade64292e 100644 --- a/meteor/client/lib/ui/icons/notifications.tsx +++ b/meteor/client/lib/ui/icons/notifications.tsx @@ -180,6 +180,22 @@ export function InformationIconSmall(): JSX.Element { ) } +export function OKIconSmall(): JSX.Element { + return ( + + + + ) +} + export function CollapseChevrons(): JSX.Element { return ( @@ -191,7 +207,15 @@ export function CollapseChevrons(): JSX.Element { export function HourglassIconSmall(): JSX.Element { return ( - + + + + + ) +} + +export function SortAscending(): JSX.Element { + return ( + + + + + ) +} + +export function SortDisabled(): JSX.Element { + return ( + + + + + + ) +} diff --git a/meteor/client/ui/MediaStatus/MediaStatus.tsx b/meteor/client/ui/MediaStatus/MediaStatus.tsx index 85d03932f1..9f85def83d 100644 --- a/meteor/client/ui/MediaStatus/MediaStatus.tsx +++ b/meteor/client/ui/MediaStatus/MediaStatus.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react' +import { useMemo } from 'react' import { useSubscription, useSubscriptions, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { PubSub } from '../../../lib/api/pubsub' import { getSegmentsWithPartInstances } from '../../../lib/Rundown' @@ -36,12 +36,14 @@ import { isTranslatableMessage, translateMessage } from '@sofie-automation/corel import { i18nTranslator } from '../i18n' export function MediaStatus({ - className, playlistIds, + children, + fallback, }: { - className?: string playlistIds: RundownPlaylistId[] -}): JSX.Element { + children: (listItems: MediaStatusListItem[]) => JSX.Element | null + fallback?: JSX.Element | null +}): JSX.Element | null { const { rundownIds, partIds, partInstanceIds, partInstanceMeta, partMeta, rundownMeta, showStyleBaseIds } = useRundownPlaylists(playlistIds) @@ -52,7 +54,7 @@ export function MediaStatus({ const rundownAdlibItems = useRundownAdLibItems(rundownIds, rundownMeta) const rundownAdlibActionItems = useRundownAdLibActionItems(rundownIds, rundownMeta) - useMediaStatusSubscriptions(playlistIds, rundownIds, showStyleBaseIds) + const allReady = useMediaStatusSubscriptions(playlistIds, rundownIds, showStyleBaseIds) const combinedList = useCombinedItems( pieceInstanceItems, @@ -63,16 +65,11 @@ export function MediaStatus({ rundownAdlibActionItems ) - return ( -
    - {combinedList.map((listItem) => ( -
  • - {listItem.name}, {listItem.playlistName}, {listItem.sourceLayerName}, {listItem.sourceLayerType},{' '} - {listItem.status} -
  • - ))} -
- ) + if (!allReady && combinedList.length == 0) { + return fallback ?? null + } + + return children(combinedList) } function useCombinedItems(...items: (MediaStatusListItem | undefined)[][]): MediaStatusListItem[] { @@ -249,25 +246,27 @@ function useMediaStatusSubscriptions( playlistIds: RundownPlaylistId[], rundownIds: RundownId[], showStyleBaseIds: ShowStyleBaseId[] -) { - useSubscription(PubSub.rundownPlaylists, { +): boolean { + const readyStatus: boolean[] = [] + let counter = 0 + readyStatus[counter++] = useSubscription(PubSub.rundownPlaylists, { _id: { $in: playlistIds, }, }) - useSubscription(PubSub.rundowns, playlistIds, null) + readyStatus[counter++] = useSubscription(PubSub.rundowns, playlistIds, null) const uiShowStyleBaseSubArguments = useMemo( () => showStyleBaseIds.map((showStyleBaseId) => [showStyleBaseId] as [ShowStyleBaseId]), [showStyleBaseIds] ) - useSubscriptions(PubSub.uiShowStyleBase, uiShowStyleBaseSubArguments) - useSubscription(PubSub.segments, { + readyStatus[counter++] = useSubscriptions(PubSub.uiShowStyleBase, uiShowStyleBaseSubArguments) + readyStatus[counter++] = useSubscription(PubSub.segments, { rundownId: { $in: rundownIds, }, }) - useSubscription(PubSub.parts, rundownIds) - useSubscription(PubSub.partInstancesSimple, { + readyStatus[counter++] = useSubscription(PubSub.parts, rundownIds) + readyStatus[counter++] = useSubscription(PubSub.partInstancesSimple, { rundownId: { $in: rundownIds, }, @@ -275,32 +274,32 @@ function useMediaStatusSubscriptions( $ne: true, }, }) - useSubscription(PubSub.pieceInstancesSimple, { + readyStatus[counter++] = useSubscription(PubSub.pieceInstancesSimple, { rundownId: { $in: rundownIds, }, }) - useSubscription(PubSub.pieces, { + readyStatus[counter++] = useSubscription(PubSub.pieces, { startRundownId: { $in: rundownIds, }, }) - useSubscription(PubSub.adLibActions, { + readyStatus[counter++] = useSubscription(PubSub.adLibActions, { rundownId: { $in: rundownIds, }, }) - useSubscription(PubSub.adLibPieces, { + readyStatus[counter++] = useSubscription(PubSub.adLibPieces, { rundownId: { $in: rundownIds, }, }) - useSubscription(PubSub.rundownBaselineAdLibActions, { + readyStatus[counter++] = useSubscription(PubSub.rundownBaselineAdLibActions, { rundownId: { $in: rundownIds, }, }) - useSubscription(PubSub.rundownBaselineAdLibPieces, { + readyStatus[counter++] = useSubscription(PubSub.rundownBaselineAdLibPieces, { rundownId: { $in: rundownIds, }, @@ -309,7 +308,9 @@ function useMediaStatusSubscriptions( () => playlistIds.map((playlistIds) => [playlistIds] as [RundownPlaylistId]), [playlistIds] ) - useSubscriptions(PubSub.uiPieceContentStatuses, uiPieceContentStatusesSubArguments) + readyStatus[counter++] = useSubscriptions(PubSub.uiPieceContentStatuses, uiPieceContentStatusesSubArguments) + + return readyStatus.reduce((mem, current) => mem && current, true) } function useAdLibActionItems(partIds: PartId[], partMeta: Map) { @@ -347,7 +348,9 @@ function useAdLibActionItems(partIds: PartId[], partMeta: Map) _rank: adlibAction.display._rank, content: adLibActionDisplay?.content, }, - meta + meta, + adlibAction.partId, + undefined ) }) .filter(Boolean), @@ -381,7 +384,7 @@ function useAdLibItems(partIds: PartId[], partMeta: Map) { const meta = partMeta.get(adlib.partId) if (!meta) return - return getListItemFromPieceAndPartMeta(adlib._id, adlib, meta) + return getListItemFromPieceAndPartMeta(adlib._id, adlib, meta, adlib.partId, undefined) }), [adlibs, partMeta], [] @@ -487,7 +490,7 @@ function usePieceItems(partIds: PartId[], partMeta: Map) { const meta = partMeta.get(piece.startPartId) if (!meta) return - return getListItemFromPieceAndPartMeta(piece._id, piece, meta) + return getListItemFromPieceAndPartMeta(piece._id, piece, meta, piece.startPartId, undefined) }), [pieces, partMeta], [] @@ -520,7 +523,13 @@ function usePieceInstanceItems(partInstanceIds: PartInstanceId[], partInstanceMe const meta = partInstanceMeta.get(pieceInstance.partInstanceId) if (!meta) return - return getListItemFromPieceAndPartMeta(pieceInstance._id, pieceInstance.piece, meta) + return getListItemFromPieceAndPartMeta( + pieceInstance._id, + pieceInstance.piece, + meta, + undefined, + pieceInstance.partInstanceId + ) }), [pieceInstances, partInstanceMeta], [] @@ -556,8 +565,11 @@ interface RundownMeta { rank: number } -interface MediaStatusListItem { +export interface MediaStatusListItem { _id: ProtectedString + playlistId: RundownPlaylistId + partId?: PartId + partInstanceId?: PartInstanceId sourceLayerType: SourceLayerType | undefined sourceLayerName: string | undefined partIdentifier: string | undefined @@ -569,6 +581,7 @@ interface MediaStatusListItem { rundownName: string | undefined segmentRank: number | undefined partRank: number | undefined + invalid: boolean rank: number status: PieceStatusCode pieceContentStatus: PieceContentStatusObj | undefined @@ -580,12 +593,15 @@ function onlyWithExpectedPackages(obj: { expectedPackages?: ExpectedPackage.Any[ function getListItemFromRundownPieceAndRundownMeta( objId: SomePieceId, - piece: Pick & Partial> & { _rank?: number | undefined }, + piece: Pick & + Partial> & { _rank?: number | undefined }, meta: RundownMeta -): MediaStatusListItem { +): MediaStatusListItem | undefined { const showStyleBase = meta.showStyleBaseId && UIShowStyleBases.findOne(meta.showStyleBaseId) const sourceLayer = piece.sourceLayerId !== undefined ? showStyleBase?.sourceLayers?.[piece.sourceLayerId] : undefined + if (sourceLayer && sourceLayer.isHidden) return + const partIdentifier = undefined const segmentIdentifier = undefined const partRank = meta.rank @@ -593,11 +609,15 @@ function getListItemFromRundownPieceAndRundownMeta( const playlistName = meta.playlistName const playlistRank = meta.playlistRank const rundownName = meta.rundownName + const playlistId = meta.playlistId - const uiPieceContentStatus = UIPieceContentStatuses.findOne(objId) + const uiPieceContentStatus = UIPieceContentStatuses.findOne({ + pieceId: objId, + }) const name = piece.name const rank = piece._rank ?? 0 // if no rank (Pieces), should go to the top when "natural"-order sorting + const invalid = piece.invalid ?? false const sourceLayerType = sourceLayer?.type const sourceLayerName = sourceLayer?.name const duration = piece.content?.sourceDuration @@ -605,6 +625,7 @@ function getListItemFromRundownPieceAndRundownMeta( return literal({ _id: objId, + playlistId, name, sourceLayerName, sourceLayerType, @@ -612,6 +633,7 @@ function getListItemFromRundownPieceAndRundownMeta( segmentIdentifier, segmentRank, partRank, + invalid, playlistName, rundownName, duration, @@ -625,12 +647,17 @@ function getListItemFromRundownPieceAndRundownMeta( /** This is a reactive function, depending on UIShowStyleBases and UIPieceContentStatuses */ function getListItemFromPieceAndPartMeta( objId: SomePieceId, - piece: Pick & Partial> & { _rank?: number | undefined }, - meta: PartMeta -): MediaStatusListItem { + piece: Pick & + Partial> & { _rank?: number | undefined }, + meta: PartMeta, + sourcePartId: PartId | undefined, + sourcePartInstanceId: PartInstanceId | undefined +): MediaStatusListItem | undefined { const showStyleBase = meta.showStyleBaseId && UIShowStyleBases.findOne(meta.showStyleBaseId) const sourceLayer = piece.sourceLayerId !== undefined ? showStyleBase?.sourceLayers?.[piece.sourceLayerId] : undefined + if (sourceLayer && sourceLayer.isHidden) return + const partIdentifier = meta.identifier const segmentIdentifier = meta.segmentIdentifier const partRank = meta.rank @@ -638,22 +665,30 @@ function getListItemFromPieceAndPartMeta( const playlistName = meta.playlistName const playlistRank = meta.playlistRank const rundownName = meta.rundownName + const playlistId = meta.playlistId - const uiPieceContentStatus = UIPieceContentStatuses.findOne(objId) + const uiPieceContentStatus = UIPieceContentStatuses.findOne({ + pieceId: objId, + }) const name = piece.name const rank = piece._rank ?? 0 // if no rank (Pieces), should go to the top when "natural"-order sorting + const invalid = piece.invalid ?? false const sourceLayerType = sourceLayer?.type - const sourceLayerName = sourceLayer?.name - const duration = piece.content?.sourceDuration + const sourceLayerName = sourceLayer?.abbreviation || sourceLayer?.name + const duration = piece.content?.sourceDuration ?? uiPieceContentStatus?.status.contentDuration ?? undefined const status = uiPieceContentStatus?.status.status ?? PieceStatusCode.UNKNOWN return literal({ _id: objId, + playlistId, + partId: sourcePartId, + partInstanceId: sourcePartInstanceId, name, sourceLayerName, sourceLayerType, partIdentifier, + invalid, segmentIdentifier, segmentRank, partRank, diff --git a/meteor/client/ui/MediaStatus/MediaStatusIndicator.tsx b/meteor/client/ui/MediaStatus/MediaStatusIndicator.tsx new file mode 100644 index 0000000000..715878bf92 --- /dev/null +++ b/meteor/client/ui/MediaStatus/MediaStatusIndicator.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import { PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' +import { assertNever } from '@sofie-automation/corelib/dist/lib' +import { WarningIconSmall, HourglassIconSmall, OKIconSmall } from '../../lib/ui/icons/notifications' + +export function MediaStatusIndicator({ + status, +}: { + status: PieceStatusCode | undefined + overlay: string | undefined +}): JSX.Element | null { + switch (status) { + case PieceStatusCode.OK: + return + case PieceStatusCode.SOURCE_NOT_READY: + return + case PieceStatusCode.SOURCE_BROKEN: + case PieceStatusCode.SOURCE_HAS_ISSUES: + case PieceStatusCode.SOURCE_MISSING: + case PieceStatusCode.SOURCE_NOT_SET: + case PieceStatusCode.UNKNOWN: + case undefined: + case PieceStatusCode.SOURCE_UNKNOWN_STATE: + return + default: + assertNever(status) + return <>Unknown: {status} + } +} diff --git a/meteor/client/ui/MediaStatus/SortOrderButton.tsx b/meteor/client/ui/MediaStatus/SortOrderButton.tsx new file mode 100644 index 0000000000..44a089d213 --- /dev/null +++ b/meteor/client/ui/MediaStatus/SortOrderButton.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import { assertNever } from '@sofie-automation/corelib/dist/lib' +import { SortAscending, SortDescending, SortDisabled } from '../../lib/ui/icons/sorting' + +export function SortOrderButton({ + className, + order, + onChange, +}: { + className?: string + order: Order + onChange?: (nextOrder: Order) => void +}): JSX.Element | null { + function onClick() { + switch (order) { + case 'asc': + onChange?.('desc') + return + case 'inactive': + case 'desc': + onChange?.('asc') + return + default: + assertNever(order) + return + } + } + + return ( + + ) +} + +type Order = 'asc' | 'desc' | 'inactive' diff --git a/meteor/client/ui/SegmentTimeline/withMediaObjectStatus.tsx b/meteor/client/ui/SegmentTimeline/withMediaObjectStatus.tsx index e93277fac5..40be71117b 100644 --- a/meteor/client/ui/SegmentTimeline/withMediaObjectStatus.tsx +++ b/meteor/client/ui/SegmentTimeline/withMediaObjectStatus.tsx @@ -39,6 +39,8 @@ const DEFAULT_STATUS = deepFreeze({ previewUrl: undefined, packageName: null, + + contentDuration: undefined, }) /** diff --git a/meteor/client/ui/Status.tsx b/meteor/client/ui/Status.tsx index 0ab87ac272..6ff7ef6b51 100644 --- a/meteor/client/ui/Status.tsx +++ b/meteor/client/ui/Status.tsx @@ -11,7 +11,7 @@ import { EvaluationView } from './Status/Evaluations' import { MeteorReactComponent } from '../lib/MeteorReactComponent' import { PubSub } from '../../lib/api/pubsub' import { ExpectedPackagesStatus } from './Status/package-status' -import { MediaStatus } from './Status/MediaStatus' +import { MediaStatus } from './Status/media-status' interface IStatusMenuProps { match?: any diff --git a/meteor/client/ui/Status/MediaStatus.tsx b/meteor/client/ui/Status/MediaStatus.tsx deleted file mode 100644 index 81e546682c..0000000000 --- a/meteor/client/ui/Status/MediaStatus.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react' -import { MediaStatus as MediaStatusComponent } from '../MediaStatus/MediaStatus' -import { useSubscription, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' -import { RundownPlaylists } from '../../collections' -import { PubSub } from '../../../lib/api/pubsub' - -export function MediaStatus(): JSX.Element | null { - const playlistIds = useTracker(() => RundownPlaylists.find().map((playlist) => playlist._id), [], []) - - useSubscription(PubSub.rundownPlaylists, {}) - - return -} diff --git a/meteor/client/ui/Status/media-status/MediaStatusList.scss b/meteor/client/ui/Status/media-status/MediaStatusList.scss new file mode 100644 index 0000000000..f10405da9b --- /dev/null +++ b/meteor/client/ui/Status/media-status/MediaStatusList.scss @@ -0,0 +1,51 @@ +.media-status-table-scrollbox { + max-height: calc(100vh - 182px); + overflow: auto; + margin-bottom: -40px; +} + +.media-status-table { + width: 100%; + width: -webkit-fill-available; +} + +.media-status-table-search { + float: right; + margin-top: 0.5em; + + .media-status-table-search__search-input { + &::placeholder { + color: #333; + font-style: italic; + } + + border: none; + border-radius: 1em; + width: 15em; + padding: 5px 0.5em; + + background: #f5f5f5; + } +} + +.media-status-table { + .media-status-item__rundown { + width: 17em; + } + + .media-status-item__status { + width: 3em; + } + + .media-status-item__source-layer { + width: 4em; + } + + .media-status-item__identifiers { + width: 4em; + } + + .media-status-item__duration { + width: 5em; + } +} diff --git a/meteor/client/ui/Status/media-status/MediaStatusListHeader.scss b/meteor/client/ui/Status/media-status/MediaStatusListHeader.scss new file mode 100644 index 0000000000..052a7d01cb --- /dev/null +++ b/meteor/client/ui/Status/media-status/MediaStatusListHeader.scss @@ -0,0 +1,37 @@ +.media-status-list-header { + > tr > th { + position: sticky; + top: 0; + background: #ffffff; + z-index: 10; + + &::after { + content: ' '; + display: block; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 2px; + border-bottom: 1px solid #d7d7d7; + border-top: 1px solid #d7d7d7; + } + } +} + +.media-status-list-header__sort-button { + background: none; + padding: 0; + margin: 0; + border: none; + text-align: center; + min-width: 1.5em; + padding: 5px 5px; + + > svg { + vertical-align: top; + margin-top: 0.4em; + height: 0.4em; + width: auto; + } +} diff --git a/meteor/client/ui/Status/media-status/MediaStatusListHeader.tsx b/meteor/client/ui/Status/media-status/MediaStatusListHeader.tsx new file mode 100644 index 0000000000..dc99b28699 --- /dev/null +++ b/meteor/client/ui/Status/media-status/MediaStatusListHeader.tsx @@ -0,0 +1,66 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { SortOrderButton } from '../../MediaStatus/SortOrderButton' + +export function MediaStatusListHeader({ + sortOrder, + sortBy, + onChange, +}: { + sortBy: SortBy + sortOrder: SortOrder + onChange?: (sortBy: SortBy, sortOrder: SortOrder) => void +}): JSX.Element | null { + const { t } = useTranslation() + + function changeSortOrder(newSortBy: SortBy, newSortOrder: SortOrder) { + if (sortBy !== newSortBy) { + onChange?.(newSortBy, newSortOrder) + return + } + onChange?.(sortBy, newSortOrder) + } + + return ( + + + + + + + changeSortOrder('status', order)} + /> + + + changeSortOrder('sourceLayer', order)} + /> + + + + changeSortOrder('name', order)} + /> + + + + + ) +} + +type SortBy = 'rundown' | 'status' | 'sourceLayer' | 'name' +type SortOrder = 'asc' | 'desc' | 'inactive' + +function matchSortKey(sortKey: SortBy, matchSortKey: SortBy, order: SortOrder): SortOrder { + if (matchSortKey === sortKey) return order + return 'inactive' +} diff --git a/meteor/client/ui/Status/media-status/MediaStatusListItem.scss b/meteor/client/ui/Status/media-status/MediaStatusListItem.scss new file mode 100644 index 0000000000..85ca774837 --- /dev/null +++ b/meteor/client/ui/Status/media-status/MediaStatusListItem.scss @@ -0,0 +1,94 @@ +@import '../../../styles/colorScheme'; +@import '../../../styles/itemTypeColors'; + +.media-status-item:nth-child(2n + 1) > td { + background: #f6f6f6; +} + +.media-status-item { + > td { + padding: 5px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + > .media-status-item__rundown { + font-weight: 400; + max-width: 10em; + } + + > .media-status-item__duration { + text-align: right; + } + + > .media-status-item__source-layer { + padding: 0; + } + + > .media-status-item__label { + font-weight: 400; + } + + .media-status-item__source-layer-indicator { + position: relative; + color: #fff; + width: 3.5em; + text-align: center; + font-weight: 400; + font-size: 0.85em; + line-height: 1.75em; + margin-top: 0.25em; + overflow: hidden; + white-space: nowrap; + + @include item-type-colors(); + @include missing-overlay(); + + &.media-status-item__source-layer-overlay { + display: block; + content: ' '; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + + @include invalid-overlay(); + } + + > .media-status-item__source-layer-label { + position: relative; + z-index: 1; + } + } + + > .media-status-item__status { + padding-top: 0.2em; + padding-bottom: 0; + + svg { + width: auto; + height: 1.3em; + } + + svg.hourglass { + g > path { + stroke: #8c8c8c; + } + clipPath > rect { + fill: #8c8c8c; + } + } + } + + .media-status-item__part-identifier { + display: inline-block; + background: #363636; + border-radius: 1em; + padding: 0 0.25em; + color: #fff; + font-weight: 400; + font-size: 0.85em; + } +} diff --git a/meteor/client/ui/Status/media-status/MediaStatusListItem.tsx b/meteor/client/ui/Status/media-status/MediaStatusListItem.tsx new file mode 100644 index 0000000000..50c2c8cb67 --- /dev/null +++ b/meteor/client/ui/Status/media-status/MediaStatusListItem.tsx @@ -0,0 +1,68 @@ +import React from 'react' +import { SourceLayerType } from '@sofie-automation/blueprints-integration' +import { PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' +import { NavLink } from 'react-router-dom' +import { RundownUtils } from '../../../lib/rundown' +import classNames from 'classnames' +import { formatTime } from '../../../../lib/lib' +import { MediaStatusIndicator } from '../../MediaStatus/MediaStatusIndicator' + +export function MediaStatusListItem({ + rundownName, + rundownTo, + status, + statusOverlay, + sourceLayerType, + sourceLayerName, + segmentIdentifier, + partIdentifier, + invalid, + label, + duration, +}: { + rundownName: string + rundownTo?: string + status: PieceStatusCode + statusOverlay?: string | undefined + sourceLayerType?: SourceLayerType | undefined + sourceLayerName?: string | undefined + segmentIdentifier?: string | undefined + partIdentifier?: string | undefined + invalid?: boolean | undefined + label: string + duration?: number | undefined +}): JSX.Element | null { + const sourceLayerClassName = + sourceLayerType !== undefined ? RundownUtils.getSourceLayerClassName(sourceLayerType) : undefined + + return ( + + + {rundownTo ? {rundownName} : rundownName} + + + + + +
+ {invalid &&
} +
{sourceLayerName}
+
+ + + {segmentIdentifier ?
{segmentIdentifier}
: null} + {partIdentifier ?
{partIdentifier}
: null} + + {label} + {duration ? formatTime(duration) : null} + + ) +} diff --git a/meteor/client/ui/Status/media-status/index.tsx b/meteor/client/ui/Status/media-status/index.tsx new file mode 100644 index 0000000000..fe1d4e4a76 --- /dev/null +++ b/meteor/client/ui/Status/media-status/index.tsx @@ -0,0 +1,180 @@ +import React, { CSSProperties, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { + MediaStatus as MediaStatusComponent, + MediaStatusListItem as IMediaStatusListItem, +} from '../../MediaStatus/MediaStatus' +import { useSubscription, useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData' +import { RundownPlaylists } from '../../../collections' +import { PubSub } from '../../../../lib/api/pubsub' +import { Spinner } from '../../../lib/Spinner' +import { MediaStatusListItem } from './MediaStatusListItem' +import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' +import { useTranslation } from 'react-i18next' +import { MediaStatusListHeader } from './MediaStatusListHeader' +import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' +import { assertNever } from '@sofie-automation/corelib/dist/lib' + +export function MediaStatus(): JSX.Element | null { + const scrollBox = useRef(null) + const [offsetTop, setOffsetTop] = useState(0) + + const [sortBy, setSortBy] = useState<'rundown' | 'status' | 'sourceLayer' | 'name'>('rundown') + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc') + const [filter, setFilter] = useState('') + + const playlistIds = useTracker(() => RundownPlaylists.find().map((playlist) => playlist._id), [], []) + + function onChangeSort(sortBy: SortBy, sortOrder: SortOrder) { + setSortOrder(sortOrder === 'inactive' ? 'asc' : sortOrder) + setSortBy(sortBy) + } + + useSubscription(PubSub.rundownPlaylists, {}) + + const { t } = useTranslation() + + useEffect(() => { + if (!scrollBox.current) return + + setOffsetTop(scrollBox.current.offsetTop) + }, []) + + const filterItems = useCallback( + (item: IMediaStatusListItem) => { + if (!filter || filter.trim().length === 0) return true + if (item.name.toLowerCase().indexOf(filter.toLowerCase().trim()) >= 0) return true + return false + }, + [filter] + ) + + const scrollBoxStyle = useMemo( + () => ({ + maxHeight: offsetTop ? `calc(100vh - ${offsetTop}px)` : undefined, + }), + [offsetTop] + ) + + return ( +
+
+
+ setFilter(e.target.value)} + /> +
+

{t('Media Status')}

+
+
+ + + + + + + } + > + {(items) => ( + <> + {items + .filter((item) => filterItems(item)) + .sort((a, b) => sortItems(a, b, sortBy, sortOrder)) + .map((item) => ( + translateMessage(message, t)) + .join(', ')} + sourceLayerType={item.sourceLayerType} + sourceLayerName={item.sourceLayerName} + partIdentifier={item.partIdentifier} + segmentIdentifier={item.segmentIdentifier} + invalid={item.invalid} + key={unprotectString(item._id)} + label={item.name} + duration={item.duration} + /> + ))} + + )} + + +
+ +
+
+
+ ) +} + +function sortItems(a: IMediaStatusListItem, b: IMediaStatusListItem, sortBy: SortBy, sortOrder: SortOrder) { + let result = 0 + switch (sortBy) { + case 'name': + result = sortByName(a, b) + break + case 'status': + result = sortByStatus(a, b) + break + case 'sourceLayer': + result = sortBySourceLayer(a, b) + break + case 'rundown': + result = sortByRundown(a, b) + break + default: + assertNever(sortBy) + break + } + + if (sortOrder === 'desc') return result * -1 + return result +} + +function sortByName(a: IMediaStatusListItem, b: IMediaStatusListItem) { + return a.name.localeCompare(b.name) || sortByRundown(a, b) +} + +function sortByStatus(a: IMediaStatusListItem, b: IMediaStatusListItem) { + return a.status - b.status || sortByRundown(a, b) +} + +function sortBySourceLayer(a: IMediaStatusListItem, b: IMediaStatusListItem) { + if (a.sourceLayerName === b.sourceLayerName) return sortByRundown(a, b) + if (a.sourceLayerName === undefined) return 1 + if (b.sourceLayerName === undefined) return -1 + return a.sourceLayerName.localeCompare(b.sourceLayerName) +} + +function sortByRundown(a: IMediaStatusListItem, b: IMediaStatusListItem) { + if (a.rundownName === b.rundownName) return sortBySegmentRank(a, b) + if (a.rundownName === undefined) return 1 + if (b.rundownName === undefined) return -1 + return a.rundownName?.localeCompare(b.rundownName) +} + +function sortBySegmentRank(a: IMediaStatusListItem, b: IMediaStatusListItem) { + if (a.segmentRank === b.segmentRank) return sortByPartRank(a, b) + return (a.segmentRank ?? 0) - (b.segmentRank ?? 0) +} + +function sortByPartRank(a: IMediaStatusListItem, b: IMediaStatusListItem) { + if (a.partRank === b.partRank) return sortByRank(a, b) + return (a.partRank ?? 0) - (b.partRank ?? 0) +} + +function sortByRank(a: IMediaStatusListItem, b: IMediaStatusListItem) { + return a.rank - b.rank +} + +type SortBy = 'rundown' | 'status' | 'sourceLayer' | 'name' +type SortOrder = 'asc' | 'desc' | 'inactive' diff --git a/meteor/lib/lib.ts b/meteor/lib/lib.ts index e02b93acf5..33e6a63438 100644 --- a/meteor/lib/lib.ts +++ b/meteor/lib/lib.ts @@ -93,6 +93,15 @@ export function formatDateTime(time: Time): string { return `${yyyy}-${mm}-${dd} ${hh}:${ii}:${ss}` } + +export function formatTime(time: number): string { + const ss = String(Math.ceil(time / 1000) % 60).padStart(2, '0') + const mm = String(Math.floor(time / 60000) % 60).padStart(2, '0') + const hh = String(Math.floor(time / 3600000)).padStart(2, '0') + + return `${hh}:${mm}:${ss}` +} + /** * Returns a string that can be used to compare objects for equality * @param objs diff --git a/meteor/lib/mediaObjects.ts b/meteor/lib/mediaObjects.ts index fedd7c9aa6..d59a024f61 100644 --- a/meteor/lib/mediaObjects.ts +++ b/meteor/lib/mediaObjects.ts @@ -25,4 +25,6 @@ export interface PieceContentStatusObj { previewUrl: string | undefined packageName: string | null + + contentDuration: number | undefined } diff --git a/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts index 3161f18639..038e6e3548 100644 --- a/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts +++ b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts @@ -252,6 +252,7 @@ export async function checkPieceContentStatusAndDependencies( previewUrl: undefined, packageName: null, + contentDuration: undefined, }, pieceDependencies, ] @@ -411,6 +412,16 @@ async function checkPieceContentMediaObjectStatus( } } + let contentDuration: number | null | undefined = undefined + if (metadata?.mediainfo?.streams?.length) { + const maximumStreamDuration = metadata.mediainfo.streams.reduce( + (prev, current) => + current.duration !== undefined ? Math.max(prev, Number.parseFloat(current.duration)) : prev, + Number.NaN + ) + contentDuration = Number.isFinite(maximumStreamDuration) ? maximumStreamDuration : undefined + } + return { status: pieceStatus, messages: messages.map((msg) => msg.message), @@ -427,6 +438,8 @@ async function checkPieceContentMediaObjectStatus( : undefined, packageName: metadata?.mediaId || null, + + contentDuration, } } @@ -617,6 +630,7 @@ async function checkPieceContentExpectedPackageStatus( } const firstPackage = Object.values(packageInfos)[0] + let contentDuration: number | null | undefined = undefined if (firstPackage) { // TODO: support multiple packages: if (!piece.content.ignoreFreezeFrame && firstPackage.deepScan?.freezes?.length) { @@ -633,6 +647,15 @@ async function checkPieceContentExpectedPackageStatus( scenes = _.compact(firstPackage.deepScan.scenes.map((i) => i * 1000)) // convert into milliseconds } + if (firstPackage.scan?.streams?.length) { + const maximumStreamDuration = firstPackage.scan.streams.reduce( + (prev, current) => + current.duration !== undefined ? Math.max(prev, Number.parseFloat(current.duration)) : prev, + Number.NaN + ) + contentDuration = Number.isFinite(maximumStreamDuration) ? maximumStreamDuration : undefined + } + packageName = firstPackage.packageName } @@ -656,6 +679,8 @@ async function checkPieceContentExpectedPackageStatus( previewUrl, packageName, + + contentDuration, } } From 7e4edfd6963f63916d2c4fe0d766896919c9a654 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Mon, 24 Jul 2023 18:32:02 +0200 Subject: [PATCH 025/479] feat(MediaStatusList): search box clear button --- .../ui/Status/media-status/MediaStatusList.scss | 15 ++++++++++++++- meteor/client/ui/Status/media-status/index.tsx | 7 +++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/meteor/client/ui/Status/media-status/MediaStatusList.scss b/meteor/client/ui/Status/media-status/MediaStatusList.scss index f10405da9b..2317777697 100644 --- a/meteor/client/ui/Status/media-status/MediaStatusList.scss +++ b/meteor/client/ui/Status/media-status/MediaStatusList.scss @@ -11,6 +11,8 @@ .media-status-table-search { float: right; + position: relative; + margin-top: 0.5em; .media-status-table-search__search-input { @@ -22,10 +24,21 @@ border: none; border-radius: 1em; width: 15em; - padding: 5px 0.5em; + padding: 5px 2em 5px 0.5em; background: #f5f5f5; } + .media-status-table-search__clear-search-input { + position: absolute; + right: 1em; + top: 0.35em; + opacity: 0.3; + cursor: pointer; + + &:hover { + opacity: 0.7; + } + } } .media-status-table { diff --git a/meteor/client/ui/Status/media-status/index.tsx b/meteor/client/ui/Status/media-status/index.tsx index fe1d4e4a76..2296b971cc 100644 --- a/meteor/client/ui/Status/media-status/index.tsx +++ b/meteor/client/ui/Status/media-status/index.tsx @@ -13,6 +13,8 @@ import { useTranslation } from 'react-i18next' import { MediaStatusListHeader } from './MediaStatusListHeader' import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import { assertNever } from '@sofie-automation/corelib/dist/lib' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faTimes } from '@fortawesome/free-solid-svg-icons' export function MediaStatus(): JSX.Element | null { const scrollBox = useRef(null) @@ -66,6 +68,11 @@ export function MediaStatus(): JSX.Element | null { placeholder="Filter…" onChange={(e) => setFilter(e.target.value)} /> + {filter && ( +
setFilter('')}> + +
+ )}

{t('Media Status')}

From e32adc32c7ba1fc5e9172b1b6b7fcda434f3db8b Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Tue, 25 Jul 2023 13:00:09 +0200 Subject: [PATCH 026/479] chore(SwitchboardPopUp): refactor pop-up to an FC --- .../ui/RundownView/SwitchboardPopUp.tsx | 279 +++++++++--------- 1 file changed, 137 insertions(+), 142 deletions(-) diff --git a/meteor/client/ui/RundownView/SwitchboardPopUp.tsx b/meteor/client/ui/RundownView/SwitchboardPopUp.tsx index 89a2219863..8d07c789ca 100644 --- a/meteor/client/ui/RundownView/SwitchboardPopUp.tsx +++ b/meteor/client/ui/RundownView/SwitchboardPopUp.tsx @@ -1,6 +1,5 @@ import * as React from 'react' -import { withTranslation } from 'react-i18next' -import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData' +import { useTranslation } from 'react-i18next' import { StudioRouteSet, StudioRouteSetExclusivityGroup, @@ -27,149 +26,145 @@ interface IProps { /** * This is a panel for which Route Sets are active in the current studio */ -export const SwitchboardPopUp = withTranslation()( - class SwitchboardPopUp extends React.Component> { - render(): JSX.Element { - const { t } = this.props - const exclusivityGroups: { - [id: string]: Array<[string, StudioRouteSet]> - } = {} - for (const [id, routeSet] of this.props.availableRouteSets) { - const group = routeSet.exclusivityGroup || '__noGroup' - if (exclusivityGroups[group] === undefined) exclusivityGroups[group] = [] - exclusivityGroups[group].push([id, routeSet]) - } +export function SwitchboardPopUp(props: IProps): JSX.Element { + const { t } = useTranslation() + const exclusivityGroups: { + [id: string]: Array<[string, StudioRouteSet]> + } = {} + for (const [id, routeSet] of props.availableRouteSets) { + const group = routeSet.exclusivityGroup || '__noGroup' + if (exclusivityGroups[group] === undefined) exclusivityGroups[group] = [] + exclusivityGroups[group].push([id, routeSet]) + } - return ( -
-
-

{t('Switchboard')}

- {Object.entries<[string, StudioRouteSet][]>(exclusivityGroups).map(([key, routeSets]) => ( -
- {this.props.studioRouteSetExclusivityGroups[key]?.name && ( -

{this.props.studioRouteSetExclusivityGroups[key]?.name}

- )} - {routeSets.length === 2 && - routeSets[0][1].behavior === StudioRouteBehavior.ACTIVATE_ONLY && - routeSets[1][1].behavior === StudioRouteBehavior.ACTIVATE_ONLY ? ( -
- - {routeSets[0][1].name} - - - this.props.onStudioRouteSetSwitch && - this.props.onStudioRouteSetSwitch( - e, - routeSets[0][1].active ? routeSets[1][0] : routeSets[0][0], - routeSets[0][1].active ? routeSets[1][1] : routeSets[0][1], - true - ) - } - tabIndex={0} - > -
-
-   -   -
-
-
-
- - {routeSets[1][1].name} - - {((routeSets[0][1].defaultActive !== undefined && - routeSets[0][1].active !== routeSets[0][1].defaultActive) || - (routeSets[1][1].defaultActive !== undefined && - routeSets[1][1].active !== routeSets[1][1].defaultActive)) && ( - - - - - - - - )} + return ( +
+
+

{t('Switchboard')}

+ {Object.entries<[string, StudioRouteSet][]>(exclusivityGroups).map(([key, routeSets]) => ( +
+ {props.studioRouteSetExclusivityGroups[key]?.name && ( +

{props.studioRouteSetExclusivityGroups[key]?.name}

+ )} + {routeSets.length === 2 && + routeSets[0][1].behavior === StudioRouteBehavior.ACTIVATE_ONLY && + routeSets[1][1].behavior === StudioRouteBehavior.ACTIVATE_ONLY ? ( +
+ + {routeSets[0][1].name} + + + props.onStudioRouteSetSwitch && + props.onStudioRouteSetSwitch( + e, + routeSets[0][1].active ? routeSets[1][0] : routeSets[0][0], + routeSets[0][1].active ? routeSets[1][1] : routeSets[0][1], + true + ) + } + tabIndex={0} + > +
+
+   +   +
+
- ) : ( - routeSets.map(([id, routeSet]) => ( -
- - {t('Off')} - - - !(routeSet.active && routeSet.behavior === StudioRouteBehavior.ACTIVATE_ONLY) && - this.props.onStudioRouteSetSwitch && - this.props.onStudioRouteSetSwitch(e, id, routeSet, !routeSet.active) - } - tabIndex={0} - > -
-
-   -   -
-
-
-
- - {routeSet.name} + + + {routeSets[1][1].name} + + {((routeSets[0][1].defaultActive !== undefined && + routeSets[0][1].active !== routeSets[0][1].defaultActive) || + (routeSets[1][1].defaultActive !== undefined && + routeSets[1][1].active !== routeSets[1][1].defaultActive)) && ( + + + + - {routeSet.defaultActive !== undefined && routeSet.active !== routeSet.defaultActive && ( - - - - - - - - )} -
- )) + + )}
- ))} + ) : ( + routeSets.map(([id, routeSet]) => ( +
+ + {t('Off')} + + + !(routeSet.active && routeSet.behavior === StudioRouteBehavior.ACTIVATE_ONLY) && + props.onStudioRouteSetSwitch && + props.onStudioRouteSetSwitch(e, id, routeSet, !routeSet.active) + } + tabIndex={0} + > +
+
+   +   +
+
+
+
+ + {routeSet.name} + + {routeSet.defaultActive !== undefined && routeSet.active !== routeSet.defaultActive && ( + + + + + + + + )} +
+ )) + )}
-
- ) - } - } -) + ))} +
+
+ ) +} From e574bbb09e62e34ddacea80d326e3087e539edec Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Tue, 25 Jul 2023 17:28:02 +0200 Subject: [PATCH 027/479] feat(MediaStatus): Rundown panel WIP --- .../styles/supportAndSwitchboardPanel.scss | 23 ++++++- meteor/client/ui/MediaStatus/MediaStatus.tsx | 16 +++-- meteor/client/ui/RundownView.tsx | 1 + .../MediaStatusPopUp/MediaStatusItem.tsx} | 0 .../ui/RundownView/MediaStatusPopUp/index.tsx | 66 +++++++++++++++++++ .../RundownView/RundownRightHandControls.tsx | 48 ++++++++++++++ 6 files changed, 146 insertions(+), 8 deletions(-) rename meteor/client/ui/{MediaStatus/MediaStatus.scss => RundownView/MediaStatusPopUp/MediaStatusItem.tsx} (100%) create mode 100644 meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx diff --git a/meteor/client/styles/supportAndSwitchboardPanel.scss b/meteor/client/styles/supportAndSwitchboardPanel.scss index 4fea7580a7..80e3f22e5e 100644 --- a/meteor/client/styles/supportAndSwitchboardPanel.scss +++ b/meteor/client/styles/supportAndSwitchboardPanel.scss @@ -30,7 +30,8 @@ } .support-pop-up-panel, -.switchboard-pop-up-panel { +.switchboard-pop-up-panel, +.media-status-pop-up-panel { position: fixed; bottom: rem(8px); right: -1px; @@ -98,6 +99,21 @@ } } +.media-status-pop-up-panel { + top: auto; + z-index: -1; + white-space: nowrap; + overflow: hidden; + text-align: left; + box-sizing: content-box; + width: 28rem; + max-width: 28rem; + + > .media-status-pop-up-panel__inside { + width: 28rem; + } +} + #render-target .switchboard-pop-up-panel .switchboard-pop-up-panel__group { width: 28rem; border-top: 1px solid #d7d7d7; @@ -144,12 +160,13 @@ } .rundown-view { - .support-pop-up-panel { + .support-pop-up-panel{ color: #000; background: #eee; z-index: 1900; } - .switchboard-pop-up-panel { + .switchboard-pop-up-panel, + .media-status-pop-up-panel { color: #000; background: #eee; z-index: -1; diff --git a/meteor/client/ui/MediaStatus/MediaStatus.tsx b/meteor/client/ui/MediaStatus/MediaStatus.tsx index 9f85def83d..a16148e0c3 100644 --- a/meteor/client/ui/MediaStatus/MediaStatus.tsx +++ b/meteor/client/ui/MediaStatus/MediaStatus.tsx @@ -350,7 +350,8 @@ function useAdLibActionItems(partIds: PartId[], partMeta: Map) }, meta, adlibAction.partId, - undefined + undefined, + true ) }) .filter(Boolean), @@ -384,7 +385,7 @@ function useAdLibItems(partIds: PartId[], partMeta: Map) { const meta = partMeta.get(adlib.partId) if (!meta) return - return getListItemFromPieceAndPartMeta(adlib._id, adlib, meta, adlib.partId, undefined) + return getListItemFromPieceAndPartMeta(adlib._id, adlib, meta, adlib.partId, undefined, true) }), [adlibs, partMeta], [] @@ -490,7 +491,7 @@ function usePieceItems(partIds: PartId[], partMeta: Map) { const meta = partMeta.get(piece.startPartId) if (!meta) return - return getListItemFromPieceAndPartMeta(piece._id, piece, meta, piece.startPartId, undefined) + return getListItemFromPieceAndPartMeta(piece._id, piece, meta, piece.startPartId, undefined, false) }), [pieces, partMeta], [] @@ -528,7 +529,8 @@ function usePieceInstanceItems(partInstanceIds: PartInstanceId[], partInstanceMe pieceInstance.piece, meta, undefined, - pieceInstance.partInstanceId + pieceInstance.partInstanceId, + false ) }), [pieceInstances, partInstanceMeta], @@ -585,6 +587,7 @@ export interface MediaStatusListItem { rank: number status: PieceStatusCode pieceContentStatus: PieceContentStatusObj | undefined + isAdLib: boolean } function onlyWithExpectedPackages(obj: { expectedPackages?: ExpectedPackage.Any[] }) { @@ -641,6 +644,7 @@ function getListItemFromRundownPieceAndRundownMeta( rank, status, pieceContentStatus: uiPieceContentStatus?.status, + isAdLib: true, }) } @@ -651,7 +655,8 @@ function getListItemFromPieceAndPartMeta( Partial> & { _rank?: number | undefined }, meta: PartMeta, sourcePartId: PartId | undefined, - sourcePartInstanceId: PartInstanceId | undefined + sourcePartInstanceId: PartInstanceId | undefined, + isAdLib: boolean ): MediaStatusListItem | undefined { const showStyleBase = meta.showStyleBaseId && UIShowStyleBases.findOne(meta.showStyleBaseId) const sourceLayer = piece.sourceLayerId !== undefined ? showStyleBase?.sourceLayers?.[piece.sourceLayerId] : undefined @@ -699,6 +704,7 @@ function getListItemFromPieceAndPartMeta( rank, status, pieceContentStatus: uiPieceContentStatus?.status, + isAdLib, }) } diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx index ec775721b6..0c9bb2d0c5 100644 --- a/meteor/client/ui/RundownView.tsx +++ b/meteor/client/ui/RundownView.tsx @@ -2867,6 +2867,7 @@ export const RundownView = translateWithTracker(( ({ + dataResolution: TimingDataResolution.Synced, + tickResolution: TimingTickResolution.Synced, +})(function MediaStatusPopUp({ playlistId, timingDurations }): JSX.Element { + const { t } = useTranslation() + + const playlistIds = useMemo(() => [playlistId], [playlistId]) + + return ( +
+
+

{t('Media Status')}

+
+ + + + + + + + + + + + + {(items) => ( + <> +
    + {items.map((item) => ( +
  • + {item.isAdLib + ? 'AdLib' + : timingDurations.partCountdown?.[ + unprotectString(item.partInstanceId ?? item.partId) ?? '' + ]} + {item.name} +
  • + ))} +
+ + )} +
+ +
{t('On Air In')}
+
+
+
+ ) +}) diff --git a/meteor/client/ui/RundownView/RundownRightHandControls.tsx b/meteor/client/ui/RundownView/RundownRightHandControls.tsx index a6868fd7f4..8d96725165 100644 --- a/meteor/client/ui/RundownView/RundownRightHandControls.tsx +++ b/meteor/client/ui/RundownView/RundownRightHandControls.tsx @@ -22,8 +22,11 @@ import { SwitchboardIcon, RouteSetOverrideIcon } from '../../lib/ui/icons/switch import { SwitchboardPopUp } from './SwitchboardPopUp' import { useTranslation } from 'react-i18next' import { SegmentViewMode } from '../../lib/ui/icons/listView' +import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { MediaStatusPopUp } from './MediaStatusPopUp' interface IProps { + playlistId: RundownPlaylistId studioRouteSets: { [id: string]: StudioRouteSet } @@ -70,6 +73,7 @@ export function RundownRightHandControls(props: IProps): JSX.Element { const { t } = useTranslation() const [onAirHover, setOnAirHover] = useState(false) const [switchboardOpen, setSwitchboardOpen] = useState(false) + const [mediaStatusOpen, setMediaStatusOpen] = useState(false) const { onFollowOnAir: onOnAirClick, @@ -94,6 +98,12 @@ export function RundownRightHandControls(props: IProps): JSX.Element { const onRouteSetsToggle = (_e: React.MouseEvent) => { setSwitchboardOpen(!switchboardOpen) + setMediaStatusOpen(false) + } + + const onMediaStatusToggle = (_e: React.MouseEvent) => { + setMediaStatusOpen(!mediaStatusOpen) + setSwitchboardOpen(false) } const availableRouteSets = Object.entries(props.studioRouteSets).filter( @@ -180,6 +190,44 @@ export function RundownRightHandControls(props: IProps): JSX.Element { Take )} + <> + + + {mediaStatusOpen && } + + + + + changeSortOrder('status', order)} + /> + + + changeSortOrder('sourceLayer', order)} + /> + + + + + + ) +} + +type SortBy = 'rundown' | 'status' | 'sourceLayer' | 'name' +type SortOrder = 'asc' | 'desc' | 'inactive' + +function matchSortKey(sortKey: SortBy, matchSortKey: SortBy, order: SortOrder): SortOrder { + if (matchSortKey === sortKey) return order + return 'inactive' +} diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.scss b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.scss new file mode 100644 index 0000000000..3f76cbe52b --- /dev/null +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.scss @@ -0,0 +1,72 @@ +@import '../../../styles/colorScheme'; +@import '../../../styles/itemTypeColors'; + +.media-status-popup-item { + .media-status-popup-item__label { + overflow: hidden; + text-overflow: ellipsis; + } + .media-status-popup-item__countdown { + font-size: 0.875em; + text-align: right; + } + .media-status-popup-item__status { + > svg { + width: auto; + height: 1.3em; + } + + > svg.hourglass { + g > path { + stroke: #8c8c8c; + } + clipPath > rect { + fill: #8c8c8c; + } + } + } + + .media-status-popup-item__source-layer-indicator { + position: relative; + color: #fff; + width: 2.75em; + text-align: center; + font-weight: 400; + font-size: 0.8125em; + line-height: 1.6em; + overflow: hidden; + white-space: nowrap; + + @include item-type-colors(); + @include missing-overlay(); + + &.media-status-popup-item__source-layer-overlay { + display: block; + content: ' '; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + + @include invalid-overlay(); + } + + > .media-status-popup-item__source-layer-label { + position: relative; + z-index: 1; + } + } + + .media-status-popup-item__part-identifier { + display: inline-block; + background: #363636; + border-radius: 1em; + padding: 0 0.25em; + color: #fff; + font-weight: 400; + font-size: 0.875em; + min-width: 2em; + text-align: center; + } +} diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.tsx b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.tsx new file mode 100644 index 0000000000..9e6ab9fb60 --- /dev/null +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.tsx @@ -0,0 +1,80 @@ +import React from 'react' +import { PartId, PartInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { SourceLayerType } from '@sofie-automation/blueprints-integration' +import { PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' +import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' +import { TimingDataResolution, TimingTickResolution, withTiming } from '../RundownTiming/withTiming' +import { RundownUtils } from '../../../lib/rundown' +import classNames from 'classnames' +import { MediaStatusIndicator } from '../../MediaStatus/MediaStatusIndicator' + +export const MediaStatusItem = withTiming< + { + partId: PartId | undefined + partInstanceId: PartInstanceId | undefined + status: PieceStatusCode + statusOverlay?: string | undefined + sourceLayerType?: SourceLayerType | undefined + sourceLayerName?: string | undefined + segmentIdentifier?: string | undefined + partIdentifier?: string | undefined + invalid?: boolean | undefined + label: string + isAdLib: boolean + }, + {} +>({ + dataResolution: TimingDataResolution.Synced, + tickResolution: TimingTickResolution.Low, +})(function MediaStatusItem({ + partId, + partInstanceId, + status, + statusOverlay, + sourceLayerType, + sourceLayerName, + segmentIdentifier, + partIdentifier, + invalid, + label, + timingDurations, + isAdLib, +}): JSX.Element { + const timingId = unprotectString(partInstanceId ?? partId) + const thisPartCountdown = timingId ? timingDurations.partCountdown?.[timingId] : undefined + + const sourceLayerClassName = + sourceLayerType !== undefined ? RundownUtils.getSourceLayerClassName(sourceLayerType) : undefined + + return ( + + + {!isAdLib && thisPartCountdown ? RundownUtils.formatTimeToShortTime(thisPartCountdown) : null} + + + + + +
+ {invalid &&
} +
{sourceLayerName}
+
+ + + {segmentIdentifier ? ( +
{segmentIdentifier}
+ ) : null} + {partIdentifier ?
{partIdentifier}
: null} + + {label} + + ) +}) diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx b/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx index 285f918745..96694be185 100644 --- a/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx @@ -1,12 +1,15 @@ -import React, { useMemo } from 'react' +import React, { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' // import classNames from 'classnames' // import Tooltip from 'rc-tooltip' // import { TOOLTIP_DEFAULT_DELAY } from '../../lib/lib' import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' -import { MediaStatus } from '../../MediaStatus/MediaStatus' -import { TimingDataResolution, TimingTickResolution, withTiming } from '../RundownTiming/withTiming' +import { MediaStatus, MediaStatusListItem as IMediaStatusListItem } from '../../MediaStatus/MediaStatus' +import { MediaStatusItem } from './MediaStatusPopUpItem' +import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' +import { assertNever } from '@sofie-automation/corelib/dist/lib' +import { MediaStatusPopUpHeader } from './MediaStatusPopUpHeader' interface IProps { playlistId: RundownPlaylistId @@ -15,45 +18,51 @@ interface IProps { /** * This is a panel for monitoring the state of all the Media for this Playlist */ -export const MediaStatusPopUp = withTiming({ - dataResolution: TimingDataResolution.Synced, - tickResolution: TimingTickResolution.Synced, -})(function MediaStatusPopUp({ playlistId, timingDurations }): JSX.Element { +export function MediaStatusPopUp({ playlistId }: IProps): JSX.Element { const { t } = useTranslation() + const [sortBy, setSortBy] = useState<'rundown' | 'status' | 'sourceLayer' | 'name'>('rundown') + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc') + // const [filter, setFilter] = useState('') + + function onChangeSort(sortBy: SortBy, sortOrder: SortOrder) { + setSortOrder(sortOrder === 'inactive' ? 'asc' : sortOrder) + setSortBy(sortBy) + } + const playlistIds = useMemo(() => [playlistId], [playlistId]) return (
-

{t('Media Status')}

+

{t('Media Status')}

- - - - - - - - - - +
{t('On Air In')}
+ {(items) => ( <> -
    - {items.map((item) => ( -
  • - {item.isAdLib - ? 'AdLib' - : timingDurations.partCountdown?.[ - unprotectString(item.partInstanceId ?? item.partId) ?? '' - ]} - {item.name} -
  • + {items + .sort((a, b) => sortItems(a, b, sortBy, sortOrder)) + .map((item) => ( + translateMessage(message, t)) + .join(', ')} + status={item.status} + isAdLib={item.isAdLib} + /> ))} -
)}
@@ -63,4 +72,67 @@ export const MediaStatusPopUp = withTiming({ ) -}) +} + +function sortItems(a: IMediaStatusListItem, b: IMediaStatusListItem, sortBy: SortBy, sortOrder: SortOrder) { + let result = 0 + switch (sortBy) { + case 'name': + result = sortByName(a, b) + break + case 'status': + result = sortByStatus(a, b) + break + case 'sourceLayer': + result = sortBySourceLayer(a, b) + break + case 'rundown': + result = sortByRundown(a, b) + break + default: + assertNever(sortBy) + break + } + + if (sortOrder === 'desc') return result * -1 + return result +} + +function sortByName(a: IMediaStatusListItem, b: IMediaStatusListItem) { + return a.name.localeCompare(b.name) || sortByRundown(a, b) +} + +function sortByStatus(a: IMediaStatusListItem, b: IMediaStatusListItem) { + return a.status - b.status || sortByRundown(a, b) +} + +function sortBySourceLayer(a: IMediaStatusListItem, b: IMediaStatusListItem) { + if (a.sourceLayerName === b.sourceLayerName) return sortByRundown(a, b) + if (a.sourceLayerName === undefined) return 1 + if (b.sourceLayerName === undefined) return -1 + return a.sourceLayerName.localeCompare(b.sourceLayerName) +} + +function sortByRundown(a: IMediaStatusListItem, b: IMediaStatusListItem) { + if (a.rundownName === b.rundownName) return sortBySegmentRank(a, b) + if (a.rundownName === undefined) return 1 + if (b.rundownName === undefined) return -1 + return a.rundownName?.localeCompare(b.rundownName) +} + +function sortBySegmentRank(a: IMediaStatusListItem, b: IMediaStatusListItem) { + if (a.segmentRank === b.segmentRank) return sortByPartRank(a, b) + return (a.segmentRank ?? 0) - (b.segmentRank ?? 0) +} + +function sortByPartRank(a: IMediaStatusListItem, b: IMediaStatusListItem) { + if (a.partRank === b.partRank) return sortByRank(a, b) + return (a.partRank ?? 0) - (b.partRank ?? 0) +} + +function sortByRank(a: IMediaStatusListItem, b: IMediaStatusListItem) { + return a.rank - b.rank +} + +type SortBy = 'rundown' | 'status' | 'sourceLayer' | 'name' +type SortOrder = 'asc' | 'desc' | 'inactive' diff --git a/meteor/client/ui/Status/media-status/MediaStatusListItem.scss b/meteor/client/ui/Status/media-status/MediaStatusListItem.scss index 85ca774837..e7b341a3be 100644 --- a/meteor/client/ui/Status/media-status/MediaStatusListItem.scss +++ b/meteor/client/ui/Status/media-status/MediaStatusListItem.scss @@ -36,7 +36,8 @@ width: 3.5em; text-align: center; font-weight: 400; - font-size: 0.85em; + font-size: 0.875em; + line-height: 1.75em; margin-top: 0.25em; overflow: hidden; @@ -89,6 +90,6 @@ padding: 0 0.25em; color: #fff; font-weight: 400; - font-size: 0.85em; + font-size: 0.875em; } } From fbb0bc7e622afd6309b020fcc48c9f4469f37da9 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 26 Jul 2023 18:09:07 +0200 Subject: [PATCH 030/479] feat(MediaStatusPopUp): WIP --- .../MediaStatusPopUpHeader.scss | 5 ++ .../MediaStatusPopUpHeader.tsx | 1 + .../MediaStatusPopUpItem.scss | 52 ++++++++++++++----- .../MediaStatusPopUp/MediaStatusPopUpItem.tsx | 1 + .../ui/RundownView/MediaStatusPopUp/index.tsx | 24 +++++++++ 5 files changed, 71 insertions(+), 12 deletions(-) diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader.scss b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader.scss index 707bfd1e22..8867823add 100644 --- a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader.scss +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader.scss @@ -7,6 +7,7 @@ font-size: 0.875em; letter-spacing: -0.5px; font-weight: 500; + z-index: 10; } .media-status-popup-item__sort-button { border: none; @@ -14,4 +15,8 @@ margin: 0; padding: 0; } + .media-status-popup-item__countdown { + text-align: right; + padding: 0 0.15em; + } } diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader.tsx b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader.tsx index de8463ba8c..3be15195ba 100644 --- a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader.tsx +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader.tsx @@ -25,6 +25,7 @@ export function MediaStatusPopUpHeader({ return ( + + diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx b/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx index 96694be185..03e3250aa5 100644 --- a/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx @@ -4,12 +4,14 @@ import { useTranslation } from 'react-i18next' // import Tooltip from 'rc-tooltip' // import { TOOLTIP_DEFAULT_DELAY } from '../../lib/lib' import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { MediaStatus, MediaStatusListItem as IMediaStatusListItem } from '../../MediaStatus/MediaStatus' import { MediaStatusItem } from './MediaStatusPopUpItem' import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import { assertNever } from '@sofie-automation/corelib/dist/lib' import { MediaStatusPopUpHeader } from './MediaStatusPopUpHeader' +import { RundownPlaylists } from '../../../collections' interface IProps { playlistId: RundownPlaylistId @@ -32,6 +34,28 @@ export function MediaStatusPopUp({ playlistId }: IProps): JSX.Element { const playlistIds = useMemo(() => [playlistId], [playlistId]) + const { currentPartInstanceId, nextPartInstanceId } = useTracker( + () => { + const playlist = RundownPlaylists.findOne(playlistId, { + projection: { + nextPartInfo: 1, + currentPartInfo: 1, + }, + }) + return { + currentPartInstanceId: playlist?.currentPartInfo?.partInstanceId, + nextPartInstanceId: playlist?.nextPartInfo?.partInstanceId, + } + }, + [playlistId], + { + currentPartInstanceId: undefined, + nextPartInstanceId: undefined, + } + ) + + console.log(currentPartInstanceId, nextPartInstanceId) + return (
From 26a8613cd64d7e077dfd7ff2092555c1b7805f4d Mon Sep 17 00:00:00 2001 From: Alex Van Camp Date: Wed, 26 Jul 2023 19:28:46 -0500 Subject: [PATCH 031/479] fix(playout-gateway): improve handling and typing of TSR events (fixes some logging issues) --- packages/playout-gateway/src/tsrHandler.ts | 53 +++++++++++++--------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/packages/playout-gateway/src/tsrHandler.ts b/packages/playout-gateway/src/tsrHandler.ts index b0d2f36c57..451b1d62b7 100644 --- a/packages/playout-gateway/src/tsrHandler.ts +++ b/packages/playout-gateway/src/tsrHandler.ts @@ -51,6 +51,8 @@ import { unprotectString, } from '@sofie-automation/server-core-integration' import { BaseRemoteDeviceIntegration } from 'timeline-state-resolver/dist/service/remoteDeviceInstance' +import { DeviceEvents } from 'timeline-state-resolver/dist/devices/device' +import EventEmitter = require('eventemitter3') const debug = Debug('playout-gateway') @@ -886,27 +888,33 @@ export class TSRHandler { // Note for the future: // It is important that the callbacks returns void, // otherwise there might be problems with threadedclass! + // Also, it is critical that all of these `.on` calls be `await`ed. + // They aren't typed as promises due to limitations of TypeScript, + // but due to threadedclass they _are_ promises. + /* eslint-disable @typescript-eslint/await-thenable */ + + const emitterHack = device.device as unknown as EventEmitter + + await emitterHack.on('connectionChanged', onDeviceStatusChanged) + // await emitterHack.on('slowCommand', onSlowCommand) + await emitterHack.on('slowSentCommand', onSlowSentCommand) + await emitterHack.on('slowFulfilledCommand', onSlowFulfilledCommand) + await emitterHack.on('commandError', onCommandError) + await emitterHack.on('commandReport', onCommandReport) + await emitterHack.on('updateMediaObject', onUpdateMediaObject) + await emitterHack.on('clearMediaObjects', onClearMediaObjectCollection) + + await emitterHack.on('info', (info) => { + this.logger.info(info) + }) + await emitterHack.on('warning', (warning: string) => { + this.logger.warn(warning) + }) + await emitterHack.on('error', (context, error) => { + this.logger.error(`${fixError(error)}`, fixContext(context)) + }) - await device.device.on('connectionChanged', onDeviceStatusChanged as () => void) - // await device.device.on('slowCommand', onSlowCommand) - await device.device.on('slowSentCommand', onSlowSentCommand as () => void) - await device.device.on('slowFulfilledCommand', onSlowFulfilledCommand as () => void) - await device.device.on('commandError', onCommandError as () => void) - await device.device.on('commandReport', onCommandReport as () => void) - await device.device.on('updateMediaObject', onUpdateMediaObject as () => void) - await device.device.on('clearMediaObjects', onClearMediaObjectCollection as () => void) - - await device.device.on('info', ((e: any, ...args: any[]) => { - this.logger.info(fixError(e), fixContext(args)) - }) as () => void) - await device.device.on('warning', ((e: any, ...args: any[]) => { - this.logger.warn(fixError(e), fixContext(args)) - }) as () => void) - await device.device.on('error', ((e: any, ...args: any[]) => { - this.logger.error(fixError(e), fixContext(args)) - }) as () => void) - - await device.device.on('debug', (...args: any[]) => { + await emitterHack.on('debug', (...args) => { if (!device.debugLogging && !this._coreHandler.logDebug) { return } @@ -918,7 +926,7 @@ export class TSRHandler { this.logger.debug(`Device "${device.deviceName || deviceId}" (${device.instanceId})`, { data }) }) - await device.device.on('debugState', (...args: any[]) => { + await emitterHack.on('debugState', (...args) => { if (device.debugState && this._coreHandler.logDebug) { // Fetch the Id that core knows this device by const coreId = this._coreTsrHandlers[device.deviceId].core.deviceId @@ -926,7 +934,8 @@ export class TSRHandler { } }) - await device.device.on('timeTrace', ((trace: FinishedTrace) => sendTrace(trace)) as () => void) + await emitterHack.on('timeTrace', (trace) => sendTrace(trace)) + /* eslint-enable @typescript-eslint/await-thenable */ // now initialize it await this.tsr.initDevice(deviceId, options) From 5b5c1c22f2998b05ec0aa9ff9ea171ae253145fa Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Fri, 28 Jul 2023 16:15:02 +0200 Subject: [PATCH 032/479] feat(MediaStatusPopUp): finish styling, icons, etc. --- meteor/client/lib/lib.tsx | 10 ++ meteor/client/lib/ui/icons/mediaStatus.tsx | 33 +++++ meteor/client/styles/statusbar.scss | 5 + .../styles/supportAndSwitchboardPanel.scss | 8 +- meteor/client/ui/MediaStatus/MediaStatus.tsx | 21 +++- .../ui/MediaStatus/MediaStatusIndicator.tsx | 25 +++- .../MediaStatusPopUp/MediaStatusPopUp.scss | 21 +++- .../MediaStatusPopUpHeader.scss | 90 ++++++++++++++ .../MediaStatusPopUpHeader.tsx | 45 ++++++- .../MediaStatusPopUpItem.scss | 26 +++- .../MediaStatusPopUp/MediaStatusPopUpItem.tsx | 9 +- .../MediaStatusPopUpSegmentRule.scss | 14 +++ .../MediaStatusPopUpSegmentRule.tsx | 10 ++ .../ui/RundownView/MediaStatusPopUp/index.tsx | 115 ++++++++++++++---- .../RundownView/RundownRightHandControls.tsx | 3 +- .../Status/media-status/MediaStatusList.scss | 12 +- .../media-status/MediaStatusListItem.scss | 15 +++ .../media-status/MediaStatusListItem.tsx | 4 +- .../client/ui/Status/media-status/index.tsx | 28 +++-- 19 files changed, 435 insertions(+), 59 deletions(-) create mode 100644 meteor/client/lib/ui/icons/mediaStatus.tsx create mode 100644 meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpSegmentRule.scss create mode 100644 meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpSegmentRule.tsx diff --git a/meteor/client/lib/lib.tsx b/meteor/client/lib/lib.tsx index 0ac95ffebc..b566f23913 100644 --- a/meteor/client/lib/lib.tsx +++ b/meteor/client/lib/lib.tsx @@ -181,4 +181,14 @@ export function getEventTimestamp(e: Event): Time { return e?.timeStamp ? performance.timeOrigin + e.timeStamp + systemTime.timeOriginDiff : getCurrentTime() } +export function mapOrFallback( + array: T[], + callbackFn: (value: T, index: number, array: T[]) => K, + fallbackCallbackFn: () => L +): K[] | L { + if (array.length === 0) return fallbackCallbackFn() + + return array.map(callbackFn) +} + export const TOOLTIP_DEFAULT_DELAY = 0.5 diff --git a/meteor/client/lib/ui/icons/mediaStatus.tsx b/meteor/client/lib/ui/icons/mediaStatus.tsx new file mode 100644 index 0000000000..8457f37c20 --- /dev/null +++ b/meteor/client/lib/ui/icons/mediaStatus.tsx @@ -0,0 +1,33 @@ +import React from 'react' + +export function MediaStatusIcon(): JSX.Element { + return ( + + + + + + + + + + + ) +} + +export function MediaStatusPopOutIcon(): JSX.Element { + return ( + + + + + ) +} diff --git a/meteor/client/styles/statusbar.scss b/meteor/client/styles/statusbar.scss index 8c35d6086e..122ddc32d4 100644 --- a/meteor/client/styles/statusbar.scss +++ b/meteor/client/styles/statusbar.scss @@ -153,6 +153,10 @@ &.status-bar__controls__button--segment-view-mode { padding-top: 3px; } + + &.status-bar__controls__button--media-status { + padding-top: 2px; + } } } @@ -175,6 +179,7 @@ border: none !important; margin-left: 0 !important; background-color: #eee; + color: #000; &.notifications__toggle-button { background-color: #acacad; } diff --git a/meteor/client/styles/supportAndSwitchboardPanel.scss b/meteor/client/styles/supportAndSwitchboardPanel.scss index 6ff1f74a93..a7074e64da 100644 --- a/meteor/client/styles/supportAndSwitchboardPanel.scss +++ b/meteor/client/styles/supportAndSwitchboardPanel.scss @@ -31,7 +31,7 @@ .support-pop-up-panel, .switchboard-pop-up-panel, -.media-status-pop-up-panel { +.media-status-panel { position: fixed; bottom: rem(8px); right: -1px; @@ -101,7 +101,7 @@ } } -.media-status-pop-up-panel { +.media-status-panel { top: auto; z-index: -1; white-space: nowrap; @@ -114,7 +114,7 @@ padding-top: rem(15px); - > .media-status-pop-up-panel__inside { + > .media-status-panel__inside { width: 28rem; } } @@ -173,7 +173,7 @@ z-index: 1900; } .switchboard-pop-up-panel, - .media-status-pop-up-panel { + .media-status-panel { color: #000; --panel-background: #eee; background: var(--panel-background); diff --git a/meteor/client/ui/MediaStatus/MediaStatus.tsx b/meteor/client/ui/MediaStatus/MediaStatus.tsx index a16148e0c3..469153625f 100644 --- a/meteor/client/ui/MediaStatus/MediaStatus.tsx +++ b/meteor/client/ui/MediaStatus/MediaStatus.tsx @@ -11,6 +11,7 @@ import { RundownBaselineAdLibActionId, RundownId, RundownPlaylistId, + SegmentId, ShowStyleBaseId, } from '@sofie-automation/corelib/dist/dataModel/Ids' import { @@ -168,6 +169,7 @@ function useRundownPlaylists(playlistIds: RundownPlaylistId[]) { playlistRank, segmentRank, segmentIdentifier: segment.identifier, + segmentId: segment._id, invalid: partInstance.part.invalid, invalidReason: partInstance.part.invalidReason, identifier: partInstance.part.identifier, @@ -185,6 +187,7 @@ function useRundownPlaylists(playlistIds: RundownPlaylistId[]) { playlistRank, segmentRank, segmentIdentifier: segment.identifier, + segmentId: segment._id, invalid: partInstance.part.invalid, invalidReason: partInstance.part.invalidReason, identifier: partInstance.part.identifier, @@ -351,6 +354,7 @@ function useAdLibActionItems(partIds: PartId[], partMeta: Map) meta, adlibAction.partId, undefined, + meta.segmentId, true ) }) @@ -385,7 +389,7 @@ function useAdLibItems(partIds: PartId[], partMeta: Map) { const meta = partMeta.get(adlib.partId) if (!meta) return - return getListItemFromPieceAndPartMeta(adlib._id, adlib, meta, adlib.partId, undefined, true) + return getListItemFromPieceAndPartMeta(adlib._id, adlib, meta, adlib.partId, undefined, meta.segmentId, true) }), [adlibs, partMeta], [] @@ -491,7 +495,15 @@ function usePieceItems(partIds: PartId[], partMeta: Map) { const meta = partMeta.get(piece.startPartId) if (!meta) return - return getListItemFromPieceAndPartMeta(piece._id, piece, meta, piece.startPartId, undefined, false) + return getListItemFromPieceAndPartMeta( + piece._id, + piece, + meta, + piece.startPartId, + undefined, + meta.segmentId, + false + ) }), [pieces, partMeta], [] @@ -530,6 +542,7 @@ function usePieceInstanceItems(partInstanceIds: PartInstanceId[], partInstanceMe meta, undefined, pieceInstance.partInstanceId, + meta.segmentId, false ) }), @@ -552,6 +565,7 @@ interface PartMeta { showStyleBaseId: ShowStyleBaseId | undefined segmentRank: number segmentIdentifier: string | undefined + segmentId: SegmentId invalid: boolean | undefined invalidReason: PartInvalidReason | undefined rank: number @@ -572,6 +586,7 @@ export interface MediaStatusListItem { playlistId: RundownPlaylistId partId?: PartId partInstanceId?: PartInstanceId + segmentId?: SegmentId sourceLayerType: SourceLayerType | undefined sourceLayerName: string | undefined partIdentifier: string | undefined @@ -656,6 +671,7 @@ function getListItemFromPieceAndPartMeta( meta: PartMeta, sourcePartId: PartId | undefined, sourcePartInstanceId: PartInstanceId | undefined, + sourceSegmentId: SegmentId, isAdLib: boolean ): MediaStatusListItem | undefined { const showStyleBase = meta.showStyleBaseId && UIShowStyleBases.findOne(meta.showStyleBaseId) @@ -689,6 +705,7 @@ function getListItemFromPieceAndPartMeta( playlistId, partId: sourcePartId, partInstanceId: sourcePartInstanceId, + segmentId: sourceSegmentId, name, sourceLayerName, sourceLayerType, diff --git a/meteor/client/ui/MediaStatus/MediaStatusIndicator.tsx b/meteor/client/ui/MediaStatus/MediaStatusIndicator.tsx index 715878bf92..098f48ac1e 100644 --- a/meteor/client/ui/MediaStatus/MediaStatusIndicator.tsx +++ b/meteor/client/ui/MediaStatus/MediaStatusIndicator.tsx @@ -1,19 +1,24 @@ import React from 'react' +import Tooltip from 'rc-tooltip' import { PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' import { assertNever } from '@sofie-automation/corelib/dist/lib' import { WarningIconSmall, HourglassIconSmall, OKIconSmall } from '../../lib/ui/icons/notifications' -export function MediaStatusIndicator({ +export const MediaStatusIndicator = React.memo(function MediaStatusIndicator({ status, + overlay, }: { status: PieceStatusCode | undefined overlay: string | undefined }): JSX.Element | null { + let icon: JSX.Element | null = null switch (status) { case PieceStatusCode.OK: - return + icon = + break case PieceStatusCode.SOURCE_NOT_READY: - return + icon = + break case PieceStatusCode.SOURCE_BROKEN: case PieceStatusCode.SOURCE_HAS_ISSUES: case PieceStatusCode.SOURCE_MISSING: @@ -21,9 +26,17 @@ export function MediaStatusIndicator({ case PieceStatusCode.UNKNOWN: case undefined: case PieceStatusCode.SOURCE_UNKNOWN_STATE: - return + icon = + break default: assertNever(status) - return <>Unknown: {status} + icon = <>Unknown: {status} + break } -} + + return ( + + {icon} + + ) +}) diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUp.scss b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUp.scss index af60899ee7..49b1c7b416 100644 --- a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUp.scss +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUp.scss @@ -2,11 +2,30 @@ height: 700px; max-height: 50vh; overflow-x: hidden; - overflow-y: scroll; + overflow-y: auto; overscroll-behavior: contain; position: relative; } .media-status-panel__table { width: 100%; + table-layout: fixed; +} + +.media-status-panel__empty-message { + text-align: center; + font-style: italic; + padding-top: 2em; +} + +.media-status-panel__pop-out { + position: absolute; + top: 20px; + right: 20px; + left: auto; + opacity: 0.5; + + &:hover { + opacity: 1; + } } diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader.scss b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader.scss index 8867823add..a38cee4fb5 100644 --- a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader.scss +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader.scss @@ -14,9 +14,99 @@ background: none; margin: 0; padding: 0; + text-align: center; + min-width: 1.5em; + + > svg { + vertical-align: top; + margin-top: 0.4em; + } + + &.inactive { + color: #b3b3b3; + } } .media-status-popup-item__countdown { text-align: right; padding: 0 0.15em; + width: 4.2em; + } + .media-status-popup-item__status { + width: 2em; + } + .media-status-item__source-layer { + width: 3.5em; + } + .media-status-item__identifiers { + width: 3em; + } + .media-status-item__source-layer, + .media-status-popup-item__status { + text-align: center; + } + .media-status-popup-item__countdown, + .media-status-popup-item__status, + .media-status-item__source-layer, + .media-status-item__identifiers, + .media-status-item__label { + padding-bottom: 0.5em; + padding-top: 0.1em; + + &::after { + content: ' '; + display: block; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 2px; + border-bottom: 1px solid #d7d7d7; + border-top: 1px solid #d7d7d7; + } + } + + .media-status-item__label { + &::after { + right: 1.75em; + } + } + + .media-status-panel-header__filter-input { + position: absolute; + top: 0; + right: 0.5em; + width: 12em; + border: none; + border-radius: 1em; + background: #F5F5F5; + font-weight: 400; + padding: 0.1em 0.5em; + margin: 0; + + &::placeholder { + font-style: italic; + color: #b3b3b3; + font-weight: 300; + } + + &:focus-within { + background: #ffffff; + } + } + + .media-status-panel-header__clear-search-input { + position: absolute; + right: 1em; + top: 0.2em; + opacity: 0.3; + cursor: pointer; + + &:hover { + opacity: 0.7; + } + } + + .media-status-popup-item__playout-indicator { + width: 1em; } } diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader.tsx b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader.tsx index 3be15195ba..3bbf41ffa6 100644 --- a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader.tsx +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader.tsx @@ -1,16 +1,22 @@ -import React from 'react' +import React, { ChangeEvent, useCallback } from 'react' import { useTranslation } from 'react-i18next' import { SortOrderButton } from '../../MediaStatus/SortOrderButton' import classNames from 'classnames' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faTimes } from '@fortawesome/free-solid-svg-icons' export function MediaStatusPopUpHeader({ sortOrder, sortBy, onChange, + filter, + onFilterChange, }: { sortBy: SortBy sortOrder: SortOrder onChange?: (sortBy: SortBy, sortOrder: SortOrder) => void + filter: string + onFilterChange?: (filter: string) => void }): JSX.Element | null { const { t } = useTranslation() @@ -22,6 +28,22 @@ export function MediaStatusPopUpHeader({ onChange?.(sortBy, newSortOrder) } + const onFilterChangeCallback = useCallback( + (e: ChangeEvent) => { + onFilterChange?.(e.target.value) + }, + [onFilterChange] + ) + + const onFilterInputKeyDown = useCallback((e: React.KeyboardEvent) => { + if (e.key === 'Escape' || e.key === 'Enter') { + if (!(document.activeElement instanceof HTMLElement)) return + document.activeElement.blur() + } else if (e.key.match(/^F\d+$/)) { + e.preventDefault() + } + }, []) + return (
@@ -50,7 +72,26 @@ export function MediaStatusPopUpHeader({ /> - + ) diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.scss b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.scss index 3cbccf29a9..73f09fb2c7 100644 --- a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.scss +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.scss @@ -2,29 +2,49 @@ @import '../../../styles/itemTypeColors'; .media-status-popup-item { + &:first-child { + > td { + padding-top: 3px; + } + } + .media-status-popup-item__playout-indicator { width: 1.5em; max-width: 1.5em; } + .media-status-popup-item__live-indicator { + background: $general-live-color; + width: 0.75em; + height: 1.3em; + border-radius: 0 1em 1em 0; + } + + .media-status-popup-item__next-indicator { + background: $general-next-color; + width: 0.75em; + height: 1.3em; + clip-path: polygon(20% 0, 100% 50%, 20% 100%, 0 100%, 0 0); + } + .media-status-popup-item__countdown { font-size: 0.875em; text-align: right; font-weight: 400; width: 5em; - padding: 0 0.15em; + padding: 0.1em 0.15em 0; } .media-status-popup-item__status { max-width: 3em; padding: 0 0.15em; - > svg { + svg { width: auto; height: 1.3em; } - > svg.hourglass { + svg.hourglass { g > path { stroke: #8c8c8c; } diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.tsx b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.tsx index b7badb2a3e..f284a5a491 100644 --- a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.tsx +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.tsx @@ -21,6 +21,8 @@ export const MediaStatusItem = withTiming< invalid?: boolean | undefined label: string isAdLib: boolean + isLive: boolean + isNext: boolean }, {} >({ @@ -39,6 +41,8 @@ export const MediaStatusItem = withTiming< label, timingDurations, isAdLib, + isLive, + isNext, }): JSX.Element { const timingId = unprotectString(partInstanceId ?? partId) const thisPartCountdown = timingId ? timingDurations.partCountdown?.[timingId] : undefined @@ -48,7 +52,10 @@ export const MediaStatusItem = withTiming< return ( - + diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpSegmentRule.scss b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpSegmentRule.scss new file mode 100644 index 0000000000..9b48be8f15 --- /dev/null +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpSegmentRule.scss @@ -0,0 +1,14 @@ +.media-status-popup-segment-rule__line { + padding-bottom: 0; + position: relative; + &::after { + content: ' '; + display: block; + position: absolute; + bottom: 2px; + left: 0; + right: 1.5em; + height: 1px; + border-bottom: 1px solid #d7d7d7; + } +} \ No newline at end of file diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpSegmentRule.tsx b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpSegmentRule.tsx new file mode 100644 index 0000000000..b939178056 --- /dev/null +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpSegmentRule.tsx @@ -0,0 +1,10 @@ +import React from 'react' + +export function MediaStatusPopUpSegmentRule(): JSX.Element { + return ( + + + + + ) +} diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx b/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx index 03e3250aa5..5a703c9cae 100644 --- a/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react' +import React, { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' // import classNames from 'classnames' // import Tooltip from 'rc-tooltip' @@ -12,6 +12,11 @@ import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMes import { assertNever } from '@sofie-automation/corelib/dist/lib' import { MediaStatusPopUpHeader } from './MediaStatusPopUpHeader' import { RundownPlaylists } from '../../../collections' +import { MediaStatusPopUpSegmentRule } from './MediaStatusPopUpSegmentRule' +import { mapOrFallback } from '../../../lib/lib' +import { Spinner } from '../../../lib/Spinner' +import { NavLink } from 'react-router-dom' +import { MediaStatusPopOutIcon } from '../../../lib/ui/icons/mediaStatus' interface IProps { playlistId: RundownPlaylistId @@ -25,6 +30,7 @@ export function MediaStatusPopUp({ playlistId }: IProps): JSX.Element { const [sortBy, setSortBy] = useState<'rundown' | 'status' | 'sourceLayer' | 'name'>('rundown') const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc') + const [filter, setFilter] = useState('') // const [filter, setFilter] = useState('') function onChangeSort(sortBy: SortBy, sortOrder: SortOrder) { @@ -32,6 +38,17 @@ export function MediaStatusPopUp({ playlistId }: IProps): JSX.Element { setSortBy(sortBy) } + const emptyFilter = !filter || filter.trim().length === 0 + + const filterItems = useCallback( + (item: IMediaStatusListItem) => { + if (emptyFilter) return true + if (item.name.toLowerCase().indexOf(filter.toLowerCase().trim()) >= 0) return true + return false + }, + [filter, emptyFilter] + ) + const playlistIds = useMemo(() => [playlistId], [playlistId]) const { currentPartInstanceId, nextPartInstanceId } = useTracker( @@ -54,39 +71,83 @@ export function MediaStatusPopUp({ playlistId }: IProps): JSX.Element { } ) - console.log(currentPartInstanceId, nextPartInstanceId) - return ( -
-
+
+
+
+ + + +

{t('Media Status')}

changeSortOrder('rundown', 'asc')}>
{!isAdLib && thisPartCountdown ? RundownUtils.formatTimeToShortTime(thisPartCountdown) : null}
+ + {filter && ( +
onFilterChange?.('')}> + +
+ )} + changeSortOrder('name', order)} + /> +
+ {isNext ?
: null} + {isLive ?
: null} +
{!isAdLib && thisPartCountdown ? RundownUtils.formatTimeToShortTime(thisPartCountdown) : null}
- + - + + + + } + > {(items) => ( <> - {items - .sort((a, b) => sortItems(a, b, sortBy, sortOrder)) - .map((item) => ( - translateMessage(message, t)) - .join(', ')} - status={item.status} - isAdLib={item.isAdLib} - /> - ))} + {mapOrFallback( + items.filter((item) => filterItems(item)).sort((a, b) => sortItems(a, b, sortBy, sortOrder)), + (item, index, otherItems) => { + let line: JSX.Element | null = null + // The Segment separators (rules) only make sense in rundown mode + if (sortBy === 'rundown' && index > 0 && emptyFilter) { + if (otherItems[index - 1].segmentId !== item.segmentId) { + line = + } + } + + const isLive = + currentPartInstanceId !== undefined && item.partInstanceId === currentPartInstanceId + const isNext = nextPartInstanceId !== undefined && item.partInstanceId === nextPartInstanceId + + return ( + + {line} + translateMessage(message, t)) + .join(', ')} + status={item.status} + isAdLib={item.isAdLib} + isLive={isLive} + isNext={isNext} + /> + + ) + }, + () => ( + + + + ) + )} )} diff --git a/meteor/client/ui/RundownView/RundownRightHandControls.tsx b/meteor/client/ui/RundownView/RundownRightHandControls.tsx index 8d96725165..917d3c9f51 100644 --- a/meteor/client/ui/RundownView/RundownRightHandControls.tsx +++ b/meteor/client/ui/RundownView/RundownRightHandControls.tsx @@ -24,6 +24,7 @@ import { useTranslation } from 'react-i18next' import { SegmentViewMode } from '../../lib/ui/icons/listView' import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { MediaStatusPopUp } from './MediaStatusPopUp' +import { MediaStatusIcon } from '../../lib/ui/icons/mediaStatus' interface IProps { playlistId: RundownPlaylistId @@ -207,7 +208,7 @@ export function RundownRightHandControls(props: IProps): JSX.Element { aria-haspopup="dialog" aria-pressed={mediaStatusOpen ? 'true' : 'false'} > - + .media-status-item__label { + position: relative; + + > .media-status-item__label-container { + position: absolute; + top: 5px; + left: 5px; + right: 5px; + bottom: 5px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + .media-status-item__source-layer-indicator { position: relative; color: #fff; diff --git a/meteor/client/ui/Status/media-status/MediaStatusListItem.tsx b/meteor/client/ui/Status/media-status/MediaStatusListItem.tsx index 50c2c8cb67..1449e12b8b 100644 --- a/meteor/client/ui/Status/media-status/MediaStatusListItem.tsx +++ b/meteor/client/ui/Status/media-status/MediaStatusListItem.tsx @@ -61,7 +61,9 @@ export function MediaStatusListItem({ {segmentIdentifier ?
{segmentIdentifier}
: null} {partIdentifier ?
{partIdentifier}
: null} -
+ ) diff --git a/meteor/client/ui/Status/media-status/index.tsx b/meteor/client/ui/Status/media-status/index.tsx index 2296b971cc..577c20bfaa 100644 --- a/meteor/client/ui/Status/media-status/index.tsx +++ b/meteor/client/ui/Status/media-status/index.tsx @@ -15,6 +15,7 @@ import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMes import { assertNever } from '@sofie-automation/corelib/dist/lib' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faTimes } from '@fortawesome/free-solid-svg-icons' +import { mapOrFallback } from '../../../lib/lib' export function MediaStatus(): JSX.Element | null { const scrollBox = useRef(null) @@ -41,13 +42,15 @@ export function MediaStatus(): JSX.Element | null { setOffsetTop(scrollBox.current.offsetTop) }, []) + const emptyFilter = !filter || filter.trim().length === 0 + const filterItems = useCallback( (item: IMediaStatusListItem) => { - if (!filter || filter.trim().length === 0) return true + if (emptyFilter) return true if (item.name.toLowerCase().indexOf(filter.toLowerCase().trim()) >= 0) return true return false }, - [filter] + [filter, emptyFilter] ) const scrollBoxStyle = useMemo( @@ -83,8 +86,8 @@ export function MediaStatus(): JSX.Element | null { - + @@ -92,10 +95,9 @@ export function MediaStatus(): JSX.Element | null { > {(items) => ( <> - {items - .filter((item) => filterItems(item)) - .sort((a, b) => sortItems(a, b, sortBy, sortOrder)) - .map((item) => ( + {mapOrFallback( + items.filter((item) => filterItems(item)).sort((a, b) => sortItems(a, b, sortBy, sortOrder)), + (item) => ( - ))} + ), + () => ( + + + + ) + )} )} From aa19fcc3b5bfad05158677701b838673578a4dcb Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Mon, 31 Jul 2023 14:46:06 +0200 Subject: [PATCH 033/479] feat(Device Triggers): add support for setting shift register operations --- .../actionSelector/ActionSelector.tsx | 89 ++++++++++++++++++- meteor/client/ui/TestTools/DeviceTriggers.tsx | 2 +- meteor/lib/api/triggers/MountedTriggers.ts | 47 +++------- meteor/lib/api/triggers/actionFactory.ts | 8 ++ .../StudioDeviceTriggerManager.ts | 16 ++++ .../blueprints-integration/src/triggers.ts | 12 +++ .../shared-lib/src/core/model/ShowStyle.ts | 6 +- .../input-gateway/deviceTriggerPreviews.ts | 12 +++ 8 files changed, 153 insertions(+), 39 deletions(-) diff --git a/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx b/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx index fbbab7f95a..705a99ddc7 100644 --- a/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx +++ b/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx @@ -10,6 +10,7 @@ import { EditAttribute } from '../../../../../../lib/EditAttribute' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faAngleRight, faTrash } from '@fortawesome/free-solid-svg-icons' import { AdLibActionEditor } from './actionEditors/AdLibActionEditor' +import { DeviceActions } from '@sofie-automation/shared-lib/dist/core/model/ShowStyle' interface IProps { action: SomeAction @@ -92,6 +93,9 @@ function getArguments(t: TFunction, action: SomeAction): string[] { case ClientActions.miniShelfQueueAdLib: result.push(t('Forward: {{forward}}', { forward: action.forward })) break + case DeviceActions.modifyShiftRegister: + result.push(`${action.register ?? '?'}: ${action.operation ?? '?'}${action.value ?? '?'}`) + break default: assertNever(action) return action @@ -133,6 +137,8 @@ function hasArguments(action: SomeAction): boolean { return true case ClientActions.miniShelfQueueAdLib: return true + case DeviceActions.modifyShiftRegister: + return true default: assertNever(action) return action @@ -173,6 +179,8 @@ function actionToLabel(t: TFunction, action: SomeAction['action']): string { return t('Show entire On Air Segment') case ClientActions.miniShelfQueueAdLib: return t('Queue AdLib from Minishelf') + case DeviceActions.modifyShiftRegister: + return t('Modify Shift register') default: assertNever(action) return action @@ -180,7 +188,7 @@ function actionToLabel(t: TFunction, action: SomeAction['action']): string { } function getAvailableActions(t: TFunction): Record { - const actionEnums = [PlayoutActions, ClientActions] + const actionEnums = [PlayoutActions, ClientActions, DeviceActions] const result: Record = {} @@ -386,6 +394,85 @@ function getActionParametersEditor( /> ) + case DeviceActions.modifyShiftRegister: + return ( +
+ + { + onChange({ + ...action, + register: newVal, + }) + }} + /> + + { + onChange({ + ...action, + operation: newVal, + }) + }} + /> + + { + onChange({ + ...action, + value: newVal, + }) + }} + /> + + { + onChange({ + ...action, + limitMin: newVal, + }) + }} + /> + + { + onChange({ + ...action, + limitMax: newVal, + }) + }} + /> +
+ ) default: assertNever(action) return action diff --git a/meteor/client/ui/TestTools/DeviceTriggers.tsx b/meteor/client/ui/TestTools/DeviceTriggers.tsx index 6f0d743ee2..8344355d84 100644 --- a/meteor/client/ui/TestTools/DeviceTriggers.tsx +++ b/meteor/client/ui/TestTools/DeviceTriggers.tsx @@ -104,7 +104,7 @@ function DeviceTriggersControls({ peripheralDeviceId }: IDatastoreControlsProps) .filter((preview) => preview.actionId === entry.actionId) .map((preview) => ( - {JSON.stringify(preview.label)}: {preview.type} {preview.sourceLayerType}{' '} + {JSON.stringify(preview.label)}: {String(preview.type)} {preview.sourceLayerType}{' '} {preview.sourceLayerName?.name}{' '} {preview.sourceLayerName?.abbreviation ? `(${preview.sourceLayerName.abbreviation})` : null} diff --git a/meteor/lib/api/triggers/MountedTriggers.ts b/meteor/lib/api/triggers/MountedTriggers.ts index 5fbe29f5e4..580157595f 100644 --- a/meteor/lib/api/triggers/MountedTriggers.ts +++ b/meteor/lib/api/triggers/MountedTriggers.ts @@ -1,16 +1,23 @@ -import { ISourceLayer, ITranslatableMessage, SourceLayerType } from '@sofie-automation/blueprints-integration' +import { ISourceLayer, ITranslatableMessage } from '@sofie-automation/blueprints-integration' import { AdLibActionId, PieceId, RundownBaselineAdLibActionId, - ShowStyleBaseId, - StudioId, TriggeredActionId, } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' -import { ExecutableAction } from './actionFactory' import { IWrappedAdLib } from './actionFilterChainCompilers' +export { + DeviceActionId, + DeviceTriggerMountedActionId, + ShiftRegisterActionArguments, + DeviceTriggerMountedAction, + PreviewWrappedAdLibId, + PreviewWrappedAdLib, + IWrappedAdLibBase, +} from '@sofie-automation/shared-lib/dist/input-gateway/deviceTriggerPreviews' + export type MountedTrigger = (MountedGenericTrigger | MountedAdLibTrigger) & MountedHotkeyMixin export type MountedDeviceTrigger = (MountedGenericTrigger | MountedAdLibTrigger) & MountedDeviceMixin @@ -63,35 +70,3 @@ export interface MountedAdLibTrigger extends MountedTriggerCommon { } export type MountedAdLibTriggerId = ProtectedString<'mountedAdLibTriggerId'> - -export type DeviceTriggerMountedActionId = ProtectedString<'deviceTriggerMountedActionId'> - -export type DeviceActionId = ProtectedString<'DeviceActionId'> - -export interface DeviceTriggerMountedAction { - _id: DeviceTriggerMountedActionId - studioId: StudioId - showStyleBaseId: ShowStyleBaseId - deviceId: string - deviceTriggerId: string - values: DeviceTriggerArguments - actionId: DeviceActionId - actionType: ExecutableAction['action'] - name?: string | ITranslatableMessage -} - -export type PreviewWrappedAdLibId = ProtectedString<'previewWrappedAdLibId'> -export type PreviewWrappedAdLib = Omit & { - _id: PreviewWrappedAdLibId - studioId: StudioId - showStyleBaseId: ShowStyleBaseId - triggeredActionId: TriggeredActionId - actionId: DeviceActionId - sourceLayerType?: SourceLayerType - sourceLayerName?: { - name?: string - abbreviation?: string - } - isCurrent?: boolean - isNext?: boolean -} diff --git a/meteor/lib/api/triggers/actionFactory.ts b/meteor/lib/api/triggers/actionFactory.ts index 7a40a1a7d7..4c7792171e 100644 --- a/meteor/lib/api/triggers/actionFactory.ts +++ b/meteor/lib/api/triggers/actionFactory.ts @@ -34,6 +34,7 @@ import { PartId, PartInstanceId, RundownId, RundownPlaylistId } from '@sofie-aut import { PartInstances, Parts } from '../../collections/libCollections' import { RundownPlaylistCollectionUtil } from '../../collections/rundownPlaylistUtil' import { hashSingleUseToken } from '../userActions' +import { DeviceActions } from '@sofie-automation/shared-lib/dist/core/model/ShowStyle' // as described in this issue: https://github.com/Microsoft/TypeScript/issues/14094 type Without = { [P in Exclude]?: never } @@ -577,6 +578,13 @@ export function createAction(action: SomeAction, sourceLayers: SourceLayers): Ex return createShowEntireCurrentSegmentAction(action.filterChain, action.on) case ClientActions.miniShelfQueueAdLib: return createMiniShelfQueueAdLibAction(action.filterChain, action.forward) + case DeviceActions.modifyShiftRegister: + return { + action: action.action, + execute: () => { + // do nothing + }, + } default: assertNever(action) break diff --git a/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts b/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts index 2ed95e7bf4..0ab7df6c2b 100644 --- a/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts +++ b/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts @@ -13,6 +13,7 @@ import { DeviceActionId, DeviceTriggerMountedActionId, PreviewWrappedAdLibId, + ShiftRegisterActionArguments, } from '../../../lib/api/triggers/MountedTriggers' import { isDeviceTrigger } from '../../../lib/api/triggers/triggerTypeSelectors' import { DBTriggeredActions, UITriggeredActionsObj } from '../../../lib/collections/TriggeredActions' @@ -22,6 +23,7 @@ import { DeviceTriggerMountedActionAdlibsPreview, DeviceTriggerMountedActions } import { ContentCache } from './reactiveContentCache' import { logger } from '../../logging' import { SomeAction, SomeBlueprintTrigger } from '@sofie-automation/blueprints-integration' +import { DeviceActions } from '@sofie-automation/shared-lib/dist/core/model/ShowStyle' export class StudioDeviceTriggerManager { #lastShowStyleBaseId: ShowStyleBaseId | null = null @@ -102,6 +104,19 @@ export class StudioDeviceTriggerManager { return } + let deviceActionArguments: ShiftRegisterActionArguments | undefined = undefined + + if (action.action === DeviceActions.modifyShiftRegister) { + deviceActionArguments = { + type: 'modifyRegister', + register: action.register, + operation: action.operation, + value: action.value, + limitMin: action.limitMin, + limitMax: action.limitMax, + } + } + const deviceTriggerMountedActionId = protectString( `${actionId}_${key}` ) @@ -114,6 +129,7 @@ export class StudioDeviceTriggerManager { deviceId: trigger.deviceId, deviceTriggerId: trigger.triggerId, values: trigger.values, + deviceActionArguments, name: triggeredAction.name, }, }) diff --git a/packages/blueprints-integration/src/triggers.ts b/packages/blueprints-integration/src/triggers.ts index d50e6aea8d..46e0b1d534 100644 --- a/packages/blueprints-integration/src/triggers.ts +++ b/packages/blueprints-integration/src/triggers.ts @@ -4,6 +4,7 @@ import { SomeActionIdentifier, ClientActions, PlayoutActions, + DeviceActions, } from '@sofie-automation/shared-lib/dist/core/model/ShowStyle' export enum TriggerType { @@ -281,6 +282,16 @@ export interface IMiniShelfQueueAdLib extends ITriggeredActionBase { forward: boolean // TODO: Change this to use `delta: number`, as opposed to `forward: boolean` } +export interface IModifyShiftRegister extends ITriggeredActionBase { + action: DeviceActions.modifyShiftRegister + filterChain: IGUIContextFilterLink[] + register: number + value: number + operation: '+' | '-' | '=' + limitMin: number + limitMax: number +} + export type SomeAction = | IAdlibPlayoutAction | IRundownPlaylistActivateAction @@ -298,6 +309,7 @@ export type SomeAction = | IRewindSegmentsAction | IShowEntireCurrentSegmentAction | IMiniShelfQueueAdLib + | IModifyShiftRegister export interface IBlueprintTriggeredActions { _id: string diff --git a/packages/shared-lib/src/core/model/ShowStyle.ts b/packages/shared-lib/src/core/model/ShowStyle.ts index 9f791c9f24..333692018f 100644 --- a/packages/shared-lib/src/core/model/ShowStyle.ts +++ b/packages/shared-lib/src/core/model/ShowStyle.ts @@ -102,4 +102,8 @@ export enum ClientActions { 'miniShelfQueueAdLib' = 'miniShelfQueueAdLib', } -export type SomeActionIdentifier = PlayoutActions | ClientActions +export enum DeviceActions { + 'modifyShiftRegister' = 'modifyShiftRegister', +} + +export type SomeActionIdentifier = PlayoutActions | ClientActions | DeviceActions diff --git a/packages/shared-lib/src/input-gateway/deviceTriggerPreviews.ts b/packages/shared-lib/src/input-gateway/deviceTriggerPreviews.ts index 49671b70e2..738c486029 100644 --- a/packages/shared-lib/src/input-gateway/deviceTriggerPreviews.ts +++ b/packages/shared-lib/src/input-gateway/deviceTriggerPreviews.ts @@ -22,6 +22,17 @@ export interface IWrappedAdLibBase { item: unknown } +export interface ShiftRegisterActionArguments { + type: 'modifyRegister' + register: number + operation: '=' | '+' | '-' + value: number + limitMin: number + limitMax: number +} + +export type DeviceActionArguments = ShiftRegisterActionArguments + export interface DeviceTriggerMountedAction { _id: DeviceTriggerMountedActionId studioId: StudioId @@ -31,6 +42,7 @@ export interface DeviceTriggerMountedAction { values: DeviceTriggerArguments actionId: DeviceActionId actionType: SomeActionIdentifier + deviceActionArguments?: DeviceActionArguments name?: string | ITranslatableMessage } From 43763fe1bdb3228c542de7412291f173b4e1b525 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Mon, 31 Jul 2023 14:53:04 +0200 Subject: [PATCH 034/479] fix: test --- packages/server-core-integration/src/__tests__/index.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server-core-integration/src/__tests__/index.spec.ts b/packages/server-core-integration/src/__tests__/index.spec.ts index e5850b1e0f..c4a290a5df 100644 --- a/packages/server-core-integration/src/__tests__/index.spec.ts +++ b/packages/server-core-integration/src/__tests__/index.spec.ts @@ -118,7 +118,7 @@ describe('coreConnection', () => { // Subscribe to data: const coll0 = core.getCollection('peripheralDeviceForDevice') expect(coll0.findOne(id)).toBeFalsy() - const subId = await core.subscribe('peripheralDeviceForDevice', id) + const subId = await core.autoSubscribe('peripheralDeviceForDevice', id) const coll1 = core.getCollection('peripheralDeviceForDevice') expect(coll1.findOne(id)).toMatchObject({ _id: id, From 16895bc41e8b8571109a915f48846f6bd52d522e Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 2 Aug 2023 16:38:30 +0200 Subject: [PATCH 035/479] fix: ensure that Shift register Ids are only positive integers --- .../actionEditors/actionSelector/ActionSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx b/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx index 705a99ddc7..6a793c4612 100644 --- a/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx +++ b/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx @@ -406,7 +406,7 @@ function getActionParametersEditor( updateFunction={(_e, newVal) => { onChange({ ...action, - register: newVal, + register: Math.max(0, Number(newVal)), }) }} /> From 246125c2474f7696ddb2f7a614286c2b14b05ecd Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Thu, 3 Aug 2023 15:41:03 +0200 Subject: [PATCH 036/479] chore(Media Status): add docs --- .../docs/user-guide/features/sofie-views.mdx | 17 +++++++++++++---- packages/documentation/src/css/custom.css | 5 +++++ .../media-status-rundown-view-panel.png | Bin 0 -> 186208 bytes .../img/docs/main/features/media-status.png | Bin 0 -> 303739 bytes 4 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 packages/documentation/static/img/docs/main/features/media-status-rundown-view-panel.png create mode 100644 packages/documentation/static/img/docs/main/features/media-status.png diff --git a/packages/documentation/docs/user-guide/features/sofie-views.mdx b/packages/documentation/docs/user-guide/features/sofie-views.mdx index e552c01ae4..e26a46e3dd 100644 --- a/packages/documentation/docs/user-guide/features/sofie-views.mdx +++ b/packages/documentation/docs/user-guide/features/sofie-views.mdx @@ -163,6 +163,12 @@ The Switchboard panel can be accessed from the Rundown View's right-hand Toolbar Technically, the switchboard activates and deactivates Route Sets. The Route Sets are grouped by Exclusivity Group. If an Exclusivity Group contains exactly two elements with the `ACTIVATE_ONLY` mode, the Route Sets will be displayed on either side of the switch. Otherwise, they will be displayed separately in a list next to an _Off_ position. See also [Settings ● Route sets](../configuration/settings-view#route-sets). ::: +#### Media Status panel + +![Media Status panel](/img/docs/main/features/media-status-rundown-view-panel.png) + +This provides an overview of the status of the various Media assets required by this Rundown for playback. You can sort these assets according to their playout order, status, Source Layer Name and Piece Name. + ## Prompter View `/prompter/:studioId` @@ -251,11 +257,14 @@ An API endpoint for the system status is also available under the URL `/health` ## Media Status View -:::caution -Documentation for this feature is yet to be written. -::: +This view is a summary of all the media required for playback for Rundowns +present in this System. This view allows you to see if clips are ready for +playback or if they are still waiting to become available to be transferred +onto a playout system. + +![Media Status page](/img/docs/main/features/media-status.png) -This page displays media transfer statuses. +Rundown View also has a panel that presents this information in the [context of the current Rundown](#media-status-panel). ## Message Queue View diff --git a/packages/documentation/src/css/custom.css b/packages/documentation/src/css/custom.css index 6210231f10..f519323b11 100644 --- a/packages/documentation/src/css/custom.css +++ b/packages/documentation/src/css/custom.css @@ -71,3 +71,8 @@ h6 { .navbar__search { margin-left: 1em; } + +.markdown p > img:only-child { + max-height: 50vh; + object-fit: contain; +} diff --git a/packages/documentation/static/img/docs/main/features/media-status-rundown-view-panel.png b/packages/documentation/static/img/docs/main/features/media-status-rundown-view-panel.png new file mode 100644 index 0000000000000000000000000000000000000000..d8f13954fe9dd587544639222a9b894d31a055e6 GIT binary patch literal 186208 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

iI#ydc}@IBJ6C@aEeemCC6z^EPg?^|sRJxPpqr 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

ClJZF|?$)O9Rjhn99Cv@&}M=n!8GYdAJ3mahW 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Ν7j6lj)w6Fv=khx7WF+_&K6Baguu^BYYGtRbMFL8YCEc zW3NZ=j8d61SLhx0xN+Vtaf~=77$WIsKLH2DrFWSW%Rd^)TPIyXi)u5rWjlF<`XF=) ztl|vg`Xl!`3x`Ys4BB>sQg-N+l;V@PHPVg*Ws9@}zWMZ8*tJD>un%>Ia;O*X~mzgZqr$mmIfd zk)JQxiWT%}Du5%iEQv%dfjOM`l}<&~nw^4pXuGqX+XcY|#MTAL4%< zt6Y{N+O7!MVpo2T138pil;n%GFAR@!=kv}Az9Oy5or=^lOtva$si3|c^px8%zJXjE zMlZ|#56Y$Qo4SP1s|zfKpfS_M9eb&Mhr+HKzvgOqkep*0{V@c1sOpeY;=a3)%*7k7 z{63%81)d3m@9F8rJYP~q(uAd5ZoRtNRgzsDhemf$7X>Cg*I`;_x1I#L zg75gRD&UnDR|1!FLA5*RZ9?kN1%I&g-JC(Qk4vAfb7fjqx@{VkESZzf^NxCP@O59L zjK@7J49z{XnZoNN3P}80MI^lSld2=ao?R~VX zD~R;wB86Z%FCq0@Tdv#RuJ-!j5CFqxVw|kl)P(Cd)uC3RylcCvg<}EO-K{&*mZuBs z|LC1}0F-xi<6pL&WC7l3zWJ)mw3vOY?IBj)yq-nK&~6}@YkB;nRvu3;3h(Vg?XK(f z2A&)o47Yl8*Shw?`-7<$y1K{L>-c${IBv@hQ-|lLXj9&HFnhQrBDUFf@cUk8ZwrnD$g1dF+-_?Dn(?n)|0g0XxA=*#gPa^O60O*PG zM^9CXo4C_rA4J@H^t}n90)l?MLdVIAJK$yAbbh~XTqv-rPA9f%)N?X|cQ~5lC*4cI zi{sN*vGTfZToo?XO*iNV)oPGD-ly^e)2u)|>)L8G%4<5ru7Qs>7kUKM068YeuIL}# zN>v9Rpq`Vs^Yj5-NheJDYoH~wCr^BgXL2agCN$^u8~l@BU&_g}36HPcgCoooUn!1jLCmK7n zXoN2)OD(y5O{8iQ0h?nh_k1mw=dtg!1LzO8asNG5SE2iowG6~Jd4iZyTWq$iaEm+? zyik91^v7kRd+(KAky)MeTD|Ix$1(NvJW(Z&4%syE)1(Ub%G65ZF8_eKTK>5>=`7N1 zx$F&R`0J=4mq7KfF8#IhX5s9O4ud}BTH691^aLeH^(V{nU}nEO2nAWq9VDujjchV@ z0xOzQ7r4yROW0@ba~F2y<2dwNdCSnXt@#zy&Zu9W?8)=KK57lpl&SuIk@pL4$oo0V z%c-9UTyU5I6%4qGX`|U)xl&}6KW6Z88Xf86RuF~zd-2u+sPQw@Dm%`=P6Wwn&OPq! zA)b@v<7~CV%+!jZVqFqF+*5OEp7^0FO;MY9Y5@07y2CcI@)DvEe@J44>11a7-x1!> z3f>u)7R)BX*jIRUFzezg-OX!`d+NiaGNd*FvP=Bf#4~ikXR7vR-AfU{MKiEroWtVO z@Hr>Q&aH_pO4fiKrP^0UTzr z+ZMelPMM*5fi&ZHq}%aOL4JAM)mv+7VR4vdfof)jq{F5x^Ed`HFkyXvS>EY?cj=i|k#QB2b1E}k#YYyhfZp!fJqK-0 zEqX@(FuW2fUfN>O$j^e=lTVi>gn%zBhy_aB0%R4)! z@aR5~DSZ1_1{dnnh@ItOhtkKt(Aij5aq)f4(;%cTd>r4EE3=4{;ozjwt7!iqx{cky z?FQe&;L%6OIXRyNtHQ8HBX5q0)junt3f&`m3KTFL&Stl9F|JOrR+ZLSo-6Cjzh7hx zq7`&YsXrawtSjp!<-Nc&D5orYZ=KGkWi12+wc{HFZcdA3c=H*3e|UFU-)Wvm^Q^70 zh+peY*coIoo4IM&BlZXGN|q%X{1<|rO{_}YZaVsVfR=iGK2>E`2X*RB)(V!3F`vYc zkFZNuUZe-)^@{_exyr(F;q?c4p^S(>V@gCsCaFf_0SbLYp5APio12Htzd}R*2pzac z@mshFH*wd~8lAhLu+`&Rzvn&ey>0_NhFdlHhg-Sp@i`tEDkM+zikG;8k=&lwqNGb< z^*5*^6ex~wwf~%wyusF0Opwg0ySd*01_A!>Dpp*K$}l*I_Hl%(`uC|2-yyH#~;85uv$t@9_K>EbbZ72yVossdHk)_TZJtDV3e~kDqu^| zZoYoPVI6ilr&sbTpx(&53nT#(Ig#^uzoA8|fh2ZW5_Bzt(GH^9|8*9K{=o zY}w@M*}-;ozD0EVHLnLWLD zf4~ai4E$wM{fux}`dp#;=0lspgcUn*zV3r;-c>;^W>CG>qh8osv|Q&-2Vx*FUFnd) zT=r}vAb6?g&;Ir8dZVNKf&YsUCCc9G%|Q0u;wz(7YmT;m@-a((fn~J)2WqviN-(lEg>*i0i?|&}V4}U@b z0m*$ktHf5{P9+MJf>lPiXxV0%xBGt&FTe_J_LSC;cFB4pg(?mBs!wjQZuS$mE` zcd9bg#PNAKfz5 z&muyGM3RhyOnb=ufc{YAWcS3?xaYY~&#NKg`K%8W?o^g3YsIO=c8nCY^5^NuFo>Ln zwt|i$h-$mxEIiRQDOyO2CsKUnY3e#0$d7Z;)35+!=GXKVHi+P?hs`@@r}3{J8@nFD z)C{(xX^lv!#0|_>>ReXMK79-QZ9r?TyB$rrQyL=avTkLPgae}qlav{!N2{@;=$lo1 zH1gW=Tl0-jB~ah|;BmmiX0tNXY%#~RdOv)spM)Cx+Y|j-^O^|ds@G_3(yCL!(3D7(Kmwys~Ydd6A?&>Eh= zEIn_bGp4f$ki=kWr-$#~jm2??(h=XQCt+}*Wr?H-Biz3w$KgWRe`FGz31@5X3b32B znb7^}|CpHo<@^YS++}0;%n9DuULq}dGNRo;Ln3_!fSxm$Am0#Fw{0VEZc7iyD*?lu zV>F*2TH>k6I{+aydIMb}EIKSsqaUFnR)hC$1nqzqmQ(xqFa%310>>XE!4n6*i4-S2s7+01o;-UW5#*v3j2Kn`juw+BJ9rWY1AowD6&V` zjOLk!9JlZu)5pjSQ6zZ)encaZxS0I$)jas9BP&1#rH+Yko;d?p3$u5jM$vH~l$GucX-uua?l;=Q%mE5x*`Y z7f}CMVM;yNnr^p!+*U~kJb|SH<4@Lh?#}H?0{8aWz|xal3~(Kfx1}Ri=ad|+3(RUc z#?f8xxMfM?cx5~K+6relFR36AL z8ngV&URgw`PLLreZ|sU%5L~$oWR_axovT}lZJ|Z-c}Y2v$HlWFRn(YhP|oRAE$0g(lvTC&(dxAZM;!)UWc;8Z>K@1Xc3NKN7zPAp+ONx* zEl-H*O^3bP->^-jr7US@!mowAVqdIC48HTBrUJKaECqTyTO-2{F2fRsCy@0Ve?P&>>XbiiRTd=F0nWA+Y1Ld0t0H#6MA!Lb}>LUEap|L#fz`|bR__fvd=MvL{g47 zy21MVC7oz#fz#61yfc1@kc68PcV7yUoJjFqujLYrtoSz!vc zXI|VhWZH`7oND=2W->6_B^-sVU^N*|y?J}!wn%N|DSC?Y*0gaF&W4cr;ro$$IWVlJ zrP1Ih^i;9+TuL^Plc+_PKH~EwZ03C*x6uM3bU3&xL!D183jQ{8yzM+VjT0vG$mW%c zm`BQs9j`@{yz-*=!t77(Q!+y0j5UrEFQ|B+ zH#X#};p5unL}p@LGU4b*rE@==%5x*K8w0<2zNE1=Q6PwIG`qWt@ZwtIW#{^$%OoDG z*?$s2UM)|lw>?(|XC{bZ0UmS^d@;UeVRt|p;Y#!#UhTW*!{CEpN>6U@d2ae4^5q7w zKBHl7p*du7fQnh#S!6+$^#=YaJctM(mpNvmZ)FQB-=a29UDei@x6#z>3QZgynSbya z$b*)gsTtG1g>^$T9@P|B_>FR1WS*{xHNltruASbKb`gM*@-3Cli1^I=RRW?tLRMqp z+h^M$6US@DVmfuKZUPyBSOa5t=IotSUa^;+1HWl-e6G{Mf|6M(6ym;dM_*&SMKvo^ z!l0!UZfR#@h@05U;6iw6d0~;rXjhzQ=kpQ2fE~+4_YGK!qusH$6VV}RqFh{ zPhb;t(^V3U@x@H#1ADcFywR+u!n~)8=-$?N?O+Tdd4g6AnCX|+5JvzYrUWFec#Us&ZBycM2wNdUiB`t&7S&`5 zq`)0!RW-#Z5+1}{yH3(2IENl)C`l%)GtINFaqk2>#Eat(-y|gBzI{6woy$oq3o<+x z^bqXawV4JH5!0wmnIkOo-(^&6Qc#d!gn^0t-VOSUM2>fSc^J}(K@sFc`rWQvX>ZNe zi-rA(b@o=`k`_$D&Px>i@j@o1;X6)}`~y*DdevR?s1qydVlic2nq}bk#k{PBgNuC& z_v~dvC!*o2%-|PuMBjl=O+}YTg>$o$ptPEhcbH4lQij9My9V}TJ=7m- z%=f=g2G~a$!~<+fESZVivkA2txT~Lbk;Qa0lJjPX{8H&YD~=KT^U|S+Q{wZjdO`yN z2-=QJW=4%CG;H-Ty9#^1=b>vXnPV{9H%-_7>g?^-kwfn0I)%~@qMQ5KLI)}GsQbAi zXn1b2_`_S+0321JSH#RHI@{Nijy`p8hsC35WNdnyS|(S>=Hib! zbD8b(+al3fn(87lO0x>f;k@8*8k*l>74|v?k3g9}gy0lo)$s7LJ-H{dTQ!uN%6Fm6pq=;gn$5$T5Mxz@G9<$!r6Me(1c}iTMu(*f)rS;}Q1R=?h0KaY8B)(} zh@hsHKq;v0IGH$@Sjhb1-=dKNc~8k$YJTl1<}5PbF1lbn{ zVvx+OV*4rk#JOD#q6Az4T7F3NGYoeF+6FUdQr0@%sE698Oz(q*qe$3zD}|hPqdqV$ zf)qRkNy$EelfpuLjuw7H&Qu>-T$Cd%Mh<$|LwT(^8H|rIH11a1tTURIKwCxP%GVk` zCqEyDzDCw&qsc5SpM4?ejT#k}i^R17>x@f}?bMi^ljyqjqYJ&bVB*ibwojBR-e4eA z@7Hhf=JaWgh=WOIbH|*=^snF-X<_3shDpPY-BVVh$b7U&*at%q0If-{W;mrLejIPEF@>JXi#Z5QaWU&fH4nH5i8)C4C z2;=_sBX(NEv<9w*F+zUR$!tVIwh}gqh9&$6@U!Drwzxfe)%dDMvAw%7IqRrSG&whR z)W$m*o4qA|z}U{7$5_1;n;LfIU0}L=SW0#xmp(kJ-)j3@nrf_+g&YujunfmP%}T|W zGv5+L*J3oDC;w->si@rt$qmiasiSZSPGq9M{GxSn5dW|J=4Y7?eqJ_LmThX0RBL_5 z{g8iWmoBg5qto-@G=&=EN)HV^!`ikTIDy~Zr&BYtmV77eX-=Eckv39V&GnQJ)G9X~ zJyYBzOEF~=YV%RsCcR*| zkTeU{;%O=yQYkgqzb89)%!ZO8Y{oI0;Eo-npwlJxg5e<7mLhP1$#fk|e^Fa_Doj87 zTOnNwy_)|--f8_MlNo2IVAQ5j1`^FHA^QUL>=^keiR{~~{pT;GDPe2W;y6p68od#g zoGIyQjw*zqEfR{1y)V70GQ`>^{R=m_A(jgmK)>fY`B?19#>L5;J#>^x~7 z;AgPNZ@z3=Yc(C-J7=!o>-V`!r_mEE&LV9$;a{7|g)QENY8Xzg;w=S48AlWDPSi@0 zKE%_R&tNO2GZ-RSyk1w;OYNhYiEg`*B~@Lk!q!aYJ(JbNea7Y}A$C5ok&vZET{+qN z)I}Jf`^qpON7UmB?ImMyQ=kY+NA3e&^rKJMUF}Vx3|FE+c}`-d8K8r3k*K4dYRb1F zDYlg@^xf~UY_uEpA_)EB9H0aJkNH9`1b-ThuTC5-8U8>Vb?VuW$2M5IyrpBg`C+l3 ztIyJhNEB5+f4sICh|HrkAjF6O?m>I<&UM>ND;QzO*<4l}VY5vcJw5<*xALc9u4_dz z$QQ!&rI9A~2ssj9=bSGBKYw$JQ*Kz|_8?day9$$ESPXX&JfYM4m1FUtUxGlB{ViY! zH0h@*LLi41Oau*a)z8{RICiJK>M>Z%3z(N_R8Y*e$5$F)Tq3{5iYu-M|M7Cb5+0EV zrFb3qWbHHKKO2NBD4ygC3n-kOkv5wRGLID77m_O=`;SYGh_|iS&mebKw)M_&p zG}hQ-NM-z%neaL`qiE*TwrCf?l>10Tx$G%vsN`=tb}Ig73av!(6x$rjmwlhgL7y$V zwkQ3!)jK>Y2VV$I|Afu6Xi%xwr*Wk5^`Y;s5zBN)h1y@R_}Lf}ybFdew1tATNZ%L# zvP{z(-LH(ZpyLp0E;_%-Hw$L|p+Db8G`9tZN!HvD`7;--wsuXhS>8o&L9K03S!|bt z%ikH^$@8Vwr}(n??su(mwcfZ7vL&HYcQuHXeB-*2aiOoE^Tm)JTs!2F_8Oo+2vI^b z<5BD!o#1=Jz@_(`9=4#ygnThZe$K4LKlA_#mM7xKbTlTp{)yD@G(GfCdDWXTefU|) zpDIm0Spi)3C8;E)KA9z3Wgpt*@pzK?x0q~_3jFrK_gCAkGRZ~DQiUNk*J|iO0rmdz4ic7{lpV;)Noo2GVGu~x`v6g}si#1DKZ}p& zclAl2(&gJal$CXaEk(4~2D<+zZF)x9qAB5bS~y-ZbXnPTYt6weFu;FHe7w|a1HC|JtJ*>-<4N@a*rIw;3Y)j#vk_p714>1rFR!!41hQgtUg zS*+d5b>XF949|};*iQTboYemy8g{?r%v+~91tKB&JgEPuiahix)~3d$@J9E0DlPF& z)$0>~we|*Uw$rLXUV@Up+0G$`lNk2b*;cBqSLe<82H%LpoJcI#Vk?@#esf z+5`ArKWKQ(HEPd)aqHq%c4;WzfV4!pAjDV9j^!Dd_3dILA2>H$~AHAz<-VQAc9kA>%m<^&E?T3cTKCi5vh z3i~IO^VhHct_H2o2GPQorx8(0Q{5&R@ze}=qq$es*yv9HEl>`fe93%bQ!`X1sov$q ziKxf0IaK=}w!G#Q=+x~n+n+J96N-a>b^B8>Rd#bggt)5p@lsI9Ah4Hc5r{?#ybO!i znCzy5m7+2cr1kfUloAExL zx`7D>9oAV`AsenM?>qP=oRAL%-bb0GG>VCJH_G0^#Fo$JAvYdQ>k6T$COx2=ye2>` zeNDmtBY_^VIrepRYuFsSN*p%rP~d>F(|W;#n%z7aAqQGZlB(p+1DwD7O8)*zx5wLB zUkHtEl%uo8nf(c&u`8H79kI9TZUQcqW3n>t)(--w@1oUZ0mt9*gZKHGrT<5OFM84Z zY822&%e1qi5Jz{S?8w4n#alv$#rDz_s<{@cdNECNW`Xjr z50}};D8o7^RX@y5$(n(77)juk;^57frYJtKV@$;_hork+WG1&D->mG_3D{lR zevf5jhy&Wev&+o5*54|(R2Byu#i6RV<1xfFhNtwo((jns5MO_p7fRZ*JPNS&*wtb6 z`g~t~FLbEUBAGP)^D&o^df{<#ho7dY63#j)ESlWfK*t^x7rcMA4}o1KWJybob@ugF zw`!QTBW$endJL`fiP+MQ@e4kg!NVFPzMw|A;=z|j%GU$NT7cAuUy#R=BvBCZ19hh^7mu=o@zH35u)&mUe#dS!?HyvawJW95VR>%f>`j5 z7Pl*ftp)R4hTLLiBIt>ZU<2~D-l@RTkT~xqCAj6A^%?*Odnv*3`BIbp@zSzskECaV=zUqE7H9obgGwCw)TT;#ZS;m(6SOHD_3F9`jEK8Kaq!-68* zrnOAg3|Uoc3;?(StFiJvDS!;PBzmN?f-cxUj?)l2%FVF3H~<>yP$VElSpn{((PAsU)wd3{8?$ZOue3NgS-Kh1L`BWNbxQd{DU1!b zD%T+wR=UUFTSu?|$fxvvQzZB`d-3Z6Ku7sEBF@a$g56&q9Z874=f>xP(uo7yCYG+DB?(4`?PX zGE9L`R_cOX0zT~h*>^l-OVR~r#is}p+%8R-bng|C8zcn_Pdi>>eVBp zpH@51p)N?CXO`ZIJlbxe`a{VQpock73nk1zUaN>9ZN4;{>dmBSF`w}{5-i!*t?w88 zTxQb($`|_u5}WglY?2WdtGJ5sMA-wA0+vR%^7JJ%F{%-Oyq(t%aFXZO1>>7m06h}&~!4@K{} zJggP*bTOIM@(yG~>Y|s91ISx$dxOuwgeItIaAqj}sX~0HoTnnz#RXRUMKM4a_gRNy zSlrja?k*>?W+r%?``Mdj2N^<;Ml5E?&)r3eO4Xlb6s7`$6CmgA zhlMv-1X`aW#Wktmo4a#786GdJF*B6lMqKswl5@u%>}MHyjfM&a()Am_W{cRU^KH1+ z@GmqL$~<}R61U;WDIvZwHKFTYPvrzwEuu;Yw5}D2;-Y?5VdS|GwL*QdtZOP0SlZVb zLH#*&@H=cF)x0ncr(66(c90cO9Lmr`miSbi-hMC3>cSm9^$9M&G(+!FLw?Q|Vm@b2 zu_UM}5=7z`8X!>m&39Fp)E7A)-BD8!I=D!$l(5-e(1jlfx05)TJdvh0I?t~P4?d|? z0RnwL@=CU^Ar zyo|GN^u^<(=-tXMx&g@$)bBE2UM&yTXx*a)AL%C=#N9B{fr?t^`Ln?9t>V?nMCCpt zASv$efb)s)bIbihL)-EVKZ(;qhV0AE8lsCDt_X?##bZmV#gQd8_;tnA?147%#O3MP zc0g2iVz>B3<>Qh=p(EXyShoIY1+R2(bJS)icO~^dvx%ZvT1jNa6ghV;hXQ-<`jp6F z|B66*k>3HVyp#lYsK>?nX(TtPmpd<~Zg*MBI(kkF4sk#-Z7_ zXi%_k^$qG0vzq9egX*oaKrGAe(bXXfc~KQhb+qQ}_yH`2t;9c5$n=h^ovX|2Fr)X| zAnO8gKWluZR~Zck^0m3Dd==x-AuS2?LgdTCDaCRgQ7#byp|1lX%uHz?E~(1 zb+A+vkSis+3k9c=HmIXAF`8Ap>%KLimm!d#D&xsd=FVg$`~XQTcSB2{eEPja+b0l# z(xTe26Y}VToxt!ajXcUc&)im3+MP`7^^qNrwLkh(0)cBL82vHxmEZ1-NXfi&H(zF_)r>e~gU^huY_)?b`8ZCZEZm-4 zzUgw99Z{WF-fcx{CUD8b2wZ$2HXWk50|Y*vOzIz~+;gAAG^y-&TYmjbJNwarh^flU z#;4-B7&l>l!%LoB@qW(poxGr#ht+sW_lc;SEpS-0}!s=NEp*<~vTF;gkkW-Em9G$$J5?X2b4kpqJrgI)=Xu+rwn$bF33P2o^Nyn-W zm*(wb{gI4FMEEl!UwNnLV#(mI25KZe%~XHRpDFo$vE>j#oQb1L_2_Rr?(>e;)VZN} zk-0>3nn8ve=l3Ogw)YA=pCzt4uCIm{pd(21ea?fRw4%zH@l`qTK_`hq#6P7IkBaoBwh zmVeAWR_zL~*9#||c`Y{*j6FFX!KH-$ZsvRipO~@&^x2%uhU#L7z_^kNPz{N~%ig@# zi~6TK=i5&h@fF zNF#lZ(*7_VjcdiDpav%rpXoOkpdFRFcaGO1yk)nB<)6Q{#P zGpmuzQkveWr&oVFRs=3J_TA?*z8S}?@FGvTqlv>(L-gnImV5sV&&KTlTC^_g^g zqs2~4dZ-Atk-l=LnF$*3`wmn!I~;{pvT16fJCiz@P>8~ck+(?GnB3aBY$ni5H$@7Y zX_6agg6TD7JpVBhiQ>z%hDRP^1SD%w7Y{^P!OGR||)qy47Jjlo0o73NsIRN3% zB*e%#RVzUz`)u=<5?$1~!3Tx{JU}NqdbO&{qI1NPC>A1nW(O0|v}Cc(ZuDS6z7MOn z!NOX_t^;r9lZsqo>x3l)J3~J|udikY>$Hz(X?$12yRE)x;X%1Xi+fnxGTIn>`XSrG zLbOhrCcWk5r$ufKKxck+cSRE=+9(e;TkZ?*qHWka(h?pA$ub5aVxU@LF<-_rbxWH9OzZ4!F1s^PrG4^V&rk?M!24iCB5kzRHB?+Tee)=2rn0n(h z1~RbfYJ4B^0JiFiJX9?oOFuzAXMf_3K{?8CN!cfimt%^<@Eu)UsRgp>+j|hMa^pNP z@tRKvGg9rAHt;ZwO>T8Czd;so&6j_og7JE|8wEdC4*JAoDEcZjd5|MAm%1VO)OWHw)wElIw!BF>v^W*oA>U!HG`0Ec#a`fM#Bd;+1P`WOA%1!YF zd5b7a)Sr&&fK@a|l1mqe86TMd+Di&9$H8Kk3ELJ>HDO}f^OSWl_{4_$Ht!W5_aBUg$i8Jxcu327l7vrgT=!ZK4kOM@Mp zAq)T5AK%ihmW~z*^VOsfGWb7*ucpz0_b!6?HK}A%68k|W(zbQzXrMR!`Y)}dk^8I6^@p0_iHSl#wxp02YFy!p)z!rV zKIFcQ&`97{&)DNtYU-wl|9mW4#f~fxE-~^@K-AX)Ay({7wd5bXGAa|Am$yhf%f0>5 z{&>~6eEEHkopp(EQW8~P1Ycd8bX99GrIRWMk>6ZV-)Waf**S=6 z)eIIB%5o1O^B9WCw+>%C*Et|rg~M()b3G3&H#3LdZ-+d}giMl!P4A1U&1BlGxI6-% z;(kk7T-L$U3HEgt+w|=}2lXG{-+na)GjQ(i?n`>?P_!__gN*gpS&ju`dn}h7sNd!ox;Lr?A-#&DAAsi%&-Z2)DDo zhBPG#**-r=gyY)7xV3mA3%U^8F6Vbn6~^hahTw37-k!*7)IUc6o;sW~kEF`I>GqKg z>d_1dB|C0XhoMW5iz7{ohP`J-Tf(#1X%lrLdKUxVu^)1Mum|On4dzhPk{;eU$s*fC z!Mt3sj6&B*-UZ?R1d4oFgW2Ums%j|1-{s~1e2G(hrXTt%ah=9P58(>$85KnMwyYu8 zmLcrm+%c77Yw zTs`Wf2s#e*0+FWxs2w<1V`;t;%-9pMoADt|re1xNOcAOlKwb9D;UDm@WSy~?0{hb< z6fT;QG;4a>>BOf^=c~DYNn|AX-cREXGaEg5=@!1=tx~sgeBNk5N}PYfXP)D9U@*B_ zN6sKQZ{z2vshR4CA7BiN@WXy5c!8sC!q3razz-4m>(y+i+Gm^h$nb=n%HHHjM*ADEPY#SOhr*f zRgNf7J%00ZT2&-5F`UaKVH~uBhcAO>>n-n;ibfjJ!1dINIQCZ)ta(3gdfr)Lssz(x zOyhK{<|V}vx%scRW*j^=!zZvNC2ZNS#TJXbsD?%G0=;24)U`g|i6fAW6|5IGmb^OI zk>NAD>Ae%+_KC|2vpnZ^UzbOIP20dY^_#Y}Uz3j--&*YyyBM7x^z|RHvau{o9Up}{ z7mT#Saoab)a__9QPxzky42I_aXt7vtqpdA8>91&$JkCaRckJ_Y$T2o(g~y-!2h6uW z)S1GA%k6#x1A}wOB7GS2q={%7xnOz5b0c>h zZi2H~O8Y3h>}sDF<%NG;yDEI^mK$B;pKgHy;=$oo|FCcE7_NFoSDrJ_%lLd4_Y-6a zLAKBucy_#CkZsjEwLVQPLsxiFDZR>r5|}=tN8pZPhSdOzi|6-!a~;!GU_{Nxk|0v|QdzOElUjqH6-D>xRyr)4NzG>z%{z8`&Pg`Z zF4RN7*&lq)M{nqnEpi7lZ1>&PbCAUUD%pq09&%j3oXSM}To44Yx!~08(DY&l0cf4Y zv?8A&$u@zNO{_&xUNPC%^pN0|0y8wDx_Q*Y_Fy+wUa_(G)SbpVk8?V}{L+a)RuUB7 zdSW*oBr+U;tT9H`pG5Y)zP)9(m~r2GfkXitVI(He;%ac-kW)0!_+jB{1(N2>lgxlw6$)(uh0nY*Pg(=ci))N`R~qdn z%72IW69ZN*lIGn{IE{>5?NjJ?9M||I1p8r3O^)3u7>_Iv#Q8U&(x3IVFJELU=;y&(r}nN8T!9*XEhcXQ_s6?+gVY>Z|t6=1D| z=KQt*jNG#&mi=GauaI;qnZ0e$dLA_b8#aHT*+_c2=G1(9 zW<-9o((n&|wWX0c?0ty+96C`)aKobp>#hMXd3>Vc8O+y|O&lzHq#C3ycQ-X=r9JK( z;h8NG;4wL$t-WaNxH8<3+xB-kH_)lH)J)YVOOfMuK5@lTXJWEk)r|m`qNaPb5-eH| zi6S8ci{%Iq}jf};*|2D-TxtDU!cALdKDua~p>pz#n1b#ckGpjDa3hMCJ>kY#*IpvT-Z!@- zRXv1y`o1ZNMsD*tpu2d~PC&%^aW>m(;pNjIV8FFz!dcfCSD%$vV)wTc-RMB( z`jprsOH+*dtJ?)ZgS^AD%F{T=>p5-p^cJAH@z44=sDa*^Q1>fA_7t-V*MmdYYRrlg^nK^y3H$LVB@pL4EVP=DZTtmPZ=JuoPgeBS=( z%a{CLz1$~}-8@2)Txj?Uk%T--nwDv%7@oRgeb1($25T4FgG#|sQA~CfDaG~m9)h)W zhb+12-rr30POz|N>QJH``@+u{ENiR0UG;f!wg{cry*?BS#;+p`pJ?R}p|jk+MVKl> z+5}V=qNRE_9=WkKINl463)~_@cNP11uOC%CdZKVF>lqQERr)d{!@|30N7@Z_LK^h! z{d@_5&2yOD{QVf`@>T39b^r?*eh-hNPZ6zTC{>d{ODjVoSjH%X+bNpC;)9t=JMZ(R z{NN%rQxQYjFn7=B=s|_JNAlH2=qr5-V(KOv&ZU6VY%HxFkHa;bY+OFH`3rc6V{IGdqZPt*9YM#z8izn0r{1g}`G&od$JZAY@{`0RE=h13C{^Ri zKI{Zp<8jJ7qw`1c%@&+XBirGsv8K>t{9gd!@le;@Ic{G+%jR(%owR4o^#8^W9?rgw z7zoBVW1v;*pOfQhzJ7m0_l4W2@+-y%4@w8Tv4{xt z@VT>2TgqQegkR#d38BC%7;%=RK7+8?lB-F=J9tN5(8dG9W=9d+~;>?c@;j;UFnn)`Hv;)Nng0qFCk2au*Rlv}@|Yiw!gi9l0zoD1+i z6(IrB%ToBxdt2)AI4rK3ccCsM$YI|Fq7xi@gKE;^izl4Z_{rdxCEygx8Sb9-TjjM_c>?pcbxZ*`{|CcKGo{6R@btcHRtbn zDs!orU8nv7xoKZ(`sO%F1he+7(;f7;8h!$i2b9~z>nb{ala-}owykpTBeXY)=%4v` zufEKT+uVz!+ZP*v0KJiE*6Sj>PqQ$e)#6m=ukXX|3lQ;Q0VNCRF@D@&Ds}TafM5gE zQ2S@qy#(#g_nv4iZdS;xjHxJ62X>H}7x*juMnV3OC*oQ-*us5rnPg5UmDe@=NWX3C zv*DZ(wT-X39`;YnN*HlcaXb%dP@c0}nCB%n7K5%@4q9Gq2J<`DY`d z_P*Flcn@cIyHl-+73a2sJY#o&EO{Olz5;2p)0ueoEw zWZ3q_85~mwz^706$o3_b;V-Vp52X)}q%|9p(x_ze#?kT&#rNUJi%ILWk=i1T@JfVc zB|}ko{W->eqEK=3_sBG$LbNoPq>mQBNqj%|{C~w2Uy$R}Lqv4(?fWx{s~`;09s1TC zIcg|+7eBB$NEy?ZC2a3f-wX0dNjH7C)nJ%Vd}+&#mq94ZwEOTb7BAzA60A+VqWOso zD12~V`M~*ekMpvYP-SO4UKVm4Cl7h~QERwgIjadzJB*(&sY5u1bf$*#i%vbt(LhjG*@Jst&+MH$xiA zxS=M;2xf0mrI=inwQpNeJ$Q;jw2-Zy{dAraH|vb<*0a-3;d+SyYbEb@w@Nw)A189_ zdj{>ek`cUYgi}|g464SGg|)L!BbO1-v^DQ0vCw#dbh-@rkVRE=J#Ltx0%4~;8Oe{H z4(3CtBokAW3TDVCgta+usO`hUOd*W_Oq;OGiYS^>SsCV06?r|m4Lk#4;ML&ZwEe2j z_MB7Z=p}+6IUlF9zGSJPRB|MynvMp-Fi-^|f1{c0Refb63yD@m84LZPlkP}TCH*3A z1;xr}?}2&&u^F3u9>~AdM`v8fiNv1=!4lhb7q^Bz@_V)4#@7jiPkEwvk3P>2f1ohQ z5AV4_(!yN-IIdALtsmxg48PIv6(em=CYXEk1CP-bbYantsNv1HA(Nx8ezKxd7PEHf zOu7_RyjE0N&oq>=58;(Gc?kmczVE^VMDnUF87e-tzFQj16yoH^$<|&p|DM5ST^Rn( z0J^2!#+KqR{oUhq+8-Qpk7>LBLwDjb00@r#2~gG$MG{~%CyKT>6zZ-tgm({kAt|9+ zL(xF9MJs{zhQ}_qZw{#PCvT|s^oS25fJdw8qbm11eb{1$I+`PEHH|p*t^QT7Eu!(9 zuIa92ut3#WmO+XF{iQ_I@8#IZ9sU;9zQac+1KDZvn+(0C!b9BV#aNnn8cW>T&1y_| z?C+09#pkh<4zj^OoV#vU$HylcWO-bq5r_BOAM=E-uv-cw%05CSZ;MEh1%yl?+JzRE56-C|E{F2Sw0K{I$! zFNa%?XGMJ>VSYtWzSW2myPMwbWG81A&OKotjl&D?kwwG1<8LmC;C z?A^%84?As&R2yFPfvI)dcX(1Go*QRQsCRLIw)cm~1jX^k21<5z9NmUTDtWj8y6`XH zI_nhW(q#XEB+i7Cz9(L5p93W}yRd5J$YyW(SbEH3q{ZF}BfgJ7%cD^wF?=^s>EEc{ zGiO0$6o)^B{1pu^?TavdBAi6A@6vg^=3x+JH|pTyNdlj zfXhdaXl^icdOyKwlO*8@%PFfgiDF8_Q=TK`spPg>RTxj$WU|U4ENb;999Pk)LxZ56 zsYH4Pd0+y9hS)0?ZQUy^-eiQlrlo?cSu8?CBCarg)5Damfhu;Dgf7_&vG5;rOryJ-_ zJ5dZ|x2Xn98B215Sh6I}LkRIPerjeQUliYtv3FbYbQ7X=d!}sP`vnL{P0g6x*>JX( zG8j4Pug!^Z3cGH(X~Bq-%&WuRXzB@8QpymLWc(b)<)cLXtxb{X7;nqK4^`C+5@x}M z-fzGdkG%?vOG`(;u0cdf`^sk8g4o5*g!V`S?ez?r{LC!)-e~y^6#L=rdV429>JHT5 ztqGO~$yq$=(SEh75T%KI4&K`+0gOD`@G_Qc`w0>ih)OZE&Ce38K>>%O{NFQ|6^|T0 zmPg%de97-9S8ZJvKmO48lN?(+)(vmXwq;_HaBsSdKoovFb=J)JTEYrqyWC;9{h!dQKFz^&kj zCXLMR5Q4-pW3Fc);C!)|=23nT1A`xVkr~fGpNGWbTK3p8E1+|6DEc=6!u95=yA?mHono`DC3~ zdhmqSqa7QvVYGO!lx8Y4!5nUnp%Su`ZOXdWNGORi;(M)@AjPHpn3!PZHMK>hjiZpIiA&}eUPvKgL0UE!+ zh0@0D^!>?C>6TMoXcyg2#F6*vi|1{uy~&La1?n-#<)~!Aj)o-*`&B3iwLdr^tmcD- z)3C>()F?wMF27I^{}vm1{cx!!O;*5<5M1M&-S=@c6ClaIb+{vMI%4R~8?JC&j!Ctz z+{-F)reiftWl4#|P+a3U6+ zNy*+b{;S5C-OBe!?Uj$MLrtrVOQBaMgM+2PujOP#Pl9hONolAf2i4nTl$hmuR7mdr ztx*9LjASGq@Aue0BOc|d6};x-xwKlZzx@}RdXpUEwt5wimPShEbBQ;a^OVu|NPR+2 z8G_mH@~VyeH!_tI9DzR4=y3jd19vTs)V@6gBg2?2gZCnS4J-PXVWJ_0+8fUFG4N)h z_bdLhk~*xL588z*Nq3drnlC4M=;jc{jB@~oN3>+JT3D!o;Uu(0V=I?DO--zFXgepxRkS;FaFT5GexL8P9H{e-QSc(ULA^R3 z^HHEJMB5hg6brAZkiUSI@u7Eg^YnY@gYTy4arH+ zTLYfNP)y!M+qO=^Z)S#o2vtk|x)6N4EjA@ru2%~KpTun}Bdf0e;96(BgM*Kmeqy_T zbM1@D2d8$9Bw-Yky8HawORM?B`O=H&t#c@=Te=}o&9~e+k@XfH9y$k8ryr} zIExaoLLP%SwxgHYm|d_PmG!sYv~f&py6F(?yNG`w#m_D3ATa`7R&p`LB+m3%@>;S` zUW_)pqbuRpO+)1H(Pa>>bK$QeqRu1HMak8ClZMt%Nr}xvHM4u#M12{-``Zls^;fa^ zI_Nxz#+bcANn2DiWo+vgpgB(R#-lkna}PrZF6Kg%G8Z;6Bz?m3gVF3xqGk}?Tb@U} znrkn4yWIL81&j6}&%ZO0sbn22PZ6capsszWkjAycwg<3R5nQ(mLSDlJVl&&N^VJ2K zFoHrB5D6cpD{p$;#JfLy?6;`TMp5sC1J#AJ^u9TqwS=k*%kK{N*-7P;uy)028X#xi z)#6LIZlG?HEwvE@^?#n%jxopl?B;%Jj22C_(-VvLO2~G~d3<75GBSSnlSmi^A!?ox zEYdVnVhr~05mV@R16p%-N4Tf~tJ7-0XbOf2SB)v%l1O*CdxyA@Stcp7Z`2eKtq>)X z^EK$3cpR-e3z}=5J1Rg*3+T_YS$ceTabX>(@QC6v2t|z-WGG;Z;Y~{$z4cs9XL^s< zcV+_N0~^NGF4SGi=Ufq0L9or{p68qcS`Qwy_?p=tn3rSKy$)%f)}Wqn$QIyWL7>YE z`5q7y8Tm@UmEAtx(04Hp@Y3~+M%T1KkAxk5*4_IVI_o(8Lj%$;z=+Nk^>v_tlF}a* zSKM!n0W>;S-dpYo^j(uZ|KE~vUK@k)wYogf^*-&ZohP+YE+-%rX3OvVibgciz=Nq3L#UY zOh=L!>rueR_X1t7G_D=6M)41gE5FLiTQ!etEliv3FU6XiS2B`8RU-T<8zUAp0ax13 zx0ok(>PCi}dYKun9PO78{$|DMhWlM$9jznhGOD`tnEnveAVe+OYyBLy;TM8luPr2v zB_#u0x1kU`ty-(YvHTMX{U+D_0!!eU zan&~AhLjG0z_J0VC|k-;m<=ZcHkWtN10Mcxya#@u zH#v{RQJzc(VeGZ5uhY|Mgt$3>WL+c+Q_LCG(dt_87?KZRA;Uwx?$6Qdw^C>8`t_lg znyg$S26DQb20)uHmq&pdk6J1>? z-R|=C^R1++4l4Ua9l4!v8^2!H06EMBr`K-@!3w17o}Z^zch$r+`2AI7bG#Eh^ryd;ES$e#_?6O zhz%((s=U|qaMEYygQ(&x1sN3_E<@?QxDFe%-;=&EjXB}%`0E!r!BSAFJp4SOyd9Pt z3oMRn*D*20G3NS2mP@ir+{%mYlfjO%62zu_>62n7-Q5~sMu|8Ymw=wIk?^_o%IZ=l zob}HyB{XXzgPrWIE7FGZS+h(VrzZ1}jdn0lC^Aic?lrc^mM$PcaI`$%zBIHwMtNJu z5gWxhQ66+|o5~SZVW2KdWvLzcB!=ChYS5KLIm?Ll(Mn$Y1)2D&0GCQK`!n=MC}XB*ABr% zuM+O^9G(nx-*k0bb0{Ae^@4^C*U(l~f~u4;-@}Z@ZTOpxoKnnR+Fm~|uupH;F)(yL z9LEuJ`o=G_Ig2}QRBnD?(O)2!zwW2=$ztyrG>*|-a?3?kEgpmn2siK4cG4MsLCc0j z9U4OXV58*co~M!RAU7R!871BT5q~BppY`+h^+Xgz8_HkIEfAq~)uo_DmE%cP%!L?rq%6+5@bBbN&^C$48UbKv5@n z?i@!OD4@SzQ0%SKW?HHAB#zTYjaPPk&8M2K9U+2@SYKH^KjodLXjWufS6jlyesknx z6w+s^B|>XP+1_*WbLwD0xs3FvPBgPL49-t`6HCBccj*0VreIdns4@ieU!)`%pQ$gc`F}$_eVsii) z^#R;m8(@5IJZ#o9HhjjumR|?h-EJ?ZbykB2oT+)jfsMAa$X zMI6_;71tG7PD7EI+Bj`GNkLQ~jYVg8u2mD(jpxXnLz;w;D{Bt z^>TqO96fzu*AO)89Bg1-!Ll(YJbx5z>c#+V6Vm@Yi`QFL0dhe=oHZ5{&|X5FiCZ1$ zLmuK18SN{h8cFXpd{bxo^mFa^SufC1nD+mU4lfUi9PXHect&~(8Hc~}-1F-G{r|`w4?xEf%vHLhbznMF@dauRzjh4d%E?+E& zB|QBaI8mew|LDth1OvMa}iGR~9K#Q^e=^uxaDf$5Z3 zk9iobK@7YpGNZ})1DVm!P{}g=b~WQG;|H%k2BE4rI57m>gwRqST$Ard!GU_=&w1>~ z5442@ig@OHt{mN&KcmNKtX1@H8gonalVpIaJ-335*r|qfOy13QTn|3B(WIz-vaBEW zma?~jXd3g8{e2-TXiqqX<3C$sy0c==<{Dh#C|gjvL2?h=*EZLvMb)6*j#+3Rm4(sO zH8r14SY>*W0nYLcYG}v3SgD;~AI&HGU@7n5tA_?tuBsQFRH~jvR&d?>NCz*d#cq#| zYl$+R*8KC6C1AAA=h`;(l4-nJI5kOdq_EYvJx6K{2yCwx8(}~c#L3K<3qFE8{o*Xz(WY z?a27e#*qWz7Z44q#}VpFB3pqKi_MA~A}Zfq`f}l-Xb|iSpGvhSqCnL`C#^F~H~*nn(p<>y z7C?ax&=HswIJkeq*#9K**z@`L}VmE)#p z1eu#j2$(*2;Ag5t%6gELz?T)d;mH$&N%fUe-v3}Yw~b}pQkN$}Wx{-8x>ulp0tLNd zwB6uxw2qwS6Xh7KzdHFsM}m3AQ?~Q9-0+CZR&@*)?>$0)q~mIFFFlAF*1Kdjs8*QQxxq}RuQKhhRvg&yIWSUx!VpvCySFt+NfZ*C2+k{ z33=*RRZ`hDP4OLa#5eGOspa+!8Zix8uP`1_`7uIwOX2&Lw(bmaSCz&Hu@hf-G3ZKH zbTe8#Og(;pn$rAOtW8!Z;3ms%)MS=FT(PX_)0L_(z>mX<5o0A z0+-3CW=$^2+Y#F8h@7>wdniRDF|{&T7Nh1W`?e2zUIEAXDgT=)pH^HGxlNh=IjWl~ z6cGY+UL#s=htmk-*txKxja5ZKU` z6LMD%HU$MB%b2*zg?TXW91P3dU}b(4xDr0ax??h$ifvm4uoi5@G9{h!uQoc?spL1< zWkaMh)Q!^8HI0Kdc}$6^Dc~X3{=QCK<$-th(rc=Rb*FP}93O*$L{$6pCt^(uCtJtP zkW&U8A%))iLf!6Lq2O0`*5wb&8fzxN4Zx33$R_5kVTMj)neG!uJ2J6V$c~a;dO>)Z z5d#DG-wJC}ksSMreslTw_7#WA{l%q}6D*P19vu z{QGJ78=ao5QZmYNlgdgvt_`)mxxKrSmSwHl_%QSxWqzwELYc0!f*+Qx;vb(#Cx%>0wK z!x(#dMaf9>Y{(HK-T;))e*>{+tHFO6-i^vn{|oKxmm;7&RYb7)nqdv6bhM?78~%h& znhNVAc2k0B8fPWUhIc*XY+g5g#7Y9eDQy5Adp#Mb#gMcwKn$S?0J=kg?ih;*LB1G;|Pv)Dn)Vx<|9J=D*py(|I?>PD9rkQLv;W2 zG`tn=e``Dcd&Z_d*-aU!M?7((E>K-w$Knz$M45g!A>{_;`Z+T zC-sZDjdg2g9rPLTNngVcUb4oQ#wJo)3|kDW$LK!XERqy6b!0mwRs7GN?OjH@`-8ia zKRbUWWPA7&|B{fVl5DQKVptl@GpK>Go2!VJfza)eetftlDoFSpzXCRLHm(%k>KES6{$>$ms~{sj?^`Q-V-NA zGuZF@QDD}p>w5&xGFR9rJf9Gi z?;>i~IA}{wUl&yP6UG=%pJ6l!osycW{k! z3f&`2ue+x`{=F*~|LRI-;S_)GiDFNS3*PsmKgIC#4zK)QPfS2O_`he7m2KD z(VqdEWYVdv;Vx>94&p9Wj_%kHCHA?JAA9xx-NClppIQlFZUDwLF8{eD|JpW2gZ_cO ziN*S*-p7rd&FK1lUOZY9iGEd?(JBl}{Frq9-n7wa(#*vC{Y4#jWUyhzw^+CVr_Kj3 z1SxCetGwQMnA6Y(47Hxe`v7)y0YfiC5rD>j@5{e#3k#%h%Bi>aF@Wlm(|FN{xYo}W z1_m|yklkHp-X{qA?$w#hV1aRso(3y(>PRYZ@w>U-x;J2l%znN>pMQb%VGi}nx~Tw> zX4d^&YTCcO5KGz!l;YZ~*t3=Tsxx(`osT?khcz0Q)L7^!=M*JgENIoP)hN}oR*wSi z1>Qy<`eBLNFecy*^B?9^K*l*m4~l4q?d(!F<5Ig-W#`Y8F%07dB+Z$d4Wxh7H6u>Q zr)*9wO;LE{k{)aZxC}npJRf=k2{R`)C1WmKtzGuwq_)2;o)n0Twk1rBY1P-RgkG~0AC@s8#pLgU)*?aRl8gTd5KH! zK3K&HPr)ikDHpWH6U!QP8LoX$coAzj{=z8iiHZy%7x&H$_ZyU_5&Ve>F;874;1K=; zSpL%OkR^@6=td6hhu>-YTDihm6=?$27p)t%15PZhT;+lzdYM&~ZT*<3DngLbm>5YJ z`$5eIl`v~5a*KQUDa)&!WLy4Td(&+hW0(BP&0;~xI^PlLOiVZCz*D@3MuAcdp=jqF zJ4&v4?i`Qb!c6*}q%d&b3dO_1YJ7W!ntXv=vYK^Zd$!6C4{0xE4oJVdnVqY$A=4rq zJtJj)ARti2OC#0W@c;!4#C$V)VD&z-xyh3oah8?Da&ci-)=OrV@#uPmHI;*XVFHD~GIk8kIU{m`q0?BEU^QMBlBv9g}>zroHuBcxwF(KKWweh)ORAhIB zvytR-b5Dp1wlNf=ea2S5q(ARo5X82b6IA0=>qjY#2G&IgSTpxS%y3IbspaN*&$dmN zbm|Da6wPZ96mMtHKDPQ|>|PbfL!7!odhv&KE(xw_4TF{W6`e1=ZRr;@f8mQ-=5~&L zk9rM0guh+51)<#+C@(_c&vg)#+EO>lvtGTRQlZbUUbp}14yU?k&7Up!(pma+Q8U`Q z^gQx_Ea}c^*kB%~@A-m$N!q_(-tL->^E#vW{MYce$9D~Ex&t{{D^$0miMfAnpcEiM zBuX=ySn%O>iba_j6b{@E7@6==!q2d+$ND?uyzNeJK)NVMBeyf;2kFOz;p4SinnvCY zrhmNGKMo9cgCp-uptX=y;CE$-93`Ui&_EQuSfOGoS9fu@iN<-CAtC^%)U0MFXnmfr z`m$FKD`qF;_QrfLS$SrOijh%Pr|91WR&B4Ay3Je#uCG=BHi+(`5l;`SYeSr!1li#= zFeAH$JvwZzOg0DcZF+GE<)lFend}`=@BNg@G0`%)xhNJWUCSJ)1)VBu+7ygE%*ROe zrhqD{qHhbs?cVjP7|kVnTlSG>4=V01IAuHyE6w^WMeX}rsZYOP z5^7<~O?|F^yg+3$3`|)1!f4cp#I79A@Hx&ZEwEvb4QmDU_jBTZD1pXhDxvJl)aEHy z-bCqJG~sKC*_Jeba4c2X-{vEJVpwACV9r3XmqY#L+NP{pXr62HE} zJ3i%Np35u&Y%E{RXkC>N*@XKmR}=L+n4-*lsO>&Mv*hu2 z7if|%iM-5$DI;gB2jF^3J9cc^4sJe{bO^dLgGxq1d;d6fn%+!FZOx3K-#1}fi~qII zq;k6>7jzZqE`2}YYG!isIHJ_CFmP{w8Gl@c&C}rwcJ=!xD(dMJu_Gn_Q z_jxXC%-zp7`KoNX-0)1;uFDOsu*y#xOilLp=jJDW`VVIT#P-^`4DBP}Uf_qfrsJ4|2q)UAbYS^aqzQFS_ zs@|f4`1AK@7_k#IwI*QwjWgTK%A18SqstXNVr^?Cv?+X3}+W&f`x z!{!*V4{lt>03+Y|(Zfs8p5;J&Px)jHj{_~AjQM&xkgYR}0t(Z1!lb7FR{fP0Pr&r6 zi2mRL8q>D6u3XVtCAodOdJK`b)-W!?5t_qW0zh&^pUY&ABshNImO91(-H_^}Ld(hc z%S@w4w)68$j`;aPQen?P41b%i|5feBSB=4_MDYWPX4SR$_TT_IEVB<}ECYTZjguf6vt|D+529!VpIH$XI~-@KfME2;rKJKW?UNo@uf{rtARdFRd;l!+V?7BvZ~$qYGZI8?2e#Hg!=JN z)fBx^5W79~k}r&{Y?Hj4sj3o*3UvfQYEFkNxN1JUIz#0fY@>sphZR%l;uP@r{fd?F zHeRq4gbF0~9M13ji|s@1rO-za^RrlC6jj)>lvBw&)xG{Yocff91Bu!nb9cVA-bhrc z{3Z{sol}L}K7;D_N%>ljSNO16Al2}PSqcVaBBQ&7fphbRz}|!spa;7{C3iR8u(%d5 zg)8+^bO{P;(^wPoH_~p30z96o_d^ZaSpct?VjOWb#Iih*0sH}d6;e=~jk+;mynoD? zhxYPse`mibX3ea1lO(rVwf%fsO;BD*AG3{HxW8jV6%GAcF$gP4$AsEq!O!u1%6GKU zjvX^O+L($5qlqM{ajP-%EW)*w^jibWdi&g;MRADALXuM-kiu8?x-z=?=EG!-w-f?M z=uBMgiNtgzRShwo(b=4u;YUljge-|?DfqEU_UKH?U(I)Shfhy{;Hj{HTNr-omO(3u zzh*VI5w~X9CfY#xYh$RgIgoYUd~@E2$LHQjjON?Bpv>@nK=&`)sL%43yx^;Bc*!py zOi}Hxi(eL#wdXm~&xNEtHg|lnoBm$?DC3^28ZLL>Lzjpk}Ug?#wZS z=A53DZ(5Ct-^>_lE!sVecCo*DAo2F5mM7}8db;wC?q3?_eTpZ@ex*h)KD|u?>Xx)` zqhq8Pj`a14x*o|v9n4>G-D!paA=~YIfiI5ybZfTtpyFvWjLaeW5ey^lZ-#I7-mMf{1E$;L7R(_`@zRCK`7rFBnHcW~C0e0^c= z#qJ$S+`YOQX?UCVz((N{gP2f?eqsB3$E=z9f-@fUB(@#sdM(ZA$;w3)v?F?6GeQ^l ziiuf@tr#v@H zKg|9X54I6wOml&n}1P*ra^l%@LFxmf(=T!?$8-+UCSj?Wxsya1*wr10T%S(^s6*nv6+#l*%{iYgPH zw?}pQlv0;;ppE|1XT9WoGIN1K$-FE7fnrC250*6HLj5P`%yT`zim_^L_0Yi**Zz;d zcrH9&Iis62sQheoXs%BuQF#qe02-*`^Ya`(kp>`vNm@Jqi*^5i1RPPVHhshcPo6gC zEy!Cw3o}fPXZ6AI4imhkp5T0+$21|+lSf#5TrZ&VH$I_a6 z3G=*PjL1GNhH4=#5Ob&6PTb)U-zswzF<3o2{bQm$*1Cr1T|1BUlgL)EaKDTyF9PH1 zP9;6YH9p#C*SKmn#=H#1)^pI3uyUrVxI`R$!2e;QtdUlURT`Dv|LJ50c6ff%;7ld1 zpVBb%9<|pg8}X`{i+yPG*$jTeRGBSZId}C-8Z7moPUJhN)o1cWk%r2h&_o?Rye`E^ zwkGv2PlptAw|2M`m0}PFW76_cSP)b6BG8R>v}X!DcAELYqYZpJ}M0I_tyrm|1fo zKX^sc|8Y{LIduwcs^7g@r99d){x5NY`NN(6FkP-71@oskVU!7CBr&D;Fd=LAA*TIz z7Xq$$X95u~J1dh(FB473!yWNte{x+cKKg_r&pmdc_V!~`%4m6=+T2e4=Zef>UY0MkN*6(T%Loagj_>Zy|_@~KpO7U95zbVx9#8>H*O6@y_1yl@PPcpT$Mh|6Q zev+?a_f|w=f;u?u+~Bug!!(Rn5`#+9#=d?QQ6W+1V(kaxE4GIRuc~hbL;pte^NmZF zj7o^l2h_4FS8+v+Yd8;6#s#$v{(v|zJTHfpIDQR}{<5C4MOy5h-2BnB`?os>4NnCyw)SMNhmbxFn~i}HHD{yHZ-i27^W%)`Nw7Tix! z;EUe)5l(|<{u1o$3D|yThk{Yna)z$PCV}5nJ)DDZNCShha#JgrDl;|H%|!kX3||C% z+a@IW1=UI1J_LkaRUzs_T+QPQ z$EL?KB#arp+EeSMVnx#6-9qD%|CJw$jqXpue(|{89>5vf0okLl$5il4O2CvjKlP$I z=K*ArOje@`O&TM^T^RWBsfE@Wq_ANJj#l}Ow|3O1*kgvtZmmL-%vk&cNoXbAo7~2D zd~U^H6y^U-)VFm8?r}I!rnm(`%1e?o6J|SI+MXkJ;!9g z#H~4}Qlt4Ak@7fR04SYeUSuZrH_gAHr~yiPu#Y#kO2a*(MsB5~jZ+LjbEv&OX!C6- zJBT=PJRt46bLNJK#a`A=UByCdg8I|Uv*>R(<{R*_0HVK3*LS(N;SeXyeDClXW~4-) z-rV9ooY;x6ljlvp;bs#F7`vWt8s)peaXmC1QaBplZP=-OW7Tl*Q$+#B+XlY6ebd%{ zIVs7%in|6|U;NYr!!Jl^Xo$;gJQ2>#qp~??$j%5k;z6EJwBg%pwVF(=0XIcdU_Zf zJ@$3o-u&$$tT@ZP8_eSfbWvZED~_?6-uu-Zsj*d`hTzij&~WZ@?;r19^$nxmbVfY7 zPHvceRpJ?^g1LY_L#P8Z-84o4={S7LcdR!!Pqau>f@?27{By~8Ob>$frkJd*^sqiu z0EpN3=mzbSF)wEzB(uiTG~~B5wL^jqQFh0at(HQ48(F#=&c(S3dR|+~o$cG)hYKF8 z2Tb%VD(B0i;qH5H6Ar%K-dF#0V7)PgN~zH6ap<^~(cONF2AFPHeXVhz#btk%5sYQl zeeQZSA_#+msS^U}rgK+t$fRJhc{tHl8qA}Ljo;7Y8|ige0c)6FSZ`#nxZ~D2T({CF zn-0;Zl;dDbFVh!P!u-$AeP5IGA-`3&)n5op2I`_@#<`!z4V>vK2tzkYH?D1A@n4`6Wp`~K^1~%SkvNEGYFy*(E`|Rj| zNqBcx7^|Po+)U#Tu|;TvLR(iAsM=~_YSpT#wDWy-{9qaXP)@zoy4up*u;Sp}bk|}B zf|HRUW~GVIzm)!_eks$#h!b1-)p(B~o%2kYX9T&Aj+7cXwxQNonK&DgSOU*UjqFz?tld)e zF1u2p4~}UIRV%g#G5mpemi7_=G9s~dgqtc=2m~1|wwmj66nvr(G{Y5X1 zVV9DBC;G?Gpc*&u2YjlY^BrvEgk~VWN!hSC?8v-harnYQZ#mYf3l3+&lm8U8e!wFu zk9H8mYjX;opxBSnRP&@i@HT{U$~z$M#cQyPSup@ZlH=* zODVR+*!vLL)Vr;@&-$t?UQ5yG1{O@_5Ht_We%{KS_>lXJKYE5df(n}&S0P#>zntl) z|L-2xiK4rNoH^zmBxSJ2?IBHz&2VulvKXW+x)^@9>zm^TKx}QI9Pi1_Ns&m|G^exY zhb0}ac2Kd^XWT`F*LsNUsrid0 z+V?>rT9UH9m_yFQG27SYtVS>FS4ortuJj;+6?yAU>b34G?b*O&PPQ1p@`~}grC~_Y z!2QI=P207M^M6GDHqr0NQQQ!oBL;{v~yLPzXPsAX1K=&`!cIX`ksds5*>i`npAIANMf zUpPOzg+;r`r`t|!vHs0=-TF+UwAh--v4dPRYs|Rz%*+Q5XgdS^P7HvcQA+vmsV-;5Ey|a-)V%`hA5{|3M3_U}l#1Nub zMBj|%*dsY~!o*zli~o>#pTC0T;UM*~tQD|<=^%bi-wL0sj`-?z%Mj%*txv1Snc>9# z9PMup6gKzq-wxXuSxAW^=n}Fk=)hc9vU{nPNI0XfN6oF0PonkPN)m0bJL+AHj{J9} z!jsDz{^hYj(wp}cxh8nSFJD6Tyit;wuRi+HSLkR0UoL)KxLc-@5qk5Y|7u@}GJP?V zg?1ZTdD$q8$J2T?fKbJCL^+{($GiF7Rjo_zrT{4{*__7UJB_Z>M)&d`2iWyiqdaOY zH&4}F1$cO42I`Nh>|6CKzX&D!ZO99r?!J<6^Kcs)N<<^-AI+@T{Djtn44j3fsxnZ0 zDN2Kl*5dOK8j&banMFy?^H>A_s;lGB5L%CxT|4(WvhJ})=_U`>uSzCKBKV z>0`bGu5(bq%)*EsD^k-wdZ_1H^k^<0CPHteV&6B=|A#gAAh;rmk)c$0d4w<61IxrA z|CgKu7mItg0WUAfx-w!p37)Y5oj}#9Mt`Mj;CTDkXF1(k46H!Tn!Tg>xxId$u%U+= z-zg$9-7(;+Sa8SKVG$)Fz%S!*4CPpQj7Fz@;-AVVU_JEB&%}>xUg@D$z}Ga1xMn`M zH@{@FGAjA+D12~6#KS!`-y#>kch|BZu4^fE&`zl%-q|nts;n_OVAv?av=1Rj!O3Tx z9-wBipD zE&pbsrjG{Cv0%HJZ%QLt^>J{lPF&7=>JMjoBg$^Cc?3#jLfCtP;n(Q`3gHpLh3sQvkAUi`z^10-lt!BsrR}b zzyO~giMHkU`8NDR`)`+)ljn^pD-BxGvP5{O#t(Q@>{jTAX zg<+O`AbNELkHr!rz6@uo12_;zRkvB)BXGF)`8!q3*@g_VVF#X{r<&LhYHr^5I>pWV z$cv<0*7i?&PgmqJvHZIQn3Pm>u@V=cuUdTXrBu~e_@&H9g@VN+_O1=v90CAq zADIX?V6q~?v1BQ?izAyO6W`en_846^5XzD+gEAdqr;jXUEbn&OgQxRjgCgvfcHmyp z%h9n5^p}};^r8Le_c%7a+HOY)hwW@%S|o8Drp0>D>fz?`%OqQ|Jj!Dgp zYvmo#X_a3&{6_wXrt`&aNaLDf+j3|Zly_6rH`dH=bt|Z8dJrn%sA?sTGqsjV|KpR< z=Hz1JWl~#cIUV?~WVTy=(6b9Ng4a(h&UcOEH$4-gBSw!>6^ZBYYngTFUAl;-nV$&J zdakEZdn;yKK7v`0Xn5T0`UlxR8Iqvf534);IpF zLr)Gs*Zq*_pOEX(v@BhrCwW*OPB`;fqZQChGtuQS;z{kx`nmq9wBwpD5*~x~ZS_TF zK(DE%_Z>+TNDk9IO8QCXdV85c^lHmv6h?fNDP$&BfISV=9bNiJ5c?`@@WJRJeKYDH zd-_Q`bCCUQCzB%nd&F_i`D}!;Vp@iqrVRY6+YNNxR19{B?L;cpDctoj*VcoDp6vE* zVyJ=goaU0{AF20o{yT+NHXkcsGtuU^bweB5<8@Zps5nitY{Sdp`@q6n@$Y?fM z0&M^jLQj~;JF?tKt=iv|Dv7FjvnQ(8O<|)wXJq@fJ5^M=m<`OVA?Imb`kU2$=M+r# zboN92LxwlUeXNi6T~EU9>X8^qrP1($K7;vc%_=I$5U1o2qIsT#l&>(gU(l91W1?gt zH!;$$OljBdI6_elLtfs0(z#o0(0&*AhSl{aN1T;!+Q%guQe?5)5_0L}dox{Wg+j>l9*g&BKx6SO}a;(UW3}0X2)NHPfJde&DSW0kN0)i*wd6(8e6^$N2p%2uyH6l zpYgs?KW}#ujMRP%us9Uv(NDVT4?OHr{oLr&74L()x5VEZshCr8bg}PFBx^|{cYwI( zBEFe$?#g%&%2C83HaCu+WiNJye@1A>S`=Ug8w+4i~cgW0uU-B zR|J+S_~2;#m+84gpP>f4&<_qZdZXv8E)*zF`L_icabK)L^xL=TcoP%LG1|L>;Y6#p z&t3l?%Fa42s&x-fVcs|WBF*LfvmA}q+5HTA_x8Y;F}NlxjEoV9EgRxCO!I{J zbKWqV^eq8WV&UCuJS}s%7|(IeS~yavO0k&5ig_TRz8G}4%5z+4j6oMJrd8Y^(9LD4 z{hKo50kS&VDZ87~GM1T%$Efo+ahX9MnGeF-CA3DE!8G59ihnzH?$cTQvn9IgLb>cN znL>e{%O&96x9a6WJ-yQG4u&_F#sRzFHLGkq zHx__n3b)jPKz8fHAqvMSE*>=;a}?Q^4xq@u7qi9_A>so0#F!1CCfP*&!D)_#Mur2?hk~ggA)^q2H^t>2Dzy8jxSF$HD{$n6l7X955NQ7y;AOK z`_+O3AK!|{11gYw_w|L;qJax-L4EBbhRPDeowE@Ci>n!cu;2zOWE(=>f*5<+X-xTc z3>PREv$~ZuG;2R4#HgR*k5r$NMTZ5~_MBBd31X-_$$*vP&ZslfwUu~_(e74U=61nBBwG@q zQ;&T{&Gt#1g8ocPJ^l083DpP;l2bn-D@wkJ?E!QU)cmU8IjnL?j{Aw0U0A*HmD<#rHc@4TJGb7;zoYTIEknpD+5tI>6#X1cae7~P_$X7=LEg-QV@vC$K; zfx+LJT2L4F<$Pq)$TlOq(KfWqpg*12_~d%Xh6;WvyLxiH{HLFq-TQSaFK2!ManG8k z7U7tl^;|B6nxsBaC*)7dHFtV5p}%Jn>(L#5$mEhq|kI7_88(zGkqO zvrkV(;luJg^=yH4_KcYt<3sDA@id%zjpiJUMLFgrVRG>3*^b!M>UthdD+ttq=yMuK z!<#Y@`JoCrgJ-CWlH2j1w5Qx$08kGbTCLT5E&{1}VH!bNSz6lGGXVL|a}B1$L}NQw zegLUx^Q)HRDg4SEqbBv~{bD%N2KM2_;z@7G>-IKEQQO3!b=Wkvq)eLwyXhDjcf?4`FEH z=DX@&u5{Kcl&-x=^X4g0`wx6j6ZMBZCrz*dDt-Xa2Lp%MHuvh4Z5+rFZ-s2PG&1FpKp;AHqs5|FwETs_tx^wE%?ss5;vyS~^hJW%w)+7_8L>x0D>5@bWzK4A6v6Td_o z$?Fjnwle+01IuYq1rAR?das}#T`xE3gAfjOKN{~Jy%Q1FG|_+P?^yq!zguj4xN;BM zh!{O1dyj_^KC5b$*EwzhQh)>xfw14P4D+i@X~4!*{FMYFtIv2FY;fWh)TAo-Kzx=g zk;7S~+qTf7+Z_M9hZ|aB4d6vd{;wJbym?y^g7QZRuR!lEy1-t=X{29u)?C!h>h6Zm z>95Thba1lH-(nDv8jGl4wQzz4cigPFWUW{3Sa()8N)Lh9W~12hZc0yiz7`nbvJJjDIjrJS8(mvN+R)BEl_ z;a(5JrB&6H-d6@H1EyWW%NFB(OY@@K)C$c7lN`@*$?N5Z5|`HB=G)VOwL6EkvY*VX zv2DcCf0|pDTIK?3z`Lv~vOP0cPMMn7NV9?y(BdOuVdi^jB|e;KE<6{RsIu+mTY_+( zPX?4TA(4^hyCPTnqZg7I*)f7Ac-$(GvV8C19i1d?7H^ROp1pe9BG=weE;!U5`z4|X z%swCP7~)mhn^x76R|-~2IzX(>GcQeK2s7qMxY7w%ZD?88b0n5Y`a*7!9F~{?6)bO7 zWfT+=wx5=5dJ+#+Zn=aIvCse-<600~m>KrU&*6Gr^fk#=LI^tv<8y zgyQi_IgNTjq(qxu5zN5zd|2cI@{jSIhs7Z=`?7>R!tin0D3G@jRrcC#x|e10I$f(`@lD@V);nZxrtMQHstmTouZCtkFDT z!E>Hu6`L+V=yDIDC5#5{krdcu+&Z%1l+T6-8!(!oh&AhD?5Y_&{E`wqHVCEla9LD{ z8b`q=`nz30TI8X+`S3r&_`psOPlUx_hsDFKtOIoaL&L98HdK%} zB*O0cY2W21oBGIXF;AmIerR;V{BV4SPv$1U*@O|9BwRIsUHX^p{@C?i>F3=#G5r~-r@>Wt4{skUNcd*KLOFcq zoSzEnNr#*QR8*4MI55J(!j^kmTU1OD9}xba&Lxz_4&00mrrCBU_qeGFZX2D8nIDuQ zf@psxX_zYMY~Kq@nPv|K@Xnmc#zSIg?k=_@MH~(iEw=2tt51(-<*#k2;CPC!!T2FP z7RDKzjAA~7kNiXUZ3i(}{N;W~VC2&ycxWIoAAZE*w#V61sK;(=yZK>JnFi7KGA$_k zz55=eJ-Pje8+_D?^(}$v&tC7Jlp34KlyqSJaLcJ;^!;7|^6RTO!GU(7YtwVw_Ls)8 zH$*Yp;qy55#F3C7ZzUU4h(IQUg+ov7N*;{*)(_pKIUi=3yRO>4en`;xN70x=|6R04 zZs6_Yu!heT)lF5V%Qd|OjZpo7`wAQj1Ifa)Y}^ygrylF0c8?*-yOuTx-Pcye_D8J+ zoqMsj4esXSzWYCX4`E(kyksU_pIZOC>Nh)K3=Xz@(C{fcwRhHTX}0F?4#qd*wZQ`~ zR$%KR7Bvubme7`BiSDypO;Rh4o#~2%x;d(x)LdW-=}v<_=HK#jheFZcjP*B4@QqYX zLN|h{1X}qDh%WN}%Nz4r zN7fu-1n@Chcy_newI9G)I7VEk7ivfH6#To%Nsr(Dbz|hj1No} zGB5sFCkwH^>U8*a5L;%9E`2EMfy{gYhS3v%Eot8={n z<6oX4BzD{!$ve2}PrE;MUN=2kLQ*+Hl#3trmHP8C`PcAWxBK$PPvI4^V#w71s%$@R z$hHT5mi*dE{ND_GnLon@g62oTAs?+fXed%Yg2!QgjJH)tyk2B}Kl=}lMh@j)W{@mH zleggcJ^z(>R zA7@eF5bV+%+(Qv^HJ|gGszyD%nbj$I`F=3XZ%v_66K#iPu>GJ}F~57(;S1#g1vxIK z`))%H9zc)hJx}>PmT|xKoyiyu6nX7ie1x9+2XpK07J{)!HrJ=PH!q*NH?JQb3aVFd z(l0J9bA_DZmvLkls)45|uGxJw?S73VDv#a!A%%nk_mPu*rT% zpyNn_UtTvfjEm187GI-gM%O%zj>JniHr?>5XbPi;;qBMcp=x0EDI4)Wn0%}m>a`Xb znKWs{f`$AB6!utQZ=kL?ak9en*-;8$8G!P*Ws;3#7jufnO?np?FY@ayDoAx^6xE3E z0h;|H>TU&g1=atUZJ)CT$Yd z&P5tYa$)opvy;xS8Rtg^j3t)axOSko+}gez)&vYC6aJUcRX)f24xC-=I;i64=Rvc4 z8IQPHW;V0J}~co=?)sRv_u1`08OHr9e8hH;yW)T zn;DtQ19C7oQtX93)2pE_3&RD6^1)^$F!TMFo;7?AOLn3}Et%Xl>GIwW;w?X;9)m`w zjoSX~m^UUu@cky&zw!Mfz$(f1KFyO&?)yi#lB$Ty^tM+r!yIfnj^kw@brz0;pm0K! zxGlpBrP0SZrO~RDFTcYTNM0dNz+lKO5 z8>qSdTCISje@v@hcqqC(X63k?wuCmQX(5HCK2wjz2vWuJjK*b?w}OUZ%Y<<@GTS_w zbN(qsj9)$?9Fy-Cl)tkzCU+7Db9U`fy^hRKc^J@1Rd`cX>%El|rgwbV-F3|*#WX#< zf9)4i*Vl;09tckV%qT6h`D%j<1Ie9Wy53=>vC+2xA$OT8*FxZQ0s3yn`g`&Mhx>=l zgN(+B)KFdsI`{ez1sQKO$|9>t;p@?YhW<+9873x{p9vdb{_R2pjcU|>o zL+l)aeU++d)ssOtT6xGHXg}d=locZvRgz$5O@GsJ;d`OZT?m1nPk`m6?I-DfBmArW zM)(7qAVKWsZyTyw^?lv1`o=$$7_Ww_@1F7LKzy<+#LpFGWWEDd^<}~GDAxT-X$jy9 zE{`LCvdp}R0pMnTgww9xnk``>lZ#E@?(0xnl3};NP#YzQimr<*#CcEnVRQ4llu!nfH|0YjseO3sGd-HJIBn zfOT35UnpuU#-L%SraEQi+ANq^&PC|@&!;io)v5q&?6vkEQK! z8rwFgz|lM=h=O$ zG<|!1^8$c?y>Cl+4@*H|R9|H>2whdmZuKkN<-_|noGb8n1CI!uRTj?*F$+K2@kdyI zhn|$eV_75ofbPOLD^>wAEfqc6XC@WSkyJMQtPK3Bw3e_o+;kW?a_iAD=MHgRHrJ_x z>u5-uK7z)!F-}pgS`c`-2JZ_j+)n%8{@0^Q9%_w8@o>kmd_V}Wx~w02W-wu-wXY!B zY;awHVe@ICU1dhYHx>6WFPb~t8U%D-Hs2|mc)=*mpc(tQ{4z@lR*(bAv}^IVQh!K& zoOQW#1XEEhM?apwx2(I@1C%l5dZeYVD<)0HT+xL>)}OqGPGzXqz_!8byN`}Z3N

    *lN9uChWkyG=R-4{D%89j2iGgDw|u_rfqOFh5zf|s{WPAd=Bv`>&2&FrykM-* z*Pm@`CGgXs5PI?EtzSy{LWF~;fL}>$+!Q*3O|}UnVjo^YHxX`U^liINXmtH(<*V6Z zV54nU@JX@DC(F908ttjT0U+CpbMrDVd{e1JbiffETC?t>uX8?Jj(==Gn&fi|H^mlx zBA3I%!%{)0_|f}5buZ^hVbkRdjFwj8Gu1O}JxuG6Vd_8-;pN%kd1g-q;zg}Ed!VnX zan4d(Pt~Ow?z($2Ru!+WB;%LjoW1qx7NHZK@Kq$&RRmtn-?g+bh~=WH>OLzI^$m=r zLS*iaxNhe+8*l;&(7#;LOyEJ;5D5R{J%pN%krCKnZ8V@Y%2ieH-KY5TOmh~iOxdr2 z`tv+Y3MWC!pR=)Goy5DJDLhSF%~DPd&k7$Bx~|-}1h`zB%|Fu0LTE8x8vyv(;aeVN zX9%to+X59dBNzr2mim6Jajs1X4|&tQw(BeV50p1~jU_ccE5L-StKHv00ZI?E&TH)3 zyJ`cdh2@gZS>Tcik*V~f;>|lkEuf$V4%^T6+7bQYZF;9##>qRTFI&5IUjO-QZ*5^V z(KZ&u{$sslCDAtgw~jp4?QzAk0ow<2g&Q7mQ!Fl9K9O}%9)R3q7u#H6GBR8H_ zs6?6Jz_#dXKyoh81<*f*jW5q{J%DcM%6;3HN*@bD_^i^M1ap*ABsP~)GnK!Fr@mgK z3O%_O)8UX@XZr1EQJ~8`I6J99L0?rzAl)G{JA29~;+|`d5OKGAT1eh?#ikL7Uz({t z@?jdHVrO@ytlq+UJ>Lf^i8iurTPOKGQp)G-gL=z9=dyrPVh`E-?X>okv+bzxk?Pqw zyia>Hky>PHvXAx+u;s7X-N9KR0bUnRiG;4HAN66JI$KP)4^;bJGg(VhB{eRjhW{;K zPk8)PYB{zp+Hx6B6o>ERl^4{Ud_q)F-u|m?KG0;RPb))e)DS?Tb9`LdKE*vw(7X=Q;Vo9y!w>MV*)ivf?v0?5}F%Es82 ziKCX&;4=Ga&q(tf)ttX3R+6SZp-%?R(uE+d_AzAfAobnn11QRp^2$G``}wF`9yLNY ziB*t$UI_>uWI_l$M5>dmW2E$Hg32Jm(jFYnbfSpX=;XZ}U`;0K+x~Pd zh{JfRgIOaTb)SuCKSY!L`9-Q@cdXFig8Icfd*$*=;cT#Nh&ahY_I2W7T%NZTlMUL+ zdK3e0Yn2Hw9)hC>`e>}_A;eE6dFjnF%C`VYDW9VApFEW>@;&oo&orBC$g~FdnoU+FUz20yMs-L4mdH&eVd1wX>mGF5 zFQ?(PoV@W}n$@eI;N+E})1zXki%3U)9NdBO{ZaBXfX_lAu+^Qkr!7{E(KogMJghX7 z%N)f}vyD+>oje{m4jLI1-{@2Wkb`pOT#~9BDmW`DUmxh(nRNx0F%{5|S%6=! zDpcg8A*NA-&W+65N~u}(-~Z(TJ05SnPB(;9?T}(mN{Chj!h;zkdDq@uy{mwcP4g*H z`deT6=!n8xEjd!340xYGV9SaB{;AXdLyQOg!VAh`f{-jCan{6vg^zDa>deS)RRTD9$R(_=7~gjTacw)M=y+&tvGf#m?Zhv zJo`(Mf6-9I5~*grn7+fP6z~zk<-1|ik`98!pmNl*F2EN&s z_?oEq<#rO|6t*<94o>xT8#Ll{!(xYq2)d3YL;>C%85BCZjxjBK>vNvs@2c<$j5+sY zSZH12TlQF0b_#5%mG;-*6eM`FtN4R5n?79U)yb>u-x6@yZ=&*Pst8;CmM;$85P#q9 ze>C8_MNg{BN^k7Kf#u`|v&_-$?T`a2P9lZQWE&&*?PBBev$FN3Xgm^Q5zyDaaP@`L z2GDrkkLa_c=xVa7ZViTb=(Db@geir_dw}$C(wBQoop!^43gWTNe+jh(HRY4tJ9RLB zj%>6<#3)7jQ5p?5laOBhh{l`H8i<#UK%^Z3*ID8oO4x3!#h^D)LU$^ujP}dhjDQdJ zL|jR3=Y)$=$I;u_5Ka&L4f&xKLNbskhloS>z%8SeHr$Qr4_#-k9#>KVJKLms+J*5C zrqJTj@&;I7tnPA4S_9MoYT0WmIC<-GW6KDC><3DUj?5GrMnSDpwa}3oH35#t?GlxI zhH$>~iyCHfpP`f4Tb0rdOpg^i9@qBTW)oWwo-zc!I!5A-18ucQ7@4@Sg2c;4^lNtH z-hfcSjZwus_Ep1op@k&TS1jHkY+ME>O9v2)Eq^w00`zZ;=PTj1W>!-oEcqP(Yj&{1~GRbEL#u_ME8Zc3M)} zC|Q-s8sCi7M5~~`M~}9c<3*-(?ITmHlIah-sNElK9V37^+pxIJ;)+IF>hYH{dly|b zhG~$zZ5()*^uQ*oBRa2Yc!O?Fla9VK{05AxXQ#WdF^WiZHpZjrqYEx+}p^U?vZZE5Qq?%An2Q zvyt#c^_6>gGqHyo?>p_`o-(d4Ro>j>9@n4c%tnO&%)G`tOYfL3Bm64x|K=>L9s*)~fxrvoN29lVf*gTB&0^C7p<^ z(xKau68~Q=0VZ%mx3h68MkvTGrWzQF1`cP9Uui9!KV84m*$m@JvG`&G60IoSebcht zf8VGGU$h`4!at29q;D)X3#Mk&HSdbtqkaPh3Y;dx+a%cM$7#DOcl?BUZ9IF{?GwPx zI08L;_KW(QZ=H;Ie#+>`Q=(^CAOipY0};P1b!?}^pxEI~_4$UWXK9!d2Zvlh_rB*1 ziSI4XrRRpgd!11G&p$e=&(LA7oj=C<6bzJ_hjCgmr`$Rv&@`JVj zjcnECFSPPjIcg~+w{kmY9`>#5=OaQ+TfWLbMdY#ATQn`&t5x*VXRo$*_Jl#ytM!p2u82Nmu9Ust9 z6vNgvqLl8Q^Q@S31EIbfy?1+)w$9diBH~ToWIWa}_C$1^aBf(D*>@4RfvX8C3=u>- zWH;Nr|7)Asj@uXSVgTW>U;JDHPsNUI-t`u~uim76P!Mm%7L(cS0a-Xtd)zM?W2TT| zbLhx!gu$`$GdOHMv2L;R9zKOzGiKX}Z_o=lrD{|ag2xVAs31q~U$mN8V#T-94`hEU z2p}a5X5uDRKlhaCkrz5M@PTrXEy)K=m@L1A+Gx^=Fd?U=3g;;>xe}7;uzuV&x+>(| zSr_NyfFGsE`vN$L95az6Ojj6I zk$Xv67t3auat%Zbl9>Q)8u z#VK#|i#-hJtiGn@U*qy!CfvW2ser~`|c;s(5Va@%za&{=DPFJ z#$LnP%RlPPvT@`CoHj({~ zgYZJt@5!L?S7C7uKpf;nn(FqtFQk#plfz_Thpf^(2Fb2=(`e@e`|hz!9(PHe<{Y;@ z$dF;i2H_a_#ITkectXhgD$_~xSnh0yRD(`?_WY0&ZjNI4e1-2f>Yb&UrsC`D@%ITS z20J|+yWY2PJyK|Qi0WI0GI3o&Gj-+H6Ttw1*`ZCiE2_h*eq3>jXW9T#e!Bc;&``QV z*w^@PQ<{7M&eC5H0eHg8$*uYcjtRv2~vrMxMV!LKDV*<-@xz`50nc|000f=y`g>w5S2To;A24&kQBc z&~S^XNr-b&D20m>m~?ggtZy}>ho86<)=EgwM(r}2Qpb%gUs6i%xAxiqC`Gk3gS2-` z?ubx5d#8)9y;3seR8u5ji6BdmY#Ev_!vN|gA*Xbv8g&y&3dKDyEZ0XFZRO?eD8iu&+qG=)YtQR<=?>77n;sA#{4~oxK4kPs4YF7 zr?My9z*fxW=`$p}Yh1Yzuxor6XcRj(Fq*O)$J7$e!_6}pxI=wt_eNpi94iUOM;|Sr zQohN5E9A4FcNh&@7gg*yyc(JnaG*Qb<|%PwIH3O)qkfb+yBbTqnB3J8?jaANWo|hm zggf9&cVPVzovzzr#A{?Pdiml;J(SdjGK>>ZPr%^u}xJ*I= zF$|`sl73!A<1kfdPKU^ttp|89LbZ4=NWDfLRcSLE<>dUzpG=`gs*Mr$bu=qBsG=)O z9MLqx=4upVGKH>pm6Z11RI^G&y5qIAnE|b)Bp+`#Vr+&1cL)u*9AGOr5KU@)8HXh5kRsPB5jufIF zwP`^)xaMi3uB0}e(dKF^trY(~NSL=Y2t}jYyty(ZKmnwaiutt{9^)h~s6;b%uVQ^L z_w;>>_Bb?*P$oUth=reX^itOhoZ+f|s(y-wl+6IljJWO9&so;c(= z1PK|PpmWA$L%pi*M!DZz&lZ^27th1arLX3O>7EYUcyeh*P+EC~b~el~#mff3Jk7vm zp;4z+U(pS}Js`wDI8<672;A4*KV7WxW;700JGR6qY{#ie5F}VLsFfYvfoaMR(lbfo z3pg=9pFyT1A(;MpO zZ%jNCg?9EE)1v?S`Z!~T-yx$R-p|n9wLE_{Ld{a$fQPVsfu1G3N-`oMl^bJ-XoVWi+_JsvE^*rU=dQ8&i@okIB0_Q{hL{xW9QQ{jvL)F)+ zmhZ3o%ExT)YKeNYc4kcRgk)=)D9o~94#=epx748dkas_87=#9+i}mXDLH2N#u0Q!g zB2YHMF%Y(d6}+gJfA)pi{iGSg6naExYI>*>GifJUw2{Y5F$IYW6|zL=y3pX7)V6DW z12Jv#0gs zI=J3Zg7Hr5I5-wfg?fiJ7)CPOvU0Nfo0%Pmp+C3V3z@N_Hk~ zj6(oT<+jZ?DYT8^U_(|T+lmn zXBEVz7iK~7q0cQum0x#nQ5kX*sSh7O!TH%~qu~w49`8F+xTYT-Mj86==53Prau$Cu zlC#}HVm-592H`7|ab4{e+|-;IzwlXQ3) zWBhzI5(-=^%W$irap#rZRV$y8BwC>PU}-^AsDh!`(3Ii#bRpqVYD1hBy2lz&&S=En z91Y%H6CdCb9_p4|GnZR4c;##~E=f7#j5W)WV4&1}0rLa-TW3dhp$e=sPDtc@bBvQbjVGra_-UnT4uZ7N0|c}2CM zE{Ay&hm6*n~c(~9fp_K=fHIjXrE~6ngPM85jN`1a? z#TyX{K0qc8o#A->x%h0~Rtt>*ZNVTkI@ut6vP-wVY<%%&IuvLS+0~nAtm@7dzVYRR zQ|_^d%KKe1(6Nf8&=SsjbU$~`eQ8M~zx1LXdyNOlOWv+D1R7&$(LYl137zN+q1kdS zB(ddjSvQce9SUzohM4|JG?a~mw3h*6W5LaAs})lRt$)Dn_yOVFqds_16eYpTEODWo z3eFZH;5Eh6$yx&XuoVmQwI{=iU)->c%D|;FKQ*EJI9id+(eR4g*T>eP6R?drQj7(G z3x_o2%)n{4`uxt(x9AKF`GyBp%Xd~RgZ*UzGZyVz9NhOzMl#|#a`Q$bG1?r&<;a#P z*pIK%t^iS%kMpS#o;+Lg%n_*;-XXZVEiOZ1D2^0w$sMbe|^&CVC!PJrH zgahN~%JlEjhLVTN+Rj8}DNzZd62S=sh&n>nD-ZDL9>O_rFq%t!+eG@|EHABkM1D?DmsB)+;hLM}FUUgHOo#%VGT!yuoE^TyRfI*nOp zG#jHTZO6e`sUQUOdf60g3z>}IC(qzGMcS@7J}K~0uCu5QPt3X036B?KFwrRkoa5vr z``VvpFfhEP%KPVB1WdmI`h2wJfbZt=Y%ZMCw@0Qh(80q8&gm?G-lbKy$L9y;9#B?L z2Xn-CQj)`!Y?ZvVjsPO(BW+RbgGL7n(ys$^6gLw)i&-7l*vJ{q#KHz|PbV3K;E(qr z_7Bk>XmptAMi$G7!P@y zTIqjuFScqv%gf^VY6hO!M}bV$eT6cGItp&9P8-Ut@7h59E_9%}OQWqPA@XIsC`$wL z9IKN(o4XryQyqW+ZL<--IkHvYg+cY!iYIPO+puUyZ~iQI^3r>3ny`$gZ;Lt4U0iZr z5rv0i1k<>WJsJksONk~|GAsUl7*7~yW9p`=M^X6c>L%+EZq%$cGUDs8_FjWat;9%k zz8XWzK`gg*HN4m%JFvi~XV97j)bB^f4qBgko&)yAo+!}V#cHjOFKUdJ535|g98?5* z?e_x(1x)TMgDx-C@QYobTqhG3{b$uX>gszNxKh|Usl1b{D6RB~vJ>LG`$5#mW8UwAUTh=H(QL^ii+X3dmuX-`6F36$nLT4s#OnpPp&Ab#~uew_XUk#2H zF`REK)b24YwtH}-3DdB=$faXwI&5(=&Xh1LZ@h_90zg)dbcyvwPxqglPo7KP={Yj% zS%txoEcOAKp5w-SuzThBQ z{VsDsZbM)T@xUmhBP#prjtZ7RDwQ_!A>cZGPaytxE&kk* zkMO(D!EXs+>i=CDbjM~z#dOF^GW>f7B$t0(Y6tgM5!v3({yQ(R-f}5XTS!V&kVop@ z*TFX>(iVM$C(G+mGK3`ZuWTA1TF`!zfB*I8+Bc9k%mx0t4LM>#LPA30jg3Y5f9DCg zIks7_ z-TK{L>q-4;PKnaP;SeS0h3{`A=$~#E*%^ZmRK#qmm1Biyf5$FM-<69tqIt&b%(|A+ zu`fCa4e0z`l*J|5!rzIn`;)5r%OjEs|JBM}gV19+k8suQm!lW2nw}BNw1eF7sp%jx z&{WZX*CMtV`*+iC0)9-GeUJPBH=JRKC_Wd59?a0s-azut?kN4!9mfdViXimx<<|JK zNfl|}=T41N#*3=+#Z1wF-+k~`1s4&2*FM(EKXgZ%@_3|Uz798kkIh7WW5w@Utnfi5 ztxbg%2{G#xxU-G4A?w;%gcA;u-VLp$G()33fw zo@r=z;=PFn|2+7h_vakF6esQxcULDX|1%XzAA}m8tl0LOr1(KAj zz{A~H7(8BTXmqr(Oq1uKHjCky!fR#w{+&WE1`Hnqj!vCYqLG6t<3xY#b_AZjuldyw zfaZ<_@`16KnSvQUXZn=}*DIEXbhdp?xPgW03?1t8p>#Y=9pAE#9Tj_k@vF{C7z?wn zewSfsMcwm=hiR$375HZLnd`bbB|aM8DoT^*Mtel@OH;2jiSorGF~ZVau|8_u@w$8r z?I}NO9M{yMopJbRk3nqU;3(gxCyV}U`qCCnz=^uC*p&WWp)!;vn=#^7#E8l0cRA++ zb*O{scK?GJ_3Ev!Zs!adLuvr*Oj`XwEkw#}mF-z9XSMGeP?nw2sT*Oq^%l#aq>Ca6 zE5f&`PFV=l09x)Rdcjf|$jGN#wR@raEF`-T^OlYbx5{CbA;nuUbwRMvVf- zmQrpIQszB*tq$p9EpyalYLPIheCp@#FYkyvbcz>g2jJxI5vwv8`Y+D$9ZdE8f|+?B zjJS&`Z+8>-nrfc&aAC0|SmX5y-rbVAlO0Ufva7e0#q|{JQl7iYz459Nx7H->^=+D2 zIMMh037LynvsLb7ICk@)gMJCsCtQe1&`dkipeAumAsU>4YvNyDO16Xf zWItTUGp4k|BRsr|E7j!}T!|L@q_S{6X(;fuN$|mFjc&k!;etu7iKoho42N+xxV7+h zC!6~*4K`21PGuXf>yW6C)X498^96zT37d;Vu72QYD!&eMkPVi&~~dfF%A> ziEge_>AY?8T5LhSlgw;9O-k><%M}e{e>JOH=}9qPx5=@e;%0NRH)?o%d_2#=f#cJt z2*JZ`%$3a-MBOj~QvE z`G;EdKTPinL9t^wyPKrKHRsox3#_JA{*r_Zcie9rER&ohTkBK!4pXLfUcv_se`@E7 zmwrOav(l{7J`x-8}E$?0Ho*Nxg~K!RI^Akj`yqgTksEL&5&SB zvPuz0FKM2i_}$UPEj-E-jiaaDL~q3)pv z#2=$kDbLT^7kBtN>oS8Njzc1O0p)cO$4)1Q1X^a!(h2Cf`T>? zKR#VgUx>qTf7Z3!7Tt)|57WIDq_0B;_oN`e8=3ii0`NDiA@tVs#z?uMivL9JjLfV4 z#OD6m*aTGN1f#x*A-zEsf_U-<|%`Tq6w z5wmO+#DNqu>%FO?G)BRcg@50-Fx=I6l6$^5X`?mzG-~7cbZSL&rP0S0C!uH+~ z@e?(a(i`@JK)uhJD%5yRZdEfiE{A#?h@5T5F|l{jM;l5Y8=|?M`Z@Afoi*2My_L9` z;Ii5b{x9O9W3vM{Gj>3G0Y|cjx5z=smW%_ixKKx4Lr0dy&Jhl`;Pj62VG{BSB$z*UubMNpA^8b!Q&3Igd;V@`scE!xVpuzN&SC-ek z+QiaEDzn`=>^?COj=`@yX@0L%aEuKGkHA>WU3{Q^j*TTQ1`O0OJac-@?YH zT#e>ZE|5lld1QKm25l`0k-3;%XZEqRqeGus0`@J@LMk!9-6Pqsp@n)Yh5kU(BW$ z#1?)1?Py^Ku=_x`Fyi>S-kpz^nw`JkW{yqob=TY-QJS9+FR{Gc=-L$YAs<7Ri`)mA z%wrARrgPX5HQ6tuuToP{@@gq?78+iHu8;gcH6ux2w9xeR`#N{7hR-YgC2Ef=Gs=i@ zITxU{h^^B}1MaHp18e0{TP4*el%{jsq_!vMS?TZ|mk3)oZ7ZF(<-fSzb(USrG?*bG#EjS`@ee{J8-`l>62QBxgjmczhB%mwl|dxKDm_Ce1r`a${%#C zk6O&tEIs0|i`IOL?Ij5eA?dMF1)b*@&_3z;f8c{H;$Kwi~% zbD~(WtTjm9YLF+hcCvnB_#GFs4M8-{xA=QTr|hxB=1;v{l?QwF~H!InaIEpnL38>$Zk;;7?yOzJM>pQqU6(WVO-n z6gm&S2`Zp~EZ)-ix4GTkjF7>_KlxS(D;js}E!)&9A|D1n$F zuevIiCUq}S2@x5+12pqB|mcJI`@#Sd4>wtq(T(hAFv7d5Zqk{cNpAd zfSu&`m*=gmXRCJi+tjIGr&Fn-L90&2MM_Ikb z;I*ODCdWeAI~`aHk&CVG>M818AEv8!)Uc_Y`&d4Wj6{YTI&$b;#oWi6cBqAYI|q1Uv*G-bPXdyu04&KU)trH9(ujS zyF*&j!cS=`2Qq+r?fu0mm;0r{-WMOTQI{9OEy6kxoq8J?m0w0%T4q`F2goVumO=I} z;rIn;mX03w?4-{uxeIMm@jNyT`?xa}e;3-aswcO1=1>lWv%%id6v;AjJwfJ9apfUR zfrftqj}ZW^K_%pQOUG~50&pdKjB5#lhhK4<{@9xX__!LN<(8r?l=qKwpKVE)A!uOg z3F(KVJoD$2jR1EKkpa5R=&797%v(viGah$>Wz2Xgq| z(dE(-^^?rSXlp9xT_H37(8hvlA-8Z4Yrl`)X$fkesmxS@8~cUXHk%_0<+*BH1=^l< zOf&eS#)V$w-k8Ud+CE;wEN1Y;%4|WG%wp{QVi#!nq4fm0w2G2W@b}q&sfFy9G$w{ELGI1{J@@FgnJ9-ftNwQ?Y&6aD6Bx6 zTXn8>$hiFiHdqm4KGlQw;n%pLRm+)g;W55bqV5M}i*3ykO)V675m+(scoWM2X7(K( z>&I7PA7mI^SZijvpk7I0EoOq@ad=~8RlGZ^XCAIx<;#1`yTy;SGuEg~=e=UZ>EE?s z8#bhutl>bt124U!9gBq#N*m>)tkD>}?iilv?~Rs1Px2{v;#nZeQw%@@zza00=*r zl4<)lMWR(dBuH?w#?FYMj-pmCt!+ieU8M)Ahn(DPdG;`BhUGDsX~#e5yrWBMF-QgO zP_p{Kzts;e)>^HRrDoMIrRG29CQ6maaKK)Kbi>BW`ORE(wr+fSz4Q*#xj7QaV%Yv( zIh4`&V-3mF#)#EDre_7WYFRPy*~r5!E?nKBAQ$R(S)4x(EvKu`-qpfy6Z$%d*6lGa zpj=Hn`IlUmKzhc^ z(#a=9&;TSZ)YXnEiajN$dKv%tmY>RO`f4DKe$mjth~{J2YHvIC%@Po{<=Oo+=x{jD z%i}$n`Oh~;59_B1p$hw&sM11tV#RtaHDJvLW{aJN9CopUTP$jJTgz{};P!T6zjF&_@?eCb6oro0)K7*}RNtc^NkJ8Aj zIn1fjBOI!91tJK#e^m+~_w+2Z2;>~gnyGF(m#g4qHt2H_I8X?{O#kZdK2zm>u&SM! zyb_ieC~0^Zww90X&C6dzib^xh1?>7XL?s1BtX&SoX+#Vd9#w-4g`?oNXm{%0eTU+_^=A(= z|Ag@J*qhS=j}V7DSHy8zd)bCs*5W!VUJ!^QPU*Sw;+KWT7G=*`)vnJ7z2nc0t%E+M zqM1r=>%)*Xdm*v?ZlS337dqr}!_@n2VJsD*P6fAWn9K6EU=xF99fy5)Sm@=A=xTH~ zMcEDUZ6hD?Og%1mf-}W@Yc&8TY=oz9s6Niuf1Im0augxENS_ylO#TWb$?A zqUn9-7j;xk>m;0v`@d(IXA*9XxW0C&Vo{>`a^WD2m9=xr`GTbBy715lRJjUs#NF6w z%9W}%N<@SRVVzC?GzX-ngz^S6X4wQB7Lb;^b7Ab%eyk^vB%L?v{Xo9&UNh{y&e-f8 zan!Dmh`ZPNLGZ<_CSiv3{*AkBQ?xB;ceojXO$%>jAmj5M}>bEw->4ns4^@z0Z~r9_>Tt?xlJ3w`)Y3_21GtO6SEVZA2nyptbGxb z%#!=Uvo_u!wU0m+#JCm{euA)FhX2`jT|7vgi}HYK-b%V$?%I#_%;o2A%~EyAzp>7| zAl;C2phTX9zEn$<+|C$thl21(L&L^>Ud;q)T;gji6wM zX6VLAL0OAb;E5u$cY#FsdA7oXL1cSy&9<=`tD2a^$GQpuZ8GTEjm%G?kr!Zm*QT@r7+ubVPGR zU+Bt#9NdT|wg;B`GyooeHFqi;9s{$$JYLp()SLuG(3kC>4(3mEeDo=sYv?5Y? z;qh@>p11A>} zg9z+HjEp?GP?1lMrkh6=P{V#QrK4VNv%l9^QThY*K%4#q7iW@km%c}7KR_GZv%6DF zSs&+1$t4`a%N=dc_~yaRjONPMc@0hOMiwu!I2~5}R_{r-eO3lCH{9t>G#sczex*E7 z+*3kzwwcFGyl2Fpy7t84xBLxceDa#5k}6rn(+%x zy~cT=m+#pI5b{#StAf5~wCiaueo>3t@1Jt&pY2q-{By)Dbua%D)6yPjmo#o#6RvJd z?5;3f6QnLaSHbJ$(kXF>uCvo4S})v?XN_5TAOG5|&%-T|(WPA28`WhElRnQ#n zx+&a$Xo9xmyFRet;!HP^ql7JQjLe-Tj)aeQkn88gNlW|L`Q%oD(sNIf5V|mQ|+S)RIq-Um&|Hj-z4?Wxee zESMC>ZWSb15;ZmOu2T&?Q%z{Ysq0JD0U~6g(aHqQpJWmY-`JM2gSGo=dDR^iC# z)rJE6O4JeCn}>sk0XY}1{piHYMa8NSEBR4Lht-NBH;uQXNmKcIt;pGggJz=%fheh9 zMAOf$i_3~f=Ss$!&+HD>N(fxjafwNXHC%DvzI?30_{faETSkebt4#G0dp~8kH+Rux zZ8%Q-57RsPkWBlZ1lYON2sMaN$=(W6?;1 zu_VWbiSJ{+H{i}xhNgU{*++YYNUQJ)WC2y4gmL0`osXJ_(^t4$!$pR_J14o$IZole zp+-vW{-C92BfYR58tTjZEK>LV?jB|?Yoh7CEY9zepL(YdD(J1-dF8a(z)SloGo4yG zlNRkw?P=$fx<|F2si5NIVjfh`d-%?CGtG~QL)mk|0sMCga{sLA)6-1MK&$*!64AHh z=}Opu^hh1bnVPF6x8uL7Rk@Gz444`G*@*Dv3-AF$jwbG;@N*aJR{!HBRM0!rd#q(% ze<6R4!&~`ePrB+x-*4U+V0J`Hixa+1IiF}bSY^9LB)sv(@NujpHmkVPerEe??e$RK z1ueXYuma9b>!912;2kzAqXZ0)@>G5%?9K>Ay(~{heshmZQ&2c7tILv0vJw!r0A;)*PL2oeg@QT!m!`hZ$tcqqo5!oxcRO;7| z4z8gnTeWTbLNQW|b1##6AGSCz`Ph}UDy?M2*Y-766+61CB!nd=Y2)sO3(`tXx?6Tv zol%64=ZL+?`X@?CJnn9t0HKkwBK`u55#V2rl&JvAXlJIl{-TyjQlNcXBn(IyQmhQz zMu*1gYYJ8rjyJ~Yb~5}_)yMb?SK7*Xd4J?d8$B$X{A7izvYC80(@?3~@36MCO+<71 z7Jd9fioj#y%z;TPDNep3If_Y7Sm*{=2cbk=Ph8uY@an;Pi0uSO>`c2z3>qSX>vEub z*K|4X?QkF-ieU`Fj$(51VsPU`^61z**wgH}7+&*omg9v{$1rj6_01?~CXcORX1+$r z+*|xm;SrX?`PN5c3zO6;+wOM*_36UJUvq69R~CeTUtR~i!ADYiC1Lw~Srb?gXGP>U zE)Xf7bz0GEFY@_O_jL%znG~qkuAb4omNV~@ppyB(G@bJJE#PR+#>)FN68E%7M-uNX zi}$wf5u@brv>t#Lb$pc#-YB76>beVsx#oLzw~`1?zBKj?ZaGWSBNQp^p-@RJQ!lZy zaLHSgUT{}!AvGKlpQ2XBym-?`Ki*`GApGYQ1Pm3>NAW+9`YK!EV>Z-T6gY>f(rPm& zRTfB-*N5sdX1*RYj6gg4rDYL0FLS1R!YoUk?%tp!zn{W%Nbq=US^+4Aa6j)>jq>=5 z!mTLK$3rvoF=XDiB*wKrV5v_YU0#1|EL*WBFQ!~)0l3PP#!>+jmqtwv{NB=1E{hX? zoC9xX%N9}v{^mM`U$bW3W=^8~{UA@h^;u!$>qF%9yY&_Ep3n6t4mM5;UJ5HR>$bmR zZnr<7%gZCs)c_Cjd^*%|&r4@gKhR^5#;NmUGZ>-}HI$btLjANg*E@#x844!{Y->oG zU43HUMahJ?&o(9G^&@V#qX*d9dttnNC;13 zZ9E`_weQ$0`4wcjR+~)5I@0sTNZku!$$T{LltQY_oWo+04f-8{p2=DRq)m@?OfA*| zgjK#IzDUWMhL3n`&t^ZRNNw_ms=S3rHGm;nVY|dPpSA&1<`U2~(^OBxG;@#|;x|F< z{k%FLi=O=NC`>X+WFI7w(Lnzb7V{n|pek{7TZS{Pro0mB?`0W(;Z5wI(JHjh>ee&5 zZ+82XVtX+`)lF&PcWSLFQ6BqCBggN_8>A)JeNs;kv>9q-QsdibVE8l#;kmZj*y-tJ zC1G7uzO@-4qAe272Yd5XhDXC@m+O6A#Uc7B!N$3YZ?QrVLrdhzd7SvVfoc1eGiv zPgxuv#kSZ8yHYEAdO;KhroeL4oLxfj{D&4S=t3-C39`uqKJU?QUN zUHhk-&Q1ppCneZoH7u4IDH%9LUFMX@TC}gvZ;p7OB&n0MD#>XHADpgXKhga7?l3|o zvbP(LK5TPG|Bz)F;=2Ta%1uY~D`||poZG_WxksJ*4+DEPh^Jvwuq}x@VlK$a%nhDj z_&V8B&Wf=Yb_}9kk)$DBzGd>{j(0rYV+e*M zUFxfww?Y{StvYojA@YJuhh)Y4u}I+awS>%}`XXS}kd1P2sQ$=!{#+b=q!y5nbYJp? zx95!M)lv<(?QC>wkfaZG;8fXv(P@jZ$n|~#QYXIa<+AfluV2A1aMp$ zvuF^Qw}R098AD9&QxB2C_53P5(8>=~9xH{foA=`@)`RV)EAd7#arPQ(I#mpqMtq}J zD#slZO*z)kwwON+_>Dk2kA@zLZzD>spe5R=iUX1r2t9r}=~^+?czT6-D~C(l3nO}6 zc_LIapYsQBC`zUcgO`Jp?;Td9*C7U+l1u=FCBs7zRS6_R=@_M>#sSHe4{lNpD=v>= zjR#2cD8`3|NixHQi8E;zwhVL!+SfyxGo_cXL-#B7Q@kepI$69UaT+;Dqb0^+90smV zBPS&19p2w#pe>?Fxr<_@x|V#EH{GMRlI4Pv8WV?%i7iywQB!)}_AjgWAU$(qUq`MK zuYC&m+!)Se z@Xg;>d*GoDrbac!k;b06wp-ky)T63sX4;54zK@O&^=h74N@L8<<~ku9b(s=uG@TGvrg{~Hr$jCYWN$yk@*nFhGI?>tK9^hcnEfFkW^zRYJ3`EY zB~y@uS{%dTmN?2HxI}ZNJnYU^SyBAl+~-s3b|f|`i0|un z3Gi=qyN3?hF=j@h7)=&!0K)^FL)QXq#zbLv;5K zP#ypRa~JaPpN3{8CZa@+8f{fPxwkHsjHed&@^&6#dERDN>=2KZmoRnP4lRD84Ayg1 zoG$u!KR-@}RcWwN+?s(5`*YzQJUG-xZdg)1Dp5L;?Je80IY})J@;Vom6%yf&E@d;I z;JhLB99e}L`W8Bb3Z%RJVR`nK>H=z49k#}VPpcvXfG@-OAgqP<0RM#Yt-Z+CSsuNJ zT>{Xsr5j8(pec{vei-S?P?duJv1j-Xt&e>iCS+@J7KRB4f=tVf*nGGQzeUZY?}`}o z@rk4FNJ=m#M`UawYh=R4ef5ZMp0&s_`^WoZ^w=MPy_l$Zcgg?I(pw;W7Gp-jUTvv z2MbJN@BrWJR*&cOiD?mx9lbV4C$H!dA|BrQiFpA)yZMw70IhKNqo3Qs4T~3%0UH)A zUTsF2HU3P#FnqrxjTZ7WkJ@ICjIwQ|07i}mpn*|-ih?`oXtC`|3M6kilN8@Vay|wTNBGWcDh7}KQVmq)rEI|7cYAT& zLW$huGm3e>s3HB(7_Haz7g0>qR%<5b^uhMxj1`Uyc`jUIM(QHV7LEKM$3gT<`g-u* zf8eeWh44~Pm9SoXVOKWH)Y$@zKr2bSMikiuiqUQuNTb!NKk`2WWwU+LYF<^WlFV|A${)f~Rv>jVcZLFbm>gKNa~jiw}lX4>(OFs;rv zqeK*dBYk6LP!6YS1!DcUMbhV?SO0`Uph-?>KXdBe{KXDCMCc1gSBCv<|6-kgOK#Fw zmJ>M!O@Db0ts2j{;{*O`DApMFdOKjOxB#6#_r4U{k|}kJ>OY{Ua@s$jXx0CLdZPR# zN)wUZ0RGLZQf#~YWiaF3iTzJj(fTi`shs;iiA0_7Ker_+OZq=P8Oc@ISZUO8=*^1_ZV=QbtLj zM_@rf<6MhhwbDFkJ{zVlTWcA#M~;d3TjI`;84$D4IsUtyAb9@q1ARG%Z7n>OCT!B zI#Dzos9E!in$(j46_06mV|(zy4RTHZNZq~2?fq)d+mR?G0cE%ilV1<+CGyEZ0V z32T20UyV~>J{N8}(13EdU&*eNJKLb`va}P4{@?Bt_mZ4J+_a&kt)0gHDFy-6?Zv?F z&x3lpb^#Ufd+~Ckw{wb3pYcZ-Sp7aAK6ZOB0PXAuDyW$krj9PKsQCy2YP3=Z^U}7j zl?=gZqxHEASj{V4uenCKTUUZ60WW#ZRX?_Z_!$%&YDIOH`bz{-zNU0+MlQw(d@|ItWaPh3?8BHq zqcBM(?_FFg?`WWiVj^mNr!~Y7D$gUmc0=)-7jcuois*W>^zy4bC@$oZ+AP&8`RM^e zdltjA<`|)J?>zSbq?G z`p9Mq4QpbBCZzH~T@|QR^kylBIGovR>@v5V6i4dPRS2rHh-Q zwgw~r{I8Bn;QK#MKW%SRhn>o-ttJUL`nGCJh6D^1m-1%#lJkaopFrAw&&|Fq;iayS zQ&^3QAx3njEjezg@^t|m*0zoj-<)&yJMW?dm$I#O+bKO@G8u~d`5_NY{GzO6`XWBf zLd_MBXbnvrWv5D9S?BN-2_?v6cX?65gu2~D&vuImvL(|d9iR!w03gzuhVS}wWak~z z0)#;EbsC+sTK`}urtDV7E=Sp(64EQWOPXTj{cC)pnx|DT^RM4G2RXWmdPGg!X6BeY zslbV*gRl2)>IUjr)4yXq-_fhMh~1v4T(s9Uq2GNiBk?w zDROA|Bz**Rl%_vY{k(qIgC?t(4qt9f>aGTlV;$cP4iUJt7QgaigSt!2?gJw!U78(9 zaIcq)534Ur1z(h^9a#m(qH>U_iCc;pw*AiI!p84ekCv&uwu~PasiS=QquRqeD{k=s z6{-4y1tKaa2D^E+>5*DB)qmxsgsKEef?Dcne1?7yx9$)*X~kW}BrfU8QwDp8TM=&g z&Q;g-Fy@Vzi)-s>a<1z87Y@`3qy2O@IgZxzTrSoduduirmlPMxM-?IPFwVNxR(w4i z6xyq)O|7o{bOi4dqZxoL_DcVIJyQkmYN!Ys?xW30U9JK`}{4%BIc=sr6W*r}MVFjD!&2>-<>E zV0U@mR`UX3bh@?4x3>hC1?P#dEadxpG)gEVm|KieTyhw+Q6FES zShV)Xph!a+h0;RHq>^nvWFqDVwp71?xE4YVHRUtUVOH07m_c0^>QT~9Ui^Z?s?^7o zdwb3jmt2D4x#4Y>LVZ1OW(K3Y;Yl4d_abLdqzzTeKc}C)lbR)a>_w!ev6U7ih)rF`*O>s^Bmfi0=nj> zZXgIKJAtdeZxa(|D*brLi%VS%1u%)Dhyv3Lu0_vuLA>NGG>gn|w#Lt*{V8o1Hw({< z#QV|LfS2WB*o$W0uf9hfUk#scd}K0W{RMR}iAPRU02*CGRJA*g1D32X>B`$XFZR=L zaHj32k|cMeJ=3wvywnaZ`wCSZI1^aAf4{fOrtJ7+9XsksU31sH zcj4&0X=|{Tc2lMK=vqTaYr$N{Pa}NaQ2pfZlhJK3*F^rH@bneV>i?62M_R8Z-Bl=o1MAzX(2;lvK-z!~kdG}SF0bIl5yOP%JogJx<4$1L| z-g2d;Mc(VJ3(R_#?FLLMyr*Hc$>c7C@oewHW?4LmeU@>M(70L7>6&777Lrxl1@YC; zrPyxKI!qL^4Q0IcJp5wSif4=e4-{OLdva&eQub&!%AR)0adm9Q}gQ zN1tkMhRMDTo$ekQNr0J7J;f|Ipckiuc6vF#Y_pQ4yN8jGh0aRAZU)Bpp*8rf+LlC_ zlFai?5iD?jp5U=GOn-?SjIE^*TV#(^XoFyG?=ty- zO9@0v#jH;iD5W;ED?~%MN9MD&d5yZr4QO9{`l~t>gq^W1><-cZL_k%i6rfe7)n>2k zef@_}(F-eTLpabM)#)fZK2NAPgV|QS|7+4D3m$b5;bm?T);%}FL$C+Gdp4^Kp1@3cdonvE;~iSr`&h7Od$Osf(|jV8mIkgzK~#OK?SdqH^U$P=ru@as_4 z%LY;E>7Z4yx=F9Vsf08?H|BWHbBRP1&mJY#%t+UaZ zUYT8}+cEALR_QxtkUCS1^J3w0D;Gkw5QA39P2futa3C=-F;$G&hfGK1$}7rdzYtgX zZSQi(j}770b4t%Rmsdq|sgfB;of8wu!Z#KRn;%OCPIam_<#Mba(MqvbzCU++s5hYe zwqdmS=x7>3O){~9C%uxY9LGt$$L+HXI-P+jqZ_PYu%0=$cp;3xwkYQp1Hu#`E}*n} z{4`+$1B)LHZT35p4cvv6y&DFQ}2CSb+snIG|xgsxxDp^fAM&~^nq|Dk_6Q<{d zKR3L(A;%;r#vLbsnhbHlPzNpNN|J{_G%v*7RXe#`l_D*zRtiz5KIwMV^;<|!2rUWN zqUPz0#BV?R_={TGT9i^kRm!6sY&{d1QSeQA@Si1Ep3~r8ISk2)&eEO%Cp*}0-j~PE z)ER8U*>=(eW7!KU#c5sZg$hITF23A)5WrtgYT#9*$k>0lWpLUBUeW$-7pX6@zwpR? z^YZ!gb^p2K5(mY?ySP6LKww(`eX7}*T{74$m>>juk> zJ(BIWHuFp*EO3UFBk4zJ@;!;@%Mb$Ej__gge4=HTXEaXrpFAz8CT5ku*qf%mslT*p z)ogLv^XO`Ihnybj1zuy#KSah(Y2vTt)zm>VXB}yU4jfoCYB)Wmi62s~?_tzl__YS4 z%L3EVh)k~;I$pi6=KU2sK8$tNjsN&ivR#b1XAAaQT-%6T7>`RKn zi-WBT2|d{^x>{WS4cr}nzEn*gFMr+>fx-zP5c-pu-s@a+R z4;sq<`7sqD&a#}`SuZPM*z#V`m$uUTmYMSDD= zohPF$96H?(9XF%_t<`6}>1`NQVQOAw?ej)B9SAT;mg?Q@93hl8-AF+UL&DJ_!_2qI zaJ6!lM8BN!PK->Lgk}#TR+K%uSaH(6pKbBaA0E#LfoZgqy68#?Fbu=beJ zI_N&;cmiQ6nnKn3Y)eBk)bOj7Y;n*1I`W?(z2$+anhu|MZrYZhV*B;ZioN`2lL;5d z+Ao;mK1QxHdl$3B4F^_Ne6RH_!^JD6@0}P+tIcS4J+F@BSHie5mLq_DCC4$Mf z;3jU;Ppc83*y&iexNnbHQl|YTa=-|rx5@B3-8$fOo&T=gO7vJs9J03c+NF2@OGBl` zjGFrMkeKToyXsW#f(!oObhvIJUxia0Iy4K!i<9)MRrwb$%&1!l3r&&01rr5D6Ra;x@MfioJr+>x60R^)hLFeQjvms}MN$WFRuI^vFW@ zOfT=uM_ys>_mZx1o}7&tPSzM7B5svw|M9dB$BA5QmNNWrNH?)?E1SyL#xn}6ozh2V zmGWzukAJ$9MBPoH^Y^XJ(hJ>W2~3}DN<=Xc!cQ%M{8fSpbb2~@VF;f@%lF3gq_mv+ zv!)u6NFzR2!>&%x`Bh-)w;9o4&6pE!<^VYiiN0&!%r>wnMj0)ucm5X&2Q8L#r zL78Jj1EaT~c6LCCB`U;W^o&RC#XTC7!(ERKk%My~GsmH-bJOiy5_!G30G7Igu~-m9 zH?Cl+BCnuAKb*)H;OV_{H)V=PlSfYgs^sT9U zey=l_mj}~&=*We`Q~4y$quYKLH~V!*S;6asvXT+8_Hv={{StmN-U4+)pd@+YDm_wG zDw0@DRao{p;hXmQ!QXR@0)D4D`-KnJ+L_Bf7$T{khzTY_vn`b zE#!}D)ZCh7HHWjsl_Ay1N5s+~g{kc^ZyW1h%CCZFr?Nx9@DY5XoZzNY{L1dR z^TxovegLB14Sg3I8(nF(PPU^}tD{TpdwNDJCbVGE)<|td3V}@eS|m^=hRY6WFqjWi zqO8~VUAl-ALK)2-g-EbM+hbm-Q2s*2j~dTYWvHHswH+Qg$0KECkDHFuWfUj;8(zTx z?&(VRY!9fmR_)t{(r(mpbaH3K#G)4&80p^k?k^u>J$eQn3E;1JzIi@wgZf`}k_`gC z%oqH)AFzFCdN3|Rw$_-IMFi6up6GbgqRtwfy!fH-rN4>45c$fd79?_@bKFd%5CXzI z*XTNNtF7|eh3XC~t1B$TkJd?EIB+J;)H91-4p>}^{gC;~dRZ`4l%a~vDquF{I?nU1 z(N4kQ-ew>#yG^OorHe-tNkRXvBEZ!b_q>Qm#EAZgy5sBGdz?ILE!#!hQ~58lG@FtU z$?}-Ut@2N{+D`NFu)<@_gYW#x}8t_ zdT#+L_xlqNRDqC>V)Ok;i`$>--;`)%C-VAkX9~Lh z-V5D_8`CKpK31e_w%jz28un1gMcQ_0CCFpu-gCN;%GkkzgwHhPp8&L+$AST5?f9>J zo#9y7Fu_xP#T9LqE5L?&;>1yH-_GJoJRLzF5jXEC;v;z;3iw7T4mNV{1B@VsDYOv8 zRz5N;P(y#C1R429M`Te-U3lE5NsDa|z8P1moi{OE@EQV0mPXC}8ugntss>5zD-^_8 zT4Qij@&1Iy2{SM=AFIm*RGE@gC-kzIx2rSt2a>Je8C0+vCc3qOe;_OM_cMnudmhGWqWG~Dr}W2J19ZZ4he3{Ao=kjQ^B)U@IruL ztpULv!h~8Q$uQXQ$;PRkv8G{SW>An|SwU=*A~s56xp1Uivs*;Y@$Y7hvZ=;2bJ?jB zbL(x<42ws+yISx|4v*%7H4)|J%q=a?!o#FxBCWY5?d{l&dj z<|pl!<@xZAyQiX`$?IchXp;1=AKT+$CwM}klk&BICIO`{h=G(87;ATG9Df(ypq7>;C6k-iu%fT0AahJ)c}S?EgDR`&m(c40H0EiB1WZ z-|yVcn@kHLBvQYVQP|IWERRY~S?Mtv;A(T%I;-P{H`E1HG&3SvF$F9G6>ijD0@G}Efa|`-j_k&qCNZMH=%oa;-x@kt&g?2w%tOSYWk->BXo5L zri_HKOfmE**#1g#`P~C`o_PkZ^T{-08EVC|HAsSm7-4j;*SWde{fRTaYg21Tn$4s9 zw0>N5i;i}Ei90lZ+GNsQ@74JEZR)oWiGlbhqyFD9jJ55v&eIGLdjKZOd%@#McB@{u zJF%dka=3@F{);ow=e+3>nC9E^?(*RkX*K1of&1p!K&S?s{tt93bW106zyx2670cJ; zQmrI=N_MCIN!iBvPk)O84S{`B+f1DvH4Z~ARy%Rhv7LgI5pEx|`cb)jc&z#pgPk!0 z<5?^P`#2a>qbGmxDri%JL$3mSjDhXl&rAm0atgI`_QJv?$WXBlql#KBs5Kbp8UE;yRo`vspXLVi_~xR@EtEj^iEOU9F0FS$xwlpqqzfTQgm% zd)61b{YSgkQO9yX@r+$E>{x7y!qFj_LuTbS!t@8?nmlzy;LTBmhei>8OnN&-egaydpEU)6_O| zy=Qj^Vwz*1$ff?yVxdfQKrO3pxzxT&-Go=WOQgG}+By4+70Cas>4FbId;+8WbVYIX zbFt-hyDiw#J!R71q`5#XkAG$5SGyVi4UZ@SW7oY#fr*hR1|8oWW_X6ODZ5LHXTO@J zx|MWtC0p6XAYMMMh}OAI{Nei4;6ei_?U5to8hW0$$KRebHB!MD5R*?SRuW^z8y*EK zQuxCf^Y}7P=#ZGZTu)=u;H2f~a~r~8Yf226?_~Lm8RT0Sxq5$XH|&y;Ue#k#w5DdE z;@b3?M1yP9V*u;H5Gv_96KvoGiU1HXaH%hWI?CYvCO#;zXywXIt=u~eTvbT%^{$ct z6SRU_TC6O=9pO8J!(2`HHLA>YNP`p15y{>9*{&BO)eemfIXza8vsi zm^*(=E$$7=-@{j-y4T|--RY>`nxUDh>FxMov%+5deWjGG4N|dVQMcpxMOD}b^`-)e zjA!j0Js}mW^&Wg~9+_kbrZ0mGCx=^jG=2r5*;9Q!$?W}%;d{-8(eg6NA z%=PW`0Oj(8wk2@mH4!e0Ldo2*yKd4_3tvx&*jl%EN%G1N1`m#yff^>VW2Z_)Stg? zULmgbx57%GH_lsey?W;7y03Jty0BJzd#ro4UH~=0-Yj8zRnd(eMyhYQ>f~=-*kgT; zd;Jqg!e0VssshmN%UG+y0WWgzrxvI^DkH;Gz*U{oG1SWp#$WGv0LPam1kOt$bj7v7eMvaJq}}zprT8rM5$jLz(PF;VL>1N}F(raTcG!YW!d+uvE+>TJZnX2wMq$e>TTrl6~lA zury*v`TwkgwPN&qrhqY7(mXvCtN6HQQ=@el1`MtCI&<+|LV<{SK)CuJ&fF?-2jcs8h#t z0Azp6&veL8ln`Fog>t;(ShHo-&X%OVy5Z`pgN~1QNz+5o89@+sbf(~RmSA6)iD13c zV0%)flSX&9mnXJ?K^CL^iO~EpvdCPeY}rDc2I6lCOeVv)t90K&BbR?pYjgbP@d24=dHYeuddd*=fL+vgX{@O%zh=$O(%-0DkKY_ z5;RJ>w$&tHBkZ#OB$B-LDhixO=ip7!dd|YPd-MUxYLzSjuS|-v<^0ZA*|#=)p>OV* zw9mP|-M?`8nUUv%l&VIMymolm&_(8&T+okuPkhac&f{zl#!v-n#SQ*!8(p=sa@_Cf z1x-|f`?44BrKnz2m(uXlI4QcJH9r5VCVwbl z`}CCcb4*c_ck%f%MbUUuRjkHv$`7&KHtMK2(4F;0`rI?p%@rAye_LQH)*f@ZQ_%fV zDDXQdNcI?=-L~NK?A`D%b+lH>Dj>t6T57GFzqphoZ@NnZW!=AO)Woq?2#MAs7@d%- z*+0C%G*B7S!IKIp#6v;(?snZAIUATFw(EVhnm7>!CKAaYWJ^1Q-{vW{P|Vl-FXn3> zg{H%cY}rS>cj>JF|Ll#C@(_YHxRnH1ZO|2w3Txdd8;(Jj8({Hi?O*WR^KErhym!l< z*zX)%ujr0If-xx>IAD6{3r~3=n#ef5EDt~8s5dh;+c=c(zt$|oOABE?`LE5s2_}`bXWDRwdQ>0${f)|b9-ZosqItJ zvEK+Os$Y#`)1RTEDp>r&pSYV*LV|6-7PfK zl;v&>n)%EN`v+TvE-5e@<+J*Ny%0gSjN382V$+ttCQ4{0KBO5_PkqCLlOvzSgUP#3 z^G!is0ls1X2mEz+d~j>_5W+Ts7~U_P7g}ilUSalfz=;PyI2!X*;l^^#ck6LCp>h)G zMaW?bQ3LI3&}2H0L&g2J_Y`h4I{C;814h1d<+OyT=m6fdBt-fxy9|DX`INyms9O~P zKs<)`T`sMPI2);X%(w5}4Sx;4|3t@HdOHxMP{wlEk2Ydt_lKeUhkkpadjm!%MPkm; zb(TbOd7k#Ef8=trhv!$$<5$Mt0!--=P)8B#GhWaW7m`RFZ^MOI=D;t4%&!_{8m4Lx zPrU7FMS5t#@zZvf2{1WIY$bjQ%M0;J)+nbP0(V7~^L)VCZVBapIX@YRdh5k0vB3yl)As+<0g9 zQD|%wp&J3lWnUqE{pU-%RNjNr)zxG@ZUCkxsOoZmhvJ-W5?&DUH-xlchO^v|IOc54 zPrXoXqCgJuax;i;iF{E=rL~Iets-d5mKSPWo4g8vJ9l<5bYh3cywqL%@;k-{?D4;K zg*2Otu#~96tUY}(=gVD$iB^AJgh7klE=uYm3cOft;3Z9}B=H|wrpmb{PIQ&&6{c`H;GZ}IpGHuwriCYsCtr@!+11cPLv#lI=4DV-J&$buUQ zg|g80nF4W$XE#O`_;dSbH@^|G3I17s3W*@LnIHNe)$Qs{eISSzO8_YR5A!tct#pQ9 zu8?R4^y-8$`d^~nGh_1@foX4z_w~v02jBi^0)8)O6#m*W;J;ef?|491Z zw7YoypG$mBmpS%O|LZ@3<7SHgq8EOY`hYyx>>~MCH#(z=gUxfFXT{MRUr9g&B38Q<{q)t`;IMNbH(Yexp113R6CCa=pe~& zE9;%i3f&Gcg>_J1L$RaeUvcl*=Ra#4@{1vPIq*Euu4Ek%LVGuP6|e^utS0)Dnc{>|<@>=8ZJEd(s=RA5FGZw8F-aK+vck1^MKC?C4EP8uax= zDVl#RrghEWjJ%EQvNO5nau&-0SJR@BY}JW?D$433X8k7t$kEy85xsxL@Eg=$5ZRxI z2`u8dZdGRdH*=`;dKZ`=4oNw`CM!KdazV{?SE)oDYlZK6X@BFh8kqU=jLC|#*yoz@ zlEuM;;-XA}NnKknZ5>-6^<09LT;wT!%K}5=-rHV?ZIKL3LL0J^rFzdUp`o$8s9^>k zAgNFM(A~ur-&Jobk(l)`A55KY{+!16+HJyyH0_Ue$I4_E(05R*%BCp4|)85q?n+S ze6=C^N7elQW#4}9)@1}Xj)o9!jPo?g(Cn=d7(*zM)}GRRnx;gWFS-lasbcwew|@_4 z?BNER#RMtPcsqlfWX(3YDGh;z8x}57L^c^uOLIS|Vb^O<35%yb&XY~VNs$*BnUT1A zc2`>WPL=Y4AHZq^a%4@m`GRRq7H;m(3lVm%13ODqgmnz}ea8<@2e9xaBbjSXDSr>d zv`Ju1<(SWhH)+*`OJ8~5gMHDu8S+1S6W3R<+YKfz7GECYT3X0$bRp^4Mg-(hKfI~Ib*wR0yLLp^o zfqqix=xbW!(mjZS89OS*QNj6ZgtcNN4P`~gBXe1SQ04Bhh7nb(?7C`ya$&Kg0M`f4 zir!x8$(>M}>xt^QPJsD>9PFzt->|YraXG)`Xv+52B25Sv@G-#5X#SW>&rpn|kPs0n z7&&n2(%8JSLQlZ80=xPbHCPGP9&olJF}#y0ZL>l>=HN!FMz%X*12V&`UuB6T_r{4V z)P?0oZBcDh+V~?KzJKz|IL=L4(4L+41OxiRz5;^t>iu`rYMa(DXP*n39_`d^Vu4($ zqv}TRHVdj+8_86=BJRZ`>y7#R<0ErUc88Ax))oR-Qimv}klTf8;;I_Hiu$vp4s?8# z>>5}jE_$=N;)9wQ8LEzpXu(2DT~&FK(Y@SmXH+w@$6LeHqb8TzKpgfD=xeR&-K{rF zn~x2Nwk_M4&lWoZiCR_N1GxhLGktBkHh%vDaogxHZzqNcWd zrUBC%m42>d&jlY&D!7pNl3;o@Q!9oOBGKEhO?qYsr5_*2qL_1LRL7?pHHEH^KO!ey zk2s=@`rh4jJ--#jG-jf^~KgT;LXalg=ns};?|Lu z{Z6fhfV?QW8JRil^s(BWOudoc6%Swo{MP)SSg#(V zd~up5zA7543d;1$;sEEOWWcJbjI>re%3rMB5uQ)zfjtl_IZ2Xh3p5&%VcQl)XGcEV zjxjGa2aA5Ag5fyR5?=ZTNX8EtTJw#^KP0M*!+#wc`8g>W!K^HXq~DSPiOmh$qDAL~ z2M3cU-7%cJZhL>K3jZMogZxd56obYCf#!hpQcwFE;?QbNU)zz;y^dyK7Xfz5^BdA1 zpPmv1da#ZGEr;qvfT&aq9k2c{j8e(_L)ZvXI=C|o`kE>~2vTXQnaQn^Nn zji#_4Wg2S%Ef)f1>0_!#R3H!uT@E$b0FtA(q4BXbl`DPtcom+PtPFmFwiv#3SxEOz<>S+BaMQ?FoMqT>wNMRAND894tvxs*E$?>b zwj8k|R-PQfxw$ADIbq$?41X&_tgY%w52JN6f!6bP-$_kKHWod}ycywA>I28K%LZ|i z9qib$Gzh7QLuuWXM0^*3GU-aJZ-nT;Zwn19dMHuAv6qC7mk9hPk*vhfgJLHrMxw`s>+Ga%PM zSDw&y>zN8#U<^ z>ersrvbE>dXhy>4Ec1QSg>w1~0WXLkX!RuUXCVE=V^N6B&0nq5TWdq5 zt`w1Qn-#7#SUvL~k?MyUNvdeq8DEGJbwW)| z#vp?)j?J@z_9}23%r4Dnaw?()x^l3WJ2}udQUG2y-s~VO!Bwvaee6kSRyILHNu7S* z`rT9A-L{U|;?Y=)3ldf58kV}RPUO($>+Si?ckgrBT21+$)pPhKBAP=d#os@mO_>V9 z_uU-K&TklA$VJ;jTLN0#?Yy^0zhYbPZVXIm+rq*HLnO~BHb3I&zNVk1EFFT9P&Y0K z4-!H$wr*no@0j357yQDwC(E$K7sK<|DsQoxk4x)6?!UtFKmBmkkG8YdYSQN3V8!F> zRK8_$CGqC0dr8l$@w;nYnN`s(>$hzriZ^l5a!?RIY38rj@G;#kF1W?z?1D)*YzPR) zgt#i#)z)eV$7G4GsLQi^)R3yM`q5h%dyO-2cKGPbVmu>PdNAfl(&2@N(znlKF^L^I zt8oyq3PE(SK|)d-0vE|=El%L1t=%y?GNK-7GNFQ$b)toru6)2mnCol4*E3>RR^a+* zt>i2Z78;s@vOZO7=Q<|0Yufl3x1x7>*i-zacLG<}&}JS^Ab#^}@?G7{^{|+GUZC93 z7#61=6LWPk+&3_&L05|~7=v|607I1wPvHo!MwUh6>9Fkzl1_p)NTc3ZTbH!#S)MsG zGC-2M;{i5&1E@Von^DWK8QtQb-W>B4_=yf__7DMJ9he4blb=>0eEdwGv=zItl&A;I zEC#Oy`3mKPx%slifRofky~Ci;S5JR^ONr(fnZx|qmk2t|b>Rl<5)?=hwmL8(&*IGk z( zM|t|X*uM#W$_dG@s=?1qr*8oj1I*Fav)FWg$#rPto-i&>H4s?YvT zY5H{ndp4Z6We%{;J8}r7SM2Q<8vNEv2|<@nslU+h!elj8CMCV4y~ML^gHkG{Kb6sD zjm^JI?e%7(K5LWZstFx_ukzy*N*cE>OzU#sURHcq|Lno>pta+Flo$sJYJY9zBaDrU znf;QB*jDnGqaY$jhVJEuWOP26cVtM7K~Q!+H7|`*{+N4djh1VO8*`twu=sgvqb)|e zVZUky8-6h?uV(zg;rf;K?}voj@Xb_J8gcg+=R!qRG>mqKiLVPq@o~pY6};5H5M$ZD zAjS=THRj*i*uVpx3`AxI?Ti{EN;+#%I2P!|zWCE9J@HTlyrt7x%5XGZrZd#KBpoJt!4X`QW~T| zzO&j3u2Jo9{H_k@&v};<-k*1MF)PKzQW6HIl%Tn9GqVGL0O@g{?j01&%uD=7TyQqRffJlLaTQ6pY5Py;!!zIOSI& ztC1TwEnknFQ{q}?f{WFmdattgZbtK?WoEr~eE=}D$e#|f*z-0_*lO?NPk<9;vce*f zcB66(0CxKtA8n8*U-$Jjp08LI8-aL8ad%#UyKkZ?@!CggKyYZ;H7nRR6M?yMEkG6G=t{fSQsqG^^=tjcv<_3ex2FbP+#t znj*(gZ4F6yWZ6WcEO+sIaYbzen#?$Q!YrH^&sS-#)T+=2Uqg;5u>Wj&6OU4bspsk7 z!b5(;QNHmfcUs`(c60jQiSvMKERx$^x9Td1VnbiwF!;I3`9v> zWKXN+9-+Ir^=}tP0j^*Ri}CiOjGso6VujqrQ%!@KYO|YRmqbiamzr8xXO}~Wml_5} zXq&@mLBse31<=Cc6&I{piuGDF6A!?$`C%!Sp3YeXVlf|cOVw43w2H5{aq84tLG&(h z%cr@8pGNY@zGnPxkM$|EZ0w{9^!!?0y+vj&ql2oKxEQAJF8b^^6DjJvlbuXFZRt&B6gLH;eyE3Pg!~3GnpWb zzLdb_fTJ{n&SH#f+$EYPz$H}b4 zb;-LESqGG7B`oGx%;_l-wllZ1nV>|c@7@!-b*y&F!KY+inWKMH`g$d^YT~M#F?{8j z_e0;w3Y7fe=F`GIjT2RI=XBT(OkTA(rsRg|4+=o1p)Tr+@2Ox5lY|s3`b9>?hh07# z_{sE~Oy}A836i9<9k5vOTZ~zS@4Lf-`^{pn-M|m#X8=y@`d$y-hD{&*9t85U_MPs# z)}O_83`uT9dw^Mcv^z74<`+8~Cx}`tte&z}8W-5C7Kf(icw=0ziH3t(2KK2MRQXox zWc=k+Uv`+-1}4#ac4(6JTmoU*OW7rs-ZvCj(T&tU44Aum(=>m3YJ28&lu|Dp3bK;K zsSsR<;;wf=twoJ;WzsN9>>yGRwi*{a#7DtjFII~-^e*~xRKEa|%>+I;KKK*Y&WKP& zjcq%8u9O=;AfA(?vChZ8;$GGSA5&TVtAK&qMK3u6rY1u~`#!rVN)On;i zJZ-E23JrXJjmw@ab+SsksP7oa${Cck0Az@xr zeV*yq8gVt0+^P)K7MuJHo9oRYk5;gBhxY4TrN9OeY6^y#@NEP#%c6)iIUS|%#Bcxq zc4J*Cw@MLj=_hrno6h1h`xhwBvqO0^q0_Q8*2gWpi#E^qxTO(Fw?8mb+I|^{K!D$V z07^5=O|6oYN%614%5;@<_A&0hZpEIXHW`tAuT8Mh(0s4m@Gg@>R|Ef>SAXH>gLwqy zCn?g8PIvA;BiMp(HHOiN@@ADo^6r6eBP;aVQF%lApo|th@cbm0eMC3Zsig4YS$%PC zUEk4UUR#G%>{QV(wxT_$6CLMb9d(}OZ#U-^u#VxM@P>3rA?Q?W&v+*eYeJ;H*hIPy zYf~#q{2@b^D^|6dtA-bQ6XWj{2QKT3fjcU>lUTriT+$Vf73KmzM{D$bwSmw))_nMy zIm#)L8$UG z7Vb$81n2EecvSJ>G^(Y!-X5pOQdyq)mXi_pr(EQqJg(|R1`w#9_ji4?08|QXtpBut zr<*moR}&NlIp$$-7Ce2`-l?aJ5c3G{5=zj6XH}V$KJ-M6>mkWQQsvtrPo(Lg9rh_} zdpBZ;mfFHQkN$^&0gA1Tt<8s_8oN6N@|KDy?r#ILP+BkpM$bfs6`Vh(%6RZDCu=?J zP<5aHZKrh6n*<-bfLf5A=B5urui<)W6fT`A-S0L$qov+us!BJKTD>9B->^ALOY_xb~uRyq`(V;AI>yQ$KeITZF&n_?X$0qBI%0 zutDdtBAV3*RRuNU_Mfk4$(nvLXBmcXh};+(nm14nh`SjT zkV4|i7rA#%GS1po~j+Zm|NS6O9Or(UifzROkWz-dR|ND zws^i|JAf<(Sm3qwXpM%ToHZWs#*_fw$&@Yo`S$=gF(T+&=;v{8lkMLo16qc(kyeMj zb6&5=i1Nm8#gqG)*>Y8}j0eSTM$Y|1id7%e*2!SY2dKALf0wX|SUN6=SZ+&HgDg5v z3VcbA?l(3tApz@wq^};us(|elDvZ*-WH$(9#YmGC<6Q?r4L$I{EUvaynDyNBqM@@D zaVv)j-EH8)M@1E{UAo)aqmrdx6Vz2ef_{ z-B@nC(j)~*VS`Bd6vTu3<9>3PB3DRcnP0z_Kr1PMop)tN(px#2(UW0DB)BY-b2~@+T?<1H8Rp#HlW0;AEz6c{LQamD^NW$nNI-@O^Jv&(l*O6H;mSg+bd>K z;5S4+%k>C@Rd(0jj`4N=ef!nw&hxT0jz3mM5y@!_uC&(tqKJVr^~Ao`Vv+muHyVeb z&*;%srwxv2Z9n~7E|79#A4#3ryDIB1y!S`Eb{=kF*EWNy_f}Y@IY|to?kZK0n|gMO zg>e-7jc35I32E29{VAfvy*28cyf8x#6n{+j4I$WkXc{~h*9~*2^3tS%v$l!UrQ19{ zvgqS{H>2ttGSX#RoK9U3vH1`oN2}gfVJ=#Bu=Fd`lF;iB;`Z=Yc(4?>Ub zC?uC94*@D8Ki^0~IbKv$0R!g-2q=bDLlcl7Z>Q>i_bZJ5_A7YgPK+-ITkwzHa;V_C zKVmsHwLv3K_QS%*>!SKSU*=66dlWC!3=(ry?-36a$a&mvLagg}WnIamY6(UFwAcn4 zPqlQ^#1{n7Ju8^W51CvEg(z|q;0gDo4!?0L#wsgCP9;}BR!OCG&mQ|d95?+ewBOC@ z*{gGmv|GtoWbi!N724c`WcNxEDB_k%h?#%#rwy7?#Iz~L?4$c4RdX^5>Q>r>;^{N92tvugIibvV@OVc{)$GK7 zfNVk%zPP5)n;KiL$J**e%2k$U7cI3kJ-bh>)OE=vT1FxY{@6x}!ojZVjbV=8eFe6# zH{UG5B)<-V9c>syr|L(3OstrNesT9brsPxifQAQ+M+-Y0%#<-=z$@M4x`1t^5T$hc z<@rFk76+leM?{QCi^QH<2OE|zCvi>uPt=)0tlC8hl9LyoTY+8dh<~x)y7x@1+z*;C zOP}~(O@rs1oUvVr(-2Z8Z-xE`^}E%_ed+Y>tOXLGj@FdZd;5bj|HqOS3K~S2{h47< zV8x$4Y70ZukRD+O6zIaqyW!!aFp^n4n&sa-&B?4CudB=Ec94}?zqDVXqO!rHO|@pj zu`L1Fk};_Ef}Tpq5M&L*7uSks8!H`;a~tRJB4AB+#;s(HICR7G+jqJKEbcc-*>1iN zS#(-TQYhIt5QIMv-OCJ)2#iD}dl(#D<@$8moq_R0OME=$4~}j9+F&PP_EOqw36lY{ zc#?n)zeGF!y;P!m8AA5Q#RNuyGuh|-PY=-yqoehLx?!#IHp36C19ErhlZAoM%8m*93&KFMKS=K^6}rtHswJb(KG;bW{4rD2h!6NQD$ zF2GeKQTXO1TU@n}`up!891KC|bfM(VC9cfv(=z5Td$9V@+ z#;|qxfv0>asu;fbwsfcJd-HT3A4Lb7-t@*!4a=k%I+n_&88G68h7ahZJ{>`uf1$^>qCqwbRa><|h!mck;MgwP}zPLprrgqPQSn(Qq z!WV<+)48xT=ZpjyR4+%}O>f|1r)MXy3?*XAB<{bs=~rtINRf&ASEn72Ho6!$+K|}? zjLKLo#9%C2lvd@%RPz6-SF5nM2M9~cI*k#WO)zmkFzrH{hLT<-70A$^o_+bK8*Auc zD)!r4p}nD*NU+Sy#*2h9jSBt~8-r{w$gVUzeZ-s2LQNIi=dftECW%ek5!g|Br<-=U z1`Y~l{J1AC=p_B_e4|K772B<;U%e+dAkTi=W*v?KA1iiDOjW~ze|J71F`AFTrL?1O z61n;|IHsdwm2}e^37^Lr?o8f}=UwJ~h2neRgF-kJ%ZMQGQ@-l@#4LJ})L2z#P8y!; z@YcSpW~xm&5Ea;UwY71zyV9>yD3Q2^5k@WoBV)Q}!&*pSDjBA2Uc~1sZejsGjR;>N zatpO|CVzwt3MOR{a*Z?iVa$6>erDfxrqAmb1$wJl<`#657#s7z#cy7>5v=CMPueAE zo$jzJod|<*R!m0SXL-#h%5WkIRDsmQm-Gi<_J&OENt&H@b6?2spdD+gx)V0Mj5u$Z zkOo5|M_EkP44$`?2~XR`MZEU3Q*U2k14- zL_bHe7DrnpEP`&s_oG?o|JXBR1cSwJZvzC0DBv^O-WjX=kMZz*89Gz4Lq=#+Ln?C? znadZx4YPbtwwPq5`9)$y^5t{s6&`+cI486jQ)bkj&~I~wn-j>K5&Z-*XJkrm6AExj zO(wR-4bG;?MSX#@iz<;Y*nUN(57M0j-My zyT2_QQZ6bJo1iBBfye;4M+&IjxF-kxGuflr=bKJJ1fYyCE=lLK_Hy40=9uI7+j9p= z;)2}k^6>{;vMl?t1$*f4v4=HJe4L{*AP9ESYeU9po0#c5;TjJz0{-FG7@JjhH<_{G zo^vaY&QB{$WPr>b+;y~PV^_sJo)DuXzNiH7DNmkESh^yOnAx3?C{J&X@aWO6IHj_~ z--PDSw$@)e1_u8TjofNSHBOY$QaZm?=K9Ffha%;qeDJQS-%z%S1tcEkY2d&dvO zn&6$jZzxI5$`;bQ2YyRWn7;ejP{#T9SWQwr|75m%dw zZO`2yEuK>l-Mjp^g2X`i=jBFzW>B+^Ms55f>T}Qf?`at5GqE(_#H&lq?Ovyfc%I#b z72!r?8HxJ?n?gyhffT2cE*&%4M2cI1<5h;X53Vti^7s%j-uFlrtGUil5-XuxItav( z!e%CCMQ~}E=B@oCZa7~-4bL6RmU6_7z{ok1(=bvL=Js#|NYO!a0vnQ(xeO?WfY5y1~r`)1jvZt|edzvBH21c`V6^`jjy9edO5} zIqn#@t*XPcJ$IFCN9+`AoeS=F3HN{Z{)wtWy06K(3)FGrI@OTGUDQjt&(m&{k2m2o z_@=&aLQw0u78?*Yq-P2*%bbqL?hjKO8{%L&zGZA7LW<@F$k6pFZ$!t?)^Q<9eKB$B zn{;M+=UdfkDq?3^2t&mLGvh)=<>I{dGYRNB9p!=OKgSOCt!;G;w3%?*m7LJrM%-S2 z%P^l+)RKLjK^;=bsIv;4!5ftYdq=;apF0;SOTww>Djp%Hp{g62dwTVbK+7ludVIi0 zxgKxHdQ~y~wsr}doCXy?Y=NVob~hI1XMO!l#l9%ZUHk?dkw=h}))4LJ)uEgBw?r|C0iz(tI5E*?SX zl?i+Y&@3kWvwevKz;@AszPdLTYo#Fla?l6hj9DyXM3Ae-U;_-Bjz<{@Qu6Bj$px~Y zL|z zBnZ>qc=oQy#I*j?z`(JE)h;h5nUnrA12V~@tI49O3&~+#h;*%WN$-TxhKOyOObX{` z)EK~ecZQMm>XM>1P7{hACy!~=e;w5QcMFY|+e%D3-r0eT(lXcWHhk9u=^a(kG zS1%Glt(Azg@dna`uQ~`zU%_jMIel;birJ$>zhdo!RC`BZ1f$E^jC)R#4h+q|Gkx6#=6>rzR_bAqk?4O72236s6x^Wkjm~z~>jC@|hB~1#-Uf-e}%PezF7vpW;P}v7&QrNmHj| zgt{V>e_nv{;_@-y__%A0*14>O%p_P=>JC!Q5NzF)C>Kp}Ep+&EBKu<(O5Amv+y*x~ z`Zr@bQU`m7r|y4Ewg4uelI4b_PcNT7JApDj=|ve$Tf0pja0k=A7kj3))G%9NLy-0@4rGuke&A;?YqFhGCginI-qCS|1q zXR+kEEbqA=UjVu2ESoPY9x)Z5NOb>0E#)>Bd@yHh>HNGz1&NikbPV zbpjI0Gra%pi6@gc(A_cY^4;~*dA5jR44hb)Aic}07KYEGn&?{VUR;JZF37MF76yYH z)Le8D1nsU#b8Zo7CwGw^-IW?u&{b7A_M zObVW76!cz5MppKmI5`n7?81!ik~sWQT?!|@eGe{{%WP^uzh7%S(DpL5-9U3?81lRCgSZ=2ooJWkk_TcAP?kqQ8jhQFLpSUTB!wj zT_$U<$`-Nh9wz-F%E^`ohRnNIY6ZaC#7WYUd#^tP*V}!aDoDN9XYhKW%WT*$3$NVC zo8Arp#nMcnk)BVejo4F>iRt8yUQ-;put*%D%<4btz~YWq7L;9v8yFGVa?at0wh*&R zrOs&u*ZWfXz2)|TQt0v=1^Thbs2lu*3JNZag2e|+Ot%&LfUJ19VJAmEsMCrr8Qxum zwKhAp1fnZQBG)sJ6qHm(4|Aly2wLJRYzWABwWQqX8sH>8xsZRSx)J>~=YH}r2|4RK zq}Y_!wNsg#56r}AAM+~_?IN`CiMJUsZ?G7@dH8%>>c-4+n>sgDt*JuV3}VkWHqUNYhh9W#5`#>2)AYIeTRL9so^6Nqx(Eczgeg{N z4XaaE)4_-BS4F*?9(LHq7^T$7u$Izgv+Mr{d}k)Gh*&z6B&6dx5G~D3^;e zV7-8=npE&K=;}Qw%qn??0R8oEg`-&4OP=L>R^j>yB|m=P1_2ya3U9v*>^1b~*GAi+2A3!b)Rr>N}-Pl<%X$2jEM^GTYSFPp+ht*#X$6>h7|Y*Sw4 zCb6q;jxtELpxX~b&}68s@l+O)UvI`rnpz#F zYki#K^gSZDE!9Uo(KP+Rg;kO|yBm?F>sHU%sgBZ6B*yN&Z8B|vqP{_dpYVHrl0O3_w1z_O&+T)3W8t*2s_~3p&tPs5Ubj+M z8i}I5s-kcXayASopL_(_W++H|$Dh3?e%oe9pkv|8Fn74e09teH8%a#ipmOChmXOwm z#1~ik>^bb{OMz_#d>DlYpOarC`@NLyC^ z=G)k**1f9}j&D@&?lshgc zzrp*ujL@z~8lQ#b-z1x>E%C^H8*(-R2E>-G`d?a(mUbNFZX@|vOm|U&ubfF;d#QE} zqdg6s)nn!l`l!UEkVJON35Tvv15)k_!mTahKNkLdBXX2yRdboQS?2+% z!+tIG5-I}wdXJ$)uN_hCdF$s_&^q%hZ05ZR747jxf~ZSo>qIGD+qGj?fj5We;B8O8 z|C!HvjaIj`ih|@PHJEY5-mAF5FT$Xx)G+?@MG!fl|I`gYtfU$UQwjke$z7uwS)zh$EV&DkX zcq^d!c|Bg{F*M#8k*-N_H=;AphT!epk;7ERbbBja_rdObifsLfSzq%ah3%^z!uoA?wElEqgyXV%$G&aOGny?enwvRWisKr8AN4 zO2wWIp7Ce9jR&uu&^3GX3nDk0AF{4zU0gCCcO-CkemQRP$$E2H<>Ltv_>W)b%ok%$ z64_XcW#yS%m$MzH)Yn$9~wk{jPA$9u+7Ijfnk| zN*D9_X8(Fx9NqpU^!M-<+gVN9!r6{8G!oVz+hVsH?7!9%02V&i*eOq$te^oHmv8@t z*Y6rQFhMm$PAd?D7zuBcf$yqUJB2B^y*TJJQtpg$*7uxa_` z1;MkVnb6(nb=*MwAI^V%{ogC8hO9Up46&&>br(2`fnPLP`3_upx%u~d&bhx&RU8^Q zOe1LG{WG78h8F}()3xP;|NW>sQv1iH0m9CwEB}s4klq4tnlz>|>i&oeu){uf;y682 zAL@2BZu$?*KThQ@kJ5V)wHbvnM1Csm1flHE?hD__3_rfa^c?E`?< zl@V95>>=x4x>$>TiBAWYS;jlO%)o)XTl%R_`KO7O9`dO>S#zmGBjo>Vc(*IW5fl^v z+j0yy<5xKd(n;h!lY?E*zrm)@xUzA{z4uO!e%tS=EH=&D47gHH%e4(zwv|l>$sl;9 zvVdN%$E&#iUaITAMm3vn$kHlm*Yk|OfNwlvazP8F#O~WyPE#1*jX7P<@i!{a>V-xl zPY1XhCpWK%hlki+O}h+6K|K(T(@hUyXsFI3pb{g+C`O5okA%mXD6V~*vaWh z-Fc@N3Zw`M`K2Yj}M&&bMk)nC7MC5m^j%670QLLY0cA^ft;W7vBDJ^5ek4sq^Yv*g4%$9`*R|c*n~@2Va^eotMoB&P@|1 z0oOmZlzABca+QC; zsQdSjm<*w;w_4W4-HmW*Gs>wRcBM+x)t-FnD2=s4AE_+>QM^v~C`ZkSOzk^D`WxG6 zVa%7zYAVi5S3JqdHm0aeG7K@*ZqtAIQJu!O;xQOn90~$xd}6y>ux9ZSJu-q&P@VK; z?1WxyL@H2uTshDP*NEqbqz7V{pmS=vmVEaqx6?X+_iU(gp)SJjgbZnXU=R$|W;i`7Bjj@Cb|FF!H|JYmSDHfC}WWf$p}` z*<9zuK;KPBTYj4^G0)Sd0-RMEl&Z^X-+)iJE!`c0eijiD#P`tyYs`#PxVWy5wP*X2 z&}BP&cfkJmv7nkcMredrrfQney8te|MIk2k~6!O>VOvZWAUbSv<`x+lIZTILPYn&&sIz7ZsKBFjxqN^x|c zWA&}<4+qMfc$*_5D7N4X&RnJ9lifDYAUUlx;;3s2q%ZoJYf$HD;GwcF`?ICWlJWhD zjnL|zt!`brs)*ky!e=%sMm4_dekgK^!OlrMX8*-@H=5x-ml^~LQUCIA)dh>p6f}|; zDP6PHHVR~6;d0m=^1wnT?>~Tw->ZE{m@Mf}FWf(dNVBT;IeLq$ZGd7Nxb5k$YTLPA z^>pv5t=HKGQu=%`c0$j{(tS@|Xrp}*S16+~8zOS$D}{V8iWgEh;B100@ zc(jVnpDm0jDl`2nqjgPRWi;1uSg)yB$m=gTid zBR(#`WqB9F;@xJ4|FX)i-2uNe1jzU~f^0KLJ>Q8(a+apPreMtzzP6sTlm>Mu&+ z^dk%H(j=0x3C%J8g+am7;}oWz zpT(gcw?9usm9gH=I$$fJ+D^K6pog%g^oynVVBP7GAW{`UsO%FTne&83*P;jGyKXH4izEt z%(~sFbv8?X8G#l*-cRqaD`l&H{22;$6p5X_R@?cI@cxO?(#@Q&onO!9|`& zyNDiJMn90Z%_Q~p?qQ0R>}Gv85_W@@6;kI|>p;u&{&Xa!%G4AzyNI4Toxf)ZJn z{CKLpX%-8iWYaDbG@vLA(3|eQ0a+Zw<}Yhsv0}Z6$m|w<`=Kbm|NXn*c(1!Ecwt`i zhP3x%3ZX>FdM#{ndlLD^_OXg!{jKqB^;_=-A6b7g^qG-Vqv#P^SVWnxY7*t@SoV9N z@e1lrn>C>W=>wCO_KT)wx46g4+DQRz?Fp&Z*QaxDvND~#um<&jsAeAP@s;@O%`mS(C z4>6GV`e0Q*IIDp=jkN7GOr_PLD&M#t$dON%uT@)32a=f`fp`y)+O7sOQ?zaT=J(31 z0)YV-_an$xN?wayk+rT5_~qi{$?j`u5ikOJs=T<$bDlw7tbOj7M!$jxc-T7Dj6;F8>3|47bR_(t~ zSMr%ZW2du>i?n?H0m>Sw-rwR;n|BX^Jd6JG3y@Jd{lA!Wuj@Pfu#XL9>nLE4X5|AP z^m@+W#5%f{!&oS@c8uE#Q3-)5v_1e-h`-dbGxdO>aLbu<>z9;!r)27#EkDmye`OYy-?tn|jU`$Nc zG~k)`4KKyMFOZ(gmj#UJ@P`M|@bYgGJ!oe|H}T*!TEd!CB`=mIbit4g4;6SnnPU-D z3~0u`ty{Sc5Zzgvmk&^&!3h;S0MgSU;8u$9DJ3F|lav}!ac*#{vD-$gdvaud&#pPW zZ*MkGdLzXu8soPR*B*@j7&my{I%I9L^dMIfJcaobtY-*m%qu*$-s;)P`OjFu8+jst z>b{6_g|FydG4Yx8H#>S8BgSKF?lK$0m3D1D?soLXX$ba{;YpZy$@5%X(z$VoNFzuK z7_c*%X_W=bs21_aY4s?n=XeU#BwSv9GyP$_o3Bx5Ldbx9oxwA6%Uk}zDSis~IixlP zCn36OXEU?5`QXmesc(-XNA}sgcFW77n4zpH(426VMlLyk4Mmd2Hxfn{DWda-$N)$| zo9&i(s?Hj=Bd_fj@#GzAJzC)G&1Uod1xe@>2!7ivQ5*;$gcaDEwnTcu$=E=0;82Yj z?F|dM+;3I0YgN#tw?I0ZnCu@+?{tlPH~eW+0Fr`sh2mA(l}>l?RP->vQ(w&!$ERSe zEa$n;yjLemJ2(%g<4+5xQZpG_;(z!o{7QI+26Y^+hU^@q$15&OnZs)M}V-04%Sr^XP| z;q$|aZJ7RL&FOLo+vKCyH*vYh_B2pSi$OctsL>qu{zZtd-qy(TNbq-bERO+N_a5 zrwoa|H^4MHPr>@w&LG9tggsk4OXxvms09hiZH1%>^Yr)!x!@pNB4u%un)MgPU`OWD za+lqyaRvp&C~2uFyky1tYv*~a(oLSMgt6rdtC=Ni)NJV%?bP~MLdk<^>y<0vk(bTT#|q9 z0f-2UXO-hr7I60$xPGnPTzQZZP`(Ao6YoC4NqALu%-c0>`z37|8xHh(K88y(8ut<} zZnDIOB$DZ?DDe`VetN!=X5T;7J23l6d=f|>Op1FqD`I7(rwJht6`A|xT+wkHrZ>A?jIhSYb|%`f*Kbv(9j8AJ^}EXP-s5UYJ*?H zs;F}H9{SBb@^<@}ybwBm(rYOsYkQb;AZN8AUYdXn4Tn*jQs(Y*S49e#KL2k1I>dXu zYnm&`%T{m#1`N}MS}siW|G}mX7&;E_&E2xpq|>7< zK$M};!Si=0tY3Vk5Y3wKjq5qbU+rx8VjS6(cll_w0-u^c*)%7qqzU@UTQkys6(>>k zGx`Sst~*$2be=H1RG?rCp+3Q2WcepY*N3Nr^N|8<_j~d1TI(C_oVSN4!4?M4?IVu1 ziIY?bX)=4H;5!)Rx@ zC|dgm+$J)kc|7u_R?)5>1RcOB(uUppYt|Njer(Gn&ZydOrSKw>fa#Y6#G2s`{W(d- zc!s=u8?3Wrv{OldZ2L=Vs+j&rtW?GQU~E3UyP778#c#>#TlcrJf^R!AJBzDzn|~}gs&;z&5Y;4)_}4PhodoU=`%YFJI6a_FTsYf zqccJ@TsS*KKWA!sNjvr_9F>gPdss(&RPtjQ30!}FFc4?oM=!qEa%sF$;~)798_pi@ zj(@0_jF}0cec`U)*LbsZF@{F>?sMB7u3Vpp;`kWcJT_HWek+$0Z%O0tn8)`@U@I7w z9%T|%pGD=e4#b}PLO|9QvClVLy4>C`f7Y5P(Bpl72hEyzc-P+|j$Gx`c@a)yJR~GF zlSQo^{F`lE4p(|?)jw-F{pypPZNu&x?xc?V@_N${21o1HY&|4;@iiS=yZV~PXt#y5 zxN8j9vf*n0yT+2%NVniWXxya-wBV!-k+IKOr12>`Bq81JM}Mgkg6XAE?hIo>oMjb? zs=8^b{Gj$TT}0=xrm|J6fQs6?5V8FmsLnM8)_kP*w|IS1X;g)j$lnjrmm+1Lhd`v1!87M3|3WK2#7ZXzp;m<}p{I%T;mldOoZlN8FiI@@+v2)a3@{*n= zEj*kmQ595ptCZv=r^xf2&%;=u!G+2R&LN;>0z{+Y6m65!1ATvSwG~5qtv`yJD9y)c znCjBP4Y54yFQ|wVWtygQgE#f2Djq(?FOTTNc=kikH5x}F5zVkOu7Pc3DR{F|w^NCL8*tvi=JcIw-$c*M? zT;D!DV4Pzzfx86EvKy`qxF-_cLGb;FkJS6cf;1^mA%>LS_rCCo?Pn_#q|{W+dKMx( z$UpGngF3^&PQU8HI&G&!_+&C<{3dc}D`9f_+;`=?Jx^0%_K&1N@6?imz5a*^^#$m^ ziv7g!xs!NPal==;d!YTcel>FtkKDMR(Y6})&Ig)r4Qh^n%Q%+3X^8H;V5*M1rtk34 z5*d|;SA}mNtRfC!&EYV)O*%eD$c5nmiED*Fm9v{eEd2DOn zQ=+F%J@Hw#HV73|1bR*ZS#{YZam8w54A0AKnt)phr)8$*FVVn}2XDL>=hEk{eUCd< zmyIp7W2@~Jw0NyL84g7G2blnVArmLIj1{6+OQ2fDD&wqAi0e_D=d#1CG117$$5h=s z5N6qRPN8nv6&iKlXA}tw#Fh06EAjoqGs8`swUSdF$NJJ-QWyI~bm70}@v}hazS0QPrDaqX(pQXCu(Pdtc>F0RZYQ1@ zZoLQz`bQfi`oKMkuXCh0c)z3`x7_YY`un*`lHSf`vOU~Cc0EZChWyq$*}<#N6T-uk z?+U>R#!C<4^kRjKlGls(P{V4P*9)J5_-m|+#Cdet>QRRQ8=>~oJIg$~n$|z|vf;TU zVM&_A^)}%11iDmHf%TrB_Q3mx0gIcpgV?~62(md##o91j=@g-c=5tw0tA0-n!TNFipC!J`6PD{p#^Hv6%AY#$w~^UNmwy61o-(N%Y#_l(L*8(dKHJ zC$8H|x?NA8Blcn!HAOv2L?jyDk!kKTr-v1iQkJYAX1u{Rj%Lr?n(p@s-DeA=HQ~q& zDU0`f6j`33ir&>m%)(y6XQH@?jCM1+=aHX!S9kM92jlz>&o}He2jU*VxM+*M$*4Sg zW!GxrSXG_BjDj}>YK6=MO`jKIt9N%TS&^RVw>yWr{%JxTBiBol&(eP z{sYy=qFOS7A;0K?jbodb%V48B7x_#B2AIANBaMf-Tt4qO9DXfcVsCv=V)Gj9GI7Tn zD%W|HGr6jEAeGC2Jv??DK8^MWg|#S+{NCGy>7P3?5omd%gOPRin%il=YEsooiE<0l zruHHMqr-(K53V5U%9OO8u{S-)e9&ZY)S;HhJuy?{)&7_em~UXUFXi-0Q>!NNyj1N& zs=k#g_26#cga!?0NRO2?0_J!rP|9bhd*h;T(o;>G;|DGK-9^#}q#CJR?&eQXG0z|0J?E^Cobhycuo&6Z47P^Ap85+& z(wox2E*AQ*e-TLxQ)@}x-{W5sO+Odtig;|D;xE7c%>Cq3bL7qEI&mkcLF(my2JO!{ ziEFYzOU_h9kx2EH&wxVbpJ&qTJ$}wpiE6(U{tk*9<4sEqo zjrRAnL(46l_(NKQkbxzN$7ec_eYxCW8P@|95yo6wM}Mj2GEQ|M4@Tl?nl~fm#JIqi z)bh;uh-uR)84?!x7P|B4NNiQ_m;f1-2yQX>FM9TnYSw&ob`fu1@t}Wz869U`AfdwV zO4o(PP->eoMv|C%#{&|tP=GoG-hL+5`uxoZ=wqD`%Drsr>a~*B7nBGZHk6|OK?VP^ z+~q@Qi6r5M#c{>y?twm(&dP4TGXR6pFaFmFKRWcC&_Rr8+k< z*wk4L>w`dkE0SRDsjhO+d3EW_NrwrE_Y3WCCIcZieW1J%6Xpoi@PYQvbsRW$vaxyY z%`T7*-9p3N$=@fBtirH6#u`wSJwUks2u#nlFNC0ZLR|hLF{_TiXdcm%1RsSd5A|#B zx%EHG9wDLyY$gt**CoTjca=1jZ?_a8&+r-RFRReKeioIZP$}V ztWh)FZ7ED12~l=_QJ=wJo&?!k1A))Fz-6RjgIOa|lr>;?I2x;COx18(YVuh*{r!Qdc)l>2TW7m?k&(c#<>5py+JSA%mH z3&|rB-5LU9{e41(ZSefNTM3gEZ4U1;CeuQdQ z$*Hse>518SHqin7wh%wKQ-$Xu$YGnJCg}0N6k2Jx;^jXlD zUF$YWm06>7w`5X2191fXn0rEwm%4!qXhchQY2>pC&Q-wCm;e zJje}?{_&~C_UC%>FVoP2ylAW_0=SKwBze@{Sa_G;}F;GMxU<4|U|xd|I5K?`E&};K?=&`IEUc zQHLH`Il(oE-z|Lc+&>^doVfV?cZ@vXz2RG45WQ?oA`b$=G4R9&(y}hOfy-d*+?(** zYV^1)IL^EKsg(K83Tk5I@L{iu{-*WcEouWQ>8TAgjK|)RE34<1*a}#X_HD_1j_5SU z&r^XQ44Ry*@j<$3ayTHBnQaz~J3S!CYwHc~y=1JV>hi8&M#roDAacOu@j+B0*VjX0 z$8Xe!od5{>!(J_BYZqkm9g}9}hxIb(QXHCBeSIW5+oVcC`$EWqmC2?vqw}X-8W~1> zCpC->Y?A@^571n6>&uIbVN}E{k5qHC_O7VC$>F2nxm)>MM>;YpDM zxHi|cVjojvR}*Vz{B|5}jYId5Wm}79OCIOmocxm3x>~L^F`8 z3+;H^e=6K6f48&bdy=+fxukF3!&(R_(r-t-W|U^6%V!nCpCvx}ec^#(d;ils-?XGi zqHG2KeWN3^us>X)0paIdqF2Pvboh+0Tq7EKS#9GhGS{k5H>3MPVhe0GRLYGk#-D2* z_?&}#AtIU$MJ(yE+A&?#Uvv1@i+jQKx2Rp0@f&2EH?}r9senT-W%BN${}?)dhy29( zGPXbEWBwSf^`-jMn#~<3b2lz)8kV*Uz;gX&2qu z);X}+3y!P~#8xj`vn3PUPl;I6h=F#6^oH#I9_9dKFBSN+B)3KHYM878kB=k z)i724%yqGeE?QuDRZYJM2-B2@<6sg!?Rm*eIyM&*c|#D9zZM94KHL{W#zd^Y4KqD_ z{n>n}I{HQ~II4dI*R+IpU3$yy``JmZ=Xs1)S0^$spdvPsV9UKSdXQnq#Ng}ArRPyZ z;rnuo58X$HaoV8^?oAV^@o0NYzP6Er)<{k1Rb|QdB0x^XVz3QuVp;NZLYu8`Nl(bpTUuFmEAk zRCM5zRX3K^PF-*t2$Ra!&2MD8e8C@@Y3i%pk>v~JW_RSUZQab@PvkgOnH6?N_{&F? ze=s|B(lvO}nz0j^VK&CJT1Y_!G#~i)SM3*fhV_s^XCJ=MvUBridkx<>1aNm8?JSiW z_14HJ-Vtv4eF{@Bub-i~nSZoYCmGw^&1hqdeA$5mdx=SNX(8XEPjI ze{#5Z8O@}oT{TVpB!6t0@d$Uz;LeFTp=-;xG4M*0=M*br{ZW^aeLgF!f5s+$SBM;0 zx|qIaNI0DHaJ2#I<=z%nU;TCLiy`1xQiS1R6iy(i-z0X_f%ayH0(AKO0q_!C>4c1K{$_ zx@Xbst`$U#dM-qKqh(>_Mfuqo9mX$CdNEea`L2&}>h*&TFVulr=KUSVg4>>j&*~UX zzH!(d^S0#E7Z&uT&`{%$+r&|x=N&JdVX$c()UvKb$h$GRR(1L2Yva<>%%MVpE|_bX>9G(`$(HU zZZEFzYS!3)H^X$2wn3F|8+_QN{F%i0FNV`U&y(H1DRto2%LPBIoU%;}Gem6u_wP>k zmnFrD`*S1nUy;YRH#o;BZYmJSN6`a^`afvNZ&!IoRKtT79xmuGsSW~ACb#6EzJ2?= zlN1qD(Hm4KZF!G3`|NzDXPRX;+!7W#ymtQ|&kkIWN9&)`wggbLXC;6BResP{ud5*e zlvDTdxHQ#N)^%DmR=mZR%=(S(3yC&H)SMeeGzwxgUG~^d8PxksV%*fd-~Zp~S^YOX z$%dv(*fah3gf*k~majYBDP3X)o6aAYPLvz(gCiWjfZK$xz{PYwyUi4urfARK)@Z=@ zzkUn5Up@5Sc5%NWQq1`J^%r6>dHkl11^{ecKG0|(84>gG<1D2g>vR1|6K4ejxP~st zc0myPzZZ$Z#3BCaYaIhxtwS2_x@9&)Y5;IEC+gkw%h=$RO4H;NWaAbq{(ne^0|lVr zt6xZfX=YQv@lP~iO9L+Qqx&9HQ$uGd7r)UH~X4%iZE`<>Mi>R*! ziB%UP$daj^tcsNVYjGFebq!W3$XZ95I&U2Lf0+#I!vB*RG&gi#{j>z=jgwRqZT>Y= zluj7N?Ji_la5CK9YD%Ih`hcH7T+oBG)c;=n`Vvb`W`+7CVZ;_JloS=#vC=6g0$;EQ zbRWLyL||x;%}#d=p9eV!&VqQS7(OE%n{GJy|C;Is?BBhY4gDc%xDM@n;to#V?F~Dl ze=eTar%!Q8fItR7{nVW_9E|&I(df&MaQ5*XJ!NtbWU1rv|1=w&z`uE_sQ%)4@L~L^ zzu<9gCC2(gbO$#5@cqSZe>c3<5E*7XUEZ+pz){!#ujnFOLjUkvV~Ezoh+|!zuKG8N z_7^L2M8!ftc87`z6t?M$T0&u9fCGN9R7YHmB+1QxVm*lquB;zFO2v(1Q2v|L9`3&c z#A$H7-R<9_eaBFt^opiJ7d5&{S9v;LRI)-_9frQc9);c3WVc2WKA(Ih1mGV4w;yslS_j*wJT3Lh3xd2a!P>G=?wFt?Iylalg%WQy#ZV20#f?9|Qc40h3Fc zVdwbpjt%&>gm?a%7y%REseDXu6X&?3pq{kFh)V_p8S5O;_;cBnZ*~OG8N^!>5Tpqq zSVSXKC2)DOMwXR6R$(pEywcg~v+S^p2DYT9w@XBpNf_@jAp6!>wR=9U-;+90CuvKw zK_WBClk!L%M`!4!!a4dzE57G6yZI`ugH5b>uy~G8A-2+>TEA)VX?2YCWt{9A1Sm!h zlCq{2LaE7h#d1~fa_?0^UURc7+bi|xnXYw7k&CcY)00$G!3`_7G%bO-xZb*&K?0j% zFsG~XKqksLZFe6S?V2B|!q-T)9_(-cz#j&xlv}5CQoKmdAw+K@AFd&>Qx0EVLkLGk z78F_?k!ke2;kUQTEanb00c&?`7wftcv#ra^XL0I^8Na*xTp7=#d|(BiFiAT<`)C$g zI$(fgR+wb$HLZR=QQT+2RWpoOI@0IE^NBycQ_CR;xsWHnry6TTKi2-5@VM8GT<`qz zcgD4))0_cRe4w^fAZmphxAOUNeenKh)tgBUB5Ek0wNgP#$};lf%4Ghcd!uMq!+<07 z$PAzHuv$UHj~TD=e1qC?au8?TNfl|ih1&BT5JiO{%Sdl}SRtc{eQsu|A{X8yAZgq@ zekT#T>CbuEv;!n9+v>)8nCgXJqm};YVzu+K2vW+<2BMqt!?30vS8vu?<+4jjvqrdi zpX1(;)wCBR1jx}$kE8F?hz{kuU!(%M;Q~7IXV}2Ga?mz;5zBV#Xw)*bo(-X-s%CPS zd82ea7Y-5;P^4GfJZ1Q}Vm;Y@AEfPRS+sKtR3ok=`C?HUaxDNb^*pp zxDB9{cV*9GTHhjzJyb-`S`D}NzSuI$_^{(ky+Z81`?|iwQ}IaVyKbN%V74?Yfx#bI zFucq6bW<_pV<277?hIIhO#DRa>zF&yJnEahZYZ9v{uP$E2_7VSSJNKNdtft9LYV@& zKO)Y`(U(nlR?wU6PMtS{%mublK2a>Tm9hKKEG>s-A0*p8k6|hZ{H{+wR#yw1q>GwO zf4=bd*Mqc}Of#uPMvnHI8xC1}xn%|gRep|^o`=DduV|vl55QzKO0`$+CmyXBfkL&H z+t>ityYweCR=Zqah$4Z6>#W+{FssJET+R9g$4=qJ=T*;N`Nks;4FTA8Guh$WCD_!i zV0^njP;{-DRe~F49@SL&SE>fnMzpK7Hx3Q-r`Dr9Dt|9S7u#{j&S`@L`+2t5g#&BL zK2^gl4Sw@t|eGv+$mc?eORP{YOPdisqs&_3-c15nw^w`|2Q z*Iv!PNUV0Dh?-Q6qBr`a_|!w=gEKOwGqSTOHiL?xkH^kuX80nw*@%LnkN{72Y6P9c z0zhkYP}b?nJY40kHKYdYmjZ2MpR@^+s^K;cOg-o%I1!6#dy&-O^2Dh~t>eL!gntK2HOP5MOExSlK>Oex|TRWuM$+FQ;vaDaZ~wnw6qB zMqWBKA_{wrmpf`u%(B071v~-)ZZIQZNbcPVZz{$N_YzRT`BrB#+@2Rce*MAlK)O@jXGxUr+O`W&;=E}|Tplsl?IzifIsJpE&zB1Ni z{>o8vMEc}C2d|5n9WsG@f6URCgkPV{FJwNjJ^zf`WQmtQT(;85aNMkg)yk*HwJ}4h zJoB=M7W92J@Kov#K*QtCjt!!_ixE`2d;eK_U~$Hdlvb*e@Oe+UCf~IUYUl#P{+tte zQ>HDpmiMy2924WkWcyS;`lt-7xO#>}vS>*A%Gu0t8VZnMNRnQUC$Im>G@sKz+S;fGH4g294p@?><*zvFcgscZy+m#nY~$-qC9FuLYiUo+y_bv~#{5uQ07T2v==pHS74k7oa!*{q)H<@qmD*^stecfYv>R!OG}y( zzQDSG!Qz82j{u$svU7fER3uvxK@%G+(5cAn*5Hb zR6g9n!!cGGca*_FiHIA@!{V`|UGy1;%{c!o_qo~X|3@Q3%myy=8uK_4P6?Xe(;u}s z=P%?n*ijVwDZ)_~+cusN;(lqAr5r)tY&exZ=<$I?kFYo~mS9W_u4%V?x92`{j8#|A z3vzYPB=V=E7cZ=&y-;Mu4rTlM;F|%qn(M>5li?3 z0=HY|@(5`96Nwfydon=*CT_9Y(Jx?Ag)x7uzNgUyUanME(HsI`ysuQ8ezevY++OHq zFrcL7I~d>rR(A%s8ns8drllkD`Y769e5~f16Nw4-3{n2d@F7~Q?Q)4gLe=p**x{o> zZx#Z;V{<4vq>ppP0r#|sbdCI(xfkJYcgFMR6nTNouBqim`m)l}&6ux5jZCWX8u6Lk zoX3;P*w-8gVoY6A#|e4nvVU$2qD-kV!2Y|QjJTV#Jtpi|03s--r!x){b2JuL1KwzI z<6EB1>>hT9Wjzq7j- zD72t}THD8O!bqOPAe$MdgaD5*X2VkR1;ib9sAiE$8yF6ATb{TM2x=qc3Fj&xhbIL> z9&Mma6upOjrQ~al!%>h&Xa|~>UoGD6Hi$kTnFKeO zwwS!q!!k$GB~|fv46dy^HL|xq1Ex;6n?nxGR*G1ec)GKIrc{U9p6uf3{Us?Fq} z4me}od?ghwDJW+jKd08~k1_WCCBH2gBtyadxG=D)}mLr zlkTZD6T@nzFD7|crFSt-JoMhOG3AN#bw)p+;R2Wu#l_4kt8Nu|3M{^Q)~apjJ$*WS zr|ymr6H2ce&t{*cFD!VuJvm@5(l52JjJsN5P41tiuqnT0b}oIAvdL_F+GLWT(6}F4 z@<*<)ZAo%|`i?m8X-2tq@-gMl#R^}#H;M}n1D`N8_DXRcBY%BZ$3P+?lEy7C&-HqN z4R`{hP+AIm0I12fu)t()2SDO>PfwQSp1EB=;w(l@FnDeeVgAlRp1DHjq|$FW4e+Rk z0EC7egJnXme)ma$yp4*ZiF)2YJDk!jt~uBAB}4#C`0cSCNpbnd>2vLhgiP@jr`${X z)SEU2&-}N!=)B?aRVK&;bV#{QAis^kBm>!vKbMkcbh9tJ=I&Gw*y&)4}uLFeKL zhd9P-e+7=dR@qwpvZp`wP^WvxIL-pR+(IQZVV=bKnl{?ja4`5*>~Q9klZGCd3lHiK z#_BW~P1eDlbdSm$WfIGdIHijy0dKechbIM+V96<_?X+uBEl~k32Oix$LT4n5wE$Gsc{e0!he_*Ya%A!wP2~g%o4(q&+jXgtb7L{5Ew(2TCmjk|Ne(uIz{M*0sD4k^x9Ke{rEa-jh(wkp za!R|+I~0HNT>OiIIsOfu4SQ80U0?(joc71f0iQ9k zQ=|(N1mP3UQA21qD@D}_P1Eo%FTXDa*E{^S2sM(guY1C5#&>?2v->O`e`o)aB57lh zTM#jsYEYf0FO?CSp!80W`0i~M%xkLE!=e*X5|;&QQdgem=Z-ENRhwWMq*&BFXo4K9 z#@VF6?zN4rTCztK zbZJ5ICWUxc`9#ausBaR6@k)SCd99=JOxGG6s(B5Mc{dNUgIdp@Zhx_l;x^>#McfK+ z4WBajm$DR^6L6Ip_o2rgo(ykK125mU?~3Sq0j_MrwEo7Zh~S8^>k>SU7{I3W1jxWW zFcB`J*+N(f6^Wi8nB$h)DLH7NjltDWVn8HGcPT+djVup`6gz%otsqw+9d0P6RiMrC zZkoV+N|0PR2r2wwjTa@RNIz2k4U7?6S3m-8=v^v!>e%XWXcI}V7Pxcp9tVv)o13N* zs2vnkz1%b8nIlhr5v^a5(c^qx>d?dC0_{N6a@uKKeI&wySB#*D=*8{|1Up#-L~CqeXpWyv?yy zV<)$YIO;3D-KnIxI5!$7H+$agS|CKz9gndlphBD1mdN*Sfr}N2Eb1lX)c3yTY0v_u zP#ttU<;N8HA+Xz-8SaSKR`SYg{$_Wi1$3*ReXdbhBVVjl1VP9J*=SBr8vTYkG|q63 zOBDJmaUn&DPCeAVD^&n~(s&TKrHO!5#@~iMlM>*GFN`KQPoQdo`6H!y2Y3d8FdJX9 z%c4-hn?BFheo7|WjLb|cs>MvWVMg}_ImqC` z$9ZT%@MnK-Ms1wXf5KIt=oa}B%1FoCFi0*V0!f@dpf!MZOXifH0LB~qQ!@Izot&{3 z;_8DsUeB7N_}+E?;2%9K3`!a%CaM%*&(6TfX0HiP27GQ;Sx*Z|r#bcI*T13f1J?ER)$6W4?WowJ_1k3>ZB* zpuL^ymd2VXrOMDlxyni^-zkZ_?IxkqOqIn&Hc9TrfG`BP_j@x9r==Z_$v~~!N&%jR zyHmKMt@^uIEB}|W%Y9{MJoGciy?0JIGL89jAc-GQu;5wpUou4w(C>_e$Gy;m;EhSA zX@U^%W+V~W1;gV>aIe{$O$w=UmG3#=!*BJp!4U)3^}3~-aa>T`;W1U!axt9 zKVncfzy?gQx|r}!w<_WL(wgw^);WdET?$ZrXTaobp`w3&lfSijE22?lB{@813%8kJ z6@NxI>DDP3=PI5w2p!zSDc06$l3Tg9G&@;LC2su;)*c9{tlFrbB~rBp6VT^l8hcw? z`5`3}U#|hB#|5%uSievQMU@x_~HfM;Hj*Ek^>04pmHV>QZ1< zIl2DFn|?}8%fO6wqW`dN*=9F_Jm*V}>KRhGTf0aME{HKtsp;P!@eF@6sr6l*lU8(I zea&T0X(=O61kk%i?ADjK2xKgDzN{ zO1_B@+9 zgOq$6hJjR4pb&#lwH^$Q;@;f0aErHT%kj_+HS_o4;FuQ3p)R$q1KSb3N2bJyAtZ%z ztnMj}x<})xE1UygB-s~M^&Jnp5uA@yZm`4DH~tJLX8maos!hu8?pEI_b_r^R-{_oFkQ1it^5RFKt^tR1Fj2;F@Tl zLRQz#zfaBW7%GpW>NS4^mmkO^+=0B2>G#v-iC3$^}HM{&clKYU*43wIGsaZW_;iZU$o3 z59!2C28z(O>?#AsO(!+I^NUHqB&DtO31bOXpWox#Q0LD^i9I8xu_bPrsve5;U|(I) zC+q;hdS3exsZW+x)!M_!0>a0}z9vSuL8a-ZCWd@{MS}%f#J$LJg?4C(7qK{zb63@g zQ?)?e>zn6Qr)}hc6WR~%=s|U;t_6>EQ0nfvgUG>5W0qyypHmR3DzBEH6jO<3%98qq-|Xt-Wv^N|{sxM3PDO-V1ztd>lO@ zNUV@12F-EpE;8o>7S+QG>>Rfz@Y#hHB1io>Jty91cPTS=a_UDT)oL3WW8rR3&=;|= zf`sHiPP8=kHzsM?A<}v`rrH|pP+@C7^3$d##y`ESxIah*EyEYAzu-NIt#oy|Eug~U z<)ELh08w_qM+p@!b#Di$c{?f(~6jAb&mNoSDaS#<-q`s;8>p+<4K#pVllcWFZK z@!4BEox!xD6Fbe1oDG4L$%Et@PD6zjnhW!G84n~hR-#`^OS%S=7r$OG)B@pvjaUPd z-BBrFHCkr$7_+R)UcU#`S&f3E(yka%wbwS$?f1+*P{Y-~W1UTm-c~@8x|Zu$xixY) zC}uD0?@R41%^K1E&;@zZ=+fRhw4=HH+XLk`{^V88vP?>N=P-)qB|NfPB7qE_pTBqB zB7Y$iT(FYK)RzHg!DCh1jVQ4EF$isI{NhK?!g5>t&Z6f%bhDh#D11{l;mI<)*&*2X zKq}fq9Su%q^4jX!$J~1~tqAqLdW|&z6$Y3FA+~LCr&4St7m7Tt^?joNv;z1z@**kd zq@D3!jVpo&8!4lP{@Ma!*W8+)J4w1nWLG$ehrghJ5)|`*CRi@-d5s%uK8v9APaEDz z{9Ra@{-o;+!lXJHq9=is@^jzA^7=>eXMSR{n@2TpHccquBH0Rm=O3#>&;Q zvjVSawcOJ+)37_I*|J2MwsynGsYV8~36>y^`lEl?3GNC zUP}l8bY|>wn&vBU5kZwD(>D_-NzhPN^cP~ZLW91QnVFlv#X%xg-#})C6Cc?dp=v zGAwa3wgSkK*wI|)F7Mp!=~==6E#G(z`^Zg9u%@dwoo2UUTl1t-W=pjiDPr)Rp}3oJ zfM`Xw0v_9n)f0kEuI|A^x%;Z`xFy|&T>mH3h(NRUtt(oqcuA1)ZdY|_x<9kb@*0`urj7=+k=0d|SlhOu)@)|2=KF`#e zR2djwZmc6;uC$%@TQQ1!3;;i!Q46DpdK8cB_yqUTpH|hh2d_TNp*vRa`k>A#5QmnL zmWX@wuprqOt{O~jO*hN$H!@()GzD*scotgSj&v3c6|}6ipNLwsFul+QT_H-`uUSuB z#5|yRC4qqwa8%4rX4j)d2-ZY$7b(JQe9Pm)1Bt)`NhhSb+N0q#a(trWNSsMBb zo7$AO&|Er(s37aI5$uD4oJixO@#Gp$$Tj7+b@ikUvy>$J zi5I@e>!Ep{5o8gsMI)=OuKm{mk979y2DYhK7U%Z`2}FUqC?&erb+%-jKIjx5JT|^e z87Hq^wcR%8fbW*t!0RIi(npF&JVnuN1yg_NK6M_-^|i|Ot4!FEV?(_ldUc28nEGM* zN)^tqFt>7(mj-y&2Cfcv3W8P&3Ep=pI*v-bzk>C&1;``ON8s=#zEfqjehCEmzDdHW zql@zP*aJKkqX@REi4Z_(RtIeVGM>cIawR!Q43&7;YUi9K$!)|tyUu3#!sfbHJhyq2 zYjs#+(~M-M=dX2v$Yb_$rZ(-DNr`+{5C|a4@FN~%#mz@>dp1T755Ys5X>Vc1-c%mB z@KF^(cUf-mt^&sCC{Pi7d8M1;qT7fY+}m`S%7VS<`p&w+7hs2Oo^)e$vP^G90*`Rf z-4Tf2fDnRWob7D+A_j{@>ho4Jn$v3ZdaVkM(rQg~tm5|U5*dPd?pE9kj`_>9Uf(c? zJK=hEpB$_IcWNDcEjHA<0$GW=5EIb&TTO%`CJ;EBg8R1g;S;1Lf7%a6iykv7R zI!6{0^a-pE*=*8ka}h+r`uRCIywdU80CH%hSvZP{2UDLvV-+h85PlS-9kgGru3ker zr)M1+tJkYP_7xYlc{sYVIoQP!b+Cl z2aPlPNVMb+p=pfNwv}HJ24N#Y#a>9zPw#G3Ok4T&Tj#R|Tq}w(G;1B&-?VEFFSsvG z(rFz9&ra3p1NU&mx#Ieq+?%G}q2oYwg{Oy+^BkpGZMN{3+S12i>G%G(Nxg;=V|4C=OIu-NZ1MNn_P1q= zW`W=ei~IbTIqA3obxoA5xKmNT*1K0xhvBS1DmtAM70ccerSzEHVS523SMh-%l8*=+ zB)0Em+{LhLKYeI0s}{cal}$_Y_9S4vsCyd*3hDUC=4C-d<8Leu(3h4YF~Z23@jW({ zwAU!n&11KZo&OKo7A-&^hw!)rrCj*p|9E?ON{^r>(_aIC z-i#x}jynI~2A#+imyrG-WC$dsH)sL~AbrQ%9e|ggmg(AS9erBkF5a-_^sc}`=S>aA z<7o1Kz%%$w`v7rk_K&|Ix;xwK|AEGo-jK$q(IHZa`OTqByR!sJ?UKQdeS&PruE0C! zf}C*Ez*xLAJ^T1~ZOBNd{_KzJ0zA9R#8SqvT~Ak4{xu=-pWg_OiUk`R=^V>qV`E<>4Kgz`2enuG ztNu`{f75eda!*5GFo&ywl>9&6{Ob(~;a~OsXYv1B)?Ww~MM7_2C?qug8s?S6vh7U1 zh7#Yxrs~BJRYoC*)I?FfqBR|3u?{YWRfZ3om6I|8rE6oU%H(O&VPH#E%NLEQ6#cQfIJFUcexes3Q*{b2~k0VD(F;x>g@K7M$lqWVV*0Q#{1 z0t)d)yUeVj^n8jsb{~Of(O8cxU<43M24_^43yThAi$Fs=cg3-LP$mA0%>+pO#SDW< zL}LRMK9?ty-yk9)?upTm;$&(F%UV3F-89vM%@fM^^u8$2|SNqkJT>vI}?QyK6>sWCwC70K;&>V_(c_A6 z@w5>Wn2q8v1?ex;Nj8De!3jVxX(dRVn8d|+`KK)9rfQWVUc6(o>$k54vp%@dg?7$r zqc;owHThsH%u2eljuLJzUZdaq4=vbP<~y61rJ3&cMLhMM(P5Cz|+N;Z#c+sK0y=bHBTte|5Jo95ja0+QM<1fK=<>> z!CUrabv3$(b_DAkEh`%h>%B3F9!$q&x+mZ0Jg>+nt^1wTd9#O>M{mv#?%Lhd+R8c8 z8h3k0ehrK^f?VW{N9BW7;Et_$e3*@yItN(Iamb8wy? zv<~51;%$C))&T$aSOeRvU)yCu%Bn6gQG|_bVAi!Kfnx!WEvDHKfud?ms_6SED7~$# zHqRjguZ%J_LCx&5GjF6XCO8}F?ZA!Cn-|&cI0Is|b(+)5nrsxj=8vGo_y!j@T3AQgTm$g_F^No2D|}9i*L*iuj|#v8_?GhGhg#W)VaE zKni6J=Efx0=o7D@Hf}zihmuAhaa>1VTeU@E(q}wnf5o(SWuI( zVMgnPaQn{$N$5zf^@#7aIi}aOeuW0k^*dTL!EvJ(0@DDXn+uA<9ucz$qU=I@)NKj` z?h3=)`X?~ zsD9I3{oV);?bave&aR$4rSgTEy8_Hd2vf;_P^T2kQU!jqT?k>F~M#x{- z$E;vYEu1#G{-(ZX$ZL_U(I&K?M$FftcPpqG4f?jug%}f$=q!_k7wE2?Jggn5QckmAPS+>;@s;^pb*9s=cPxw;yz4tISTGsEoOvP3N)(%=5j%werDsCoiMe;Hw zT4Wnf5fcN+Ly+LRhG`6Knz~2Xwb1_c2f!T#4lOg6@~D`NuncUSq{vsB!=9FhD|2BY z!Dhd_gUgE3L=1DV>1R022jlE8Y6IjQ|It&sFG}=!!r_eIUBx9n2`6?Ivh_E%f#Z%U zOF{9U@v$!`q)r-eZ8xgUw83+VlJW)ZH>4VOBWMY~=w(FH-uPCnlg#&M4qt7)|a<92H#?V@P`+;_Q`^F_&Ai8C-L z6^&*AiCmz8m8;*k+VkX?*0-?+#o0&oXd&OnHj=h&D-2_}Z=4SS_%zn1HV<3MNRiKkMBk0U$M z#f&m&Fiw2(+Ut|4Tte%a6CXKbo}TrnM!T;W?3dxJR^T)?QE%gUCdh1F;(H{3JXE~_ z)eIHBwzaLUiODf7k%F?1abjKW8aW{9nR-C)wH?r#io$eGMs zD^DR9r+}<@>)oF_p+;!9XO$H~W0)F~teviW{n5NGlWRujBX{S0RUY=$#qkUk#A`my z+9-ONEOhkR60}mPuWIplAv}D-p3Yec#uQ@Vju40+O+>)LzxD3;gL-L@pnRJop!=UcveT@v1Erd$_ym|6m7?fdJTptEa>{DQoS z1qG7YXM@V-kpjz4;y!kw+B2!7;Tf*Ts%lv@wCA#qt z;spboYPtERX5O38qv%LOp0!av1@{G09DY6BHn8PXI&^k!O7Of>>7Hn*ygs{|m-6;{ zpj^fWxuvfChTy>*!nSXwThx&$|fL@>b(hdVqFPR9O{~ z>|-6%chAu8`D;W&mY_K8R!-RPrP8pMP6Quot(xdH)ljg?_ZP|{xi06Jk&B?l&cwg| zp6XyVi$@s^F3LF(w6E9=iUSIgv#C#O96BqIKB)Tii`_mvZncwvpD}M^Nv8^yK z0Ceqbzb|w%pP=!Ri=>EZYDCV}qFR@Y1EioZEA?WB3tSLSAMz$LQFqU){&ra*l6I$IX zpu-*GxjKZOjM)OKAqbF47~fY=LpM)@!}lfLJ>E2RKeThvKXb|kj_JOMq9LeQ${4di zfOfHoK4v6e|;al%2&ko3kC(}1U%2L`g+3_ zE~r-F(cicONjEDF(w5b)57%*LiAAlL4#uT#8F*}Z47W71z4qborYgT-4!Hz#r{$z@ zqrDzL!P^I9#NgyjUaCjfc%~9f-(EwQdG$Y_B7zOr6acv{>LY3oQx$7;CXWUBkBl$Mo$On zuy%4oTHEkfD{ZXs_+uztQ*TrSCwCc8_-BQ?`?>ZgQso3=dAdm-%kbS7x@WRd=BI!u zJBo#<@EV$UNbDLxnYZg=Ryxh^Q#s>^G7-o1#p?Zf$LP_SKHq2<_ZlZ5Lu$HWkB4O( zlsMZol1~7eTSf|Qf@GNNG7a5-F=gv2QjJn^z^@1Vj0N4oS)PCCPT8vLU!ZB%gdd?i zfED+lHk4t7Sfa5|oQfTK+k)N{1vFQIfOwtC7CN%k6YRMtzMGP3GX1{7qkX^FXS-sD#+K=d zt6;YlT5tt~2&YQ$plRCn7|whWbTY`87+01+n!W#_NJwnQ27A8yED?&--RDq8g*+}W zPdta6KLPYM-DjKAZ%Re5054y#|6L^V#7s4?iO;V6lplC#*658KjpXM zI>5rgc1>yx#4f*1c`^&$IGFyq&Bv6!Vcw!RVDfsbxy5&VOUug~BS5H6@S5&sg$5o( zGzl0;hv6m`5b4yMFq{reNbLO3?#0C(zBsY*_&pEe#_&-=R7u4)Uag7Qa^d*~c_^tC zzfDhM(Q;=YLp}IaB_MhR5meospIosXlVUGJpcLn&D1GgBjB*MZ%fugkP_@}bJy#j0W>(bKLofe5s4&`F*r}fYM+8OeB|I|;SXEy|n`5E(^=!vM5~z(xDY8rfG^WI@ z45}PqEr}2pk)Bc88C(XTEG^adv_wYUKs0AH?=*!_B@@_7vYM7N|wK2fUck?tT zakFxNDJv^5c?Qc(C>Ej{4;x4%+$s=;tM2QbhXEx6?U2CXriRT#jnW>QsiD$C|6Un%bE0jy9^g z9J$_2^InGw`^i~02d-^f+kB`AnoC$Jc!7kW9sba692*D2wj-9tz>Ft zWzRV;V@oXq^%K9@k+JT1h_49pH5@nGc7c#H=c6IdYk(2D<>_)KturPy*eF|e(6_HP zm4LMWxfOP-JQ`G!g&*BQsE(lgY~o0qbT?G`YYsSPb(YUffZFa=(;)=fEW$I??&6d> z6-DyU=C69YMN|6iBq4fYYItzyl8WEhT7T}Vtl=nU(fYqTdH)lk`Pkv{)t2?dS_3kc z@8T{@rQp9stPk6-o2OT7?~lcqhk8ezHu6vAzgOc65($JkT+Jy1jg+_s#M(qmX&!wh->WNX<%$+}5DcFuz&#+QSp}(OF$8(rw4Z$C?on((?^7dW zxijT@6^6K8K2A=sF6l7E_5-V=)c3*QClw4deP@1;U#ukMjn_~?z5}gL(ll8fMw%|# zd7wr}Qd5cgU>J3M?HfI`4Or{y~@W& z6UDuN&zgYbFIs?wD9=_ag_S?4%uaAk9L^Iam-!9;A&ydW5 zzKt7QXNu1S`%3T-ayn5@q6JN4e328KR@AoJuGc#k{~QQ=+-lW_umY(=(!IEBO(@Lv znS^zf&ue2<%P(SHPJQI4wLzUGV20L!S-(gQQ)aZ1dIZFsR;IDZz zaRoEKi<<|y-zr&J@2-EFU~3MS1n#j)evZ%YB%+ZEU|idU&(rqO zZNHg_Emg+OdgUPNd>~?gR)v6YHoj$OicZ}GD_6DcHPH3jEPjDkM_AOOnm<2!?jk|o zS~ypog&TZfGTdgO`EW{wZrR|cEhQqS%*8_@sT z%UXlqk)WSc0?}e)m!-JQdT__Yik+Ww4tsE&8J->M-PsW_X)fkC^#ePRAa` z(lo{1Rh6hG9GK^Psy9b%BK%D@EO!UHZO$I6w;BZHX$eN`N4MlQ+Ncc{h^yAD0?h)J zr(!H_r6fXulDSn0is6?J93kP&Ev$6%A)}5aKT_np?{Y44oB_I3S=Sk+M#FHVhIe~M zc04ZRe4Yp3bt3^D|O@MA8|% zm;S@QuFqs7)SN8oO>|r2=EC1_^&%3ji3Bj8%NcVLu7pW+qg;LP0qdwPjdN>0kiY3z zacSg$B{Np%M!3m_cH%c!185U3?3ANF%}Qn=m(y7wJcVaA``s9SdF)OyN-?d(>|4-7 zNs_Q=&=T7Fjp)t5n_bstP%zN02il2VfYDfj_eWSxJ~6QR+K(;oh4Bs}Ki7jid z>j)gXEJxg55wj!=n7(wVNfvnj)y|Ufx{~aZQy##gb6QpJ0-;!TBW}6@+SaV3WcVoZt&%tQs&g;;#(GVyVZUrnD81 zKir7`+QL5q(eR)Jp0Tnjxr_|BM#`Y7o@#s)Lq`>wx09$6;mxdFv@buiD^M$R2bv{e^_N;qEEU(43*_nrrhxz=SmcWN)r~Wbp$2J_^ zozbqdpTX+rRat^&3}Vp)ql*T*3`?MmLm|X1b6U@$`mY;$`2qx~2}Za=BoVwP+Nemk zOOQjpcOho)N9uacSeA}@&Am->r3OD~Y$Jy)MzFd_8s1)&`Ddr9mUn!D`=RG6G<+zZ z&ciI#Ao*6H)P(2rB-{(c3%tb?aaW;rZ{|-jaEET+7Q8vNoI{Zl>F#%nN7akkj~XSy zZOHng{sBk<40kk0&A4(1PrBC=Zjk9@_dh*mF93}3qR5kDmss^llpTuh8Ug^trbs+q zUwQ%(5SLFa(MP1VAEmmP(mF7#Vd_>x$>hR5tGOL*U4Hk{dKHq!4B-$e1Ov{KB){@Lz#L{=ZfnD!lo}kh zmkktY?5rB|(4T(KT-);geiOC;e65A33kJ1Sjpy2aQ>hQ!u}atu6Srf#C^NgL0ImAD z9)icgKMfAPZQD}KzSZnmeArexUZ0(gqnrn8IwFU z9}~9^1}w)!+{u}@d+Ll2FXVgD;}G6w69anmePv{(=RE8xmbBaJdQo-2-I5h-+{Pyj zy8NDRgUmwPw65Y|ujji$QNU~^en%2#=6aH)R~!gFt_ zDsPo~V{BRFxcft>*PfXgJb6D6b%fnEp6;;sLn@8zPK^;YL%9wEVRm*nP9{cRb5cl9 zv6byq;GHIl$u?458Js-RVO5?vun^H7qCMJ{xO z`w{H5Mf`edjqoHN(D*qeI+Wo82)TuQe;bYSK6zqNyIt7w16W zmQ>orX_gtnm+2RSaKKG?gep!6HFUg6D{M>NVL)Cs&`Hdpy=T^jVBTJ|8GSDSf}#m# zujUT=ODPp ztp-C{b@lsdgFa(awxQZ^yS|ZMkHF`?FrMLEzAxP6P?RpjF?+`iXX|q0Ct4Gm#ZV`& zX>%YYRaKZ&>$j9HrwQe`>lQF>8kndlX8#M#E1#~!d&6m^<{6e2IrQ2O42Vn2a#L;5-FxhJx`Q|e=1G)we`Dv8yBu<^=-q)e2CIvJxx}-E)9WgzKTSdr2H3y7qfSQ{){ZN~7o3=fulGN4NnTN7M zFw}0q%L%R&109#p^?=@qa2i<%=6b#XfY{KpgE}I|POszD0k&2Wik9k>ADaQsnyK10 zLd>b!3-OXv2SXuM_4=0-WXQD3cDr&}M028HYeVb7qot+86ImL5Wg9Z~pq3(l$(%ht z4fDrkxj*iRXpYr#vVB<$rD;ChZ-^gxv7kxW=R7hKp4UD1OFUjFo+0DH-E-r*Hhy}o zYjNY#_$ZzpV54Q9dl$t^)0Gun>H-NN&l#_ z>fH`V!Bx+EB2xF=!)+xPHrN~UHkFc0LoDMJYH4z=s*WG4O+BFre_UWvCV}p$-d{>j zkShSBEFpZ7#kCje_3Pg{=D~(`y5+{K(clHOc5D_S;*}~F=m#*8|gl|>?@7;kPN;FsF)IzW!Er@`M$gle;T!^hM~v^Y$F^hw`I zqq;uM`{ebzh+)|7?k&EhBFvUaMtMlc0(ve<&oShToO#tT^5ISyUDJqBaU=5vQtm>q zBVP0?sEM7<;DXXqqtc$-U3;Z485n)#HmYL1HCBh0^Lc)FrJ@wPL<|u#b6Dc_6ED#69{B z>3P-Yt&USm9RF}h%EJ9FD${Y51r3tzU|CiT6ncYb52P?Ve(2zsCpxNosusx?Z-@FB zhi>BoF=Hr&pG8S7Ae9m-V)zN#4BILtuj`pUqlqC z|J_OX_=Km;F~cJ(YJZi#*fFTi{S4>ZE%R)7p3ts)yQES=uDr=bUL+Uu-9JSmi#D6_ zrsue-CTcXIZxxyIY?P4KsHQ$cn6O!~;ECEQLizQ9_5M*k-nWZ;kIMZ&zFc-uu%-8h z8;2)jv?^~jKFlv_Pgu)}e$slUPu=uTl`Iym5Qs@Y7GXpKQap6`xA1NsDbm3};SoG| z`5s2wG2rNay9by-77u)Ll6ywF&Xaca*3Ks1`#k7VEZLo<6n;GEkp zs+0r1!a2Y?$*6+&KCvtxLb6%`QuhKA_$#eNAZ*@hQId+N&RuMS>=|bcuIJsw_x}$a zE?F;WzK&RK>79NMXyMlW1m)~%$=J@p(UC!K8X}Z-WxO!8a`hY1Ma46;;crHp@2)Ps zXO#n&F^5qRsWHH+{h03n+J3xOH{EvOKW8wMc^NjAz`^83?$FA$waqn4?u+Yd!dKpM1U}dyRH9 ztP-{vk}>0*bbH(~`dNFh{uq35O)g1E=<(_}5L#%EQxA`}fIQEd?IZjl#b=hhO2~bxDZmwy+)p5|z3%gR}9MAxmXR%0Y zjM^+h>D-0WOy(;)Kdl)znHJLhUQ3sODjh0Am!-bak4z+Qx_rYb* zaEW-mlB;`5HXK#_?Z?%+v1X4d@5P`OU0|3Z}|mD%udq;}v!yvhL6UmE<#M~y!xn}PA{{I1U{po5=4KlMJDLdiEE0~}{$ z-2yrY$eR#gv>gZAzsh$@F-6c^y;E$bo`kfx8K>|8`c0H<`U+4QdQl)2suQ34zdl$1 ze}o94mt_ywi*2&07#CY$VUFjy{DnDh1j=xk^x5^M$b14r&W@ibOu;MC^c?1M^S{y} zBG1P$ZRTvM_EadG650DM1fJPj2kg{2d*0V?apPH$WUnw{hF?iZXd8p<=`6$Hq|DCa z8pb651U-tX8jIQwV`}Ve27KF>J&KcITly^R-^sq2GpVKP)3zgV>r!hURMeEUK3+A( zbM0U_HT9!lh6lXF=*W22crAZwVqmw<#bElSezG8!^^QVV03Y+@3~aIWX5Jlbf%NC> z$|h4F5=f3Mw%h*;H?nka`r*O z#VD&`C6g3EP<)_|>+O;P4mJ0?!5sSchRoWPfZE=7VeEXh5@K_``p!{?q9`GTMQ;T$ z>PDqfQ-y622Xi&lp=SM6o}8Qf%53x^$`JDyQzPV}nl+QP?Y@AbzC9IKqV}LR`0Q95 zJI+|}G8>j6E*12Y_CC2IPIpPutU155LKU}~DAPFGI?!%a9wlfSR+@||q@PqJGe49) zf#dnU5!suj71Fd#RBPISdf^J_EvCY|$%r-XKgfbS>DiRx$D*T|d)5Iq`-@^!!*bPV z9z%5+fO@Lqr&CZ*L~IK>s{<`YIA{AAHXNGuR(RU8fO&0LqZMBdjBJTf}JswIc_ z5tDz_+#;V>e-b-RnYt0a%})dq)9$|!v)Hd-89hOe3uu&n=)P|xjZ?95n}ll*EcEVH z&aZc*z|Y-KJL@2DH*F^&0MU=Tohc|@M7>{)4|14r`%Cor9okj2Ke}!T7H|H3<+-V$ zQuc2s_mz>2?c!C7Sz+v{;yul8H|l2Wp1{?^Xfoe5>T+9_ukga^s>FEBQL&W`BI0Np z9t&;_?<Ha#!~MBA3sz|5c3p8;t!OyZWALdV+v24DY!1&fP{ z{g!n?$Cm#e#eW>0(6OigM=?17$@m|#-oJJp^A8T1Oy+O%?!S&)K|Yqa_`ibGqKUu7 zvH!KjqW;BY|Fcwp>whd$_v$p_XAejJS`a!wEVH-V>1qYojFjDxLzN6aNo~W4}BtE0>>*$1w$Ih zp{R+}n8T2&3~%o8x zVMR->XjE+%D3IiAi2}%rJ|$)HqrrOwcHENHyB_v({LHjucTqHu|nTP)635TM?j}^mJ9La$@4*;*Y2G)k+x>{0lsdX9rNWvn=2R7x?3gm*suiV7 zs``D4S9(gZv(j`;i!$RErVmTUt4uWeAW*wc;UXpQl}>;efq% zP*Q60Vpp}$M9&F&dIbVkQf4u@3}Y`G1v74>^zl@Vd0X5kVY-5ZxnUCDTl`0!kBpWL zj9CeZKeZA0d02~06gykT*YeFNvp=`X4;WkS4>Dw{Zq-D{{;i;w* z1syW<>ZTN-^SB7_)_&ziB(%DK%k@-%eO9JO=rE*K^M_CvR3_R*v`QmrSu zOwWV7HClqzY-X92=&R&btbX|jbVTooM(KL~q>!w+)hMNKt#gpOE+{{#tXRJq;V3L7 zY2{9*$s45!SBlcZGQH;-|6CEzqWDWf*MrHPuf7d^yebY=$qjSO4i~AxSA`{)fd-$i zR0ZBOL~wC7B4bMxdMuBncoAjJ(~FUxJZ4YC8n30k8rc>fg&WjIIzHx{&!}nHw?BCt zciia7c)$L+I$#Mr#0w^~yQ0^2=Wo_J`J#xfvBTz0QisY2}7;l9*K@zfGmwxc<|c?grk` zsl@yN>96a$lJ|e#0j-`=UgNKQj`wn(J& zFlOHeeMolI6hzcf=gXYMdQd@&y-4!|hLX6{RrEDSYQE#0TUC)3`qhEnQkfkF>$zDP zBq_CztJsdt1ya(Ahp}e&YGD}arTF3^=hj-SMv1t{jL*6fm2K@~ zbG~LCb6i$no%0jmW8uEzFBpie7?%R%ZTr-G;{W1Ujpr`XasnRt9$hg%Q|ce{ArnZt zlPySVO_0v%qbqc|G;IOCH@={7N9;b2=kLl6tKj64D7m?I&n>4_>r|q=- z0(7eRi-y;j^$qq-w3d|9UVi?O9QVGhzF2f}^#dNqFq98F2VNT8dZ)KBacnsjT{9ZH zN#4fAjxRcit|Ue^?@H6qB`Yg;tlKuEfIzMEIcgFHk9`%wiHTE3i!bcbw=0*sn0$(G z&@0MA-htW7Pws-A{A}$&JMk;S9|uF@G_q2!yT~kNrj2ovsXQg&HFffmzE1Qx4cVYK z<=LO_la=5_dxSArK!IF8aa-Z{*b>C)zSvqUN}~LzLD<^w9RQfkpidMMyAU|^3q+b* zA5j`oA2kb+<{TGO{aW5|Hw*I+yCu^BWq34*1f>$Eh5Xb0sw%i=0bem+AZT$EW$;oP1ZPkA2VWL9_P~-Nlc`?0v)WwTce_L+(Lb%DmNIoNfThM-H3&0SFluR6 z$b;mZHt3Q;Tpihh7xRen4V7?#sm|R1e0!HYCvgsCTT6V`@~XCD z(US0m3b_qpQWcDtlDY8 zsgwVd+5V{Y{WIJlflsQ_|2*bAUXvCK)%V@*(2XGn?n}4-$qI{9{@O?uGewS%MClJL zyQ_CHuOKE_TYKW~n#k=oA-z5b-<+&SCXkO1LJUWBYXz73{3S>`_R?(j8^;@8+s6yH zrd3^rJzn55&_k5>?!%q9*S#0YT7a)9w^h-u7N-o&dWPJME*Zs(3T>G$^iD0{wXjM9 zF-0GxbM+Z(rUb_?9U;!gLhnq>aOkjL1U(do;+Gh=WDOWVLM| ztJ#L&lKkLbww#n)Hyr93#yl0%vu}(e+M7x%(XOL14Ec`ycM+krjm*pOb%4{m=|)S# zrb}NqH}!*@GkG)_NXiV~9*C%T^D-Fo2i$Ijf+^>#-nmk7u%JF6P7GFC}=3uu~{n znutTH*?rmc1tBjfFY0ZSL@MBrfsN7acq5*@I-Cx*jAh@U>IfT;_n4b_)O^5I_WR?P z$_k0X@vp&)&gOBE*UYCPuKJI%rCdZ!YC)Ycp=y0!rL_rk_R4$K`F3f_r4E0Qn{je> z(D{bt*AgEgUT^VN+RbiF5C4@kE(PiibK&K6u_|I@c6ZhQFp61dT^ppUf05x4LUleq zP7-!PvIw+4*DaBs1@f_ujqr+xICZDDeL#O6v74?P{@LBoBZtoS$oFBd&nU0RXk%DO zD&A9y6!~W89h@@Z^@r_LJn=ia38d$Z0k0redu{4#DIC>qoJ*g&tNdLw{X_ZiRzwdWheB-Od07x(p z9Z!w)tsX}$2TQ^O^u(D)9np_CbeNE9#SoOv3N=c!2{yFVv3~Day)Nr4?BkBd+(0Dd zilydPa`LRlI%HG>C8#0Cy`^EMn&q0-9|&KIKooV#Z%zhk6y~%0uR|Yr)_VINj>~MI&~=*qx2Fh(1y6PlRI$-V4?VhEHNlB$4CwzCL2XMXFTFk? zeG=P@_+A;~GSvzcWn$$8J<2fQy2!Z6JJOBWO^U@f<{V;~x(m-zPuuhhOx0V1r-p&j zMprm=?A3uz9$!`F14ypSS|k7GBv}4Y$W~5quWsMGGWfgRsB@IM{Y?As`PbZ1r@o18U0ePTdIyp$I6w`0-e3A?eeM+I4p%M!WKqfB;)+(~YBz+lh1?De;bWIDex5 zP~xK>#&pS=_gC_94!_*cXXEw)tr)sdcq?^i1OPHXg|dp z!qm1VlBdTIAMP5|Gq)df6!va~Iw9wW51`W>EZ`sM<$Er@;c{DpvqvU6nsnahRlV-M zuU{#q|1GXvNFJ$o@`^&4;dhB0-22jLo)q16Ni?Y*a;37xx;HO&gbfTyT;k#LBgxylcaBtgq z$?JzU{guVoXhOokL{hHLC&kis+FyJ z=iBs2-od7wPeEF0tY*>`IDng@Da_t zbNiy3nJ4%xc1^<7ZfD6-udA`kTsp3mn@ar_>lVWe{w>x$!hQSa>GQq=WOT>Hg;g48 z?TN$piWa2%vHf=U0Zwc6LKg2(<64JjwS!U7QVVi4<+|f)KnqC0H6y!c_&g+L4{wcc zwUlJw9Y788?frqdOAJynd+u?NJ?w?CrW!;~*#o+35Ghrm_CLrUP>px2X^!v4i(D_x z03T^q8!E+p`H6LnGnEowmau;Q)Y;S!t<-Ocr~VoC!}vK;ePIUhZ6^U9_r0fWhb%DTJt-cv_(v&hIaRFJ4L?$t|i`PJYWNgHk$FC9m{~KfSeF* zc>2c6z17TMX+|N!ye`)K-GTQVuLp1LALqkE82sxVl3S7>t}Z|2diDZTS75=FBx z>~ zCs)_WdhBJB%*jXj(K@VJI2-yvUz_-jver+0b>s$*u{qVabQcHrSStPsw-%n_V|oDH z@+Ft~Pn_@)W~`58RHNQ47=@paPr2RXoLks`alngV=NWl7EANa&D7|OSK(iY=05u^s z=b7BmD@`v4@Bhk$;=kR`oY6*ulZ)5Cm3AM!KgU)lcDpp66*QmVY#!g}Vufc&jKWrQ zv>ya3aPPxO3Qb(ax+|Taqar(&6AxX^b#1?8^T{*`og`cH-=zp%0V{70dnfA+ytVE| z>>@xtpFn3=;vl;*DhV(k&iqMu3{-eM&W|~Bo&Pzju0!-1q&1a9Z!{xxxa042o^!?0;P`fn z-s!XZ&xE3Yiv`pyk9ZW5J-$~;4qzkk&U$cVS>w+?x%m{n_32^Ct=Nk4BQGLDnN&{< zdP-}p$zfM&Ez@nuUZZfUs?0@G(Pf?K5+NGp-Qs*o3M&55hY%XJ>phwu?-)_N>xH8X zcGM>RAJ*O~D$-?H*KXY1-Jx-4+}#^*++7QI8h4k*-Jx+U+}+(B8h3X%%sJQEYp?%5 zmuHN+s8JVL`DNsn85!~B6Oxk3Ny|4P?bBA%^Vhu`L93k3w`TQ~MZEgPj$qUW8hXVk z9)urO=;hCQR$b5wY5zBzt^<`~z@M9Ie(6@D-|yO(;(?)Gd%4>cPk60~tWU3?6bdR) z&X&v5>?}|@d%+Ocy>l}ag_bXNF4)R{ry1OK2{BL#kmYUDI{fIcTLil?b$lQoEkd#* zHD|e%!X0A~u4UR!4$tm{*84AWjDpqU}8gSyiyHNm2)~=9?zBpR*nN945wJuSomc$x-1FsA(H+=L zZ20_fWn^G{(+y4Sq4p1%+IC{1=LEhZdm=zQ3T+Na*CLMA^K9xZmCvBNH+rl zxp4GZ(Z{u~F>krE(i)uRd9k6JVv$Fp2kghn@W%xfifPTpdhQx_BwketxR$ z11$X#k|0F={(66cFu0ig4mknH^tZrsF1a>Pn`0TZgds|L;GW+y@dBwiAvd6<4X~s~ zNqLJb2~cCp+E~4(P%;I*w>72ah7@tO{_*#*1U7mMzM%ECR$JUB4PuINA_uRte2OA_O}w zqD_XmB}atYC=~gj7%e->&q#qptmBwpNyc9gCL=<`(@A~M;S~;RhMJPOjBsiA*f2T z{#zSaF;Nc<(TKd$r7+wk`WtN&1qu32MM9Ug7IpjN#5WrHfm*N^RinM4Jg;UQ$_ah< z32%4PopN6lACRb5S!c4H3J&$B>SraqTV+O#5ttJFZM*aroW_Y_DBwZyD6)zWp}Cpk z8Yn0GmEh&&QdA$ASq)&7-hxSzU$B6TQnu7rI0*cEN^CUG$7vw(ADwb9>>l9P>xkoS z*i})XJdryPJ(3nZz}Ap-Jq9h4Vd^wv{uYrDt91I z^~ril2l2zwwAp~@Ey{6;*8qKA8xk}mu*LYP?2~wNc=%otm9Nfx?-kf~6w;*{zzjH1 zENIF9uz%kn*vw5yqSGSe!Pxw~2Oz)UZ=b(kdjr>Zd0)Cuf=IpYL^l9=6x(8rzu&Z7 zL{YUSmli!#12a{(f@GmSJ}e^hSqdZe#dta3Eo?utB7dpDZ*N(6$q7t{O*9fN#MKm( zQr>jez2clw^c6N0&Yw&$< zTV%f%l%`vHpJr{kES5E-1q5K<`g8}-a@ z;{(ZeJrZ+DFp2YxTqtGTRpB+Cg5|u^KS#r{C zOYCpgiFEt7>O107^(_+Q?*S);j_kUvXwFPZ=JPA5<8jB`yaC9lGUk3IMj-QnZ?iGh z(-*d;>6ukaY5PZW$@}-$&s%2;q-)I@FmM^rtF<|q{gg-(gY1f3<}$f1RY8)E-c+iQ z2oj-s1_gr^eN7zzWOZAngjavR-Er}j!Zin*6hOWyf<6nu56NQvgkS9j;;vI%RUM;SSH zY<-|G-YmYO+Nn+iU@f(DI2z0%wpiBEqIk8vREj#(Rj4U^CG`ke4YoW6178l^vSkzN zQxnKHJh2FUA0nUPw#~=9oPK(Vkr4i@A(U#&6v*b*%(OZrmw%MJ748n4c06~L>Cy~|v(j;odCA~kFaH?9l^bkLv zJd}3Jb?MwwpT7AbeRVXGlPe=^d?eNe6aU}TYD!BuP%U0?0AwIa`t||5sxLTC#l= zFyO_4VHTLxos0y8h3_OL8{N&x3ow+JecfdS;ouj(yy~Ro6Ckb7Qp}86VRZ1%!qe;v z?kVCwqH%)Bo_U_K^Z>J^g!yr;{cPuCraVOz_IC0uo-+BZY=()C@Frsec7SNC#=E9* zOj>%(E0K?ep$uHz?tUQMgWs>mo6cO6Xza#b;xbj#^%PWyrvNqOXjyD13kxth=sGpv@m-?An1yTm1f1eX2>0T+xcqQ^3ZIjg}BSE>djNxV{Oy^}IbC zAJnPVSjAMF$>T*8lK))hqco%*B$(#SJkGquGvCj zZ@r!pH9(AnZBeWAE)(F^AHQrPpk@|mE4!@c;Nmtd{bdp7lF)UDjU*$U9E`NNU~>Wm5L zNVTX2KF0UQ&}%Pf)0fOzX+Rpg!z-`>i{DWSOyK6*Qx}+l_NnQaS1-xU=12H(MP;6-6 z7ehf3Az~qNbQo05q4jq&mxuWF*7hX-&qKsVMnOn3Co?xDEr6Sh&(*z?QVSzTA3(&d*vgSdf?)!@G zP8YvycF(u0TqM7VkxFvV4TfUkt!B~L96+O5e<-iXar={n002@fO}?#n*mUol=tE0E zwubmt`k06+n2~GfWaDfT_-p<@0`qhL0HN&F3;_fi_ocXsqY-));r%z@nIRzbKs-#G*t-%S>D1~1BZ_iH#*&<| zf%^FSr7F(Z4-4^VyhzBOxr0}<<*L4uFbk%VHq^OXU533HXQ!uJjY9Gf2xq9{%nsHF z#4fzz^AV_2Mn9sAgY&^C-) z!GW#O7$X4On4Bwq@usNU7P9FS93va)L2(8$)96O_TA z;d0%J>gu1pRA^qUq0p>g0S9i|@lmg{hN}H>TrnMM3$QZz?EUeZ;H+%T zfXaeBuKwh6Had#yjohcQ@2$SR5*@(*_)RIguzz zae*r_w7MIkOjZl-7Sn_5fN<#uL606^kjb9S&M&G}9nrfmgSm^e0RsU12+-?=0pN8y zG3UFnm@eG5G>pg= z=9}!vJpRD)NzT3&$A%SQYX}Vt3H7Y6p^86C}C(>-=)9*Ug9wLGVt~Tcl)M8 zaJ4pC>ufAuyhOWDAdfZp$FWI=e}RXew9k*N>>UE`7~|EW+p|&0{ho`NcMah)E3N0c zHawD<4rpY4&`qs>NMkMaFn}?>1U( z4%62y61eT!u~_J({XfcLv{~Qs?Zt@C5)cEPK;sI>O0uzxn%gvL%i4HULz;&LucY&g z&Mw;Weo_|I-AI)BcI%amSF2llfMs%*vhCHaZ!u{!Wt)84GuSc`=DetC?OH2od(6^? zBbWfp8Zq&F?7Wk5cjkK3#FqA+nF;3?W%A*H_`!t%=Co}X5#jiy#t z2$A~r$7zkS(bZ29xYyy4GjW1&zj%c}GYP)hnc98Fm+ey%Ju-0r%T1~V$5e>5SVk3C zsCS_);(rz*yQgXJHQu8+{L17sXD6E10V@g#D@TKMrU=Sn?&JLYnr zSz~HE^v@mHbPU{J17?q-XxRQ?m3fc1`w&9d_Q$-io5lUj^*pa^>-Iok`_>5?URno; zYf|SZ%)9IhiU0iemRTHUiWVS&j)fe`J~my^(;QvABBoO`d1hFB!?pyCg>Y{=M~h-X zfR@?t{qs6I0`7k5xbx>cRo}46icPO4+|fedgPIT7zJ~q2`saI{PL#Ep$0cUYXj>IB z%3<61h}uB}BViwhr}gzDXJ{tvGAE-Bf=ca%3Z__ts}++EK?ksgHVd~MN3k5R0x2V| zOdj5@l)9H0KE4`LvR?r|>wLP}O%say^`lny?J+OO;Kpu0idmhfKh7e{x=Gx%FC^Ws zQqRy`ZxMI>pLLHVc*Lzv1D@SN(ksfISzYTl7vO1w8lAySYI{0O?!Hwsv!0oZ1^Ufqq-@QKUQyN)HROAhO7*it#nNb@ zfqJ8wvXl`2;H#&%8~U(BF1zPkiN(Mxhi`&1H#p+Wz!JrVdTo3&h3+AH~^mdvLjXx*OIZUc4Hm@K_1qP#e#e#vWA6Kbl=mM}fLF67;S^?F{t&6F%TP zKFjbC@%xMfKpB_(Oz2aLZGMj4n$at)ZFf0?h5LXkNW%xXr6Plg-dXmVC;_(=!n=Ez z(>1a3YK>6VD_?6Dit^xl(!>nkwAwS<%HeD;TLS|=4W7C&;Mv0Nx_lecy3)Qd;~d`a z-amMP>Efnyclvv?j~BHk^g1%-=b7@LjB4MEEJD7AIImes86C96=3E8$7`zhbB3%SQ z#z9~S%BR@D934^Kp1qXUKN`s$&i%%wQN8Wn?)5Qx0nT1t@AQ0mm^zXX!0*1#fa2^u z50mfE!5`#c>t2jlC1LnY>!GF;y&5OS4*ZB}!wqiE4$c!iMQ8Q7;JZ!dL~=ZER64lDy0)B zw~@o*H@Xi*a~?zPg>!EIk~?MOjU0gm(*&VpW1$=x~R4cSZJ$FojL_S;i)ZLI8or;5nMzjUP z<|x5tJIT)*plN>IdiZ1CewnvNBLR_)o{AS@yzu`+WrLsgSM0 zZz8a0e3=CS{QHKr?cZ-!I1$n(3Wf9cFPKfliM~-w#}0A6j=f+Nox7+`7=X0BtT)KZ z{K^ME4~Bv#z>_~4|h-bX6Gwrb<=O)|Tl{7)sF~?Jh|V~W z-Vi!MYkC~jPFk6=v+oREsrDN7h$ z8)mxa@FO7n$X4Em3f{7VN7~2Tovo%=TwtU#l}uO<2_>;(D)RaPGq7|OnzAb>Sk?Br zrMY^sIN|w*5kVc7u_dQjR%3Z0*YY%8+5w-2a7<^REU)o%D#-WVmi&!0W*1E_O0<)| z9l3J5gZH`-U1Gp@48T>VmL4U{rnlS zQcS)()NhwL`AuBqq!n6mHhoq5C8>=keKDw4icB_>Amr9zDog&R>V@2q4mHSIWDP*3* zJaYZpE3YN}3oJ_FX|~i;o840$uHg<6G*np)FDr&#@O;v4=Q?sgqT|@`3e|@xcwDaaL1?RM zgDM{RXP>WO+r6=X0KDjhib)z0_rWMUmF?X+oV-I#s!u@B>7aYv5$~g6)X#5Q(3u^e zPP$dJp3R?ZaY=uN=~MM~g{j0?@K3gQao`_FwC|s6aqYMQ{lE9zzum>Fs=+~tis7TQ z+uJq;!n%P;M#ijVslQ0qS3~);SBbYRyAh1((;4~deg-yo)&WxkxR>-64h`|Ran zS~|YN`JdG%XjgDb{zuFl15ZAuDkS_VR}OuZs1o?XcyVTZ;)fp#Wp{Q8_0Y<+$=9n7 z6b4M}GAzl-*jq=2{aeEBPy8+$tDpq|)9|T+f5$V2Y2b>f(@zilk9NFow`Z4p&c#th z=&xYd)c!UdVQLgHFFni|1YjO{D*yuKe?5N3rj7(3c%zKF3@Q_U7hB8R#HU8T6m0{Kq_fyZ!lebCfq^|FPEJ z3H!cI$bUNizkmP0mHhwV$zQFA)BkETP>WabwzPH+U$G6uabGcXUmy|L6I=)8ux-Pa z_)ic$J=*tF0U_4Ug6eF@S-u-5&Z;)l6kUAp9q*B~|3+nhHuFJ0RP@*QU1bX)X9dfl z5(|_CE|H}%_KrbJ8|uCa9_Jo_I(TuJ=z(3?rjQGL^2L4sZGz94;6qFP7c$<>niHpv{%zS9h_V`=53X`8SDh6+_ES=uwJra%?7}x6Sk(Uwh>u(R+p;@)NLY|nT~I{Y zvGBsg0nR&A-G!Fvi#nqx&vK!b^q;D*n_ z9>Yww6JA?3fMR1BYGh+hV{*<3UDh|5Ff-vE;a(18R~E%LJoxF6K{r<*ZA0sHpn^}iu1fGWRS%yEjwwEX$57WID|FLR!P!CU@Nt&=U^yf0)PPHP%8htJCOea0_em=p%l;WbLfnKF9CIE;Y*x$7h9kAaZ)>`M zA2un07lP26V+xXXV=I4R7BY}gE;q^_eXzZ_Q+->PI?|RoPF1!b9^;3&@98e+;i3CA zQ)BnwU>Yn~#DQwtE-W;HM;1Dj((u@XxT)5cSqRi487!JIw1j6QaCvMok2Yd(S98r5 z%dc*jbS2UyV*sU2EgfZ&aLS?QcP}X-P9lNq*iy6nvJ+GQB0?FMUnj+nk)Q6BxpRmG33n(w zTD^1y*WaPos@X2i5Tf56xOsXeo9{?jJejDjqnlHh$sm#$Hzm~7?WJV`xI7W7DJgC-QJVV=M08vQlbTdouRdN1>WTvDHA)T|-ad;>Pw?*;( zXS7X%tHD@!`O19zL`%MWwR?mJGbpXj7&o8twN^RsgJ9x%M8;vtuCkhODHGKEWRke2 zsFTI|7l5ni@Kp5SY-8gNZvH6|6jPzR2)L_ms3;Wxb7?MqV59v;34|i)`^dsObj2o| za-On6tqTXWT$yiKs17gsxbTWW_iy~QhBuL^>C&};Jph9QvX*SSTM!#MsTS=Ry%}=*66Ip-z52%_r3&w6sl;f^q(H-`{< z*oGQ>A7S_P3f^1hm2ea)xjPH-jCr?dAo5$*xXAz|iMcVl_j_W$Kns$iR)GP2`iYHe z?kSalboVpKSxv2=gy=x+EnSQVZSeM9``NTh7)EJ7J4i>D5kyU*T3{Uh=25fT{;fYi!vf!6SjNS9-z@xI z3O~^d)$Gctyqso?2o<2z!}iFfT;99$WBbnVy+|Oz&g?a>Ahk?|Dq2c=Xm#M8H^xNT zB=F2miHHqxXVYjq%Kd3qqmy%#hFaNrd^RR|)nI|S&Y?>u!NvIPqZoAu-R$6?Y{aUli>FvIlGA|pDfUubXnXcieh zNkP@`HpcYPWq45BrJzh_-rGs5?VWwZ{krIt_`4i1S6pO?)L4nugD?@gSfz&H<>hRc zE6ja0DWGR$U~Ae;z@xioJJGkOOWSvE#gg2uJ#lrlN{#99JgMCTuWB@q{m|dAxrW-Q z;i#)!$IZMdc8+y|`7F%(wNih3KF>C;4);lY4PgJFN7vaIxdUBnu%?3v_Y4>x(VFF} zQMmOwE}@Cia%CVLEC@U4(Zv{TCvA15B3O8ot?x@E)!ckW?HlIRNt8Ae+kaT~mVsH^ zI)K<|rIQoGY+R$|>vt}FnXJ$eDX3j7?C~2?lu^7ReI>O*Qb=FIhM!MAKL?Q9CXjH zLI{dXNDllD{Pi z25H%U{=>C5r#Rpdhqm}$<`15&{R3re74_oIgO0f`@UaO<*P&H`BeL;f%9wh2o^zKKy)HN z)qEhME!&&8C=8V$FTdw(t<(nbPCg#5MaNav`=;`s@aQ82`_$KEw?p6^_x<-gvd1)_GrN>XPK7Oc?Ozr%a zx)VB^YDBQpu}$c&i{c^7)<64SiDPo{^U)M)>JZWNz;|}XLLCmyjQAby3VyD;mR0n! z9A6dAl5k3HwB3fBqBZZqMdJMOkA!F8f2|HQme7*z4VK@oXoTHes^L-Ze0RmXZum(Y zk|JA7F-y|CuS(hS&`#BC)4ns!#w+bYgx~K?0UN>!{Ggo^-xY@}_{oOL;(dFmRwPSw zdk1xIy&k|yF@#ID1Edp#rOlTj^8_RFz5ohKxDd%Lp^ok>{qokw1F{Gd$m%2{1mG*@ zqImo;psnNC;{RU5Nd8Gomo*6TYlET-3w*f?^Q&wrXc?RV{ zhm{*x4){=^NLOY%>e^S3sG^awk8JKn&jn->?$C zaJcPTnDhdEeG^9U_kr9gop% z<^y|_QHpm=NX#zuU9Cer1xsO2(8R3oF{;m$`AWH2xC~g04h;6y(2-v=5HBD=)#N{u z2rv8}(4gU3r$p}NmAzizKYpfXGOlXwZ$S+8W20E5Y$isnW2t|z>aXhA)BL_0Y*cH3v;@&&Br4KA4hWgS2~mHl&RJa{b9~+Y8ko5BY4zqFXpw{H zlHFpG1UB$I8yPUWpJayf!m43Ted3OF6b;kla|AC=6eu&1^AtuH>EBm2=g>3=l+`mj z?ldxZiF0&0z^n1q`ZH989ei0)%)IkeOHR55a>RyGt4T1W926LsNZ$}20u+>V2z|at znb0o*nL!DlUq^#$p-_^%Rh%;RGn9VqP$(w7q&^hrG=A+NL|#vMzZ+gX+RdoTVy8BzA6`ZWip z$a*2&do?#AV7RxlX_eC9pY{4WXpWrx2VFqC=8Xtn`a!!*1$wOaHonrknvxdIUURbI z!ZC$FENUY>3nrlgbz=cBHOUu7E6gENWfT7;mVo2bp5AAYI|;hX#xFHkiC*X;nK0>7 zIyYCu1Tgl+(=F0CCfW8ZQS;TD;Z|}p@L%ze0|VjQb3(5fN%ewO9R<^W+;`q;k}nErc-rS+hppmA6tm#Uiz~{ebqRq5ip`k9AQ=TQj6v zrv!O*b#Q0lSP4I|2)@@9=o8O zgfV|7#oG)KTN;fUZz@@9tJUpWBI&wN;cJC-n%Vi?+~1Y8)*Q=sOw+k6495`4UW61Kc(l{YIJ0pI|=0GjHcjeZm_gc>d*<}(~e!E z8}jp=@XayxQjZj+3j<2|m>>Z^1>`TPCA<5)g#|yb^ye4A7CO(0c9$@RZ6+`p*d!c8 zQ$N5KFUAwa0oRP>UcO)0HC(=S&DBAD=~y`Zjeg>IjouNuJphx^Ln&v{`>&mBvyMzjR9JyTji9< z*&gZkvN@8l@T-d;>5dfmjbF^HyyuT z_cG*eu6eoPlXE#i-#hBxPWx+s@tW|+EM%izJ>JC{BrM<2J${k(lH6-sVR|_@i9J!H zQbfKloVx}l;R4cLi-B4XrIYLR*AZr2nV$P$otlCvtV6WYk@W{D)djBt9(Q$Vsm?0v zH;y5_T%cUYzKB!GXqND!uuo=j%JwD?TBZOC3C*;~cFA<)qY5SbQzAfsDkmvKxW?@G zgsBD{naFm4R30;pj+FQZinb;_wgtP<;=>qYn#i^z3IT50*y|@Vp)_lvX*k@Z+(Nhv z#;%}?upzO*X!5&fUvQ4Ax`1~266kpk}T51=J|3WHDeWEx9!;LJs|D=4X>L zReZ$sJLV*Ntq7raKbi)QX9`emTu;R_o*VgWcvMZk_*Dr1__fcwBN#8;N}gUwFtVGd zcBbv8@73R z2618$)(4+xGv=}qR)429Jdy6nw# zx8e<&@|XpMt0B(l#L*_qZJ&r&3`@tK&gSoaSg1wE)p?b^_Z@;`TqxTtuiVNpb}Wlqh3a8^zDBpX9~0*+hX^o zvj}F=soTr5sf=9y4*dd;J5Uo=f^3vM& zdv>v4iAH`>Kc+mT`fwQpD;aW13vt&79aQ$~-dCJQL~>D2Za_5e5YSoK*3Q7^h*GA5 zb<+1h!A0+9toRe$S17acYqQ@~T{a(8(54cf>K>NO*LZUCO1;;B&%JqNrP>Xi-Rq69 zT;hHGnUsP3U@7#Rsq!{9z7 z=anIsY_(EKw}Gaplx*0!qqU`=Y9>K;gZLJZ8Vxx70%Dg@`}>!95ZO7%>fK^fI13Cj z3UX6t7>n*>e}W~KE+s57)Q;7z_u!oQ)~s-PM>+l3jl zchJG3GNXi4t~Eo)AokmtXa;*C6 z0oiGvT#eNWM=w;D1e_H0HeY)oEBLvBp=_w6|Kl{EnChY{!A8b6ZG_bHgXCJT1dtnw zm`i{6XxM_K)a=WH{WXOlZ(Ni`&SFR%0Ac6I1Gn^6Ll`o6qBza?xmw|;kud*I{!|p2 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 UO<~3u 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ʐLhenqCZxoVcalI4QvBtn&jEFhXtd@yJTH7gvv zI0KLx&Lrn36VSn!@4#s!VfxQE)W3$LMBK;))ALq+jzbL2nq6z$#Xh7i*~B3x6khBWVdnh-x08 zR!~soz5a<{C6JdWQkIr9hn*1BA`zljFZdn?~FHRvfc&`l*u0 z$|HH=i~Ya~=B6|qrGJ^k4))8kCa`&r!%y=X@`2HoeD$s?i$N89$xdnlZuu(qDeQ^t z2Apkt)5-O3v_oAT#-69_n+^015=eAD*=T4shx}UYkvQ$WsMqWZRfWuZr#%3CRRVza zQh=JJLzPmy@X_)%Q?9kU{Qi<7++IjwUnhqoDANv)k@S}VJX#ZSn!>3#U}W~eEV#B_ z_Aw@b2u%x_x3Y6OWxR-~35hq9cDvPP?Zq?_^`=Do(z>{ZM$>mU6~&+Be7v_XGSMmw zNiUx_*smW8^*V}5yte43` zdR(|R3X@u7;zb88l(_r6N0R7+h`r!Rj))?vYpq-3nzDH5wPHsq@9zeHrR#yfVeVTJ zyQIfyE~Pc+Z%G@5-*396=3+UwntZDt$a-6b^%7dZlzFAVw`M$M$}u_iY3%$^)FFH( z@wV;5z!p|n-aIw}uWPc21HJTHLkziW8V^o)ImZ^dVvdlH95(sH$05wxNO;D_LJjL@ z+geA=j>{`Lsajz%3!TQ7-#H37P3X29CeDpXTQ9{>==G$k132*qC6V%HZ7cYgJPfF2vxUi@%%?lDT}vS3jNb*iPv5c|^Y0gcoD^DFA3))#tdU+ph(ow4NH2&!@hM^? zaLD)HNy$&udE`W;0>CM=3ee5vsCO|6?@{Weyla2;igUDFfzcI85g3GTg-=t4%(}El^DsY1kRx$Hzq$s zgMUOi6O1(;28sjiugNI4FzqQ|9o+ygJ*#Ln^H!3hekJ%t<)enjGcm&V?Xljf5=dS@|v4;UJWQ?D<)TWObfC|X*9-6I4 zH!DSmJql9zbJw~LR^o?B@;v=?xuz|}ohSp+6wAVzK(inIxIhW-4Ex#kfM@19sU@N(3V`>Lx2 z{F5Z!2XL=mw9!MC&2x!1HWNj*yVl&M)RG6lXnCw7%;H092M|He} zUbFX7&7QsAx8~gaX2-WVD*HhtsZ=4tOnI@MlQW|OwcPpci-<|mXsK_n#go*~`qZlJ zZ)D!{U2b|$LmUbA_wdgj9zdIbhl7q8290+(v*aIll*_Hbi|yUyZ*Mwk1JoodF&|#y z6K0l|E_S#Ed(~qQ*kuL!7gXcF4wXWKr5C_KLZ}NjZWQ!avHzg=kUpemc5%1NovLAP zMW9h2kpM@{eyTXnBI@)3K%dNuWkzlp1~^o?mUy?Bj^7qhSCeW^5bGL~P&|BIeDm!~ z*SumU=XA8?*_d$x_uuxRW^6{8=fpCz&hVblwn|Q3N>8fG0r^=s&HgQo$Yu32BP&y4 zV11WhE{R>%%zzd1!$`%%)uP;V5JCb~X|X8_Evf7@Eq`g~T#ebLDk-Z~^CDbhB{q#KNVRJD|diHZ;M?u-Ev? zE6Gc{Jv5Fy{horY^YtpLR9R5kprj%rpB2b(R(-Nkxz1P;;Gc8TXZ# z1RI3cjr7@>svZTNJa+qrT_Dw~tX-Cr4DM_iIxY-F=sK%CqKxCw5F3oOWdW0|b|n4; zoc=;9^-}lt?%5l`hoz>V$<9sa0v(wxaKhB?ipfXZ?1=7H4LY* zjY<&i(ds5%SeH~mmG?ac40d8KCc%5HE@UJj?&gV>%*g;>|FnQ8d~)+)RMcY6wR`{{ zN;;6M@l+9zX!4$hD_)_jLP0!u?$N1m^oYYk$j!)gKr-KL$VD~Truv+^!|B@e=*cUt z3OaI=qGL6sJG~AK@$j0I3r?cboIYoWS9oH2T*Ze9`f%by&we!K+-6!=$-bsWnj}7B zNyK;s7dx#leTEajDG*kGcSM%nP2kEyOf8>BsnYVjEk&X!WH`nYq ze8ZDk?v+!N3+uhX&{;&^lhF&U3IpDfy;}%{CWqmo(CGGGGmQp1 z^}ytVmDaJoa4~ecf`OKUM_%2QUR-yiswN+?xjO)OZ;7-Hort@#P^dH5cB)p_^)tTB z8N?Lr8d?_bt5EVZyT?XY;tHd{6q)|>6`k~~pb za1dmCw4K>N0hoC6nVi|dELQwdU*{r^m)@*z=k?ad?52aj4hW39u0Dd|6GBD+ceV8@ z7cP=)``dT$G}C;_(-uy9CQ9$*RC4CT()O%RhH+6t?pLq_fm3qrjCh^dPSsbmeqmDt zP0r1yXseo&pMKtveHy=i1b~G#cTL8cH1UsI;4aMtC|hwA@>7Y>JgqQFcimImA_skc z7TZWq0VXm6!`49bM`VV*HN_Y9%X`;hN5$vG1)+%UXuC87>S-k1^DZ?Krs%~6EFsiq zeIVHG(MP9RYw?Gtd-;zQV@+HZr<2M^5N~(wwoHH5rPb;Auy_u{uBAT6=cQ`repJiD zKqJRF0k?7j7fe`|EDe_-9@E}&hSpziB9^pgUD_8*od&i!>Swn;^*wfky8u(Fg7+_4 z;b1Y*)h;fRulG6wQuVxKtX^|EK-gfB$K;>wBNKCS$XJ?58gkNZ*bmn7e-96(s+~aT z{?2;~`H;=yQN0|4OLtx9wQlmpMS#GWYJ!n=IW|v5^%?@MN{lp9`#k4@C~RBWx8Q<5 z7l{>V-l9`B?`SWgVTBC*Z2OBpo|t3a=Zg!&3@}7_;*+=a(pT~ZQnL?`o`uaW!1o2H z{ID8`N)`gg+I6MwWjRi6G;Nlx*|e+9+3gYUN<{KCHKT1}U%6^i0$ZXqEy5)=*IqW@ zI!r8n@pb4Rq)nW>7Q^-IX7L~=IfSd~-f`u?z zn-An5f=W>C{YFQK@B~h9gdL^6{kwBa_a3@MmCl_Rjw^|^-hP&JdahMraKq?2Y(ziV zjnXQV)jAgl5G)zLy$s}DEw;S^7X0I|z)HcM-Xs4Z-xll$(-nB?+2*?;4oZ>4<^Dj{E-`k?D!&83GmTE8APJrNLbdLJqzc9ljrd`RJ#!2-# zp>>*(A^ohw=)ojJpiE_ZB+UU^y<4xwnCz)` z3vRAB0r#2nLvPfze?->4jv4Nyo7}nJUHuwu-l&c*?upyzj@J?%U(<89xRv<(p7=zH#uu`qn|3 z?D`-xww{4lcRHBNNO;?$O4ljC?%D2yiVU0|-Dr7B0#@w?n0a6438jN2J_ygX<%5WGPjbsR%15el?;?7`drB%V zKk^&WaJuSK7&0ihV#Z0o6xi|xlVg@CS>_C7cYq;jym^Uv%amO6L45A)iN^sZY`j#p z`_}Y3y7;JZ!G@7xUq3$TqMN-RZJ$QyQN8#2#?Jk|Uc0`!w_>t)>p^CM>C@11J-}k$ zB!C8CW$g7$ z5FTO)3~$Uhk5nhMW!jyNTJyaS61~TXo*2ukN<9IzVAF> zn~|2002#^3;FCb^XrLAXa9Vz!0~w9vup{|W?#@(9F?1fTJbgG$F!)UjtR#r4?msnG|9*L}?J2f+CJCNI&j8a4C~R|(#PAZ}U*td_q0htH2l z1~+BGI*bO;J}^D>?xMKdG?w7m?FsO^xL;fHugl6pJTNh+=|l6$c#Yp1x3p7}IWoz- z9+%0($nX zdi~h+gW@qqBq=@F-f{Pc)4MzM2UEsCGhMLQ%);eWPP+}=gBSIn!hZ9zaXxuR%=ftd z%141!L#@6Lb-^48D{apc_jDt)U?mKgouj^6$~%G$6&l@vtY@7|E$-PqRzTf08TlYG zV_AV?yqp;c7{TbjaeAey6+vflT2Puv-|l;P_Xh+eLeKcCvB z3sn^;kT&bJnsbw>DzE8N*f`~oq_LcH1l?z@mld3+1+IXDZEiirI#r6E74c0LcfJtA zK|jIso09T>Wv3o}bri+#fMPgKgqFm2F3H%X6EB7l`SPdFM*FX1w&Lvd&ygk8b66E8z}R{Fp(~?0p>_Sz8isPoOcC(-6sAG6?l-Sc{~|K-0}HvVQRlI! zD~Wxv6rpbOX#T2jYILE=(3t?G>vPuUq_xc!9H7@p4GiyJAZG%@`wJNCjGvvH(*-&2 zm#?~=+3#FR=h@?CB&|yi<3bp&k)Y$>KwfZ9t{F0Rt+Z6`CUF_Tv6j2 zHq!#eL1n!J%2E2t6{GaMcMWZ1z_7fB{b!ubo0|%B==oOQw>O`*=@ff^Q}~8_i-x$; z<|0LkTNmRPh!#njOdNMpzkB+Q0qd=2pLq;@0e)hbW4l7 zdGcNpW4M##eCqm!RsVBtmp(B24nv~>S?j9CX)HCcw0^nPWiAK(?atMThYA(*k0xNa z=hoVK?qk*DR!$I@oaogD`G61|xIQfD{JwdimZu~P znNMEQWb!jMZI>p_4*ApyLJ*Gdm;-l`JM`21&8lgaNG#~MP2*BZn zQO=$ciC8Kd?`$yf7af{2pge`0>HsHX<*fa==!S>ob;6g&Z6y`U9L+T%bD!TXEO=(x zm^-?tQ=+3QH#~=#I9%L{SiL?8m&d%m#S>sb0cV9?p)n{&nWC4SpO`BiSOAF>D z*-laoshY9vM>~n&<9&laszFKG4-#v~49?Jbz)6 z@kzMu-LLc$D2gw6&kKQhS&BFH8Ha>e5zzJWUAhcmu6d{9A22gZT9D-6nos#q{Jk(L zZmyg6A`*T#{#0BH=C;cYOZsLTOkn{2SxkUN#Cy;(x@MKkB9Zs`hW(q?!ZLpJcgU|2 zn@$v8)yx8CLfjJjqQ&X;S$Fqog=o4MJsG--WP2hM<6Rpa!o%w9hpj*sAlfJQ&)s{g zJeZ5U*hgK+uEWiZvcv~5Sa;l3iQ8Que5UfQi`_Gyg?F$H*u+|HNTY%?pZwZhdG;zm zf&ZGBi)y$#D`|r;lHU5hlATv@dGXk%8;=Q|+_RYC=X@x=N$%;SKYeo=qU#+>b>#>H zL~6EGZ_@HLs;{yB+%LlKa~h(A_LPD#fk?HePK@+wo*Zy>>cMtXY07}O!{EZiEBNEa zD1Ze?pCL0rUl&bIR+CVFRDDl0#X(ROHyZHW&v&#*mb18u34Q@Zk1U&BZ+l&o8qF%v z-zI}Tx`{QU`R@~9zb;aj8G{DZert2RCTyGb2aKFwEU|9mk2BJiLRRC8W7L^>UV~xP zGD@!aPGma1cv<3!SbvY(dK3OD#OlD*d!wTaHnEt0b#X^ng4|fC$*4lW#AR^rg>R)6 zWN|N;gj8g$#wM>)p;Cbx#`BGXr1V_>fl*c3w(N&+@kKQjRwYXcF)wWG z?Xm|K5rq;=m3z`n^;iXgX3Gp#V({BA-f0H^v{N+K_b!2OS02iEisB8de5ctH%DP_B zm_D{Q2HoID*50-Fk_w9$I!>17(pxEokQvcIx~+UuJhqy+Zv^yo&W995B!p6OhPT7g zN&CKmSEaH?Imo7g1k7A=QTqfJpJXSfZu!XYdv{JwF|u`a0C^%K(L(G5C+jn?T8QXm zX|q-S70vJ5D4`Vn%R?4sRVb@fO9bY!H^rOXmv7EY*ythAc-~GoXPz(L(+~M;ri_2Q z{NlLVKu*H+a%A>OrEY2A! z`|SJyH}p-3wce%$phBez0@G;jf|$U+O0Ad zoG`&tSjdVkJ6F~>?4-(d=%6qoycqAP9*i3TceMn-R+RiQ&n9EV@RC~IPK~Q{7<1oF zKs4&L�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 Date: Thu, 3 Aug 2023 17:09:47 +0200 Subject: [PATCH 037/479] fix(MediaStatus): use lobby playlist sorting in Media Status view --- meteor/client/ui/MediaStatus/MediaStatus.tsx | 2 +- meteor/client/ui/Status/media-status/index.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/meteor/client/ui/MediaStatus/MediaStatus.tsx b/meteor/client/ui/MediaStatus/MediaStatus.tsx index 469153625f..cf4014c689 100644 --- a/meteor/client/ui/MediaStatus/MediaStatus.tsx +++ b/meteor/client/ui/MediaStatus/MediaStatus.tsx @@ -554,7 +554,7 @@ function usePieceInstanceItems(partInstanceIds: PartInstanceId[], partInstanceMe } function sortRundownPlaylists(a: DBRundownPlaylist, b: DBRundownPlaylist): number { - return unprotectString(a._id).localeCompare(unprotectString(b._id)) + return b.created - a.created || unprotectString(a._id).localeCompare(unprotectString(b._id)) } interface PartMeta { diff --git a/meteor/client/ui/Status/media-status/index.tsx b/meteor/client/ui/Status/media-status/index.tsx index 577c20bfaa..11650b717b 100644 --- a/meteor/client/ui/Status/media-status/index.tsx +++ b/meteor/client/ui/Status/media-status/index.tsx @@ -25,7 +25,7 @@ export function MediaStatus(): JSX.Element | null { const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc') const [filter, setFilter] = useState('') - const playlistIds = useTracker(() => RundownPlaylists.find().map((playlist) => playlist._id), [], []) + const playlistIds = useTracker(() => RundownPlaylists.find({}).map((playlist) => playlist._id), [], []) function onChangeSort(sortBy: SortBy, sortOrder: SortOrder) { setSortOrder(sortOrder === 'inactive' ? 'asc' : sortOrder) @@ -173,6 +173,7 @@ function sortBySourceLayer(a: IMediaStatusListItem, b: IMediaStatusListItem) { } function sortByRundown(a: IMediaStatusListItem, b: IMediaStatusListItem) { + if (a.playlistRank !== b.playlistRank) return a.playlistRank - b.playlistRank if (a.rundownName === b.rundownName) return sortBySegmentRank(a, b) if (a.rundownName === undefined) return 1 if (b.rundownName === undefined) return -1 From a6494bdc1fe46ab402203a63e3cfbf93bcc11a1e Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Fri, 4 Aug 2023 11:52:59 +0200 Subject: [PATCH 038/479] fix(MediaStatus): letter spacing --- .../ui/RundownView/MediaStatusPopUp/MediaStatusPopUp.scss | 2 ++ meteor/client/ui/Status/media-status/MediaStatusList.scss | 1 + 2 files changed, 3 insertions(+) diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUp.scss b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUp.scss index 49b1c7b416..9f02a92e90 100644 --- a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUp.scss +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUp.scss @@ -10,6 +10,8 @@ .media-status-panel__table { width: 100%; table-layout: fixed; + letter-spacing: 0; + user-select: none; } .media-status-panel__empty-message { diff --git a/meteor/client/ui/Status/media-status/MediaStatusList.scss b/meteor/client/ui/Status/media-status/MediaStatusList.scss index 9225a75b4b..9d755d7723 100644 --- a/meteor/client/ui/Status/media-status/MediaStatusList.scss +++ b/meteor/client/ui/Status/media-status/MediaStatusList.scss @@ -7,6 +7,7 @@ .media-status-table { width: 100%; width: -webkit-fill-available; + letter-spacing: 0; } .media-status-table-search { From 85c8cc43cdf8a61c4f94bb953be323eca38b6c7d Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Fri, 4 Aug 2023 13:03:09 +0200 Subject: [PATCH 039/479] chore(MediaStatus): add more documentation on the Media Status views --- .../docs/user-guide/features/sofie-views.mdx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/documentation/docs/user-guide/features/sofie-views.mdx b/packages/documentation/docs/user-guide/features/sofie-views.mdx index e26a46e3dd..92d2de17fe 100644 --- a/packages/documentation/docs/user-guide/features/sofie-views.mdx +++ b/packages/documentation/docs/user-guide/features/sofie-views.mdx @@ -167,7 +167,13 @@ Technically, the switchboard activates and deactivates Route Sets. The Route Set ![Media Status panel](/img/docs/main/features/media-status-rundown-view-panel.png) -This provides an overview of the status of the various Media assets required by this Rundown for playback. You can sort these assets according to their playout order, status, Source Layer Name and Piece Name. +This provides an overview of the status of the various Media assets required by +this Rundown for playback. You can sort these assets according to their playout +order, status, Source Layer Name and Piece Name by clicking on the table header. + +Note that while the _Filter..._ text field is focused, you will not be able to +use hotkey triggers for playout actions. You can remove the focus from the field +by pressing the Esc key. ## Prompter View @@ -264,6 +270,11 @@ onto a playout system. ![Media Status page](/img/docs/main/features/media-status.png) +By default, the Media items are sorted according to their position in the +rundown, and the rundowns are in the same order as in the [Lobby View] +(#lobby-view). You can change the sorting order by clicking on the buttons in +the table header. + Rundown View also has a panel that presents this information in the [context of the current Rundown](#media-status-panel). ## Message Queue View From d407a9c0c64effbb45ae19503666bf911a11db50 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Fri, 4 Aug 2023 13:10:28 +0200 Subject: [PATCH 040/479] feat(MediaStatus): filter on part and segment identifiers, but require to start from filter string --- meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx | 2 ++ meteor/client/ui/Status/media-status/index.tsx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx b/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx index 5a703c9cae..8eaa41f525 100644 --- a/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx @@ -44,6 +44,8 @@ export function MediaStatusPopUp({ playlistId }: IProps): JSX.Element { (item: IMediaStatusListItem) => { if (emptyFilter) return true if (item.name.toLowerCase().indexOf(filter.toLowerCase().trim()) >= 0) return true + if ((item.partIdentifier?.toLocaleLowerCase() ?? '').indexOf(filter.toLowerCase().trim()) === 0) return true + if ((item.segmentIdentifier?.toLocaleLowerCase() ?? '').indexOf(filter.toLowerCase().trim()) === 0) return true return false }, [filter, emptyFilter] diff --git a/meteor/client/ui/Status/media-status/index.tsx b/meteor/client/ui/Status/media-status/index.tsx index 11650b717b..d08feca57a 100644 --- a/meteor/client/ui/Status/media-status/index.tsx +++ b/meteor/client/ui/Status/media-status/index.tsx @@ -48,6 +48,8 @@ export function MediaStatus(): JSX.Element | null { (item: IMediaStatusListItem) => { if (emptyFilter) return true if (item.name.toLowerCase().indexOf(filter.toLowerCase().trim()) >= 0) return true + if ((item.partIdentifier?.toLocaleLowerCase() ?? '').indexOf(filter.toLowerCase().trim()) === 0) return true + if ((item.segmentIdentifier?.toLocaleLowerCase() ?? '').indexOf(filter.toLowerCase().trim()) === 0) return true return false }, [filter, emptyFilter] From 1b52f5ecf1a7397182b476c6b76a1991697e1185 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Fri, 4 Aug 2023 15:53:08 +0200 Subject: [PATCH 041/479] fix(Core System Settings): Cron Job Settings are duplicated --- .../client/ui/Settings/SystemManagement.tsx | 39 ++++++------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/meteor/client/ui/Settings/SystemManagement.tsx b/meteor/client/ui/Settings/SystemManagement.tsx index a7cfcaad19..e5357c71a1 100644 --- a/meteor/client/ui/Settings/SystemManagement.tsx +++ b/meteor/client/ui/Settings/SystemManagement.tsx @@ -237,33 +237,6 @@ export default translateWithTracker((_props: IProps) > - - -

    {t('Cleanup')}

    -
    - -
    -
    - -
    - -

    {t('Cron jobs')}

    -
    -
    ) : null } From 1922a97a4b863849649d2950cfd88371ae27c179 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Mon, 7 Aug 2023 11:19:16 +0200 Subject: [PATCH 042/479] chore: update documentation --- .../docs/user-guide/configuration/settings-view.md | 8 ++++++++ .../installation/installing-input-gateway.md | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/packages/documentation/docs/user-guide/configuration/settings-view.md b/packages/documentation/docs/user-guide/configuration/settings-view.md index 64529f04f1..7ff4b6443d 100644 --- a/packages/documentation/docs/user-guide/configuration/settings-view.md +++ b/packages/documentation/docs/user-guide/configuration/settings-view.md @@ -117,6 +117,8 @@ To edit a given trigger, click on the trigger pill on the left of the Trigger-Ac Device Triggers are valid in the scope of a Studio and will be evaluated on the currently active Rundown in a given Studio. To use Device Triggers, you need to have at least a single [Input Gateway](../installation/installing-input-gateway) attached to a Studio and a Device configured in the Input Gateway. Once that's done, when selecting a **Device** trigger type in the pop-up, you can invoke triggers on your Input Device and you will see a preview of the input events shown at the bottom of the pop-up. You can select which of these events should be the trigger by clicking on one of the previews. Note, that some devices differentiate between _Up_ and _Down_ triggers, while others don't. Some may also have other activites that can be done _to_ a trigger. What they are and how they are identified is device-specific and is best discovered through interaction with the device. +If you would like to set up combination Triggers, using Device Triggers on an Input Device that does not support them natively, you may want to look into [Shift Registers](#shift-registers) + #### Actions The actions are built using a base *action* (such as *Activate a Rundown* or *AdLib*) and a set of *filters*, limiting the scope of the *action*. Optionally, some of these *actions* can take additional *parameters*. These filters can operate on various types of objects, depending on the action in question. All actions currently require that the chain of filters starts with scoping out the Rundown the action is supposed to affect. Currently, there is only one type of Rundown-level filter supported: "The Rundown currently in view". @@ -131,6 +133,12 @@ If the action provides a preview of the triggered items and there is an availabl Clicking on the action and filter pills allows you to edit the action parameters and filter parameters. *Limit* limits the amount of objects to only the first *N* objects matched - this can significantly improve performance on large data sets. *Pick* and *Pick last* filters end the chain of the filters by selecting a single item from the filtered set of objects (the *N-th* object from the beginning or the end, respectively). *Pick* implicitly contains a *Limit* for the performance improvement. This is not true for *Pick last*, though. +##### Shift Registers + +Shift Register modification actions are a special type of an Action, that modifies an internal state memory of the [Input Gateway](../installation/installing-input-gateway.md) and allows combination triggers, pagination, etc. on devices that don't natively support them or combining multiple devices into a single Control Surface. Refer to _Input Gateway_ documentation for more information on Shift Registers. + +Shift Register actions have no effect in the browser, triggered from a _Hotkey_. + ## Migrations The migrations are automatic setup-scripts that help you during initial setup and system upgrades. diff --git a/packages/documentation/docs/user-guide/installation/installing-input-gateway.md b/packages/documentation/docs/user-guide/installation/installing-input-gateway.md index 5d809d74af..0eb54752af 100644 --- a/packages/documentation/docs/user-guide/installation/installing-input-gateway.md +++ b/packages/documentation/docs/user-guide/installation/installing-input-gateway.md @@ -27,6 +27,18 @@ Currently, input gateway supports: * OSC * HTTP +## Input Gateway-specific functions + +### Shift Registers + +Input Gateway supports the concept of _Shift Registers_. A Shift Register is an internal variable/state that can be modified using Actions, from within [Action Triggers](../configuration/settings-view.md#actions). This allows for things such as pagination, _Hold Shift + Another Button_ scenarios, and others on input devices that don't support these features natively. _Shift Registers_ are also global for all devices attached to a single Input Gateway. This allows combining multiple Input devices into a single Control Surface. + +When one of the _Shift Registers_ is set to a value other than `0` (their default state), all triggers sent from that Input Gateway become prefixed with a serialized state of the state registers, making the combination of a _Shift Registers_ state and a trigger unique. + +If you would like to have the same trigger cause the same action in various Shift Register states, add multiple Triggers to the same Action, with different Shift Register combinations. + +Input Gateway supports an unlimited number of Shift Registers, Shift Register numbering starts at 0. + ### Further Reading * [Input Gateway Releases on GitHub](https://github.com/nrkno/sofie-input-gateway/releases) From 13ced50451d0a42817076e9268064e9591b7a0a3 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Mon, 7 Aug 2023 16:00:17 +0200 Subject: [PATCH 043/479] chore(tests): fix method context to expose unblock() --- meteor/__mocks__/meteor.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/meteor/__mocks__/meteor.ts b/meteor/__mocks__/meteor.ts index 9eef515b57..ad6553bdee 100644 --- a/meteor/__mocks__/meteor.ts +++ b/meteor/__mocks__/meteor.ts @@ -123,6 +123,9 @@ export namespace MeteorMock { connection: { clientAddress: '1.1.1.1', }, + unblock: () => { + // noop + }, } } export class Error { From ccca40481c1e0153b4d4c4ab61cb35553d60e1fd Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Mon, 7 Aug 2023 16:01:21 +0200 Subject: [PATCH 044/479] chore(server-integration/tests): fix use autoSubscribe instead of subscribe --- packages/server-core-integration/src/__tests__/index.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server-core-integration/src/__tests__/index.spec.ts b/packages/server-core-integration/src/__tests__/index.spec.ts index e5850b1e0f..c4a290a5df 100644 --- a/packages/server-core-integration/src/__tests__/index.spec.ts +++ b/packages/server-core-integration/src/__tests__/index.spec.ts @@ -118,7 +118,7 @@ describe('coreConnection', () => { // Subscribe to data: const coll0 = core.getCollection('peripheralDeviceForDevice') expect(coll0.findOne(id)).toBeFalsy() - const subId = await core.subscribe('peripheralDeviceForDevice', id) + const subId = await core.autoSubscribe('peripheralDeviceForDevice', id) const coll1 = core.getCollection('peripheralDeviceForDevice') expect(coll1.findOne(id)).toMatchObject({ _id: id, From 64ab0b53fc9a65be4961cb2eb6521c77e2debbe4 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Tue, 8 Aug 2023 12:27:36 +0200 Subject: [PATCH 045/479] chore: Updated the prerequisites to point out that NodeJS v18 is supported as of this release. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a0f01dd70e..70ed33b34b 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Follow these instructions to start up Sofie Core in development mode. (For produ ### Prerequisites -- Install [Node.js](https://nodejs.org) 16 (14 will also work) (using [nvm](https://github.com/nvm-sh/nvm) or [nvm-windows](https://github.com/coreybutler/nvm-windows) is the recommended way to install Node.js) +- Install [Node.js](https://nodejs.org) version 18 (as of Release 51) (using [nvm](https://github.com/nvm-sh/nvm) or [nvm-windows](https://github.com/coreybutler/nvm-windows) is the recommended way to install Node.js) - If on Windows: `npm install --global windows-build-tools` - Install [Meteor](https://www.meteor.com/install) (`npm install --global meteor`) - Install [Yarn](https://yarnpkg.com) (`npm install --global corepack && corepack enable`) From 3cc74a44b8f8b7653477f7fc41ddfebdbdc6ccd1 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Tue, 8 Aug 2023 12:55:20 +0200 Subject: [PATCH 046/479] chore: update README --- README.md | 2 +- meteor/package.json | 2 +- packages/blueprints-integration/package.json | 2 +- packages/corelib/package.json | 2 +- packages/documentation/package.json | 2 +- packages/job-worker/package.json | 2 +- packages/live-status-gateway/package.json | 2 +- packages/mos-gateway/package.json | 2 +- packages/openapi/package.json | 2 +- packages/playout-gateway/package.json | 2 +- packages/server-core-integration/package.json | 2 +- packages/shared-lib/package.json | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 70ed33b34b..9f30817d74 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Follow these instructions to start up Sofie Core in development mode. (For produ ### Prerequisites -- Install [Node.js](https://nodejs.org) version 18 (as of Release 51) (using [nvm](https://github.com/nvm-sh/nvm) or [nvm-windows](https://github.com/coreybutler/nvm-windows) is the recommended way to install Node.js) +- Install [Node.js](https://nodejs.org) version 18.x (16.x will also work) (using [nvm](https://github.com/nvm-sh/nvm) or [nvm-windows](https://github.com/coreybutler/nvm-windows) is the recommended way to install Node.js) - If on Windows: `npm install --global windows-build-tools` - Install [Meteor](https://www.meteor.com/install) (`npm install --global meteor`) - Install [Yarn](https://yarnpkg.com) (`npm install --global corepack && corepack enable`) diff --git a/meteor/package.json b/meteor/package.json index 677ab48e01..567f4b50df 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -206,4 +206,4 @@ "@sofie-automation/shared-lib": "portal:../packages/shared-lib" }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} diff --git a/packages/blueprints-integration/package.json b/packages/blueprints-integration/package.json index d0d3a98393..3c2ab8b965 100644 --- a/packages/blueprints-integration/package.json +++ b/packages/blueprints-integration/package.json @@ -51,4 +51,4 @@ ] }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} diff --git a/packages/corelib/package.json b/packages/corelib/package.json index 9e752988ac..675306e632 100644 --- a/packages/corelib/package.json +++ b/packages/corelib/package.json @@ -65,4 +65,4 @@ ] }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} diff --git a/packages/documentation/package.json b/packages/documentation/package.json index 3153d34a2c..b0510c87a8 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -38,4 +38,4 @@ "last 1 safari version" ] } -} \ No newline at end of file +} diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index 0fcb7deb2d..597344cb41 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -68,4 +68,4 @@ ] }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} diff --git a/packages/live-status-gateway/package.json b/packages/live-status-gateway/package.json index 002020d6cb..9bb26856a6 100644 --- a/packages/live-status-gateway/package.json +++ b/packages/live-status-gateway/package.json @@ -76,4 +76,4 @@ "yarn lint:raw" ] } -} \ No newline at end of file +} diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index 27711cdeda..ec84b1ce7f 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -83,4 +83,4 @@ ] }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} diff --git a/packages/openapi/package.json b/packages/openapi/package.json index a2576877b0..e2072b6d70 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -52,4 +52,4 @@ "yarn lint:raw" ] } -} \ No newline at end of file +} diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index ff074f588c..09568af009 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -74,4 +74,4 @@ ] }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} diff --git a/packages/server-core-integration/package.json b/packages/server-core-integration/package.json index 4ba40a8e6e..7e2c135dc0 100644 --- a/packages/server-core-integration/package.json +++ b/packages/server-core-integration/package.json @@ -87,4 +87,4 @@ ] }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 58bbdd5942..b1129113e1 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -53,4 +53,4 @@ ] }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} From 55e02cb1a0b6d237ae9bcd02ebf54e06cf08fb26 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 11 Aug 2023 09:56:04 +0100 Subject: [PATCH 047/479] feat: remove supertimeline from Parts and Pieces resolving SOFIE-2373 (#983) --- meteor/client/lib/rundown.ts | 85 +- meteor/lib/Rundown.ts | 2 +- .../src/documents/pieceInstance.ts | 6 + packages/corelib/package.json | 2 +- .../corelib/src/dataModel/PieceInstance.ts | 16 +- .../src/playout/__tests__/infinites.test.ts | 431 +------ .../playout/__tests__/processAndPrune.test.ts | 618 ++++++++++ .../corelib/src/playout/processAndPrune.ts | 63 +- packages/job-worker/package.json | 2 +- .../__tests__/context-adlibActions.test.ts | 41 +- .../context/OnTimelineGenerateContext.ts | 6 +- .../src/blueprints/context/adlibActions.ts | 5 +- .../job-worker/src/blueprints/context/lib.ts | 2 +- .../playout/__tests__/resolvedPieces.test.ts | 1031 +++++++++++++++++ .../src/playout/__tests__/timeline.test.ts | 19 +- .../abPlayback/__tests__/abPlayback.spec.ts | 57 +- .../playout/abPlayback/abPlaybackSessions.ts | 6 +- packages/job-worker/src/playout/adlibJobs.ts | 19 +- packages/job-worker/src/playout/adlibUtils.ts | 12 +- .../lookahead/__tests__/lookahead.test.ts | 3 + packages/job-worker/src/playout/pieces.ts | 288 +---- .../job-worker/src/playout/resolvedPieces.ts | 171 +++ packages/job-worker/src/playout/take.ts | 9 +- .../timeline/__tests__/pieceGroup.test.ts} | 59 +- .../src/playout/timeline/generate.ts | 16 +- .../job-worker/src/playout/timeline/lib.ts | 3 + .../src/playout/timeline/multi-gateway.ts | 2 +- .../job-worker/src/playout/timeline/part.ts | 2 +- .../job-worker/src/playout/timeline/piece.ts | 2 +- .../src/playout/timeline/pieceGroup.ts} | 32 +- .../playout/timings/timelineTriggerTime.ts | 2 +- 31 files changed, 2085 insertions(+), 927 deletions(-) create mode 100644 packages/corelib/src/playout/__tests__/processAndPrune.test.ts create mode 100644 packages/job-worker/src/playout/__tests__/resolvedPieces.test.ts create mode 100644 packages/job-worker/src/playout/resolvedPieces.ts rename packages/{corelib/src/playout/__tests__/pieces.test.ts => job-worker/src/playout/timeline/__tests__/pieceGroup.test.ts} (90%) rename packages/{corelib/src/playout/pieces.ts => job-worker/src/playout/timeline/pieceGroup.ts} (87%) diff --git a/meteor/client/lib/rundown.ts b/meteor/client/lib/rundown.ts index 503f86f4ce..99f6576799 100644 --- a/meteor/client/lib/rundown.ts +++ b/meteor/client/lib/rundown.ts @@ -1,4 +1,3 @@ -import * as SuperTimeline from 'superfly-timeline' import * as _ from 'underscore' import { PieceUi, PartUi } from '../ui/SegmentTimeline/SegmentTimelineContainer' import { Timecode } from '@sofie-automation/corelib/dist/index' @@ -8,8 +7,6 @@ import { PieceLifespan, IBlueprintActionManifestDisplay, IBlueprintActionManifestDisplayContent, - TimelineObjectCoreExt, - TSR, IOutputLayer, ISourceLayer, } from '@sofie-automation/blueprints-integration' @@ -26,9 +23,11 @@ import { import { PartInstance } from '../../lib/collections/PartInstances' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { literal, getCurrentTime, applyToArray } from '../../lib/lib' -import { processAndPrunePieceInstanceTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' -import { createPieceGroupAndCap, PieceTimelineMetadata } from '@sofie-automation/corelib/dist/playout/pieces' +import { literal, getCurrentTime } from '../../lib/lib' +import { + processAndPrunePieceInstanceTimings, + resolvePrunedPieceInstance, +} from '@sofie-automation/corelib/dist/playout/processAndPrune' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { IAdLibListItem } from '../ui/Shelf/AdLibListItem' import { BucketAdLibItem, BucketAdLibUi } from '../ui/Shelf/RundownViewBuckets' @@ -47,10 +46,6 @@ import { PieceInstances, Segments } from '../collections' import { PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' import { assertNever } from '@sofie-automation/shared-lib/dist/lib/lib' -interface PieceTimelineMetadataExt extends PieceTimelineMetadata { - id: PieceId -} - export namespace RundownUtils { export function padZeros(input: number, places?: number): string { places = places ?? 2 @@ -428,8 +423,6 @@ export namespace RundownUtils { const showHiddenSourceLayers = getShowHiddenSourceLayers() partsE = segmentInfo.partInstances.map((partInstance, itIndex) => { - const partTimeline: SuperTimeline.TimelineObject[] = [] - const partExpectedDuration = calculatePartInstanceExpectedDurationWithPreroll( partInstance, pieces.get(partInstance.part._id) ?? [] @@ -510,35 +503,13 @@ export namespace RundownUtils { // insert items into the timeline for resolution partE.pieces = preprocessedPieces.map((piece) => { + const resolvedPiece = resolvePrunedPieceInstance(nowInPart, piece) const resPiece: PieceExtended = { instance: piece, - renderedDuration: 0, - renderedInPoint: 0, + renderedDuration: resolvedPiece.resolvedDuration ?? null, + renderedInPoint: resolvedPiece.resolvedStart, } - let controlObjEnable: TSR.Timeline.TimelineEnable = piece.piece.enable - // if there is an userDuration override, override it for the timeline - if (piece.userDuration) { - controlObjEnable = { - start: piece.piece.enable.start, - } - - if ('endRelativeToPart' in piece.userDuration) { - controlObjEnable.end = piece.userDuration.endRelativeToPart - } else { - controlObjEnable.end = nowInPart + piece.userDuration.endRelativeToNow - } - } - - const { controlObj, capObjs } = createPieceGroupAndCap(playlist._id, piece, controlObjEnable) - controlObj.metaData = literal({ - id: piece.piece._id, - pieceInstanceGroupId: piece._id, - isPieceTimeline: true, - }) - partTimeline.push(controlObj) - partTimeline.push(...capObjs) - // find the target output layer const outputLayer = outputLayers[piece.piece.outputLayerId] as IOutputLayerExtended | undefined resPiece.outputLayer = outputLayer @@ -594,44 +565,6 @@ export namespace RundownUtils { return resPiece }) - // Use the SuperTimeline library to resolve all the items within the Part - partTimeline.forEach((obj) => { - applyToArray(obj.enable, (enable) => { - if (enable.start === 'now') { - enable.start = nowInPart - } - }) - }) - const tlResolved = SuperTimeline.Resolver.resolveTimeline(partTimeline, { time: 0 }) - // furthestDuration is used to figure out how much content (in terms of time) is there in the Part - let furthestDuration = 0 - const objs = Object.values(tlResolved.objects) - for (let i = 0; i < objs.length; i++) { - const obj = objs[i] - const obj0 = obj as unknown as TimelineObjectCoreExt - if (obj.resolved.resolved && obj0.metaData) { - // Timeline actually has copies of the content object, instead of the object itself, so we need to match it back to the Part - const piece = piecesLookup.get(obj0.metaData.id) - const instance = obj.resolved.instances[0] - if (piece && instance) { - piece.renderedDuration = instance.end ? instance.end - instance.start : null - - // if there is no renderedInPoint, use 0 as the starting time for the item - piece.renderedInPoint = instance.start ? instance.start : 0 - - // if the duration is finite, set the furthestDuration as the inPoint+Duration to know how much content there is - if ( - Number.isFinite(piece.renderedDuration || 0) && - (piece.renderedInPoint || 0) + (piece.renderedDuration || 0) > furthestDuration - ) { - furthestDuration = (piece.renderedInPoint || 0) + (piece.renderedDuration || 0) - } - } else { - // TODO - should this piece be removed? - } - } - } - // displayDuration groups are sets of Parts that share their expectedDurations. // If a member of the group has a displayDuration > 0, this displayDuration is used as the renderedDuration of a part. // This value is then deducted from the expectedDuration and the result leftover duration is added to the group pool. @@ -668,7 +601,7 @@ export namespace RundownUtils { } // use the expectedDuration and fallback to the default display duration for the part - partE.renderedDuration = partE.renderedDuration || Settings.defaultDisplayDuration // furthestDuration + partE.renderedDuration = partE.renderedDuration || Settings.defaultDisplayDuration // push the startsAt value, to figure out when each of the parts starts, relative to the beginning of the segment partE.startsAt = startsAt diff --git a/meteor/lib/Rundown.ts b/meteor/lib/Rundown.ts index 7a277cafb4..78775f397c 100644 --- a/meteor/lib/Rundown.ts +++ b/meteor/lib/Rundown.ts @@ -10,7 +10,6 @@ import { buildPiecesStartingInThisPartQuery, buildPastInfinitePiecesForThisPartQuery, } from '@sofie-automation/corelib/dist/playout/infinites' -import { PieceInstanceWithTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' import { invalidateAfter } from '../lib/invalidatingTime' import { getCurrentTime, groupByToMap, ProtectedString, protectString } from './lib' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' @@ -29,6 +28,7 @@ import { PieceInstances, Pieces } from './collections/libCollections' import { RundownPlaylistCollectionUtil } from './collections/rundownPlaylistUtil' import { PieceContentStatusObj } from './mediaObjects' import { ReadonlyDeep } from 'type-fest' +import { PieceInstanceWithTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' export interface SegmentExtended extends DBSegment { /** Output layers available in the installation used by this segment */ diff --git a/packages/blueprints-integration/src/documents/pieceInstance.ts b/packages/blueprints-integration/src/documents/pieceInstance.ts index b6571a4a4d..c4773b9ed5 100644 --- a/packages/blueprints-integration/src/documents/pieceInstance.ts +++ b/packages/blueprints-integration/src/documents/pieceInstance.ts @@ -32,6 +32,12 @@ export interface IBlueprintPieceInstance { } } export interface IBlueprintResolvedPieceInstance extends IBlueprintPieceInstance { + /** + * Calculated start point within the PartInstance + */ resolvedStart: number + /** + * Calculated duration within the PartInstance + */ resolvedDuration?: number } diff --git a/packages/corelib/package.json b/packages/corelib/package.json index 675306e632..c08081b630 100644 --- a/packages/corelib/package.json +++ b/packages/corelib/package.json @@ -22,7 +22,7 @@ "lint": "run lint:raw .", "unit": "run -T jest", "test": "run lint && run unit", - "watch": "run -T jest --watch", + "watch": "run -T jest --watch --coverage=false", "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", diff --git a/packages/corelib/src/dataModel/PieceInstance.ts b/packages/corelib/src/dataModel/PieceInstance.ts index adaa5b1b74..b862918d4f 100644 --- a/packages/corelib/src/dataModel/PieceInstance.ts +++ b/packages/corelib/src/dataModel/PieceInstance.ts @@ -1,4 +1,4 @@ -import { IBlueprintResolvedPieceInstance, Time } from '@sofie-automation/blueprints-integration' +import { Time } from '@sofie-automation/blueprints-integration' import { protectString } from '../protectedString' import { PieceInstanceInfiniteId, @@ -80,10 +80,16 @@ export interface PieceInstance { plannedStoppedPlayback?: Time } -export interface ResolvedPieceInstance - extends PieceInstance, - Omit { - piece: PieceInstancePiece +export interface ResolvedPieceInstance { + instance: PieceInstance + + /** Calculated start point within the PartInstance */ + resolvedStart: number + /** Calculated duration within the PartInstance */ + resolvedDuration?: number + + /** Timeline priority of the PieceInstance */ + timelinePriority: number } export function omitPiecePropertiesForInstance(piece: Piece): PieceInstancePiece { diff --git a/packages/corelib/src/playout/__tests__/infinites.test.ts b/packages/corelib/src/playout/__tests__/infinites.test.ts index d420adc39f..c755042735 100644 --- a/packages/corelib/src/playout/__tests__/infinites.test.ts +++ b/packages/corelib/src/playout/__tests__/infinites.test.ts @@ -1,9 +1,4 @@ -import { - IBlueprintPieceType, - PieceLifespan, - PlaylistTimingType, - SourceLayerType, -} from '@sofie-automation/blueprints-integration' +import { IBlueprintPieceType, PieceLifespan, PlaylistTimingType } from '@sofie-automation/blueprints-integration' import { DBPartInstance } from '../../dataModel/PartInstance' import { PartId, PartInstanceId, RundownId, RundownPlaylistId } from '../../dataModel/Ids' import { DBPart } from '../../dataModel/Part' @@ -13,432 +8,8 @@ import { Rundown, DBRundown } from '../../dataModel/Rundown' import { literal } from '../../lib' import { protectString } from '../../protectedString' import { getPlayheadTrackingInfinitesForPart } from '../infinites' -import { processAndPrunePieceInstanceTimings } from '../processAndPrune' describe('Infinites', () => { - function createPieceInstance( - id: string, - enable: Piece['enable'], - sourceLayerId: string, - lifespan: PieceLifespan, - clearOrAdlib?: boolean | number, - infinite?: PieceInstance['infinite'] - ): PieceInstance { - return literal({ - _id: protectString(id), - rundownId: protectString(''), - partInstanceId: protectString(''), - playlistActivationId: protectString('active'), - piece: literal({ - _id: protectString(`${id}_p`), - externalId: '', - startPartId: protectString(''), - enable: enable, - name: '', - lifespan: lifespan, - sourceLayerId: sourceLayerId, - outputLayerId: '', - invalid: false, - virtual: clearOrAdlib === true, - content: {}, - timelineObjectsString: EmptyPieceTimelineObjectsBlob, - pieceType: IBlueprintPieceType.Normal, - }), - dynamicallyInserted: clearOrAdlib === true ? Date.now() : clearOrAdlib || undefined, - infinite, - }) - } - - describe('processAndPrunePieceInstanceTimings', () => { - function runAndTidyResult(pieceInstances: PieceInstance[], nowInPart: number, includeVirtual?: boolean) { - const resolvedInstances = processAndPrunePieceInstanceTimings( - { - one: { - _id: 'one', - _rank: 0, - type: SourceLayerType.UNKNOWN, - name: 'One', - }, - two: { - _id: 'two', - _rank: 0, - type: SourceLayerType.UNKNOWN, - name: 'Two', - }, - }, - pieceInstances, - nowInPart, - undefined, - includeVirtual - ) - return resolvedInstances.map((p) => ({ - _id: p._id, - start: p.piece.enable.start, - end: p.resolvedEndCap, - priority: p.priority, - })) - } - - test('simple seperate layers', () => { - const pieceInstances = [ - createPieceInstance('one', { start: 0 }, 'one', PieceLifespan.OutOnRundownEnd), - createPieceInstance('two', { start: 1000 }, 'two', PieceLifespan.OutOnRundownEnd), - ] - - const resolvedInstances = runAndTidyResult(pieceInstances, 500) - expect(resolvedInstances).toEqual([ - { - _id: 'one', - priority: 1, - start: 0, - end: undefined, - }, - { - _id: 'two', - priority: 1, - start: 1000, - end: undefined, - }, - ]) - }) - test('basic collision', () => { - const pieceInstances = [ - createPieceInstance('one', { start: 0 }, 'one', PieceLifespan.OutOnRundownEnd), - createPieceInstance('two', { start: 1000, duration: 5000 }, 'one', PieceLifespan.OutOnRundownEnd), - ] - - const resolvedInstances = runAndTidyResult(pieceInstances, 500) - expect(resolvedInstances).toEqual([ - { - _id: 'one', - priority: 1, - start: 0, - end: 1000, - }, - { - _id: 'two', - priority: 1, - start: 1000, - end: undefined, - }, - ]) - }) - test('onEnd type override', () => { - const pieceInstances = [ - createPieceInstance('zero', { start: 0 }, 'one', PieceLifespan.OutOnShowStyleEnd), - createPieceInstance('one', { start: 500 }, 'one', PieceLifespan.OutOnRundownEnd), - createPieceInstance('two', { start: 1000, duration: 5000 }, 'one', PieceLifespan.OutOnSegmentEnd), - createPieceInstance('four', { start: 2000, duration: 2000 }, 'one', PieceLifespan.WithinPart), - createPieceInstance('three', { start: 3000 }, 'one', PieceLifespan.OutOnRundownEnd), - createPieceInstance('five', { start: 4000 }, 'one', PieceLifespan.OutOnShowStyleEnd), - ] - - const resolvedInstances = runAndTidyResult(pieceInstances, 500) - expect(resolvedInstances).toEqual([ - { - _id: 'zero', - priority: 0, - start: 0, - end: 4000, - }, - { - _id: 'one', - priority: 1, - start: 500, - end: 3000, - }, - { - _id: 'two', - priority: 2, - start: 1000, - end: undefined, - }, - { - _id: 'four', - priority: 5, - start: 2000, - end: undefined, - }, - { - _id: 'three', - priority: 1, - start: 3000, - end: undefined, - }, - { - _id: 'five', - priority: 0, - start: 4000, - end: undefined, - }, - ]) - }) - test('clear onEnd', () => { - const pieceInstances = [ - createPieceInstance('zero', { start: 0 }, 'one', PieceLifespan.OutOnShowStyleEnd), - createPieceInstance('one', { start: 500 }, 'one', PieceLifespan.OutOnRundownEnd), - createPieceInstance('two', { start: 1000 }, 'one', PieceLifespan.OutOnSegmentEnd), - createPieceInstance('three', { start: 3000 }, 'one', PieceLifespan.OutOnRundownEnd, true), - createPieceInstance('two', { start: 5000 }, 'one', PieceLifespan.OutOnSegmentEnd, true), - createPieceInstance('zero', { start: 6000 }, 'one', PieceLifespan.OutOnShowStyleEnd, true), - ] - - const resolvedInstances = runAndTidyResult(pieceInstances, 500) - expect(resolvedInstances).toEqual([ - { - _id: 'zero', - priority: 0, - start: 0, - end: 6000, - }, - { - _id: 'one', - priority: 1, - start: 500, - end: 3000, - }, - { - _id: 'two', - priority: 2, - start: 1000, - end: 5000, - }, - ]) - }) - test('clear onEnd; include virtuals', () => { - const pieceInstances = [ - createPieceInstance('zero', { start: 0 }, 'one', PieceLifespan.OutOnShowStyleEnd), - createPieceInstance('one', { start: 500 }, 'one', PieceLifespan.OutOnRundownEnd), - createPieceInstance('two', { start: 1000 }, 'one', PieceLifespan.OutOnSegmentEnd), - createPieceInstance('three', { start: 3000 }, 'one', PieceLifespan.OutOnRundownEnd, true), - createPieceInstance('four', { start: 5000 }, 'one', PieceLifespan.OutOnSegmentEnd, true), - createPieceInstance('five', { start: 6000 }, 'one', PieceLifespan.OutOnShowStyleEnd, true), - ] - - const resolvedInstances = runAndTidyResult(pieceInstances, 500, true) - expect(resolvedInstances).toEqual([ - { - _id: 'zero', - priority: 0, - start: 0, - end: 6000, - }, - { - _id: 'one', - priority: 1, - start: 500, - end: 3000, - }, - { - _id: 'two', - priority: 2, - start: 1000, - end: 5000, - }, - { - _id: 'three', - priority: 1, - start: 3000, - end: undefined, - }, - { - _id: 'four', - priority: 2, - start: 5000, - end: undefined, - }, - { - _id: 'five', - priority: 0, - start: 6000, - end: undefined, - }, - ]) - }) - test('stop onSegmentChange with onEnd', () => { - const pieceInstances = [ - createPieceInstance('zero', { start: 0 }, 'one', PieceLifespan.OutOnShowStyleEnd), - createPieceInstance('one', { start: 500 }, 'one', PieceLifespan.OutOnSegmentEnd), - createPieceInstance('two', { start: 1000 }, 'one', PieceLifespan.OutOnSegmentChange), - createPieceInstance('three', { start: 2000 }, 'one', PieceLifespan.OutOnRundownEnd), - createPieceInstance('four', { start: 5000 }, 'one', PieceLifespan.OutOnSegmentEnd), - createPieceInstance('five', { start: 6000 }, 'one', PieceLifespan.OutOnShowStyleEnd), - ] - - const resolvedInstances = runAndTidyResult(pieceInstances, 500) - expect(resolvedInstances).toEqual([ - { - _id: 'zero', - priority: 0, - start: 0, - end: 6000, - }, - { - _id: 'one', - priority: 2, - start: 500, - end: 5000, - }, - { - _id: 'two', - priority: 5, - start: 1000, - end: 5000, - }, - { - _id: 'three', - priority: 1, - start: 2000, - end: undefined, - }, - { - _id: 'four', - priority: 2, - start: 5000, - end: undefined, - }, - { - _id: 'five', - priority: 0, - start: 6000, - end: undefined, - }, - ]) - }) - test('prefer newer adlib', () => { - const pieceInstances = [ - createPieceInstance('one', { start: 1000 }, 'one', PieceLifespan.OutOnSegmentEnd, 6000), - createPieceInstance('two', { start: 1000 }, 'one', PieceLifespan.OutOnSegmentEnd, 5500), - ] - - const resolvedInstances = runAndTidyResult(pieceInstances, 500) - expect(resolvedInstances).toEqual([ - { - _id: 'one', - priority: 2, - start: 1000, - end: undefined, - }, - ]) - }) - test('prefer newer adlib2', () => { - const pieceInstances = [ - createPieceInstance('one', { start: 1000 }, 'one', PieceLifespan.OutOnRundownChange, 6000), - createPieceInstance('two', { start: 1000 }, 'one', PieceLifespan.OutOnRundownChange, 5500), - createPieceInstance('three', { start: 1000 }, 'one', PieceLifespan.OutOnRundownChange, 7000), - createPieceInstance('four', { start: 1000 }, 'one', PieceLifespan.OutOnRundownChange, 4000), - ] - - const resolvedInstances = runAndTidyResult(pieceInstances, 500) - expect(resolvedInstances).toEqual([ - { - _id: 'three', - priority: 5, - start: 1000, - end: undefined, - }, - ]) - }) - test('prefer newer adlib3', () => { - const pieceInstances = [ - createPieceInstance('one', { start: 1000 }, 'one', PieceLifespan.OutOnShowStyleEnd, 6000), - createPieceInstance('two', { start: 1000 }, 'one', PieceLifespan.OutOnShowStyleEnd, 5500), - ] - - const resolvedInstances = runAndTidyResult(pieceInstances, 500) - expect(resolvedInstances).toEqual([ - { - _id: 'one', - priority: 0, - start: 1000, - end: undefined, - }, - ]) - }) - test('continue onChange when start=0 and onEnd is present, and both are infinite continuations', () => { - const pieceInstances = [ - createPieceInstance('one', { start: 0 }, 'one', PieceLifespan.OutOnSegmentChange, 6000, { - fromPreviousPart: true, - fromPreviousPlayhead: true, - infiniteInstanceId: protectString('one_a'), - infiniteInstanceIndex: 0, - infinitePieceId: protectString('one_b'), - }), - createPieceInstance('two', { start: 0 }, 'one', PieceLifespan.OutOnSegmentEnd, false, { - fromPreviousPart: true, - infiniteInstanceId: protectString('two_a'), - infiniteInstanceIndex: 0, - infinitePieceId: protectString('two_b'), - }), - ] - - const resolvedInstances = runAndTidyResult(pieceInstances, 500) - expect(resolvedInstances).toEqual([ - { - _id: 'one', - priority: 5, - start: 0, - end: undefined, - }, - { - _id: 'two', - priority: 2, - start: 0, - end: undefined, - }, - ]) - }) - test('stop onChange when start=0 and onEnd is present, and both are infinite continuations', () => { - const pieceInstances = [ - createPieceInstance('one', { start: 0 }, 'one', PieceLifespan.OutOnSegmentChange, 6000, { - fromPreviousPart: true, - fromPreviousPlayhead: true, - infiniteInstanceId: protectString('one_a'), - infiniteInstanceIndex: 0, - infinitePieceId: protectString('one_b'), - }), - createPieceInstance('two', { start: 0 }, 'one', PieceLifespan.OutOnSegmentEnd, false, { - fromPreviousPart: false, - infiniteInstanceId: protectString('two_a'), - infiniteInstanceIndex: 0, - infinitePieceId: protectString('two_b'), - }), - ] - - const resolvedInstances = runAndTidyResult(pieceInstances, 500) - expect(resolvedInstances).toEqual([ - { - _id: 'two', - priority: 2, - start: 0, - end: undefined, - }, - ]) - }) - test('stop onRundownEnd continuation when start=0 and onSegmentEnd is present', () => { - const pieceInstances = [ - createPieceInstance('one', { start: 0 }, 'one', PieceLifespan.OutOnRundownEnd, false, { - fromPreviousPart: true, - infiniteInstanceId: protectString('one_a'), - infiniteInstanceIndex: 0, - infinitePieceId: protectString('one_b'), - }), - createPieceInstance('two', { start: 0 }, 'one', PieceLifespan.OutOnSegmentEnd, false, { - fromPreviousPart: false, - infiniteInstanceId: protectString('two_a'), - infiniteInstanceIndex: 0, - infinitePieceId: protectString('two_b'), - }), - ] - - pieceInstances[1].piece.virtual = true - - const resolvedInstances = runAndTidyResult(pieceInstances, 500) - - // don't expect virtual Pieces in the results, but 'one' should be pruned too - expect(resolvedInstances).toEqual([]) - }) - }) describe('getPlayheadTrackingInfinitesForPart', () => { function runAndTidyResult( previousPartInstance: Pick & { partId: PartId }, diff --git a/packages/corelib/src/playout/__tests__/processAndPrune.test.ts b/packages/corelib/src/playout/__tests__/processAndPrune.test.ts new file mode 100644 index 0000000000..fb8896df7d --- /dev/null +++ b/packages/corelib/src/playout/__tests__/processAndPrune.test.ts @@ -0,0 +1,618 @@ +import { IBlueprintPieceType, PieceLifespan, SourceLayerType } from '@sofie-automation/blueprints-integration' +import clone = require('fast-clone') +import { EmptyPieceTimelineObjectsBlob, Piece } from '../../dataModel/Piece' +import { PieceInstance, PieceInstancePiece, ResolvedPieceInstance } from '../../dataModel/PieceInstance' +import { literal } from '../../lib' +import { protectString } from '../../protectedString' +import { + PieceInstanceWithTimings, + processAndPrunePieceInstanceTimings, + resolvePrunedPieceInstance, +} from '../processAndPrune' + +describe('processAndPrunePieceInstanceTimings', () => { + function createPieceInstance( + id: string, + enable: Piece['enable'], + sourceLayerId: string, + lifespan: PieceLifespan, + clearOrAdlib?: boolean | number, + infinite?: PieceInstance['infinite'] + ): PieceInstance { + return literal({ + _id: protectString(id), + rundownId: protectString(''), + partInstanceId: protectString(''), + playlistActivationId: protectString('active'), + piece: literal({ + _id: protectString(`${id}_p`), + externalId: '', + startPartId: protectString(''), + enable: enable, + name: '', + lifespan: lifespan, + sourceLayerId: sourceLayerId, + outputLayerId: '', + invalid: false, + virtual: clearOrAdlib === true, + content: {}, + timelineObjectsString: EmptyPieceTimelineObjectsBlob, + pieceType: IBlueprintPieceType.Normal, + }), + dynamicallyInserted: clearOrAdlib === true ? Date.now() : clearOrAdlib || undefined, + infinite, + }) + } + + function runAndTidyResult(pieceInstances: PieceInstance[], nowInPart: number, includeVirtual?: boolean) { + const resolvedInstances = processAndPrunePieceInstanceTimings( + { + one: { + _id: 'one', + _rank: 0, + type: SourceLayerType.UNKNOWN, + name: 'One', + }, + two: { + _id: 'two', + _rank: 0, + type: SourceLayerType.UNKNOWN, + name: 'Two', + }, + }, + pieceInstances, + nowInPart, + undefined, + includeVirtual + ) + return resolvedInstances.map((p) => ({ + _id: p._id, + start: p.piece.enable.start, + end: p.resolvedEndCap, + priority: p.priority, + })) + } + + test('simple seperate layers', () => { + const pieceInstances = [ + createPieceInstance('one', { start: 0 }, 'one', PieceLifespan.OutOnRundownEnd), + createPieceInstance('two', { start: 1000 }, 'two', PieceLifespan.OutOnRundownEnd), + ] + + const resolvedInstances = runAndTidyResult(pieceInstances, 500) + expect(resolvedInstances).toEqual([ + { + _id: 'one', + priority: 1, + start: 0, + end: undefined, + }, + { + _id: 'two', + priority: 1, + start: 1000, + end: undefined, + }, + ]) + }) + test('basic collision', () => { + const pieceInstances = [ + createPieceInstance('one', { start: 0 }, 'one', PieceLifespan.OutOnRundownEnd), + createPieceInstance('two', { start: 1000, duration: 5000 }, 'one', PieceLifespan.OutOnRundownEnd), + ] + + const resolvedInstances = runAndTidyResult(pieceInstances, 500) + expect(resolvedInstances).toEqual([ + { + _id: 'one', + priority: 1, + start: 0, + end: 1000, + }, + { + _id: 'two', + priority: 1, + start: 1000, + end: undefined, + }, + ]) + }) + test('onEnd type override', () => { + const pieceInstances = [ + createPieceInstance('zero', { start: 0 }, 'one', PieceLifespan.OutOnShowStyleEnd), + createPieceInstance('one', { start: 500 }, 'one', PieceLifespan.OutOnRundownEnd), + createPieceInstance('two', { start: 1000, duration: 5000 }, 'one', PieceLifespan.OutOnSegmentEnd), + createPieceInstance('four', { start: 2000, duration: 2000 }, 'one', PieceLifespan.WithinPart), + createPieceInstance('three', { start: 3000 }, 'one', PieceLifespan.OutOnRundownEnd), + createPieceInstance('five', { start: 4000 }, 'one', PieceLifespan.OutOnShowStyleEnd), + ] + + const resolvedInstances = runAndTidyResult(pieceInstances, 500) + expect(resolvedInstances).toEqual([ + { + _id: 'zero', + priority: 0, + start: 0, + end: 4000, + }, + { + _id: 'one', + priority: 1, + start: 500, + end: 3000, + }, + { + _id: 'two', + priority: 2, + start: 1000, + end: undefined, + }, + { + _id: 'four', + priority: 5, + start: 2000, + end: undefined, + }, + { + _id: 'three', + priority: 1, + start: 3000, + end: undefined, + }, + { + _id: 'five', + priority: 0, + start: 4000, + end: undefined, + }, + ]) + }) + test('clear onEnd', () => { + const pieceInstances = [ + createPieceInstance('zero', { start: 0 }, 'one', PieceLifespan.OutOnShowStyleEnd), + createPieceInstance('one', { start: 500 }, 'one', PieceLifespan.OutOnRundownEnd), + createPieceInstance('two', { start: 1000 }, 'one', PieceLifespan.OutOnSegmentEnd), + createPieceInstance('three', { start: 3000 }, 'one', PieceLifespan.OutOnRundownEnd, true), + createPieceInstance('two', { start: 5000 }, 'one', PieceLifespan.OutOnSegmentEnd, true), + createPieceInstance('zero', { start: 6000 }, 'one', PieceLifespan.OutOnShowStyleEnd, true), + ] + + const resolvedInstances = runAndTidyResult(pieceInstances, 500) + expect(resolvedInstances).toEqual([ + { + _id: 'zero', + priority: 0, + start: 0, + end: 6000, + }, + { + _id: 'one', + priority: 1, + start: 500, + end: 3000, + }, + { + _id: 'two', + priority: 2, + start: 1000, + end: 5000, + }, + ]) + }) + test('clear onEnd; include virtuals', () => { + const pieceInstances = [ + createPieceInstance('zero', { start: 0 }, 'one', PieceLifespan.OutOnShowStyleEnd), + createPieceInstance('one', { start: 500 }, 'one', PieceLifespan.OutOnRundownEnd), + createPieceInstance('two', { start: 1000 }, 'one', PieceLifespan.OutOnSegmentEnd), + createPieceInstance('three', { start: 3000 }, 'one', PieceLifespan.OutOnRundownEnd, true), + createPieceInstance('four', { start: 5000 }, 'one', PieceLifespan.OutOnSegmentEnd, true), + createPieceInstance('five', { start: 6000 }, 'one', PieceLifespan.OutOnShowStyleEnd, true), + ] + + const resolvedInstances = runAndTidyResult(pieceInstances, 500, true) + expect(resolvedInstances).toEqual([ + { + _id: 'zero', + priority: 0, + start: 0, + end: 6000, + }, + { + _id: 'one', + priority: 1, + start: 500, + end: 3000, + }, + { + _id: 'two', + priority: 2, + start: 1000, + end: 5000, + }, + { + _id: 'three', + priority: 1, + start: 3000, + end: undefined, + }, + { + _id: 'four', + priority: 2, + start: 5000, + end: undefined, + }, + { + _id: 'five', + priority: 0, + start: 6000, + end: undefined, + }, + ]) + }) + test('stop onSegmentChange with onEnd', () => { + const pieceInstances = [ + createPieceInstance('zero', { start: 0 }, 'one', PieceLifespan.OutOnShowStyleEnd), + createPieceInstance('one', { start: 500 }, 'one', PieceLifespan.OutOnSegmentEnd), + createPieceInstance('two', { start: 1000 }, 'one', PieceLifespan.OutOnSegmentChange), + createPieceInstance('three', { start: 2000 }, 'one', PieceLifespan.OutOnRundownEnd), + createPieceInstance('four', { start: 5000 }, 'one', PieceLifespan.OutOnSegmentEnd), + createPieceInstance('five', { start: 6000 }, 'one', PieceLifespan.OutOnShowStyleEnd), + ] + + const resolvedInstances = runAndTidyResult(pieceInstances, 500) + expect(resolvedInstances).toEqual([ + { + _id: 'zero', + priority: 0, + start: 0, + end: 6000, + }, + { + _id: 'one', + priority: 2, + start: 500, + end: 5000, + }, + { + _id: 'two', + priority: 5, + start: 1000, + end: 5000, + }, + { + _id: 'three', + priority: 1, + start: 2000, + end: undefined, + }, + { + _id: 'four', + priority: 2, + start: 5000, + end: undefined, + }, + { + _id: 'five', + priority: 0, + start: 6000, + end: undefined, + }, + ]) + }) + test('prefer newer adlib', () => { + const pieceInstances = [ + createPieceInstance('one', { start: 1000 }, 'one', PieceLifespan.OutOnSegmentEnd, 6000), + createPieceInstance('two', { start: 1000 }, 'one', PieceLifespan.OutOnSegmentEnd, 5500), + ] + + const resolvedInstances = runAndTidyResult(pieceInstances, 500) + expect(resolvedInstances).toEqual([ + { + _id: 'one', + priority: 2, + start: 1000, + end: undefined, + }, + ]) + }) + test('prefer newer adlib2', () => { + const pieceInstances = [ + createPieceInstance('one', { start: 1000 }, 'one', PieceLifespan.OutOnRundownChange, 6000), + createPieceInstance('two', { start: 1000 }, 'one', PieceLifespan.OutOnRundownChange, 5500), + createPieceInstance('three', { start: 1000 }, 'one', PieceLifespan.OutOnRundownChange, 7000), + createPieceInstance('four', { start: 1000 }, 'one', PieceLifespan.OutOnRundownChange, 4000), + ] + + const resolvedInstances = runAndTidyResult(pieceInstances, 500) + expect(resolvedInstances).toEqual([ + { + _id: 'three', + priority: 5, + start: 1000, + end: undefined, + }, + ]) + }) + test('prefer newer adlib3', () => { + const pieceInstances = [ + createPieceInstance('one', { start: 1000 }, 'one', PieceLifespan.OutOnShowStyleEnd, 6000), + createPieceInstance('two', { start: 1000 }, 'one', PieceLifespan.OutOnShowStyleEnd, 5500), + ] + + const resolvedInstances = runAndTidyResult(pieceInstances, 500) + expect(resolvedInstances).toEqual([ + { + _id: 'one', + priority: 0, + start: 1000, + end: undefined, + }, + ]) + }) + test('continue onChange when start=0 and onEnd is present, and both are infinite continuations', () => { + const pieceInstances = [ + createPieceInstance('one', { start: 0 }, 'one', PieceLifespan.OutOnSegmentChange, 6000, { + fromPreviousPart: true, + fromPreviousPlayhead: true, + infiniteInstanceId: protectString('one_a'), + infiniteInstanceIndex: 0, + infinitePieceId: protectString('one_b'), + }), + createPieceInstance('two', { start: 0 }, 'one', PieceLifespan.OutOnSegmentEnd, false, { + fromPreviousPart: true, + infiniteInstanceId: protectString('two_a'), + infiniteInstanceIndex: 0, + infinitePieceId: protectString('two_b'), + }), + ] + + const resolvedInstances = runAndTidyResult(pieceInstances, 500) + expect(resolvedInstances).toEqual([ + { + _id: 'one', + priority: 5, + start: 0, + end: undefined, + }, + { + _id: 'two', + priority: 2, + start: 0, + end: undefined, + }, + ]) + }) + test('stop onChange when start=0 and onEnd is present, and both are infinite continuations', () => { + const pieceInstances = [ + createPieceInstance('one', { start: 0 }, 'one', PieceLifespan.OutOnSegmentChange, 6000, { + fromPreviousPart: true, + fromPreviousPlayhead: true, + infiniteInstanceId: protectString('one_a'), + infiniteInstanceIndex: 0, + infinitePieceId: protectString('one_b'), + }), + createPieceInstance('two', { start: 0 }, 'one', PieceLifespan.OutOnSegmentEnd, false, { + fromPreviousPart: false, + infiniteInstanceId: protectString('two_a'), + infiniteInstanceIndex: 0, + infinitePieceId: protectString('two_b'), + }), + ] + + const resolvedInstances = runAndTidyResult(pieceInstances, 500) + expect(resolvedInstances).toEqual([ + { + _id: 'two', + priority: 2, + start: 0, + end: undefined, + }, + ]) + }) + test('stop onRundownEnd continuation when start=0 and onSegmentEnd is present', () => { + const pieceInstances = [ + createPieceInstance('one', { start: 0 }, 'one', PieceLifespan.OutOnRundownEnd, false, { + fromPreviousPart: true, + infiniteInstanceId: protectString('one_a'), + infiniteInstanceIndex: 0, + infinitePieceId: protectString('one_b'), + }), + createPieceInstance('two', { start: 0 }, 'one', PieceLifespan.OutOnSegmentEnd, false, { + fromPreviousPart: false, + infiniteInstanceId: protectString('two_a'), + infiniteInstanceIndex: 0, + infinitePieceId: protectString('two_b'), + }), + ] + + pieceInstances[1].piece.virtual = true + + const resolvedInstances = runAndTidyResult(pieceInstances, 500) + + // don't expect virtual Pieces in the results, but 'one' should be pruned too + expect(resolvedInstances).toEqual([]) + }) +}) + +describe('resolvePrunedPieceInstances', () => { + function createPieceInstance( + enable: Piece['enable'], + resolvedEndCap?: PieceInstanceWithTimings['resolvedEndCap'], + userDuration?: PieceInstance['userDuration'] + ): PieceInstanceWithTimings { + return literal({ + _id: protectString(''), + rundownId: protectString(''), + partInstanceId: protectString(''), + playlistActivationId: protectString('active'), + piece: literal({ + _id: protectString(''), + externalId: '', + startPartId: protectString(''), + enable: enable, + name: '', + lifespan: PieceLifespan.WithinPart, + sourceLayerId: '', + outputLayerId: '', + invalid: false, + virtual: false, + content: {}, + timelineObjectsString: EmptyPieceTimelineObjectsBlob, + pieceType: IBlueprintPieceType.Normal, + }), + priority: Math.random(), + resolvedEndCap, + userDuration, + }) + } + + test('numeric start, no duration', async () => { + const nowInPart = 123 + const piece = createPieceInstance({ start: 2000 }) + + expect(resolvePrunedPieceInstance(nowInPart, clone(piece))).toStrictEqual({ + instance: clone(piece), + timelinePriority: piece.priority, + resolvedStart: 2000, + resolvedDuration: undefined, + } satisfies ResolvedPieceInstance) + }) + + test('numeric start, with planned duration', async () => { + const nowInPart = 123 + const piece = createPieceInstance({ start: 2000, duration: 3400 }) + + expect(resolvePrunedPieceInstance(nowInPart, clone(piece))).toStrictEqual({ + instance: clone(piece), + timelinePriority: piece.priority, + resolvedStart: 2000, + resolvedDuration: 3400, + } satisfies ResolvedPieceInstance) + }) + + test('now start, no duration', async () => { + const nowInPart = 123 + const piece = createPieceInstance({ start: 'now' }) + + expect(resolvePrunedPieceInstance(nowInPart, clone(piece))).toStrictEqual({ + instance: clone(piece), + timelinePriority: piece.priority, + resolvedStart: nowInPart, + resolvedDuration: undefined, + } satisfies ResolvedPieceInstance) + }) + + test('now start, with planned duration', async () => { + const nowInPart = 123 + const piece = createPieceInstance({ start: 'now', duration: 3400 }) + + expect(resolvePrunedPieceInstance(nowInPart, clone(piece))).toStrictEqual({ + instance: clone(piece), + timelinePriority: piece.priority, + resolvedStart: nowInPart, + resolvedDuration: 3400, + } satisfies ResolvedPieceInstance) + }) + + test('now start, with end cap', async () => { + const nowInPart = 123 + const piece = createPieceInstance({ start: 'now' }, 5000) + + expect(resolvePrunedPieceInstance(nowInPart, clone(piece))).toStrictEqual({ + instance: clone(piece), + timelinePriority: piece.priority, + resolvedStart: nowInPart, + resolvedDuration: 5000 - nowInPart, + } satisfies ResolvedPieceInstance) + }) + + test('now start, with end cap and longer planned duration', async () => { + const nowInPart = 123 + const piece = createPieceInstance({ start: 'now', duration: 6000 }, 5000) + + expect(resolvePrunedPieceInstance(nowInPart, clone(piece))).toStrictEqual({ + instance: clone(piece), + timelinePriority: piece.priority, + resolvedStart: nowInPart, + resolvedDuration: 5000 - nowInPart, + } satisfies ResolvedPieceInstance) + }) + + test('now start, with end cap and shorter planned duration', async () => { + const nowInPart = 123 + const piece = createPieceInstance({ start: 'now', duration: 3000 }, 5000) + + expect(resolvePrunedPieceInstance(nowInPart, clone(piece))).toStrictEqual({ + instance: clone(piece), + timelinePriority: piece.priority, + resolvedStart: nowInPart, + resolvedDuration: 3000, + } satisfies ResolvedPieceInstance) + }) + + test('now start, with userDuration.endRelativeToPart', async () => { + const nowInPart = 123 + const piece = createPieceInstance({ start: 'now' }, undefined, { + endRelativeToPart: 4000, + }) + + expect(resolvePrunedPieceInstance(nowInPart, clone(piece))).toStrictEqual({ + instance: clone(piece), + timelinePriority: piece.priority, + resolvedStart: nowInPart, + resolvedDuration: 4000 - nowInPart, + } satisfies ResolvedPieceInstance) + }) + + test('numeric start, with userDuration.endRelativeToNow', async () => { + const nowInPart = 123 + const piece = createPieceInstance({ start: 500 }, undefined, { + endRelativeToNow: 4000, + }) + + expect(resolvePrunedPieceInstance(nowInPart, clone(piece))).toStrictEqual({ + instance: clone(piece), + timelinePriority: piece.priority, + resolvedStart: 500, + resolvedDuration: 4000 - 500 + nowInPart, + } satisfies ResolvedPieceInstance) + }) + + test('now start, with userDuration.endRelativeToNow', async () => { + const nowInPart = 123 + const piece = createPieceInstance({ start: 'now' }, undefined, { + endRelativeToNow: 4000, + }) + + expect(resolvePrunedPieceInstance(nowInPart, clone(piece))).toStrictEqual({ + instance: clone(piece), + timelinePriority: piece.priority, + resolvedStart: nowInPart, + resolvedDuration: 4000, + } satisfies ResolvedPieceInstance) + }) + + test('now start, with end cap, planned duration and userDuration.endRelativeToPart', async () => { + const nowInPart = 123 + const piece = createPieceInstance({ start: 'now', duration: 3000 }, 5000, { endRelativeToPart: 2800 }) + + expect(resolvePrunedPieceInstance(nowInPart, clone(piece))).toStrictEqual({ + instance: clone(piece), + timelinePriority: piece.priority, + resolvedStart: nowInPart, + resolvedDuration: 2800 - nowInPart, + } satisfies ResolvedPieceInstance) + }) + + test('now start, with end cap, planned duration and userDuration.endRelativeToNow', async () => { + const nowInPart = 123 + const piece = createPieceInstance({ start: 'now', duration: 3000 }, 5000, { endRelativeToNow: 2800 }) + + expect(resolvePrunedPieceInstance(nowInPart, clone(piece))).toStrictEqual({ + instance: clone(piece), + timelinePriority: piece.priority, + resolvedStart: nowInPart, + resolvedDuration: 2800, + } satisfies ResolvedPieceInstance) + }) +}) diff --git a/packages/corelib/src/playout/processAndPrune.ts b/packages/corelib/src/playout/processAndPrune.ts index 8fc4d73f90..a64301652e 100644 --- a/packages/corelib/src/playout/processAndPrune.ts +++ b/packages/corelib/src/playout/processAndPrune.ts @@ -1,17 +1,16 @@ import { ISourceLayer, PieceLifespan } from '@sofie-automation/blueprints-integration' import { literal } from '@sofie-automation/shared-lib/dist/lib/lib' -import { PieceInstance } from '../dataModel/PieceInstance' +import { PieceInstance, ResolvedPieceInstance } from '../dataModel/PieceInstance' import { SourceLayers } from '../dataModel/ShowStyleBase' import { assertNever, groupByToMapFunc } from '../lib' import _ = require('underscore') import { isCandidateBetterToBeContinued, isCandidateMoreImportant } from './infinites' -import { getPieceControlObjectId } from './ids' /** * Get the `enable: { start: ?? }` for the new piece in terms that can be used as an `end` for another object */ -function getPieceStartTime(newPieceStart: number | 'now', newPiece: PieceInstance): number | string { - return typeof newPieceStart === 'number' ? newPieceStart : `#${getPieceControlObjectId(newPiece)}.start` +function getPieceStartTime(newPieceStart: number | 'now'): number | RelativeResolvedEndCap { + return typeof newPieceStart === 'number' ? newPieceStart : { offsetFromNow: 0 } } function isClear(piece?: PieceInstance): boolean { @@ -38,6 +37,10 @@ function isCappedByAVirtual( return false } +export interface RelativeResolvedEndCap { + offsetFromNow: number +} + export interface PieceInstanceWithTimings extends PieceInstance { /** * This is a maximum end point of the pieceInstance. @@ -47,7 +50,7 @@ export interface PieceInstanceWithTimings extends PieceInstance { * - '#something.start + 100', if it was stopped by something that needs a preroll * - '100', if not relative to now at all */ - resolvedEndCap?: number | string + resolvedEndCap?: number | RelativeResolvedEndCap priority: number } @@ -55,6 +58,7 @@ export interface PieceInstanceWithTimings extends PieceInstance { * Process the infinite pieces to determine the start time and a maximum end time for each. * Any pieces which have no chance of being shown (duplicate start times) are pruned * The stacking order of infinites is considered, to define the stop times + * Note: `nowInPart` is only needed to order the PieceInstances. The result of this can be cached until that order changes */ export function processAndPrunePieceInstanceTimings( sourceLayers: SourceLayers, @@ -120,7 +124,7 @@ function updateWithNewPieces( if (newPiece) { const activePiece = activePieces[key] if (activePiece) { - activePiece.resolvedEndCap = getPieceStartTime(newPiecesStart, newPiece) + activePiece.resolvedEndCap = getPieceStartTime(newPiecesStart) } // track the new piece activePieces[key] = newPiece @@ -145,7 +149,7 @@ function updateWithNewPieces( (newPiecesStart !== 0 || isCandidateBetterToBeContinued(activePieces.other, newPiece)) ) { // These modes should stop the 'other' when they start if not hidden behind a higher priority onEnd - activePieces.other.resolvedEndCap = getPieceStartTime(newPiecesStart, newPiece) + activePieces.other.resolvedEndCap = getPieceStartTime(newPiecesStart) activePieces.other = undefined } } @@ -209,3 +213,48 @@ function findPieceInstancesOnInfiniteLayers(pieces: PieceInstance[]): PieceInsta return res } + +/** + * Resolve a PieceInstanceWithTimings to approximated numbers within the PartInstance + * @param nowInPart Approximate time of the playhead within the PartInstance + * @param pieceInstance The PieceInstance to resolve + */ +export function resolvePrunedPieceInstance( + nowInPart: number, + pieceInstance: PieceInstanceWithTimings +): ResolvedPieceInstance { + const resolvedStart = pieceInstance.piece.enable.start === 'now' ? nowInPart : pieceInstance.piece.enable.start + + // Interpret the `resolvedEndCap` property into a number + let resolvedEnd: number | undefined + if (typeof pieceInstance.resolvedEndCap === 'number') { + resolvedEnd = pieceInstance.resolvedEndCap + } else if (pieceInstance.resolvedEndCap) { + resolvedEnd = nowInPart + pieceInstance.resolvedEndCap.offsetFromNow + } + + // Find any possible durations this piece may have + const caps: number[] = [] + if (resolvedEnd !== undefined) caps.push(resolvedEnd - resolvedStart) + + // Consider the blueprint defined duration + if (pieceInstance.piece.enable.duration !== undefined) caps.push(pieceInstance.piece.enable.duration) + + // Consider the playout userDuration + if (pieceInstance.userDuration) { + if ('endRelativeToPart' in pieceInstance.userDuration) { + caps.push(pieceInstance.userDuration.endRelativeToPart - resolvedStart) + } else if ('endRelativeToNow' in pieceInstance.userDuration) { + caps.push(nowInPart + pieceInstance.userDuration.endRelativeToNow - resolvedStart) + } + } + + return { + instance: pieceInstance, + + resolvedStart, + resolvedDuration: caps.length ? Math.min(...caps) : undefined, + + timelinePriority: pieceInstance.priority, + } +} diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index 597344cb41..b2b6ce0061 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -23,7 +23,7 @@ "lint": "run lint:raw .", "unit": "run -T jest", "test": "run lint && run unit", - "watch": "run -T jest --watch", + "watch": "run -T jest --watch --coverage=false", "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", diff --git a/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts b/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts index 09a57f5d28..4d85259cd8 100644 --- a/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts +++ b/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts @@ -25,7 +25,7 @@ import { setupDefaultRundown, setupMockShowStyleCompound } from '../../__mocks__ import { SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { JobContext } from '../../jobs' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { PieceInstance, ResolvedPieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { getCurrentTime } from '../../lib' import { @@ -48,10 +48,11 @@ const innerStartQueuedAdLibOrig = PlayoutAdlib.innerStartQueuedAdLib type TinnerStartQueuedAdLib = jest.MockedFunction const innerStartQueuedAdLibMock = jest.spyOn(PlayoutAdlib, 'innerStartQueuedAdLib') as TinnerStartQueuedAdLib -jest.mock('../../playout/pieces') -import { getResolvedPieces } from '../../playout/pieces' -type TgetResolvedPieces = jest.MockedFunction -const getResolvedPiecesMock = getResolvedPieces as TgetResolvedPieces +jest.mock('../../playout/resolvedPieces') +import { getResolvedPiecesForCurrentPartInstance } from '../../playout/resolvedPieces' +type TgetResolvedPiecesForCurrentPartInstance = jest.MockedFunction +const getResolvedPiecesForCurrentPartInstanceMock = + getResolvedPiecesForCurrentPartInstance as TgetResolvedPiecesForCurrentPartInstance jest.mock('../postProcess') import { postProcessPieces, postProcessTimelineObjects } from '../postProcess' @@ -407,33 +408,39 @@ describe('Test blueprint api context', () => { expect(cache.PartInstances.documents.size).toBe(0) - expect(getResolvedPiecesMock).toHaveBeenCalledTimes(0) + expect(getResolvedPiecesForCurrentPartInstanceMock).toHaveBeenCalledTimes(0) await expect(context.getResolvedPieceInstances('next')).resolves.toHaveLength(0) await expect(context.getResolvedPieceInstances('current')).resolves.toHaveLength(0) - expect(getResolvedPiecesMock).toHaveBeenCalledTimes(0) + expect(getResolvedPiecesForCurrentPartInstanceMock).toHaveBeenCalledTimes(0) }) let mockCalledIds: PartInstanceId[] = [] - getResolvedPiecesMock.mockImplementation( + getResolvedPiecesForCurrentPartInstanceMock.mockImplementation( ( context2: JobContext, cache2: ReadOnlyCache, sourceLayers: SourceLayers, - partInstance: DBPartInstance + partInstance: Pick, + now?: number ) => { expect(context2).toBe(jobContext) expect(cache2).toBeInstanceOf(CacheForPlayout) expect(sourceLayers).toBeTruthy() + expect(now).toBeFalsy() mockCalledIds.push(partInstance._id) return [ { - _id: 'abc', - piece: { - timelineObjectsString: EmptyPieceTimelineObjectsBlob, - }, + instance: { + _id: 'abc', + piece: { + timelineObjectsString: EmptyPieceTimelineObjectsBlob, + }, + } as any as PieceInstance, + resolvedStart: 0, + timelinePriority: 0, }, - ] as any as ResolvedPieceInstance[] + ] } ) @@ -447,12 +454,12 @@ describe('Test blueprint api context', () => { await expect( context.getResolvedPieceInstances('current').then((res) => res.map((p) => p._id)) ).resolves.toEqual(['abc']) - expect(getResolvedPiecesMock).toHaveBeenCalledTimes(1) + expect(getResolvedPiecesForCurrentPartInstanceMock).toHaveBeenCalledTimes(1) expect(mockCalledIds).toEqual([allPartInstances[1]._id]) }) mockCalledIds = [] - getResolvedPiecesMock.mockClear() + getResolvedPiecesForCurrentPartInstanceMock.mockClear() await setPartInstances(jobContext, playlistId, null, allPartInstances[2]) await wrapWithCache(jobContext, playlistId, async (cache) => { @@ -465,7 +472,7 @@ describe('Test blueprint api context', () => { context.getResolvedPieceInstances('next').then((res) => res.map((p) => p._id)) ).resolves.toEqual(['abc']) await expect(context.getResolvedPieceInstances('current')).resolves.toHaveLength(0) - expect(getResolvedPiecesMock).toHaveBeenCalledTimes(1) + expect(getResolvedPiecesForCurrentPartInstanceMock).toHaveBeenCalledTimes(1) expect(mockCalledIds).toEqual([allPartInstances[2]._id]) }) }) diff --git a/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts b/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts index 956ca90d3d..eeefcffea3 100644 --- a/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts +++ b/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts @@ -11,7 +11,7 @@ import { clone } from '@sofie-automation/corelib/dist/lib' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { ABSessionInfo, DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { getCurrentTime } from '../../lib' -import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { PieceInstance, ResolvedPieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { ProcessedStudioConfig, ProcessedShowStyleConfig } from '../config' import _ = require('underscore') import { ProcessedShowStyleCompound } from '../../jobs' @@ -39,7 +39,7 @@ export class OnTimelineGenerateContext extends RundownContext implements ITimeli previousPartInstance: DBPartInstance | undefined, currentPartInstance: DBPartInstance | undefined, nextPartInstance: DBPartInstance | undefined, - pieceInstances: PieceInstance[] + pieceInstances: ResolvedPieceInstance[] ) { super( { @@ -60,7 +60,7 @@ export class OnTimelineGenerateContext extends RundownContext implements ITimeli const partInstances = _.compact([previousPartInstance, currentPartInstance, nextPartInstance]) for (const pieceInstance of pieceInstances) { - this.#pieceInstanceCache.set(pieceInstance._id, pieceInstance) + this.#pieceInstanceCache.set(pieceInstance.instance._id, pieceInstance.instance) } this.abSessionsHelper = new AbSessionHelper( diff --git a/packages/job-worker/src/blueprints/context/adlibActions.ts b/packages/job-worker/src/blueprints/context/adlibActions.ts index fd17e3c10c..346666d2c0 100644 --- a/packages/job-worker/src/blueprints/context/adlibActions.ts +++ b/packages/job-worker/src/blueprints/context/adlibActions.ts @@ -37,7 +37,8 @@ import { unprotectString, unprotectStringArray, } from '@sofie-automation/corelib/dist/protectedString' -import { getResolvedPieces, setupPieceInstanceInfiniteProperties } from '../../playout/pieces' +import { setupPieceInstanceInfiniteProperties } from '../../playout/pieces' +import { getResolvedPiecesForCurrentPartInstance } from '../../playout/resolvedPieces' import { JobContext, ProcessedShowStyleCompound } from '../../jobs' import { MongoQuery } from '../../db' import { PieceInstance, wrapPieceToInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' @@ -199,7 +200,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct return [] } - const resolvedInstances = getResolvedPieces( + const resolvedInstances = getResolvedPiecesForCurrentPartInstance( this._context, this._cache, this.showStyleCompound.sourceLayers, diff --git a/packages/job-worker/src/blueprints/context/lib.ts b/packages/job-worker/src/blueprints/context/lib.ts index 7b8cd88642..3625baa8fa 100644 --- a/packages/job-worker/src/blueprints/context/lib.ts +++ b/packages/job-worker/src/blueprints/context/lib.ts @@ -143,7 +143,7 @@ export function convertResolvedPieceInstanceToBlueprints( pieceInstance: ResolvedPieceInstance ): IBlueprintResolvedPieceInstance { const obj: Complete = { - ...convertPieceInstanceToBlueprintsInner(pieceInstance), + ...convertPieceInstanceToBlueprintsInner(pieceInstance.instance), resolvedStart: pieceInstance.resolvedStart, resolvedDuration: pieceInstance.resolvedDuration, } diff --git a/packages/job-worker/src/playout/__tests__/resolvedPieces.test.ts b/packages/job-worker/src/playout/__tests__/resolvedPieces.test.ts new file mode 100644 index 0000000000..c396f84af1 --- /dev/null +++ b/packages/job-worker/src/playout/__tests__/resolvedPieces.test.ts @@ -0,0 +1,1031 @@ +import { setupMockShowStyleCompound } from '../../__mocks__/presetCollections' +import { MockJobContext, setupDefaultJobEnvironment } from '../../__mocks__/context' +import { SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' +import { ReadonlyDeep } from 'type-fest' +import { protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' +import { getRandomId } from '@sofie-automation/corelib/dist/lib' +import { IBlueprintPieceType, PieceLifespan } from '@sofie-automation/blueprints-integration' +import { + PieceInstance, + PieceInstancePiece, + ResolvedPieceInstance, +} from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { EmptyPieceTimelineObjectsBlob } from '@sofie-automation/corelib/dist/dataModel/Piece' +import { + processAndPrunePieceInstanceTimings, + resolvePrunedPieceInstance, +} from '@sofie-automation/corelib/dist/playout/processAndPrune' +import { getResolvedPiecesForPartInstancesOnTimeline } from '../resolvedPieces' +import { SelectedPartInstanceTimelineInfo } from '../timeline/generate' +import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' +import { setupPieceInstanceInfiniteProperties } from '../pieces' +import { getPartTimingsOrDefaults } from '@sofie-automation/corelib/dist/playout/timings' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import { PieceInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' + +describe('Resolved Pieces', () => { + let context: MockJobContext + let sourceLayers: ReadonlyDeep + + beforeEach(async () => { + context = setupDefaultJobEnvironment() + + const showStyle = await setupMockShowStyleCompound(context) + sourceLayers = showStyle.sourceLayers + }) + + type StrippedResult = (Pick & { + _id: PieceInstanceId + })[] + function stripResult(result: ResolvedPieceInstance[]): StrippedResult { + return result + .map((resolvedPiece) => ({ + _id: resolvedPiece.instance._id, + resolvedStart: resolvedPiece.resolvedStart, + resolvedDuration: resolvedPiece.resolvedDuration, + })) + .sort((a, b) => a.resolvedStart - b.resolvedStart) + } + + function createPieceInstance( + sourceLayerId: string, + enable: PieceInstancePiece['enable'], + piecePartial?: Partial< + Pick + >, + instancePartial?: Partial> + ): PieceInstance { + const piece: PieceInstance = { + _id: getRandomId(), + playlistActivationId: protectString(''), + rundownId: protectString(''), + partInstanceId: protectString(''), + disabled: false, + piece: { + _id: getRandomId(), + externalId: '', + startPartId: protectString(''), + invalid: false, + name: '', + content: {}, + pieceType: IBlueprintPieceType.Normal, + sourceLayerId, + outputLayerId: '', + lifespan: piecePartial?.lifespan ?? PieceLifespan.WithinPart, + enable, + virtual: piecePartial?.virtual ?? false, + timelineObjectsString: EmptyPieceTimelineObjectsBlob, + }, + userDuration: instancePartial?.userDuration, + } + + if (piece.piece.lifespan !== PieceLifespan.WithinPart) { + setupPieceInstanceInfiniteProperties(piece) + } + + return piece + } + + describe('getResolvedPiecesForCurrentPartInstance', () => { + function getResolvedPiecesInner( + sourceLayers: SourceLayers, + nowInPart: number | null, + pieceInstances: PieceInstance[] + ): ResolvedPieceInstance[] { + const preprocessedPieces = processAndPrunePieceInstanceTimings(sourceLayers, pieceInstances, nowInPart ?? 0) + return preprocessedPieces.map((instance) => resolvePrunedPieceInstance(nowInPart ?? 0, instance)) + } + + test('simple single piece', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece0 = createPieceInstance(sourceLayerId, { start: 0 }) + + const resolvedPieces = getResolvedPiecesInner(sourceLayers, null, [piece0]) + + expect(stripResult(resolvedPieces)).toEqual([ + { + _id: piece0._id, + resolvedStart: 0, + resolvedDuration: undefined, + }, + ] satisfies StrippedResult) + }) + + test('non-overlapping simple pieces', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece0 = createPieceInstance(sourceLayerId, { start: 1000, duration: 2000 }) + const piece1 = createPieceInstance(sourceLayerId, { start: 4000 }) + + const resolvedPieces = getResolvedPiecesInner(sourceLayers, null, [piece0, piece1]) + + expect(stripResult(resolvedPieces)).toEqual([ + { + _id: piece0._id, + resolvedStart: 1000, + resolvedDuration: 2000, + }, + { + _id: piece1._id, + resolvedStart: 4000, + resolvedDuration: undefined, + }, + ] satisfies StrippedResult) + }) + + test('overlapping simple pieces', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece0 = createPieceInstance(sourceLayerId, { start: 1000, duration: 8000 }) + const piece1 = createPieceInstance(sourceLayerId, { start: 4000 }) + + const resolvedPieces = getResolvedPiecesInner(sourceLayers, null, [piece0, piece1]) + + expect(stripResult(resolvedPieces)).toEqual([ + { + _id: piece0._id, + resolvedStart: 1000, + resolvedDuration: 3000, + }, + { + _id: piece1._id, + resolvedStart: 4000, + resolvedDuration: undefined, + }, + ] satisfies StrippedResult) + }) + + test('colliding infinites', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece0 = createPieceInstance( + sourceLayerId, + { start: 1000 }, + { lifespan: PieceLifespan.OutOnRundownEnd } + ) + const piece1 = createPieceInstance( + sourceLayerId, + { start: 4000 }, + { lifespan: PieceLifespan.OutOnSegmentEnd } + ) + const piece2 = createPieceInstance(sourceLayerId, { start: 8000, duration: 2000 }) + + const resolvedPieces = getResolvedPiecesInner(sourceLayers, null, [piece0, piece1, piece2]) + + expect(stripResult(resolvedPieces)).toEqual([ + { + _id: piece0._id, + resolvedStart: 1000, + resolvedDuration: undefined, + }, + { + _id: piece1._id, + resolvedStart: 4000, + resolvedDuration: undefined, + }, + { + _id: piece2._id, + resolvedStart: 8000, + resolvedDuration: 2000, + }, + ] satisfies StrippedResult) + }) + + test('stopped by virtual', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece0 = createPieceInstance( + sourceLayerId, + { start: 1000 }, + { lifespan: PieceLifespan.OutOnRundownEnd } + ) + const piece1 = createPieceInstance( + sourceLayerId, + { start: 4000 }, + { lifespan: PieceLifespan.OutOnRundownEnd, virtual: true } + ) + + const resolvedPieces = getResolvedPiecesInner(sourceLayers, null, [piece0, piece1]) + + expect(stripResult(resolvedPieces)).toEqual([ + { + _id: piece0._id, + resolvedStart: 1000, + resolvedDuration: 3000, + }, + ] satisfies StrippedResult) + }) + + test('part not playing, timed interacts with "now"', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece0 = createPieceInstance(sourceLayerId, { start: 1000 }) + const piece1 = createPieceInstance(sourceLayerId, { start: 'now' }, { virtual: true }) + + const resolvedPieces = getResolvedPiecesInner(sourceLayers, null, [piece0, piece1]) + + expect(stripResult(resolvedPieces)).toEqual([ + { + _id: piece1._id, + resolvedStart: 0, + resolvedDuration: 1000, + }, + { + _id: piece0._id, + resolvedStart: 1000, + resolvedDuration: undefined, + }, + ] satisfies StrippedResult) + }) + + test('part is playing, timed interacts with "now"', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece0 = createPieceInstance(sourceLayerId, { start: 1000 }) + const piece1 = createPieceInstance(sourceLayerId, { start: 'now' }, { virtual: true }) + + const resolvedPieces = getResolvedPiecesInner(sourceLayers, 2500, [piece0, piece1]) + + expect(stripResult(resolvedPieces)).toEqual([ + { + _id: piece0._id, + resolvedStart: 1000, + resolvedDuration: 1500, + }, + { + _id: piece1._id, + resolvedStart: 2500, + resolvedDuration: undefined, + }, + ] satisfies StrippedResult) + }) + + test('userDuration.endRelativeToPart', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece0 = createPieceInstance( + sourceLayerId, + { start: 1000 }, + {}, + { + userDuration: { + endRelativeToPart: 2000, + }, + } + ) + + const resolvedPieces = getResolvedPiecesInner(sourceLayers, 2500, [piece0]) + + expect(stripResult(resolvedPieces)).toEqual([ + { + _id: piece0._id, + resolvedStart: 1000, + resolvedDuration: 1000, + }, + ] satisfies StrippedResult) + }) + + test('userDuration.endRelativeToNow', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece0 = createPieceInstance( + sourceLayerId, + { start: 1000 }, + {}, + { + userDuration: { + endRelativeToNow: 2000, + }, + } + ) + + const resolvedPieces = getResolvedPiecesInner(sourceLayers, 2500, [piece0]) + + expect(stripResult(resolvedPieces)).toEqual([ + { + _id: piece0._id, + resolvedStart: 1000, + resolvedDuration: 3500, + }, + ] satisfies StrippedResult) + }) + + test('preroll has no effect', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece0 = createPieceInstance( + sourceLayerId, + { start: 1000 }, + { + prerollDuration: 500, + } + ) + + const resolvedPieces = getResolvedPiecesInner(sourceLayers, null, [piece0]) + + expect(stripResult(resolvedPieces)).toEqual([ + { + _id: piece0._id, + resolvedStart: 1000, + resolvedDuration: undefined, + }, + ] satisfies StrippedResult) + }) + + test('postroll has no effect', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece0 = createPieceInstance( + sourceLayerId, + { start: 1000, duration: 1000 }, + { + postrollDuration: 500, + } + ) + + const resolvedPieces = getResolvedPiecesInner(sourceLayers, null, [piece0]) + + expect(stripResult(resolvedPieces)).toEqual([ + { + _id: piece0._id, + resolvedStart: 1000, + resolvedDuration: 1000, + }, + ] satisfies StrippedResult) + }) + }) + + describe('getResolvedPiecesForPartInstancesOnTimeline', () => { + function createPartInstance( + partProps?: Partial> + ): DBPartInstance { + return { + _id: getRandomId(), + rundownId: protectString(''), + segmentId: protectString(''), + playlistActivationId: protectString(''), + segmentPlayoutId: protectString(''), + rehearsal: false, + + takeCount: 0, + + part: { + _id: getRandomId(), + _rank: 0, + rundownId: protectString(''), + segmentId: protectString(''), + externalId: '', + title: '', + + expectedDurationWithPreroll: undefined, + + ...partProps, + }, + } + } + + function createPartInstanceInfo( + partStarted: number, + nowInPart: number, + partInstance: DBPartInstance, + currentPieces: PieceInstance[] + ): SelectedPartInstanceTimelineInfo { + const pieceInstances = processAndPrunePieceInstanceTimings(sourceLayers, currentPieces, nowInPart) + + return { + partInstance, + pieceInstances, + nowInPart, + partStarted, + // Approximate `calculatedTimings`, for the partInstances which already have it cached + calculatedTimings: getPartTimingsOrDefaults(partInstance, pieceInstances), + } + } + + test('simple part scenario', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const now = 990000 + + const piece001 = createPieceInstance(sourceLayerId, { start: 0 }) + const currentPartInfo = createPartInstanceInfo(now, 0, createPartInstance(), [piece001]) + + const resolvedPieces = getResolvedPiecesForPartInstancesOnTimeline( + context, + { current: currentPartInfo }, + now + ) + + expect(stripResult(resolvedPieces)).toEqual([ + { + _id: piece001._id, + resolvedStart: now, + resolvedDuration: undefined, + }, + ] satisfies StrippedResult) + }) + + test('single piece stopped by virtual now', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece001 = createPieceInstance(sourceLayerId, { start: 0 }) + + // insert a virtual piece on the same layer + const virtualPiece = createPieceInstance( + sourceLayerId, + { start: 'now' }, + { + virtual: true, + } + ) + + const now = 990000 + const nowInPart = 2000 + const partStarted = now - nowInPart + + const currentPartInfo = createPartInstanceInfo(partStarted, nowInPart, createPartInstance(), [ + piece001, + virtualPiece, + ]) + + // Check the result + const simpleResolvedPieces = getResolvedPiecesForPartInstancesOnTimeline( + context, + { current: currentPartInfo }, + now + ) + expect(stripResult(simpleResolvedPieces)).toEqual([ + { + _id: piece001._id, + resolvedStart: partStarted, + resolvedDuration: nowInPart, + }, + { + // TODO - this object should not be present? + _id: virtualPiece._id, + resolvedStart: now, + resolvedDuration: undefined, + }, + ] satisfies StrippedResult) + }) + + test('single piece stopped by timed now', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece001 = createPieceInstance(sourceLayerId, { start: 0 }) + + // insert a virtual piece on the same layer + const virtualPiece = createPieceInstance( + sourceLayerId, + { start: 7000 }, + { + virtual: true, + } + ) + + const now = 990000 + const nowInPart = 2000 + const partStarted = now - nowInPart + + const currentPartInfo = createPartInstanceInfo(partStarted, nowInPart, createPartInstance(), [ + piece001, + virtualPiece, + ]) + + const simpleResolvedPieces = getResolvedPiecesForPartInstancesOnTimeline( + context, + { current: currentPartInfo }, + now + ) + expect(stripResult(simpleResolvedPieces)).toEqual([ + { + _id: piece001._id, + resolvedStart: partStarted, + resolvedDuration: 7000, + }, + { + // TODO - this object should not be present? + _id: virtualPiece._id, + resolvedStart: partStarted + 7000, + resolvedDuration: undefined, + }, + ] satisfies StrippedResult) + }) + + test('within part overriding infinite for period', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece001 = createPieceInstance(sourceLayerId, { start: 3000, duration: 2500 }) + + const infinite1 = createPieceInstance( + sourceLayerId, + { start: 1000 }, + { + lifespan: PieceLifespan.OutOnSegmentEnd, + } + ) + const infinite2 = createPieceInstance( + sourceLayerId, + { start: 5000 }, + { + lifespan: PieceLifespan.OutOnSegmentEnd, + } + ) + + const now = 990000 + const nowInPart = 2000 + const partStarted = now - nowInPart + + const currentPartInfo = createPartInstanceInfo(partStarted, nowInPart, createPartInstance(), [ + piece001, + infinite1, + infinite2, + ]) + + const simpleResolvedPieces = getResolvedPiecesForPartInstancesOnTimeline( + context, + { current: currentPartInfo }, + now + ) + expect(stripResult(simpleResolvedPieces)).toEqual([ + { + _id: infinite1._id, + resolvedStart: partStarted + 1000, + resolvedDuration: 4000, + }, + { + _id: piece001._id, + resolvedStart: partStarted + 3000, + resolvedDuration: 2000, + }, + { + _id: infinite2._id, + resolvedStart: partStarted + 5000, + resolvedDuration: undefined, + }, + ] satisfies StrippedResult) + }) + + test('userDuration.endRelativeToPart', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece001 = createPieceInstance( + sourceLayerId, + { start: 3000 }, + {}, + { + userDuration: { + endRelativeToPart: 4200, + }, + } + ) + + const now = 990000 + const nowInPart = 2000 + const partStarted = now - nowInPart + + const currentPartInfo = createPartInstanceInfo(partStarted, nowInPart, createPartInstance(), [piece001]) + + const simpleResolvedPieces = getResolvedPiecesForPartInstancesOnTimeline( + context, + { current: currentPartInfo }, + now + ) + expect(stripResult(simpleResolvedPieces)).toEqual([ + { + _id: piece001._id, + resolvedStart: partStarted + 3000, + resolvedDuration: 1200, + }, + ] satisfies StrippedResult) + }) + + test('userDuration.endRelativeToNow', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece001 = createPieceInstance( + sourceLayerId, + { start: 4000 }, + {}, + { + userDuration: { + endRelativeToNow: 1300, + }, + } + ) + + const now = 990000 + const nowInPart = 7000 + const partStarted = now - nowInPart + + const currentPartInfo = createPartInstanceInfo(partStarted, nowInPart, createPartInstance(), [piece001]) + + const simpleResolvedPieces = getResolvedPiecesForPartInstancesOnTimeline( + context, + { current: currentPartInfo }, + now + ) + expect(stripResult(simpleResolvedPieces)).toEqual([ + { + _id: piece001._id, + resolvedStart: partStarted + 4000, + resolvedDuration: -4000 + 7000 + 1300, + }, + ] satisfies StrippedResult) + }) + + test('basic previousPart', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece001 = createPieceInstance(sourceLayerId, { start: 0 }) + + const piece010 = createPieceInstance(sourceLayerId, { start: 0 }) + + const now = 990000 + const nowInPart = 2000 + const currentPartStarted = now - nowInPart + const previousPartStarted = currentPartStarted - 5000 + + const previousPartInfo = createPartInstanceInfo( + previousPartStarted, + nowInPart + 5000, + createPartInstance(), + [piece001] + ) + + const currentPartInfo = createPartInstanceInfo(currentPartStarted, nowInPart, createPartInstance(), [ + piece010, + ]) + + const simpleResolvedPieces = getResolvedPiecesForPartInstancesOnTimeline( + context, + { + current: currentPartInfo, + previous: previousPartInfo, + }, + now + ) + expect(stripResult(simpleResolvedPieces)).toEqual([ + { + _id: piece001._id, + resolvedStart: previousPartStarted, + resolvedDuration: 5000, + }, + { + _id: piece010._id, + resolvedStart: currentPartStarted, + resolvedDuration: undefined, + }, + ] satisfies StrippedResult) + }) + + test('previousPart with ending infinite', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece001 = createPieceInstance(sourceLayerId, { start: 0 }) + + const piece010 = createPieceInstance(sourceLayerId, { start: 0 }) + + const cappedInfinitePiece = createPieceInstance( + sourceLayerId, + { start: 1000 }, + { + lifespan: PieceLifespan.OutOnSegmentEnd, + } + ) + + const now = 990000 + const nowInPart = 2000 + const currentPartStarted = now - nowInPart + const previousPartStarted = currentPartStarted - 5000 + + const previousPartInfo = createPartInstanceInfo( + previousPartStarted, + nowInPart + 5000, + createPartInstance(), + [piece001, cappedInfinitePiece] + ) + + const currentPartInfo = createPartInstanceInfo(currentPartStarted, nowInPart, createPartInstance(), [ + piece010, + ]) + + const simpleResolvedPieces = getResolvedPiecesForPartInstancesOnTimeline( + context, + { + current: currentPartInfo, + previous: previousPartInfo, + }, + now + ) + expect(stripResult(simpleResolvedPieces)).toEqual([ + { + _id: piece001._id, + resolvedStart: previousPartStarted, + resolvedDuration: 1000, + }, + { + _id: cappedInfinitePiece._id, + resolvedStart: previousPartStarted + 1000, + resolvedDuration: 4000, + }, + { + _id: piece010._id, + resolvedStart: currentPartStarted, + resolvedDuration: undefined, + }, + ] satisfies StrippedResult) + }) + + test('previousPart with continuing infinite', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece001 = createPieceInstance(sourceLayerId, { start: 0 }) + + const piece010 = createPieceInstance(sourceLayerId, { start: 0 }) + + const startingInfinitePiece = createPieceInstance( + sourceLayerId, + { start: 1000 }, + { + lifespan: PieceLifespan.OutOnSegmentEnd, + } + ) + + const continuingInfinitePiece = createPieceInstance( + sourceLayerId, + { start: 0 }, + { + lifespan: PieceLifespan.OutOnSegmentEnd, + }, + { + userDuration: { + endRelativeToNow: 3400, + }, + } + ) + continuingInfinitePiece.infinite = { + ...startingInfinitePiece.infinite!, + fromPreviousPart: true, + infiniteInstanceIndex: 1, + } + + const now = 990000 + const nowInPart = 2000 + const currentPartStarted = now - nowInPart + const previousPartStarted = currentPartStarted - 5000 + + const previousPartInfo = createPartInstanceInfo( + previousPartStarted, + nowInPart + 5000, + createPartInstance(), + [piece001, startingInfinitePiece] + ) + + const currentPartInfo = createPartInstanceInfo(currentPartStarted, nowInPart, createPartInstance(), [ + piece010, + continuingInfinitePiece, + ]) + + const simpleResolvedPieces = getResolvedPiecesForPartInstancesOnTimeline( + context, + { + current: currentPartInfo, + previous: previousPartInfo, + }, + now + ) + expect(stripResult(simpleResolvedPieces)).toEqual([ + { + _id: piece001._id, + resolvedStart: previousPartStarted, + resolvedDuration: 1000, + }, + { + _id: continuingInfinitePiece._id, + resolvedStart: previousPartStarted + 1000, + resolvedDuration: 9400, + }, + { + _id: piece010._id, + resolvedStart: currentPartStarted, + resolvedDuration: undefined, + }, + ] satisfies StrippedResult) + }) + + test('basic nextPart', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece001 = createPieceInstance(sourceLayerId, { start: 0 }) + + const piece010 = createPieceInstance(sourceLayerId, { start: 0 }) + + const now = 990000 + const nowInPart = 2000 + const currentPartStarted = now - nowInPart + const currentPartLength = 13000 + const nextPartStart = currentPartStarted + currentPartLength + + const currentPartInfo = createPartInstanceInfo( + currentPartStarted, + nowInPart, + createPartInstance({ + autoNext: true, + expectedDuration: currentPartLength, + }), + [piece001] + ) + + const nextPartInfo = createPartInstanceInfo(nextPartStart, 0, createPartInstance(), [piece010]) + + const simpleResolvedPieces = getResolvedPiecesForPartInstancesOnTimeline( + context, + { + current: currentPartInfo, + next: nextPartInfo, + }, + now + ) + expect(stripResult(simpleResolvedPieces)).toEqual([ + { + _id: piece001._id, + resolvedStart: currentPartStarted, + resolvedDuration: currentPartLength, + }, + { + _id: piece010._id, + resolvedStart: nextPartStart, + resolvedDuration: undefined, + }, + ] satisfies StrippedResult) + }) + + test('nextPart with ending infinite', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece001 = createPieceInstance(sourceLayerId, { start: 0 }) + + const piece010 = createPieceInstance(sourceLayerId, { start: 0 }) + + const cappedInfinitePiece = createPieceInstance( + sourceLayerId, + { start: 1000 }, + { + lifespan: PieceLifespan.OutOnSegmentEnd, + } + ) + + const now = 990000 + const nowInPart = 2000 + const currentPartStarted = now - nowInPart + const currentPartLength = 13000 + const nextPartStart = currentPartStarted + currentPartLength + + const currentPartInfo = createPartInstanceInfo( + currentPartStarted, + nowInPart, + createPartInstance({ + autoNext: true, + expectedDuration: currentPartLength, + }), + [piece001, cappedInfinitePiece] + ) + + const nextPartInfo = createPartInstanceInfo(nextPartStart, 0, createPartInstance(), [piece010]) + + const simpleResolvedPieces = getResolvedPiecesForPartInstancesOnTimeline( + context, + { + current: currentPartInfo, + next: nextPartInfo, + }, + now + ) + + expect(stripResult(simpleResolvedPieces)).toEqual([ + { + _id: piece001._id, + resolvedStart: currentPartStarted, + resolvedDuration: 1000, + }, + { + _id: cappedInfinitePiece._id, + resolvedStart: currentPartStarted + 1000, + resolvedDuration: currentPartLength - 1000, + }, + { + _id: piece010._id, + resolvedStart: nextPartStart, + resolvedDuration: undefined, + }, + ] satisfies StrippedResult) + }) + + test('nextPart with continuing infinite', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece001 = createPieceInstance(sourceLayerId, { start: 0 }) + + const piece010 = createPieceInstance(sourceLayerId, { start: 0 }) + + const startingInfinitePiece = createPieceInstance( + sourceLayerId, + { start: 1000 }, + { + lifespan: PieceLifespan.OutOnSegmentEnd, + } + ) + const continuingInfinitePiece = createPieceInstance( + sourceLayerId, + { start: 0 }, + { + lifespan: PieceLifespan.OutOnSegmentEnd, + }, + { + userDuration: { + endRelativeToPart: 3400, + }, + } + ) + + continuingInfinitePiece.infinite = { + ...startingInfinitePiece.infinite!, + fromPreviousPart: true, + infiniteInstanceIndex: 1, + } + + const now = 990000 + const nowInPart = 2000 + const currentPartStarted = now - nowInPart + const currentPartLength = 13000 + const nextPartStart = currentPartStarted + currentPartLength + + const currentPartInfo = createPartInstanceInfo( + currentPartStarted, + nowInPart, + createPartInstance({ + autoNext: true, + expectedDuration: currentPartLength, + }), + [piece001, startingInfinitePiece] + ) + + const nextPartInfo = createPartInstanceInfo(nextPartStart, 0, createPartInstance(), [ + piece010, + continuingInfinitePiece, + ]) + + const simpleResolvedPieces = getResolvedPiecesForPartInstancesOnTimeline( + context, + { + current: currentPartInfo, + next: nextPartInfo, + }, + now + ) + + expect(stripResult(simpleResolvedPieces)).toEqual([ + { + _id: piece001._id, + resolvedStart: currentPartStarted, + resolvedDuration: 1000, + }, + { + _id: startingInfinitePiece._id, + resolvedStart: currentPartStarted + 1000, + resolvedDuration: currentPartLength - 1000 + 3400, + }, + { + _id: piece010._id, + resolvedStart: nextPartStart, + resolvedDuration: undefined, + }, + ] satisfies StrippedResult) + }) + }) +}) diff --git a/packages/job-worker/src/playout/__tests__/timeline.test.ts b/packages/job-worker/src/playout/__tests__/timeline.test.ts index 5c939920b3..5f516ea4ce 100644 --- a/packages/job-worker/src/playout/__tests__/timeline.test.ts +++ b/packages/job-worker/src/playout/__tests__/timeline.test.ts @@ -1143,11 +1143,6 @@ describe('Timeline', () => { const nowObjs = rawTimelineObjs.filter((obj) => !Array.isArray(obj.enable) && obj.enable.start === 'now') expect(nowObjs).toHaveLength(objectCount) - // All should be inside a PartGroup - expect( - nowObjs.find((obj) => !obj.inGroup || !obj.inGroup.startsWith(getPartGroupId(protectString('')))) - ).toBeFalsy() - const results = nowObjs.map((obj) => ({ id: obj.id, time: time, @@ -1183,7 +1178,7 @@ describe('Timeline', () => { return rundownId }, - async (playlistId, _rundownId, parts, getPartInstances, checkTimings) => { + async (playlistId, rundownId, parts, getPartInstances, checkTimings) => { const outputLayerIds = Object.keys(showStyle.outputLayers) const sourceLayerIds = Object.keys(showStyle.sourceLayers) @@ -1237,7 +1232,9 @@ describe('Timeline', () => { piece000: { controlObj: { start: 500, // This one gave the preroll - end: `#piece_group_control_${currentPartInstance!._id}_${adlibbedPieceId}.start`, + end: `#piece_group_control_${ + currentPartInstance!._id + }_${rundownId}_piece000_cap_now.start + 0`, }, childGroup: { preroll: 500, @@ -1270,7 +1267,7 @@ describe('Timeline', () => { const pieceOffset = 12560 // Simulate the piece timing confirmation from playout-gateway - await doSimulatePiecePlaybackTimings(playlistId, pieceOffset, 1) + await doSimulatePiecePlaybackTimings(playlistId, pieceOffset, 2) // Now we have a concrete time await checkTimings({ @@ -1402,7 +1399,9 @@ describe('Timeline', () => { piece000: { controlObj: { start: 500, // This one gave the preroll - end: `#piece_group_control_${currentPartInstance!._id}_${adlibbedPieceId}.start`, + end: `#piece_group_control_${ + currentPartInstance!._id + }_${_rundownId}_piece000_cap_now.start + 0`, }, childGroup: { preroll: 500, @@ -1437,7 +1436,7 @@ describe('Timeline', () => { const pieceOffset = 12560 // Simulate the piece timing confirmation from playout-gateway - await doSimulatePiecePlaybackTimings(playlistId, pieceOffset, 1) + await doSimulatePiecePlaybackTimings(playlistId, pieceOffset, 2) // Now we have a concrete time await checkTimings({ diff --git a/packages/job-worker/src/playout/abPlayback/__tests__/abPlayback.spec.ts b/packages/job-worker/src/playout/abPlayback/__tests__/abPlayback.spec.ts index 4a74f0b87e..33bff6c0e0 100644 --- a/packages/job-worker/src/playout/abPlayback/__tests__/abPlayback.spec.ts +++ b/packages/job-worker/src/playout/abPlayback/__tests__/abPlayback.spec.ts @@ -52,13 +52,16 @@ function createBasicResolvedPieceInstance( } return literal({ - _id: protectString(`inst_${id}`), - partInstanceId: protectString(''), - rundownId: protectString(''), - playlistActivationId: protectString(''), - piece, + instance: { + _id: protectString(`inst_${id}`), + partInstanceId: protectString(''), + rundownId: protectString(''), + playlistActivationId: protectString(''), + piece, + }, resolvedStart: start, resolvedDuration: duration, + timelinePriority: 0, }) } @@ -140,9 +143,9 @@ describe('resolveMediaPlayers', () => { expect(mockGetPieceSessionId).toHaveBeenCalledTimes(3) expect(mockGetObjectSessionId).toHaveBeenCalledTimes(0) - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0], 'clip_abc') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1], 'clip_def') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2], 'clip_ghi') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, 'clip_abc') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, 'clip_def') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, 'clip_ghi') }) test('Multiple pieces same id', () => { @@ -175,10 +178,10 @@ describe('resolveMediaPlayers', () => { expect(mockGetPieceSessionId).toHaveBeenCalledTimes(4) expect(mockGetObjectSessionId).toHaveBeenCalledTimes(0) - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0], 'clip_abc') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1], 'clip_abc') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2], 'clip_abc') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(4, pieces[3], 'clip_abc') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, 'clip_abc') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, 'clip_abc') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, 'clip_abc') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(4, pieces[3].instance, 'clip_abc') }) test('Reuse after gap', () => { @@ -212,9 +215,9 @@ describe('resolveMediaPlayers', () => { expect(mockGetPieceSessionId).toHaveBeenCalledTimes(3) expect(mockGetObjectSessionId).toHaveBeenCalledTimes(0) - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0], 'clip_abc') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1], 'clip_def') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2], 'clip_ghi') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, 'clip_abc') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, 'clip_def') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, 'clip_ghi') }) test('Reuse immediately', () => { @@ -248,9 +251,9 @@ describe('resolveMediaPlayers', () => { expect(mockGetPieceSessionId).toHaveBeenCalledTimes(3) expect(mockGetObjectSessionId).toHaveBeenCalledTimes(0) - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0], 'clip_abc') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1], 'clip_def') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2], 'clip_ghi') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, 'clip_abc') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, 'clip_def') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, 'clip_ghi') }) test('Reuse immediately dense', () => { @@ -284,9 +287,9 @@ describe('resolveMediaPlayers', () => { expect(mockGetPieceSessionId).toHaveBeenCalledTimes(3) expect(mockGetObjectSessionId).toHaveBeenCalledTimes(0) - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0], 'clip_abc') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1], 'clip_def') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2], 'clip_ghi') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, 'clip_abc') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, 'clip_def') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, 'clip_ghi') }) test('basic reassignment', () => { @@ -331,9 +334,9 @@ describe('resolveMediaPlayers', () => { expect(mockGetPieceSessionId).toHaveBeenCalledTimes(3) expect(mockGetObjectSessionId).toHaveBeenCalledTimes(0) - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0], 'clip_abc') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1], 'clip_def') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2], 'clip_ghi') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, 'clip_abc') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, 'clip_def') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, 'clip_ghi') }) test('optional gets discarded', () => { @@ -378,9 +381,9 @@ describe('resolveMediaPlayers', () => { expect(mockGetPieceSessionId).toHaveBeenCalledTimes(3) expect(mockGetObjectSessionId).toHaveBeenCalledTimes(0) - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0], 'clip_abc') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1], 'clip_def') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2], 'clip_ghi') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, 'clip_abc') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, 'clip_def') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, 'clip_ghi') }) // TODO add some tests which check lookahead diff --git a/packages/job-worker/src/playout/abPlayback/abPlaybackSessions.ts b/packages/job-worker/src/playout/abPlayback/abPlaybackSessions.ts index dcd9222ea7..08f020cf17 100644 --- a/packages/job-worker/src/playout/abPlayback/abPlaybackSessions.ts +++ b/packages/job-worker/src/playout/abPlayback/abPlaybackSessions.ts @@ -24,7 +24,7 @@ export function calculateSessionTimeRanges( ): SessionRequest[] { const sessionRequests: { [sessionId: string]: SessionRequest | undefined } = {} for (const p of resolvedPieces) { - const abSessions = p.piece.abSessions + const abSessions = p.instance.piece.abSessions if (!abSessions) continue const start = p.resolvedStart @@ -36,8 +36,8 @@ export function calculateSessionTimeRanges( if (session.poolName !== poolName) continue const sessionId = abSessionHelper.getPieceABSessionId( - p, - abSessionHelper.validateSessionName(p._id, session) + p.instance, + abSessionHelper.validateSessionName(p.instance._id, session) ) // Note: multiple generated sessionIds for a single piece will not work as there will not be enough info to assign objects to different players. TODO is this still true? diff --git a/packages/job-worker/src/playout/adlibJobs.ts b/packages/job-worker/src/playout/adlibJobs.ts index ed58e8cde9..5ab0e062f8 100644 --- a/packages/job-worker/src/playout/adlibJobs.ts +++ b/packages/job-worker/src/playout/adlibJobs.ts @@ -17,12 +17,8 @@ import { CacheForPlayout, getRundownIDsFromCache, getSelectedPartInstancesFromCa import { runJobWithPlayoutCache } from './lock' import { updateTimeline } from './timeline/generate' import { getCurrentTime } from '../lib' -import { - convertAdLibToPieceInstance, - convertPieceToAdLibPiece, - getResolvedPieces, - sortPieceInstancesByStart, -} from './pieces' +import { convertAdLibToPieceInstance, convertPieceToAdLibPiece, sortPieceInstancesByStart } from './pieces' +import { getResolvedPiecesForCurrentPartInstance } from './resolvedPieces' import { syncPlayheadInfinitesForNextPartInstance } from './infinites' import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/error' import { PieceId, PieceInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' @@ -150,12 +146,17 @@ async function pieceTakeNowAsAdlib( pieceInstanceToCopy.plannedStartedPlayback && pieceInstanceToCopy.plannedStartedPlayback <= getCurrentTime() ) { - const resolvedPieces = getResolvedPieces(context, cache, showStyleBase.sourceLayers, partInstance) - const resolvedPieceBeingCopied = resolvedPieces.find((p) => p._id === pieceInstanceToCopy._id) + const resolvedPieces = getResolvedPiecesForCurrentPartInstance( + context, + cache, + showStyleBase.sourceLayers, + partInstance + ) + const resolvedPieceBeingCopied = resolvedPieces.find((p) => p.instance._id === pieceInstanceToCopy._id) if ( resolvedPieceBeingCopied?.resolvedDuration !== undefined && - (resolvedPieceBeingCopied.infinite || + (resolvedPieceBeingCopied.instance.infinite || resolvedPieceBeingCopied.resolvedStart + resolvedPieceBeingCopied.resolvedDuration >= getCurrentTime()) ) { diff --git a/packages/job-worker/src/playout/adlibUtils.ts b/packages/job-worker/src/playout/adlibUtils.ts index 64d3cb520d..e44c0f1b59 100644 --- a/packages/job-worker/src/playout/adlibUtils.ts +++ b/packages/job-worker/src/playout/adlibUtils.ts @@ -16,7 +16,8 @@ import { getPieceInstancesForPart, syncPlayheadInfinitesForNextPartInstance, } from './infinites' -import { convertAdLibToPieceInstance, getResolvedPieces, setupPieceInstanceInfiniteProperties } from './pieces' +import { convertAdLibToPieceInstance, setupPieceInstanceInfiniteProperties } from './pieces' +import { getResolvedPiecesForCurrentPartInstance } from './resolvedPieces' import { updateTimeline } from './timeline/generate' import { PieceLifespan, IBlueprintPieceType } from '@sofie-automation/blueprints-integration' import { SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' @@ -302,18 +303,19 @@ export function innerStopPieces( throw new Error('Cannot stop pieceInstances when partInstance hasnt started playback') } - const resolvedPieces = getResolvedPieces(context, cache, sourceLayers, currentPartInstance) + const resolvedPieces = getResolvedPiecesForCurrentPartInstance(context, cache, sourceLayers, currentPartInstance) const offsetRelativeToNow = (timeOffset || 0) + (calculateNowOffsetLatency(context, cache, undefined) || 0) const stopAt = getCurrentTime() + offsetRelativeToNow const relativeStopAt = stopAt - lastStartedPlayback - for (const pieceInstance of resolvedPieces) { + for (const resolvedPieceInstance of resolvedPieces) { + const pieceInstance = resolvedPieceInstance.instance if ( !pieceInstance.userDuration && !pieceInstance.piece.virtual && filter(pieceInstance) && - pieceInstance.resolvedStart !== undefined && - pieceInstance.resolvedStart <= relativeStopAt && + resolvedPieceInstance.resolvedStart !== undefined && + resolvedPieceInstance.resolvedStart <= relativeStopAt && !pieceInstance.plannedStoppedPlayback ) { switch (pieceInstance.piece.lifespan) { diff --git a/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts b/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts index d03c78b4ce..eac8ef8afa 100644 --- a/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts +++ b/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts @@ -272,6 +272,7 @@ describe('Lookahead', () => { partInstancesInfo.previous = { partInstance: { _id: 'abc2', part: { _id: 'abc' } } as any, nowInPart: 987, + partStarted: getCurrentTime() + 546, pieceInstances: ['1', '2'] as any, calculatedTimings: { inTransitionStart: null } as any, } @@ -294,6 +295,7 @@ describe('Lookahead', () => { partInstancesInfo.current = { partInstance: { _id: 'curr', part: {} } as any, nowInPart: 56, + partStarted: getCurrentTime() + 865, pieceInstances: ['3', '4'] as any, calculatedTimings: { inTransitionStart: null } as any, } @@ -313,6 +315,7 @@ describe('Lookahead', () => { partInstancesInfo.next = { partInstance: { _id: 'nxt2', part: { _id: 'nxt' } } as any, nowInPart: -85, + partStarted: getCurrentTime() + 142, pieceInstances: ['5'] as any, calculatedTimings: { inTransitionStart: null } as any, } diff --git a/packages/job-worker/src/playout/pieces.ts b/packages/job-worker/src/playout/pieces.ts index 816bd7506e..6af2c60b38 100644 --- a/packages/job-worker/src/playout/pieces.ts +++ b/packages/job-worker/src/playout/pieces.ts @@ -1,18 +1,8 @@ -import { PieceId, RundownPlaylistActivationId, PartInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { - PieceInstance, - PieceInstancePiece, - ResolvedPieceInstance, -} from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { TimelineObjGeneric, TimelineObjRundown } from '@sofie-automation/corelib/dist/dataModel/Timeline' -import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' -import { ReadonlyDeep } from 'type-fest' -import { PieceLifespan, IBlueprintPieceType, TSR } from '@sofie-automation/blueprints-integration/dist' -import { clone, getRandomId, literal, normalizeArray, applyToArray } from '@sofie-automation/corelib/dist/lib' -import { Resolver, TimelineEnable } from 'superfly-timeline' -import { logger } from '../logging' -import { CacheForPlayout, getSelectedPartInstancesFromCache } from './cache' -import { SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' +import { PieceId, RundownPlaylistActivationId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PieceInstance, PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { PieceLifespan, IBlueprintPieceType } from '@sofie-automation/blueprints-integration/dist' +import { getRandomId, literal } from '@sofie-automation/corelib/dist/lib' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { JobContext } from '../jobs' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' @@ -20,14 +10,6 @@ import _ = require('underscore') import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' import { getCurrentTime } from '../lib' -import { transformTimeline, TimelineContentObject } from '@sofie-automation/corelib/dist/playout/timeline' -import { - PieceInstanceWithTimings, - processAndPrunePieceInstanceTimings, -} from '@sofie-automation/corelib/dist/playout/processAndPrune' -import { createPieceGroupAndCap, PieceTimelineMetadata } from '@sofie-automation/corelib/dist/playout/pieces' -import { ReadOnlyCache } from '../cache/CacheBase' - /** * Approximate compare Piece start times (for use in .sort()) * @param a First Piece @@ -90,266 +72,6 @@ export function sortPiecesByStart(pieces: T[]): T[ return pieces } -function resolvePieceTimeline( - objs: TimelineContentObject[], - baseTime: number, - pieceInstanceMap: { [id: string]: PieceInstance | undefined }, - resolveForStr: string -): ResolvedPieceInstance[] { - const tlResolved = Resolver.resolveTimeline(objs as any, { time: baseTime }) - const resolvedPieces: Array = [] - - const unresolvedIds: string[] = [] - _.each(tlResolved.objects, (obj0) => { - const obj = obj0 as any as TimelineObjRundown - const pieceInstanceId = unprotectString( - (obj.metaData as Partial | undefined)?.pieceInstanceGroupId - ) - - if (!pieceInstanceId) return - - const pieceInstance = pieceInstanceMap[pieceInstanceId] - // Erm... How? - if (!pieceInstance) { - unresolvedIds.push(pieceInstanceId) - return - } - - if (obj0.resolved.resolved && obj0.resolved.instances && obj0.resolved.instances.length > 0) { - const firstInstance = obj0.resolved.instances[0] || {} - resolvedPieces.push( - literal({ - ...pieceInstance, - resolvedStart: firstInstance.start || baseTime, - resolvedDuration: firstInstance.end - ? firstInstance.end - (firstInstance.start || baseTime) - : undefined, - }) - ) - } else { - resolvedPieces.push( - literal({ - ...pieceInstance, - resolvedStart: baseTime, - resolvedDuration: undefined, - }) - ) - unresolvedIds.push(pieceInstanceId) - } - }) - - if (tlResolved.statistics.unresolvedCount > 0) { - logger.warn( - `Got ${tlResolved.statistics.unresolvedCount} unresolved pieces for ${resolveForStr} (${unresolvedIds.join( - ', ' - )})` - ) - } - if (_.size(pieceInstanceMap) !== resolvedPieces.length) { - logger.warn( - `Got ${resolvedPieces.length} ordered pieces. Expected ${_.size(pieceInstanceMap)}. for ${resolveForStr}` - ) - } - - // Sort the pieces by time, then transitions first - resolvedPieces.sort((a, b) => { - if (a.resolvedStart < b.resolvedStart) { - return -1 - } else if (a.resolvedStart > b.resolvedStart) { - return 1 - } else { - // We only care about inTransitions here, outTransitions are either not present or will be timed appropriately - const aIsInTransition = a.piece.pieceType === IBlueprintPieceType.InTransition - const bIsInTransition = b.piece.pieceType === IBlueprintPieceType.InTransition - if (aIsInTransition === bIsInTransition) { - return 0 - } else if (bIsInTransition) { - return 1 - } else { - return -1 - } - } - }) - - // Clamp the times to be reasonably valid - resolvedPieces.forEach((resolvedPiece) => { - resolvedPiece.resolvedStart = Math.max(0, resolvedPiece.resolvedStart - 1) - resolvedPiece.resolvedDuration = resolvedPiece.resolvedDuration - ? Math.max(0, resolvedPiece.resolvedDuration) - : undefined - }) - - return resolvedPieces -} - -/** - * Resolve the PieceInstances for a PartInstance - * Uses the getCurrentTime() as approximation for 'now' - * @param context Context for current job - * @param cache Cache for the active Playlist - * @param sourceLayers SourceLayers for the current ShowStyle - * @param partInstance PartInstance to resolve - * @returns ResolvedPieceInstances sorted by startTime - */ -export function getResolvedPieces( - context: JobContext, - cache: ReadOnlyCache, - sourceLayers: SourceLayers, - partInstance: DBPartInstance -): ResolvedPieceInstance[] { - const span = context.startSpan('getResolvedPieces') - const pieceInstances = cache.PieceInstances.findAll((p) => p.partInstanceId === partInstance._id) - - const pieceInststanceMap = normalizeArray(pieceInstances, '_id') - - const now = getCurrentTime() - const partStarted = partInstance.timings?.plannedStartedPlayback - const nowInPart = now - (partStarted ?? 0) - - const preprocessedPieces: ReadonlyDeep = processAndPrunePieceInstanceTimings( - sourceLayers, - pieceInstances, - nowInPart - ) - - const deNowify = (o: TimelineObjRundown) => { - applyToArray(o.enable, (enable) => { - if (enable.start === 'now' && partStarted) { - // Emulate playout starting now. TODO - ensure didnt break other uses - enable.start = nowInPart - } else if (enable.start === 0 || enable.start === 'now') { - enable.start = 1 - } - }) - return o - } - - const objs: TimelineObjGeneric[] = [] - for (const piece of preprocessedPieces) { - let controlObjEnable: TSR.Timeline.TimelineEnable = piece.piece.enable - if (piece.userDuration) { - controlObjEnable = { - start: piece.piece.enable.start, - } - - if ('endRelativeToPart' in piece.userDuration) { - controlObjEnable.end = piece.userDuration.endRelativeToPart - } else { - controlObjEnable.end = nowInPart + piece.userDuration.endRelativeToNow - } - } - - const { controlObj, childGroup, capObjs } = createPieceGroupAndCap(cache.PlaylistId, piece, controlObjEnable) - objs.push(deNowify(controlObj), ...capObjs.map(deNowify), deNowify(childGroup)) - } - - const resolvedPieces = resolvePieceTimeline( - transformTimeline(objs), - 0, - pieceInststanceMap, - `PartInstance #${partInstance._id}` - ) - - if (span) span.end() - return resolvedPieces -} - -/** - * Parse the timeline, to compile the resolved PieceInstances on the timeline - * Uses the getCurrentTime() as approximation for 'now' - * @param context Context for current job - * @param cache Cache for the active Playlist - * @param allObjs TimelineObjects to consider - * @returns ResolvedPieceInstances sorted by startTime - */ -export function getResolvedPiecesFromFullTimeline( - context: JobContext, - cache: ReadOnlyCache, - allObjs: TimelineObjGeneric[] -): { pieces: ResolvedPieceInstance[]; time: number } { - const span = context.startSpan('getResolvedPiecesFromFullTimeline') - const objs = clone( - allObjs.filter((o) => (o.metaData as Partial | undefined)?.isPieceTimeline) - ) - - const now = getCurrentTime() - - const playlist = cache.Playlist.doc - const partInstanceIds = new Set( - _.compact([playlist.previousPartInfo?.partInstanceId, playlist.currentPartInfo?.partInstanceId]) - ) - const pieceInstances: PieceInstance[] = cache.PieceInstances.findAll((p) => partInstanceIds.has(p.partInstanceId)) - - const { currentPartInstance } = getSelectedPartInstancesFromCache(cache) // todo: should these be passed as a parameter from getTimelineRundown? - - if (currentPartInstance?.part?.autoNext && playlist.nextPartInfo) { - pieceInstances.push( - ...cache.PieceInstances.findAll((p) => p.partInstanceId === playlist.nextPartInfo?.partInstanceId) - ) - } - - const transformedObjs = transformTimeline(objs) - deNowifyTimeline(transformedObjs, now) - - const pieceInstanceMap = normalizeArray(pieceInstances, '_id') - const resolvedPieces = resolvePieceTimeline(transformedObjs, now, pieceInstanceMap, 'timeline') - - if (span) span.end() - return { - pieces: resolvedPieces, - time: now, - } -} - -/** - * Replace any start:'now' in the timeline with concrete times. - * This assumes that the structure is of a typical timeline, with 'now' being present at the root level, and one level deep. - * If the parent group of a 'now' is not using a numeric start value, it will not be fixed - * @param transformedObjs Timeline objects to consider - * @param nowTime Time to substitute in instead of 'now' - */ -function deNowifyTimeline(transformedObjs: TimelineContentObject[], nowTime: number): void { - for (const obj of transformedObjs) { - let groupAbsoluteStart: number | null = null - - const obj2 = obj as TimelineContentObject & { partInstanceId?: PartInstanceId } - const partInstanceId = 'partInstanceId' in obj2 ? obj2.partInstanceId : undefined - - // Anything at this level can use nowTime directly - let count = 0 - applyToArray(obj.enable, (enable: TimelineEnable) => { - count++ - - if (enable.start === 'now') { - enable.start = nowTime - groupAbsoluteStart = nowTime - } else if (typeof enable.start === 'number') { - groupAbsoluteStart = enable.start - } else { - // We can't resolve this here, so lets hope there are no 'now' inside and end - groupAbsoluteStart = null - } - }) - - // We know the time of the parent, or there are too many enable times for it - if (groupAbsoluteStart !== null || count !== 1) { - if (partInstanceId && obj.isGroup && obj.children && obj.children.length) { - // This should be piece groups, which are allowed to use 'now' - for (const childObj of obj.children) { - applyToArray(childObj.enable, (enable: TimelineEnable) => { - if (enable.start === 'now' && groupAbsoluteStart !== null) { - // Start is always relative to parent start, so we need to factor that when flattening the 'now - enable.start = Math.max(0, nowTime - groupAbsoluteStart) - } - }) - - // Note: we don't need to go deeper, as current timeline structure doesn't allow there to be any 'now' in there - } - } - } - } -} - /** * Wrap a Piece into an AdLibPiece, so that it can be re-played as an AdLib * @param context Context of the current job diff --git a/packages/job-worker/src/playout/resolvedPieces.ts b/packages/job-worker/src/playout/resolvedPieces.ts new file mode 100644 index 0000000000..90858d21b1 --- /dev/null +++ b/packages/job-worker/src/playout/resolvedPieces.ts @@ -0,0 +1,171 @@ +import { PieceInstanceInfiniteId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { ResolvedPieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { CacheForPlayout } from './cache' +import { SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' +import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' +import { JobContext } from '../jobs' +import { getCurrentTime } from '../lib' +import { + processAndPrunePieceInstanceTimings, + resolvePrunedPieceInstance, +} from '@sofie-automation/corelib/dist/playout/processAndPrune' +import { ReadOnlyCache } from '../cache/CacheBase' +import { SelectedPartInstancesTimelineInfo } from './timeline/generate' + +/** + * Resolve the PieceInstances for a PartInstance + * Uses the getCurrentTime() as approximation for 'now' + * @param context Context for current job + * @param cache Cache for the active Playlist + * @param sourceLayers SourceLayers for the current ShowStyle + * @param partInstance PartInstance to resolve + * @returns ResolvedPieceInstances sorted by startTime + */ +export function getResolvedPiecesForCurrentPartInstance( + _context: JobContext, + cache: ReadOnlyCache, + sourceLayers: SourceLayers, + partInstance: Pick, + now?: number +): ResolvedPieceInstance[] { + const pieceInstances = cache.PieceInstances.findAll((p) => p.partInstanceId === partInstance._id) + + if (now === undefined) now = getCurrentTime() + + const partStarted = partInstance.timings?.plannedStartedPlayback + const nowInPart = partStarted ? now - partStarted : 0 + + const preprocessedPieces = processAndPrunePieceInstanceTimings(sourceLayers, pieceInstances, nowInPart) + return preprocessedPieces.map((instance) => resolvePrunedPieceInstance(nowInPart, instance)) +} + +export function getResolvedPiecesForPartInstancesOnTimeline( + _context: JobContext, + partInstancesInfo: SelectedPartInstancesTimelineInfo, + now: number +): ResolvedPieceInstance[] { + // With no current part, there are no timings to consider + if (!partInstancesInfo.current) return [] + + const currentPartStarted = partInstancesInfo.current.partStarted ?? now + const nextPartStarted = + partInstancesInfo.current.partInstance.part.expectedDuration !== undefined + ? currentPartStarted + partInstancesInfo.current.partInstance.part.expectedDuration + : null + + // Calculate the next part if needed + let nextResolvedPieces: ResolvedPieceInstance[] = [] + if (partInstancesInfo.next && partInstancesInfo.current.partInstance.part.autoNext && nextPartStarted != null) { + const nowInPart = partInstancesInfo.next.nowInPart + nextResolvedPieces = partInstancesInfo.next.pieceInstances.map((instance) => + resolvePrunedPieceInstance(nowInPart, instance) + ) + + // Translate start to absolute times + offsetResolvedStartAndCapDuration(nextResolvedPieces, nextPartStarted, null) + } + + // Calculate the current part + const nowInCurrentPart = partInstancesInfo.current.nowInPart + const currentResolvedPieces = partInstancesInfo.current.pieceInstances.map((instance) => + resolvePrunedPieceInstance(nowInCurrentPart, instance) + ) + + // Translate start to absolute times + offsetResolvedStartAndCapDuration(currentResolvedPieces, currentPartStarted, nextPartStarted) + + // Calculate the previous part + let previousResolvedPieces: ResolvedPieceInstance[] = [] + if (partInstancesInfo.previous?.partStarted) { + const nowInPart = partInstancesInfo.previous.nowInPart + previousResolvedPieces = partInstancesInfo.previous.pieceInstances.map((instance) => + resolvePrunedPieceInstance(nowInPart, instance) + ) + + // Translate start to absolute times + offsetResolvedStartAndCapDuration( + previousResolvedPieces, + partInstancesInfo.previous.partStarted, + currentPartStarted + ) + } + + return mergeInfinitesIntoCurrentPart(previousResolvedPieces, currentResolvedPieces, nextResolvedPieces) +} + +function offsetResolvedStartAndCapDuration( + pieces: ResolvedPieceInstance[], + partStarted: number, + endCap: number | null +) { + for (const piece of pieces) { + piece.resolvedStart += partStarted + + if (endCap !== null) { + // Cap it to the end of the Part. If it is supposed to be longer, there will be a continuing infinite + const partEndCap = endCap - piece.resolvedStart + + piece.resolvedDuration = + piece.resolvedDuration !== undefined ? Math.min(piece.resolvedDuration, partEndCap) : partEndCap + } + } +} + +function mergeInfinitesIntoCurrentPart( + previousResolvedPieces: ResolvedPieceInstance[], + currentResolvedPieces: ResolvedPieceInstance[], + nextResolvedPieces: ResolvedPieceInstance[] +): ResolvedPieceInstance[] { + // Build a map of the infinite pieces from the current Part + const currentInfinitePieces = new Map() + for (const resolvedPiece of currentResolvedPieces) { + if (resolvedPiece.instance.infinite) { + currentInfinitePieces.set(resolvedPiece.instance.infinite.infiniteInstanceId, resolvedPiece) + } + } + + const resultingPieces: ResolvedPieceInstance[] = [...currentResolvedPieces] + + // Merge any infinite chains between the previous and current parts + for (const resolvedPiece of previousResolvedPieces) { + if (resolvedPiece.instance.infinite) { + const continuingInfinite = currentInfinitePieces.get(resolvedPiece.instance.infinite.infiniteInstanceId) + if (continuingInfinite) { + // Extend the duration to compensate for the moved start + if (continuingInfinite.resolvedDuration !== undefined) { + continuingInfinite.resolvedDuration += + continuingInfinite.resolvedStart - resolvedPiece.resolvedStart + } + + // Move the start time to be for the previous Piece + continuingInfinite.resolvedStart = resolvedPiece.resolvedStart + + continue + } + } + + resultingPieces.push(resolvedPiece) + } + + // Merge any infinite chains between the current and next parts + for (const resolvedPiece of nextResolvedPieces) { + if (resolvedPiece.instance.infinite) { + const continuingInfinite = currentInfinitePieces.get(resolvedPiece.instance.infinite.infiniteInstanceId) + if (continuingInfinite) { + // Update the duration to be based upon the copy from the next part + if (resolvedPiece.resolvedDuration !== undefined) { + continuingInfinite.resolvedDuration = + resolvedPiece.resolvedDuration + resolvedPiece.resolvedStart - continuingInfinite.resolvedStart + } else { + delete continuingInfinite.resolvedDuration + } + + continue + } + } + + resultingPieces.push(resolvedPiece) + } + + return resultingPieces +} diff --git a/packages/job-worker/src/playout/take.ts b/packages/job-worker/src/playout/take.ts index 0147485405..5cac776827 100644 --- a/packages/job-worker/src/playout/take.ts +++ b/packages/job-worker/src/playout/take.ts @@ -12,7 +12,7 @@ import { getCurrentTime } from '../lib' import { PartEndState, VTContent } from '@sofie-automation/blueprints-integration' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { ReadonlyDeep } from 'type-fest' -import { getResolvedPieces } from './pieces' +import { getResolvedPiecesForCurrentPartInstance } from './resolvedPieces' import { clone, getRandomId, literal, stringifyError } from '@sofie-automation/corelib/dist/lib' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { updateTimeline } from './timeline/generate' @@ -412,7 +412,12 @@ export function updatePartInstanceOnTake( try { const time = getCurrentTime() - const resolvedPieces = getResolvedPieces(context, cache, showStyle.sourceLayers, currentPartInstance) + const resolvedPieces = getResolvedPiecesForCurrentPartInstance( + context, + cache, + showStyle.sourceLayers, + currentPartInstance + ) const span = context.startSpan('blueprint.getEndStateForPart') const context2 = new RundownContext( diff --git a/packages/corelib/src/playout/__tests__/pieces.test.ts b/packages/job-worker/src/playout/timeline/__tests__/pieceGroup.test.ts similarity index 90% rename from packages/corelib/src/playout/__tests__/pieces.test.ts rename to packages/job-worker/src/playout/timeline/__tests__/pieceGroup.test.ts index 0d9dbee218..4db16fc496 100644 --- a/packages/corelib/src/playout/__tests__/pieces.test.ts +++ b/packages/job-worker/src/playout/timeline/__tests__/pieceGroup.test.ts @@ -1,6 +1,6 @@ -import { getRandomId, literal } from '../../lib' -import { Piece } from '../../dataModel/Piece' -import { createPieceGroupAndCap, PieceTimelineMetadata } from '../pieces' +import { getRandomId, literal } from '@sofie-automation/corelib/dist/lib' +import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' +import { createPieceGroupAndCap, PieceTimelineMetadata } from '../pieceGroup' import { OnGenerateTimelineObjExt, TimelineContentTypeOther, @@ -8,10 +8,10 @@ import { TimelineObjPieceAbstract, TimelineObjRundown, TimelineObjType, -} from '../../dataModel/Timeline' +} from '@sofie-automation/corelib/dist/dataModel/Timeline' import { TSR } from '@sofie-automation/blueprints-integration' -import { protectString } from '../../protectedString' -import { RundownPlaylistId } from '../../dataModel/Ids' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PlayoutChangedType } from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI' type PieceInstanceParam = Parameters[1] @@ -280,7 +280,7 @@ describe('Pieces', () => { const pieceInstance: PieceInstanceParam = { ...simplePieceInstance, - resolvedEndCap: 'now', + resolvedEndCap: { offsetFromNow: 99 }, } const res = createPieceGroupAndCap(playlistId, pieceInstance, enable, [], partGroup) @@ -300,7 +300,7 @@ describe('Pieces', () => { { children: [], content: { deviceType: 'ABSTRACT', type: 'group' }, - enable: { end: '#piece_group_control_randomId9000_cap_now.start', start: 0 }, + enable: { end: '#piece_group_control_randomId9000_cap_now.start + 99', start: 0 }, id: 'piece_group_control_randomId9000_cap', inGroup: partGroup.id, isGroup: true, @@ -330,7 +330,7 @@ describe('Pieces', () => { const pieceInstance: PieceInstanceParam = { ...simplePieceInstance, - resolvedEndCap: 'now', + resolvedEndCap: { offsetFromNow: 0 }, } const res = createPieceGroupAndCap(playlistId, pieceInstance, enable, [], partGroup) @@ -358,7 +358,7 @@ describe('Pieces', () => { classes: [], enable: { ...enable, - end: '#piece_group_control_randomId9000_cap_now.start', + end: '#piece_group_control_randomId9000_cap_now.start + 0', }, }) }) @@ -368,7 +368,7 @@ describe('Pieces', () => { const pieceInstance: PieceInstanceParam = { ...simplePieceInstance, - resolvedEndCap: 'now', + resolvedEndCap: { offsetFromNow: 0 }, } const res = createPieceGroupAndCap(playlistId, pieceInstance, enable, [], partGroup) @@ -388,7 +388,7 @@ describe('Pieces', () => { { children: [], content: { deviceType: 'ABSTRACT', type: 'group' }, - enable: { end: '#piece_group_control_randomId9000_cap_now.start', start: 0 }, + enable: { end: '#piece_group_control_randomId9000_cap_now.start + 0', start: 0 }, id: 'piece_group_control_randomId9000_cap', inGroup: partGroup.id, isGroup: true, @@ -418,15 +418,27 @@ describe('Pieces', () => { const pieceInstance: PieceInstanceParam = { ...simplePieceInstance, - resolvedEndCap: 'aaa + 99', + resolvedEndCap: { offsetFromNow: 99 }, } const res = createPieceGroupAndCap(playlistId, pieceInstance, enable, [], partGroup) expect(res.capObjs).toStrictEqual([ + { + content: { deviceType: 'ABSTRACT' }, + enable: { start: 'now' }, + id: 'piece_group_control_randomId9000_cap_now', + layer: '', + objectType: 'rundown', + partInstanceId: 'randomId9002', + metaData: { + isPieceTimeline: true, + }, + priority: 0, + }, { children: [], content: { deviceType: 'ABSTRACT', type: 'group' }, - enable: { end: pieceInstance.resolvedEndCap, start: 0 }, + enable: { end: '#piece_group_control_randomId9000_cap_now.start + 99', start: 0 }, id: 'piece_group_control_randomId9000_cap', inGroup: partGroup.id, isGroup: true, @@ -456,11 +468,24 @@ describe('Pieces', () => { const pieceInstance: PieceInstanceParam = { ...simplePieceInstance, - resolvedEndCap: 'aaa + 99', + resolvedEndCap: { offsetFromNow: 99 }, } const res = createPieceGroupAndCap(playlistId, pieceInstance, enable, [], partGroup) - expect(res.capObjs).toHaveLength(0) + expect(res.capObjs).toStrictEqual([ + { + content: { deviceType: 'ABSTRACT' }, + enable: { start: 'now' }, + id: 'piece_group_control_randomId9000_cap_now', + layer: '', + objectType: 'rundown', + partInstanceId: 'randomId9002', + metaData: { + isPieceTimeline: true, + }, + priority: 0, + }, + ]) expect(res.childGroup).toStrictEqual({ ...simplePieceGroup, inGroup: partGroup.id, @@ -471,7 +496,7 @@ describe('Pieces', () => { classes: [], enable: { ...enable, - end: 'aaa + 99', + end: '#piece_group_control_randomId9000_cap_now.start + 99', }, }) }) diff --git a/packages/job-worker/src/playout/timeline/generate.ts b/packages/job-worker/src/playout/timeline/generate.ts index 82270e41bc..a6841c966e 100644 --- a/packages/job-worker/src/playout/timeline/generate.ts +++ b/packages/job-worker/src/playout/timeline/generate.ts @@ -33,7 +33,7 @@ import { import { CacheForPlayout, getSelectedPartInstancesFromCache } from '../cache' import { logger } from '../../logging' import { getCurrentTime, getSystemVersion } from '../../lib' -import { getResolvedPiecesFromFullTimeline } from '../pieces' +import { getResolvedPiecesForPartInstancesOnTimeline } from '../resolvedPieces' import { processAndPrunePieceInstanceTimings, PieceInstanceWithTimings, @@ -266,6 +266,7 @@ export interface SelectedPartInstancesTimelineInfo { } export interface SelectedPartInstanceTimelineInfo { nowInPart: number + partStarted: number | undefined partInstance: DBPartInstance pieceInstances: PieceInstanceWithTimings[] calculatedTimings: PartCalculatedTimings @@ -287,6 +288,7 @@ function getPartInstanceTimelineInfo( partInstance, pieceInstances, nowInPart, + partStarted, // Approximate `calculatedTimings`, for the partInstances which already have it cached calculatedTimings: getPartTimingsOrDefaults(partInstance, pieceInstances), } @@ -369,7 +371,11 @@ async function getTimelineRundown( ) if (blueprint.blueprint.onTimelineGenerate || blueprint.blueprint.getAbResolverConfiguration) { - const resolvedPieces = getResolvedPiecesFromFullTimeline(context, cache, timelineObjs) + const resolvedPieces = getResolvedPiecesForPartInstancesOnTimeline( + context, + partInstancesInfo, + getCurrentTime() + ) const blueprintContext = new OnTimelineGenerateContext( context.studio, context.getStudioBlueprintConfig(), @@ -380,7 +386,7 @@ async function getTimelineRundown( previousPartInstance, currentPartInstance, nextPartInstance, - resolvedPieces.pieces + resolvedPieces ) try { const abHelper = blueprintContext.abSessionsHelper // Future: this should be removed from OnTimelineGenerateContext once the methods are removed from the api @@ -390,7 +396,7 @@ async function getTimelineRundown( blueprint, showStyle, cache.Playlist.doc, - resolvedPieces.pieces, + resolvedPieces, timelineObjs ) @@ -403,7 +409,7 @@ async function getTimelineRundown( timelineObjs, clone(cache.Playlist.doc.previousPersistentState), clone(currentPartInstance?.previousPartEndState), - resolvedPieces.pieces.map(convertResolvedPieceInstanceToBlueprints) + resolvedPieces.map(convertResolvedPieceInstanceToBlueprints) ) sendTrace(endTrace(influxTrace)) if (span) span.end() diff --git a/packages/job-worker/src/playout/timeline/lib.ts b/packages/job-worker/src/playout/timeline/lib.ts index 3991d2091d..1b9c6e85b7 100644 --- a/packages/job-worker/src/playout/timeline/lib.ts +++ b/packages/job-worker/src/playout/timeline/lib.ts @@ -21,7 +21,10 @@ export function hasPieceInstanceDefinitelyEnded( let relativeEnd: number | undefined if (typeof pieceInstance.resolvedEndCap === 'number') { relativeEnd = pieceInstance.resolvedEndCap + } else if (pieceInstance.resolvedEndCap) { + relativeEnd = nowInPart + pieceInstance.resolvedEndCap.offsetFromNow } + if (pieceInstance.userDuration) { const userDurationEnd = 'endRelativeToPart' in pieceInstance.userDuration diff --git a/packages/job-worker/src/playout/timeline/multi-gateway.ts b/packages/job-worker/src/playout/timeline/multi-gateway.ts index 8c76a9e815..c02e4c1359 100644 --- a/packages/job-worker/src/playout/timeline/multi-gateway.ts +++ b/packages/job-worker/src/playout/timeline/multi-gateway.ts @@ -4,7 +4,7 @@ import { PeripheralDeviceType } from '@sofie-automation/corelib/dist/dataModel/P import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { TimelineObjRundown } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { normalizeArray } from '@sofie-automation/corelib/dist/lib' -import { PieceTimelineMetadata } from '@sofie-automation/corelib/dist/playout/pieces' +import { PieceTimelineMetadata } from './pieceGroup' import { CacheForStudioBase } from '../../studio/cache' import { JobContext } from '../../jobs' import { getCurrentTime } from '../../lib' diff --git a/packages/job-worker/src/playout/timeline/part.ts b/packages/job-worker/src/playout/timeline/part.ts index 7841c09326..bb9ddf385e 100644 --- a/packages/job-worker/src/playout/timeline/part.ts +++ b/packages/job-worker/src/playout/timeline/part.ts @@ -12,7 +12,7 @@ import { import { assertNever, literal } from '@sofie-automation/corelib/dist/lib' import { getPartGroupId, getPartFirstObjectId } from '@sofie-automation/corelib/dist/playout/ids' import { PieceInstanceWithTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' -import { PieceTimelineMetadata } from '@sofie-automation/corelib/dist/playout/pieces' +import { PieceTimelineMetadata } from './pieceGroup' import { PartCalculatedTimings } from '@sofie-automation/corelib/dist/playout/timings' import { JobContext } from '../../jobs' import { ReadonlyDeep } from 'type-fest' diff --git a/packages/job-worker/src/playout/timeline/piece.ts b/packages/job-worker/src/playout/timeline/piece.ts index 69f5d3ad15..be9d8b0520 100644 --- a/packages/job-worker/src/playout/timeline/piece.ts +++ b/packages/job-worker/src/playout/timeline/piece.ts @@ -9,7 +9,7 @@ import { } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { assertNever, clone } from '@sofie-automation/corelib/dist/lib' import { PieceInstanceWithTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' -import { createPieceGroupAndCap } from '@sofie-automation/corelib/dist/playout/pieces' +import { createPieceGroupAndCap } from './pieceGroup' import { PartCalculatedTimings } from '@sofie-automation/corelib/dist/playout/timings' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { ReadonlyDeep } from 'type-fest' diff --git a/packages/corelib/src/playout/pieces.ts b/packages/job-worker/src/playout/timeline/pieceGroup.ts similarity index 87% rename from packages/corelib/src/playout/pieces.ts rename to packages/job-worker/src/playout/timeline/pieceGroup.ts index 089f2e1ff4..bf4da90ef8 100644 --- a/packages/corelib/src/playout/pieces.ts +++ b/packages/job-worker/src/playout/timeline/pieceGroup.ts @@ -5,14 +5,14 @@ import { TimelineObjPieceAbstract, TimelineObjRundown, TimelineObjType, -} from '../dataModel/Timeline' +} from '@sofie-automation/corelib/dist/dataModel/Timeline' import { ReadonlyDeep } from 'type-fest' import { TSR } from '@sofie-automation/blueprints-integration' -import { PieceInstanceId, RundownPlaylistId } from '../dataModel/Ids' -import { clone, literal } from '../lib' -import { getPieceControlObjectId, getPieceGroupId } from './ids' -import { unprotectString } from '../protectedString' -import { PieceInstanceWithTimings } from './processAndPrune' +import { PieceInstanceId, RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { clone, literal } from '@sofie-automation/corelib/dist/lib' +import { getPieceControlObjectId, getPieceGroupId } from '@sofie-automation/corelib/dist/playout/ids' +import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' +import { PieceInstanceWithTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' import { PlayoutChangedType } from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI' export interface PieceTimelineMetadata { @@ -110,7 +110,7 @@ export function createPieceGroupAndCap( enable: { start: 'now', }, - inGroup: partGroup && partGroup.id, + inGroup: partGroup?.id, layer: '', content: { deviceType: TSR.DeviceType.ABSTRACT, @@ -130,8 +130,11 @@ export function createPieceGroupAndCap( controlObj.metaData.triggerPieceInstanceId = pieceInstance._id } - let resolvedEndCap = pieceInstance.resolvedEndCap - if (resolvedEndCap === 'now') { + let resolvedEndCap: number | string | undefined + // If the start has been adjusted, the end needs to be updated to compensate + if (typeof pieceInstance.resolvedEndCap === 'number') { + resolvedEndCap = pieceInstance.resolvedEndCap - (pieceStartOffset ?? 0) + } else if (pieceInstance.resolvedEndCap) { // TODO - there could already be a piece with a cap of 'now' that we could use as our end time // As the cap is for 'now', rather than try to get tsr to understand `end: 'now'`, we can create a 'now' object to tranlate it const nowObj = literal>({ @@ -151,14 +154,7 @@ export function createPieceGroupAndCap( priority: 0, }) capObjs.push(nowObj) - resolvedEndCap = `#${nowObj.id}.start` - } else if (pieceStartOffset && resolvedEndCap !== undefined) { - // If the start has been adjusted, the end needs to be updated to compensate - if (typeof resolvedEndCap === 'number') { - resolvedEndCap -= pieceStartOffset - } else { - resolvedEndCap = `${resolvedEndCap} - ${pieceStartOffset}` - } + resolvedEndCap = `#${nowObj.id}.start + ${pieceInstance.resolvedEndCap.offsetFromNow}` } if (controlObj.enable.duration !== undefined || controlObj.enable.end !== undefined) { @@ -192,7 +188,7 @@ export function createPieceGroupAndCap( type: TimelineContentTypeOther.GROUP, }, isGroup: true, - inGroup: partGroup && partGroup.id, + inGroup: partGroup?.id, partInstanceId: controlObj.partInstanceId, metaData: literal({ isPieceTimeline: true, diff --git a/packages/job-worker/src/playout/timings/timelineTriggerTime.ts b/packages/job-worker/src/playout/timings/timelineTriggerTime.ts index a0866e4428..102f6700f9 100644 --- a/packages/job-worker/src/playout/timings/timelineTriggerTime.ts +++ b/packages/job-worker/src/playout/timings/timelineTriggerTime.ts @@ -11,7 +11,7 @@ import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceIns import { runJobWithStudioCache } from '../../studio/lock' import { CacheForStudio } from '../../studio/cache' import { DbCacheWriteCollection } from '../../cache/CacheCollection' -import { PieceTimelineMetadata } from '@sofie-automation/corelib/dist/playout/pieces' +import { PieceTimelineMetadata } from '../timeline/pieceGroup' import { deserializeTimelineBlob } from '@sofie-automation/corelib/dist/dataModel/Timeline' /** From 4df642deeee7bc946ec7a22ab29be446609c31eb Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Fri, 11 Aug 2023 18:05:20 +0200 Subject: [PATCH 048/479] chore(server-core-integration): update client example --- packages/server-core-integration/examples/client.ts | 2 +- .../server-core-integration/src/integrationTests/index.spec.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/server-core-integration/examples/client.ts b/packages/server-core-integration/examples/client.ts index dec20015a3..481b6e10fb 100644 --- a/packages/server-core-integration/examples/client.ts +++ b/packages/server-core-integration/examples/client.ts @@ -39,7 +39,7 @@ core.onFailed((err) => { const setupSubscription = async () => { console.log('Setup subscription') - return core.subscribe('peripheralDeviceForDevice', core.deviceId).then(() => { + return core.autoSubscribe('peripheralDeviceForDevice', core.deviceId).then(() => { console.log('sub OK!') }) } diff --git a/packages/server-core-integration/src/integrationTests/index.spec.ts b/packages/server-core-integration/src/integrationTests/index.spec.ts index 334ef42429..38c82d0ae7 100644 --- a/packages/server-core-integration/src/integrationTests/index.spec.ts +++ b/packages/server-core-integration/src/integrationTests/index.spec.ts @@ -93,7 +93,8 @@ test('Integration: Test connection and basic Core functionality', async () => { // Subscribe to data: const coll0 = core.getCollection('peripheralDeviceForDevice') expect(coll0.findOne(id)).toBeFalsy() - const subId = await core.subscribe('peripheralDeviceForDevice', id) + const subId = await core.autoSubscribe('peripheralDeviceForDevice', id) + const coll1 = core.getCollection('peripheralDeviceForDevice') expect(coll1.findOne(id)).toMatchObject({ _id: id, From 5e16632908b8dd62e4470a9ba6089a56aa571b9f Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 19 Jun 2023 13:53:31 +0100 Subject: [PATCH 049/479] feat: rundown scratchpad SOFIE-2432 --- meteor/client/lib/rundown.ts | 21 ++- meteor/client/ui/Prompter/prompter.ts | 14 +- meteor/client/ui/RundownView.tsx | 19 +++ .../ui/SegmentTimeline/SegmentContextMenu.tsx | 7 +- meteor/client/ui/Settings/Studio/Generic.tsx | 18 ++- .../actionSelector/ActionSelector.tsx | 8 ++ meteor/lib/Rundown.ts | 67 ++++++---- meteor/lib/api/triggers/actionFactory.ts | 10 ++ meteor/lib/api/userActions.ts | 8 ++ meteor/lib/clientUserAction.ts | 2 + meteor/lib/userAction.ts | 1 + meteor/server/api/userActions.ts | 23 ++++ .../server/migration/upgrades/checkStatus.ts | 5 +- .../generateNotesForSegment.ts | 41 +++--- .../blueprints-integration/src/triggers.ts | 6 + packages/corelib/src/dataModel/Segment.ts | 2 + packages/corelib/src/dataModel/Studio.ts | 3 + packages/corelib/src/error.ts | 4 + .../src/playout/__tests__/infinites.test.ts | 102 ++++++++++++++ packages/corelib/src/playout/infinites.ts | 116 +++++++++------- packages/corelib/src/worker/studio.ts | 8 ++ packages/job-worker/src/ingest/cache.ts | 3 +- packages/job-worker/src/ingest/commit.ts | 25 ++++ .../src/ingest/ingestSegmentJobs.ts | 2 + packages/job-worker/src/ingest/lib.ts | 5 + .../src/ingest/syncChangesToPartInstance.ts | 3 + .../src/playout/__tests__/infinites.test.ts | 22 ++-- .../job-worker/src/playout/adlibAction.ts | 10 ++ packages/job-worker/src/playout/cache.ts | 30 +++-- packages/job-worker/src/playout/infinites.ts | 122 +++++++++++------ packages/job-worker/src/playout/lib.ts | 4 + .../job-worker/src/playout/moveNextPart.ts | 5 +- packages/job-worker/src/playout/scratchpad.ts | 124 ++++++++++++++++++ packages/job-worker/src/playout/setNext.ts | 14 +- .../job-worker/src/workers/studio/jobs.ts | 3 + .../shared-lib/src/core/model/ShowStyle.ts | 1 + 36 files changed, 687 insertions(+), 171 deletions(-) create mode 100644 packages/job-worker/src/playout/scratchpad.ts diff --git a/meteor/client/lib/rundown.ts b/meteor/client/lib/rundown.ts index 230ba8bf81..207f0e952d 100644 --- a/meteor/client/lib/rundown.ts +++ b/meteor/client/lib/rundown.ts @@ -24,7 +24,7 @@ import { getSegmentsWithPartInstances, } from '../../lib/Rundown' import { PartInstance } from '../../lib/collections/PartInstances' -import { Segment } from '../../lib/collections/Segments' +import { DBSegment, Segment } from '../../lib/collections/Segments' import { RundownPlaylist } from '../../lib/collections/RundownPlaylists' import { literal, getCurrentTime, applyToArray } from '../../lib/lib' import { processAndPrunePieceInstanceTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' @@ -275,7 +275,7 @@ export namespace RundownUtils { * @param {ShowStyleBase} showStyleBase * @param {RundownPlaylist} playlist * @param {DBSegment} segment - * @param {Set} segmentsBeforeThisInRundownSet + * @param {Set} segmentsToReceiveOnRundownEndFromSet * @param {PartId[]} orderedAllPartIds * @param {PartInstance | undefined } currentPartInstance * @param {PartInstance | undefined } nextPartInstance @@ -292,8 +292,8 @@ export namespace RundownUtils { playlist: RundownPlaylist, rundown: Pick, segment: Segment, - segmentsBeforeThisInRundownSet: Set, - rundownsBeforeThisInPlaylist: RundownId[], + segmentsToReceiveOnRundownEndFromSet: Set, + rundownsToReceiveOnShowStyleEndFrom: RundownId[], rundownsToShowstyles: Map, orderedAllPartIds: PartId[], pieces: Map, @@ -474,14 +474,23 @@ export namespace RundownUtils { const rawPieceInstances = getPieceInstancesForPartInstance( playlist.activationId, rundown, + segment, partInstance, new Set(partIds.slice(0, itIndex)), - segmentsBeforeThisInRundownSet, - rundownsBeforeThisInPlaylist, + segmentsToReceiveOnRundownEndFromSet, + rundownsToReceiveOnShowStyleEndFrom, rundownsToShowstyles, orderedAllPartIds, nextPartIsAfterCurrentPart, currentPartInstance, + currentPartInstance + ? (Segments.findOne(currentPartInstance.segmentId, { + projection: { + _id: 1, + orphaned: 1, + }, + }) as Pick | undefined) + : undefined, currentPartInstance ? PieceInstances.find( { diff --git a/meteor/client/ui/Prompter/prompter.ts b/meteor/client/ui/Prompter/prompter.ts index f0fc9045d4..2ccaab0728 100644 --- a/meteor/client/ui/Prompter/prompter.ts +++ b/meteor/client/ui/Prompter/prompter.ts @@ -18,9 +18,10 @@ import { SegmentId, ShowStyleBaseId, } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { RundownPlaylists, PieceInstances, Pieces } from '../../collections' +import { RundownPlaylists, PieceInstances, Pieces, Segments } from '../../collections' import { SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { RundownPlaylistCollectionUtil } from '../../../lib/collections/rundownPlaylistUtil' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' // export interface NewPrompterAPI { // getPrompterData (playlistId: RundownPlaylistId): Promise @@ -74,6 +75,15 @@ export namespace PrompterAPI { const { currentPartInstance, nextPartInstance } = RundownPlaylistCollectionUtil.getSelectedPartInstances(playlist) + const currentSegment = currentPartInstance + ? (Segments.findOne(currentPartInstance?.segmentId, { + projection: { + _id: 1, + orphaned: 1, + }, + }) as Pick) + : undefined + const groupedParts = getSegmentsWithPartInstances( playlist, undefined, @@ -191,6 +201,7 @@ export namespace PrompterAPI { const rawPieceInstances = getPieceInstancesForPartInstance( playlist.activationId, rundown, + segment, partInstance, new Set(partIds.slice(0, partIndex)), new Set(segmentIds.slice(0, segmentIndex)), @@ -199,6 +210,7 @@ export namespace PrompterAPI { orderedAllPartIds, nextPartIsAfterCurrentPart, currentPartInstance, + currentSegment, currentPartInstancePieceInstances, allPiecesCache, pieceInstanceFieldOptions, diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx index cec70d07f8..bb17847ce1 100644 --- a/meteor/client/ui/RundownView.tsx +++ b/meteor/client/ui/RundownView.tsx @@ -811,6 +811,22 @@ const RundownHeader = withTranslation()( } } } + private activateScratchpad = (e: any) => { + const { t } = this.props + if (e.persist) e.persist() + + if ( + this.props.studioMode && + this.props.studio.settings.allowScratchpad && + this.props.playlist.activationId && + this.props.currentRundown + ) { + const rundownId = this.props.currentRundown._id + doUserAction(t, e, UserAction.ACTIVATE_SCRATCHPAD, (e, ts) => + MeteorCall.userAction.activateScratchpadMode(e, ts, this.props.playlist._id, rundownId) + ) + } + } resetRundown = (e: any) => { const { t } = this.props @@ -987,6 +1003,9 @@ const RundownHeader = withTranslation()( {this.props.playlist.activationId ? ( this.deactivate(e)}>{t('Deactivate')} ) : null} + {this.props.studio.settings.allowScratchpad && this.props.playlist.activationId ? ( + this.activateScratchpad(e)}>{t('Activate Scratchpad')} + ) : null} {this.props.playlist.activationId ? ( this.take(e)}>{t('Take')} ) : null} diff --git a/meteor/client/ui/SegmentTimeline/SegmentContextMenu.tsx b/meteor/client/ui/SegmentTimeline/SegmentContextMenu.tsx index 26614bdc92..c36f23584d 100644 --- a/meteor/client/ui/SegmentTimeline/SegmentContextMenu.tsx +++ b/meteor/client/ui/SegmentTimeline/SegmentContextMenu.tsx @@ -9,6 +9,7 @@ import { RundownUtils } from '../../lib/rundown' import { IContextMenuContext } from '../RundownView' import { PartUi, SegmentUi } from './SegmentTimelineContainer' import { SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' interface IProps { onSetNext: (part: Part | undefined, e: any, offset?: number, take?: boolean) => void @@ -30,6 +31,7 @@ export const SegmentContextMenu = withTranslation()( const { t } = this.props const part = this.getPartFromContext() + const segment = this.getSegmentFromContext() const timecode = this.getTimePosition() const startsAt = this.getPartStartsAt() @@ -39,7 +41,10 @@ export const SegmentContextMenu = withTranslation()( const canSetAsNext = !!this.props.playlist?.activationId - return this.props.studioMode && this.props.playlist && this.props.playlist.activationId ? ( + return this.props.studioMode && + this.props.playlist && + this.props.playlist.activationId && + segment?.orphaned !== SegmentOrphanedReason.SCRATCHPAD ? ( {part && timecode === null && ( diff --git a/meteor/client/ui/Settings/Studio/Generic.tsx b/meteor/client/ui/Settings/Studio/Generic.tsx index 4be6786804..35146ef7af 100644 --- a/meteor/client/ui/Settings/Studio/Generic.tsx +++ b/meteor/client/ui/Settings/Studio/Generic.tsx @@ -232,11 +232,7 @@ export const StudioGenericProperties = withTranslation()( /> + +
    diff --git a/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx b/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx index fbbab7f95a..ee0bf7ec4d 100644 --- a/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx +++ b/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx @@ -41,6 +41,8 @@ function getArguments(t: TFunction, action: SomeAction): string[] { break case PlayoutActions.deactivateRundownPlaylist: break + case PlayoutActions.activateScratchpadMode: + break case PlayoutActions.disableNextPiece: if (action.undo) { result.push(t('Undo')) @@ -109,6 +111,8 @@ function hasArguments(action: SomeAction): boolean { return false case PlayoutActions.deactivateRundownPlaylist: return false + case PlayoutActions.activateScratchpadMode: + return false case PlayoutActions.disableNextPiece: return !!action.undo case PlayoutActions.hold: @@ -149,6 +153,8 @@ function actionToLabel(t: TFunction, action: SomeAction['action']): string { return t('Store Snapshot') case PlayoutActions.deactivateRundownPlaylist: return t('Deactivate Rundown') + case PlayoutActions.activateScratchpadMode: + return t('Activate Scratchpad') case PlayoutActions.disableNextPiece: return t('Disable next Piece') case PlayoutActions.hold: @@ -238,6 +244,8 @@ function getActionParametersEditor( return null case PlayoutActions.deactivateRundownPlaylist: return null + case PlayoutActions.activateScratchpadMode: + return null case PlayoutActions.disableNextPiece: return (
    diff --git a/meteor/lib/Rundown.ts b/meteor/lib/Rundown.ts index 14cbd14bbf..dcae64c6b9 100644 --- a/meteor/lib/Rundown.ts +++ b/meteor/lib/Rundown.ts @@ -1,7 +1,7 @@ import * as _ from 'underscore' import { Piece } from './collections/Pieces' import { IOutputLayer, ISourceLayer, ITranslatableMessage } from '@sofie-automation/blueprints-integration' -import { DBSegment, Segment } from './collections/Segments' +import { DBSegment, Segment, SegmentOrphanedReason } from './collections/Segments' import { DBPart } from './collections/Parts' import { PartInstance, wrapPartToTemporaryInstance } from './collections/PartInstances' import { PieceInstance } from './collections/PieceInstances' @@ -90,11 +90,11 @@ export interface PieceExtended { contentStatus?: ReadonlyDeep } -export function fetchPiecesThatMayBeActiveForPart( +function fetchPiecesThatMayBeActiveForPart( part: DBPart, - partsBeforeThisInSegmentSet: Set, - segmentsBeforeThisInRundownSet: Set, - rundownsBeforeThisInPlaylist: RundownId[], + partsToReceiveOnSegmentEndFromSet: Set, + segmentsToReceiveOnRundownEndFromSet: Set, + rundownsToReceiveOnShowStyleEndFrom: RundownId[], /** Map of Pieces on Parts, passed through for performance */ allPiecesCache?: Map ): Piece[] { @@ -108,14 +108,14 @@ export function fetchPiecesThatMayBeActiveForPart( piecesStartingInPart = Pieces.find(convertCorelibToMeteorMongoQuery(selector)).fetch() } - const partsBeforeThisInSegment = Array.from(partsBeforeThisInSegmentSet.values()) - const segmentsBeforeThisInRundown = Array.from(segmentsBeforeThisInRundownSet.values()) + const partsToReceiveOnSegmentEndFrom = Array.from(partsToReceiveOnSegmentEndFromSet.values()) + const segmentsToReceiveOnRundownEndFrom = Array.from(segmentsToReceiveOnRundownEndFromSet.values()) const infinitePieceQuery = buildPastInfinitePiecesForThisPartQuery( part, - partsBeforeThisInSegment, - segmentsBeforeThisInRundown, - rundownsBeforeThisInPlaylist + partsToReceiveOnSegmentEndFrom, + segmentsToReceiveOnRundownEndFrom, + rundownsToReceiveOnShowStyleEndFrom ) let infinitePieces: Piece[] if (allPieces) { @@ -138,8 +138,8 @@ const SIMULATION_INVALIDATION = 3000 * * @export * @param {PartInstanceLimited} partInstance - * @param {Set} partsBeforeThisInSegmentSet - * @param {Set} segmentsBeforeThisInRundownSet + * @param {Set} partsToReceiveOnSegmentEndFromSet + * @param {Set} segmentsToReceiveOnRundownEndFromSet * @param {PartId[]} orderedAllParts * @param {boolean} nextPartIsAfterCurrentPart * @param {(PartInstance | undefined)} currentPartInstance @@ -153,36 +153,46 @@ const SIMULATION_INVALIDATION = 3000 export function getPieceInstancesForPartInstance( playlistActivationId: RundownPlaylistActivationId | undefined, rundown: Pick, + segment: Pick, partInstance: PartInstanceLimited, - partsBeforeThisInSegmentSet: Set, - segmentsBeforeThisInRundownSet: Set, - rundownsBeforeThisInPlaylist: RundownId[], + partsToReceiveOnSegmentEndFromSet: Set, + segmentsToReceiveOnRundownEndFromSet: Set, + rundownsToReceiveOnShowStyleEndFrom: RundownId[], rundownsToShowstyles: Map, orderedAllParts: PartId[], nextPartIsAfterCurrentPart: boolean, currentPartInstance: PartInstance | undefined, + currentSegment: Pick | undefined, currentPartInstancePieceInstances: PieceInstance[] | undefined, /** Map of Pieces on Parts, passed through for performance */ allPiecesCache?: Map, options?: FindOptions, pieceInstanceSimulation?: boolean ): PieceInstance[] { + if (segment.orphaned === SegmentOrphanedReason.SCRATCHPAD) { + // When in the scratchpad, don't allow searching other segments/rundowns for infinites to continue + segmentsToReceiveOnRundownEndFromSet = new Set() + rundownsToReceiveOnShowStyleEndFrom = [] + } + if (partInstance.isTemporary) { return getPieceInstancesForPart( playlistActivationId || protectString(''), currentPartInstance, + currentSegment, currentPartInstancePieceInstances, rundown, + segment, partInstance.part, - partsBeforeThisInSegmentSet, - segmentsBeforeThisInRundownSet, - rundownsBeforeThisInPlaylist, + partsToReceiveOnSegmentEndFromSet, + segmentsToReceiveOnRundownEndFromSet, + rundownsToReceiveOnShowStyleEndFrom, rundownsToShowstyles, fetchPiecesThatMayBeActiveForPart( partInstance.part, - partsBeforeThisInSegmentSet, - segmentsBeforeThisInRundownSet, - rundownsBeforeThisInPlaylist, + partsToReceiveOnSegmentEndFromSet, + segmentsToReceiveOnRundownEndFromSet, + rundownsToReceiveOnShowStyleEndFrom, allPiecesCache ), orderedAllParts, @@ -212,21 +222,24 @@ export function getPieceInstancesForPartInstance( ) { // make sure to invalidate the current computation after SIMULATION_INVALIDATION has passed invalidateAfter(SIMULATION_INVALIDATION) + return getPieceInstancesForPart( playlistActivationId || protectString(''), currentPartInstance, + currentSegment, currentPartInstancePieceInstances, rundown, + segment, partInstance.part, - partsBeforeThisInSegmentSet, - segmentsBeforeThisInRundownSet, - rundownsBeforeThisInPlaylist, + partsToReceiveOnSegmentEndFromSet, + segmentsToReceiveOnRundownEndFromSet, + rundownsToReceiveOnShowStyleEndFrom, rundownsToShowstyles, fetchPiecesThatMayBeActiveForPart( partInstance.part, - partsBeforeThisInSegmentSet, - segmentsBeforeThisInRundownSet, - rundownsBeforeThisInPlaylist, + partsToReceiveOnSegmentEndFromSet, + segmentsToReceiveOnRundownEndFromSet, + rundownsToReceiveOnShowStyleEndFrom, allPiecesCache ), orderedAllParts, diff --git a/meteor/lib/api/triggers/actionFactory.ts b/meteor/lib/api/triggers/actionFactory.ts index 704ed9af20..539f17d859 100644 --- a/meteor/lib/api/triggers/actionFactory.ts +++ b/meteor/lib/api/triggers/actionFactory.ts @@ -34,6 +34,7 @@ import { PartId, PartInstanceId, RundownId, RundownPlaylistId } from '@sofie-aut import { PartInstances, Parts } from '../../collections/libCollections' import { RundownPlaylistCollectionUtil } from '../../collections/rundownPlaylistUtil' import { hashSingleUseToken } from '../userActions' +import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/error' // as described in this issue: https://github.com/Microsoft/TypeScript/issues/14094 type Without = { [P in Exclude]?: never } @@ -507,6 +508,15 @@ export function createAction(action: SomeAction, sourceLayers: SourceLayers): Ex return createUserActionWithCtx(action, UserAction.DEACTIVATE_RUNDOWN_PLAYLIST, async (e, ts, ctx) => MeteorCall.userAction.deactivate(e, ts, ctx.rundownPlaylistId.get()) ) + case PlayoutActions.activateScratchpadMode: + return createUserActionWithCtx(action, UserAction.ACTIVATE_SCRATCHPAD, async (e, ts, ctx) => { + const rundownId = ctx.currentRundownId.get() + if (rundownId) { + return MeteorCall.userAction.activateScratchpadMode(e, ts, ctx.rundownPlaylistId.get(), rundownId) + } else { + return ClientAPI.responseError(UserError.create(UserErrorMessage.InternalError)) + } + }) case PlayoutActions.take: if (isActionTriggeredFromUiContext(action)) { return createRundownPlaylistSoftTakeAction(action.filterChain as IGUIContextFilterLink[]) diff --git a/meteor/lib/api/userActions.ts b/meteor/lib/api/userActions.ts index 9f3bb81f10..e51b54a737 100644 --- a/meteor/lib/api/userActions.ts +++ b/meteor/lib/api/userActions.ts @@ -315,6 +315,12 @@ export interface NewUserActionAPI extends MethodContext { subDeviceId: string, disable: boolean ): Promise> + activateScratchpadMode( + userEvent: string, + eventTime: number, + playlistId: RundownPlaylistId, + rundownId: RundownId + ): Promise> } export enum UserActionAPIMethods { @@ -392,6 +398,8 @@ export enum UserActionAPIMethods { 'switchRouteSet' = 'userAction.switchRouteSet', 'disablePeripheralSubDevice' = 'userAction.system.disablePeripheralSubDevice', + + 'activateScratchpadMode' = 'userAction.activateScratchpadMode', } export interface ReloadRundownPlaylistResponse { diff --git a/meteor/lib/clientUserAction.ts b/meteor/lib/clientUserAction.ts index 72c9987fa2..d45c24c762 100644 --- a/meteor/lib/clientUserAction.ts +++ b/meteor/lib/clientUserAction.ts @@ -110,6 +110,8 @@ function userActionToLabel(userAction: UserAction, t: i18next.TFunction) { return t('Resetting Playlist to default order') case UserAction.PERIPHERAL_DEVICE_REFRESH_DEBUG_STATES: return t('Refreshing debug states') + case UserAction.ACTIVATE_SCRATCHPAD: + return t('Activate Scratchpad') default: assertNever(userAction) } diff --git a/meteor/lib/userAction.ts b/meteor/lib/userAction.ts index 7ecb2af000..f4f386bcfa 100644 --- a/meteor/lib/userAction.ts +++ b/meteor/lib/userAction.ts @@ -48,4 +48,5 @@ export enum UserAction { RUNDOWN_ORDER_MOVE, RUNDOWN_ORDER_RESET, PERIPHERAL_DEVICE_REFRESH_DEBUG_STATES, + ACTIVATE_SCRATCHPAD, } diff --git a/meteor/server/api/userActions.ts b/meteor/server/api/userActions.ts index 5447a3b9a1..8e7f835067 100644 --- a/meteor/server/api/userActions.ts +++ b/meteor/server/api/userActions.ts @@ -1173,5 +1173,28 @@ class ServerUserActionAPI } ) } + + async activateScratchpadMode( + userEvent: string, + eventTime: number, + playlistId: RundownPlaylistId, + rundownId: RundownId + ): Promise> { + return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( + this, + userEvent, + eventTime, + playlistId, + () => { + check(playlistId, String) + check(rundownId, String) + }, + StudioJobs.ActivateScratchpad, + { + playlistId: playlistId, + rundownId: rundownId, + } + ) + } } registerClassToMeteorMethods(UserActionAPIMethods, ServerUserActionAPI, false) diff --git a/meteor/server/migration/upgrades/checkStatus.ts b/meteor/server/migration/upgrades/checkStatus.ts index 45c15da650..6a37fffd9f 100644 --- a/meteor/server/migration/upgrades/checkStatus.ts +++ b/meteor/server/migration/upgrades/checkStatus.ts @@ -289,8 +289,9 @@ function diffJsonSchemaObjects( 'Config value "{{ name }}" has changed. From "{{ oldValue }}", to "{{ newValue }}"', { name: propSchema['ui:title'] || propPath, - oldValue: valueA ?? '', - newValue: valueB ?? '', + // Future: this is not pretty when it is an object + oldValue: JSON.stringify(valueA) ?? '', + newValue: JSON.stringify(valueB) ?? '', }, translationNamespaces ) diff --git a/meteor/server/publications/segmentPartNotesUI/generateNotesForSegment.ts b/meteor/server/publications/segmentPartNotesUI/generateNotesForSegment.ts index e477b30aad..1ebed521a6 100644 --- a/meteor/server/publications/segmentPartNotesUI/generateNotesForSegment.ts +++ b/meteor/server/publications/segmentPartNotesUI/generateNotesForSegment.ts @@ -5,6 +5,7 @@ import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartIns import { SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' import { literal } from '@sofie-automation/corelib/dist/lib' import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { assertNever } from '@sofie-automation/shared-lib/dist/lib/lib' import { UISegmentPartNote } from '../../../lib/api/rundownNotifications' import { Segment } from '../../../lib/collections/Segments' import { generateTranslation } from '../../../lib/lib' @@ -43,7 +44,7 @@ export function generateNotesForSegment( } if (segment.orphaned) { - let message: ITranslatableMessage + let message: ITranslatableMessage | undefined switch (segment.orphaned) { case SegmentOrphanedReason.DELETED: message = generateTranslation('Segment no longer exists in {{nrcs}}', { @@ -55,23 +56,31 @@ export function generateNotesForSegment( nrcs: nrcsName, }) break + case SegmentOrphanedReason.SCRATCHPAD: + // Ignore + break + default: + assertNever(segment.orphaned) + break } - notes.push({ - _id: protectString(`${segment._id}_segment_orphaned`), - playlistId, - rundownId: segment.rundownId, - segmentId: segment._id, - note: { - type: NoteSeverity.WARNING, - message, - rank: segment._rank, - origin: { - segmentId: segment._id, - rundownId: segment.rundownId, - name: segment.name, + if (message) { + notes.push({ + _id: protectString(`${segment._id}_segment_orphaned`), + playlistId, + rundownId: segment.rundownId, + segmentId: segment._id, + note: { + type: NoteSeverity.WARNING, + message, + rank: segment._rank, + origin: { + segmentId: segment._id, + rundownId: segment.rundownId, + name: segment.name, + }, }, - }, - }) + }) + } } else { const deletedPartInstances = partInstances.filter((p) => p.orphaned === 'deleted' && !p.reset) if (deletedPartInstances.length > 0) { diff --git a/packages/blueprints-integration/src/triggers.ts b/packages/blueprints-integration/src/triggers.ts index d50e6aea8d..1a7cf92cf9 100644 --- a/packages/blueprints-integration/src/triggers.ts +++ b/packages/blueprints-integration/src/triggers.ts @@ -188,6 +188,11 @@ export interface IRundownPlaylistDeactivateAction extends ITriggeredActionBase { filterChain: (IRundownPlaylistFilterLink | IGUIContextFilterLink)[] } +export interface IRundownPlaylistActivateScratchpadAction extends ITriggeredActionBase { + action: PlayoutActions.activateScratchpadMode + filterChain: (IRundownPlaylistFilterLink | IGUIContextFilterLink)[] +} + export interface ITakeAction extends ITriggeredActionBase { action: PlayoutActions.take filterChain: (IRundownPlaylistFilterLink | IGUIContextFilterLink)[] @@ -285,6 +290,7 @@ export type SomeAction = | IAdlibPlayoutAction | IRundownPlaylistActivateAction | IRundownPlaylistDeactivateAction + | IRundownPlaylistActivateScratchpadAction | ITakeAction | IHoldAction | IMoveNextAction diff --git a/packages/corelib/src/dataModel/Segment.ts b/packages/corelib/src/dataModel/Segment.ts index ae8c09b267..5cc6469c33 100644 --- a/packages/corelib/src/dataModel/Segment.ts +++ b/packages/corelib/src/dataModel/Segment.ts @@ -9,6 +9,8 @@ export enum SegmentOrphanedReason { DELETED = 'deleted', /** Segment should be hidden, but it is still playing */ HIDDEN = 'hidden', + /** Segment is owned by playout, and is the scratchpad for its rundown */ + SCRATCHPAD = 'scratchpad', } // TV 2 uses this for the not-yet-contributed MiniShelf diff --git a/packages/corelib/src/dataModel/Studio.ts b/packages/corelib/src/dataModel/Studio.ts index c13aa2eab8..444da29d63 100644 --- a/packages/corelib/src/dataModel/Studio.ts +++ b/packages/corelib/src/dataModel/Studio.ts @@ -49,6 +49,9 @@ export interface IStudioSettings { * Default: 1000 */ minimumTakeSpan: number + + /** Whether to allow scratchpad mode, before a Part is playing in a Playlist */ + allowScratchpad?: boolean } export type StudioLight = Omit diff --git a/packages/corelib/src/error.ts b/packages/corelib/src/error.ts index 2ef98be3a1..5ee5318fd9 100644 --- a/packages/corelib/src/error.ts +++ b/packages/corelib/src/error.ts @@ -53,6 +53,8 @@ export enum UserErrorMessage { DeviceAlreadyAttachedToStudio = 38, ShowStyleBaseNotFound = 39, NoMigrationsToApply = 40, + ScratchpadNotAllowed = 41, + ScratchpadAlreadyActive = 42, } const UserErrorMessagesTranslations: { [key in UserErrorMessage]: string } = { @@ -105,6 +107,8 @@ const UserErrorMessagesTranslations: { [key in UserErrorMessage]: string } = { [UserErrorMessage.DeviceAlreadyAttachedToStudio]: t(`Device is already attached to another studio.`), [UserErrorMessage.ShowStyleBaseNotFound]: t(`ShowStyleBase not found!`), [UserErrorMessage.NoMigrationsToApply]: t(`No migrations to apply`), + [UserErrorMessage.ScratchpadNotAllowed]: t(`Scratchpad mode is not allowed`), + [UserErrorMessage.ScratchpadAlreadyActive]: t(`Scratchpad mode is already active`), } export class UserError { diff --git a/packages/corelib/src/playout/__tests__/infinites.test.ts b/packages/corelib/src/playout/__tests__/infinites.test.ts index d420adc39f..151984374b 100644 --- a/packages/corelib/src/playout/__tests__/infinites.test.ts +++ b/packages/corelib/src/playout/__tests__/infinites.test.ts @@ -14,6 +14,7 @@ import { literal } from '../../lib' import { protectString } from '../../protectedString' import { getPlayheadTrackingInfinitesForPart } from '../infinites' import { processAndPrunePieceInstanceTimings } from '../processAndPrune' +import { DBSegment, SegmentOrphanedReason } from '../../dataModel/Segment' describe('Infinites', () => { function createPieceInstance( @@ -442,8 +443,10 @@ describe('Infinites', () => { describe('getPlayheadTrackingInfinitesForPart', () => { function runAndTidyResult( previousPartInstance: Pick & { partId: PartId }, + previousSegment: Pick, previousPartPieces: PieceInstance[], rundown: Rundown, + segment: Pick, part: Pick, newInstanceId: PartInstanceId ) { @@ -454,9 +457,11 @@ describe('Infinites', () => { [], new Map(), previousPartInstance as any, + previousSegment, previousPartPieces, rundown, part as any, + segment, newInstanceId, true, false @@ -543,6 +548,7 @@ describe('Infinites', () => { const segmentId = protectString('segment0') const partId = protectString('part0') const previousPartInstance = { rundownId, segmentId, partId } + const previousSegment = { _id: previousPartInstance.segmentId } const previousPartPieces: PieceInstance[] = [ createPieceInstanceAsInfinite( 'one', @@ -573,14 +579,17 @@ describe('Infinites', () => { dynamicallyInserted: Date.now() + 5000, }, ] + const segment = { _id: segmentId } const part = { rundownId, segmentId } const instanceId = protectString('newInstance0') const rundown = createRundown(rundownId, playlistId, 'Test Rundown', 'rundown0') const continuedInstances = runAndTidyResult( previousPartInstance, + previousSegment, previousPartPieces, rundown, + segment, part, instanceId ) @@ -601,6 +610,7 @@ describe('Infinites', () => { const segmentId = protectString('segment0') const partId = protectString('part0') const previousPartInstance = { rundownId, segmentId, partId } + const previousSegment = { _id: previousPartInstance.segmentId } const previousPartPieces: PieceInstance[] = [ createPieceInstanceAsInfinite( 'one', @@ -634,14 +644,17 @@ describe('Infinites', () => { plannedStoppedPlayback: 5000, }, ] + const segment = { _id: segmentId } const part = { rundownId, segmentId } const instanceId = protectString('newInstance0') const rundown = createRundown(rundownId, playlistId, 'Test Rundown', 'rundown0') const continuedInstances = runAndTidyResult( previousPartInstance, + previousSegment, previousPartPieces, rundown, + segment, part, instanceId ) @@ -652,5 +665,94 @@ describe('Infinites', () => { }, ]) }) + + describe('scratchpad', () => { + const playlistId = protectString('playlist0') + const rundownId = protectString('rundown0') + const segmentId = protectString('segment0') + const partId = protectString('part0') + const previousPartInstance = { rundownId, segmentId, partId } + const previousPartPieces: PieceInstance[] = [ + createPieceInstanceAsInfinite( + 'one', + rundownId, + partId, + { start: 0 }, + 'one', + PieceLifespan.OutOnRundownEnd, + true + ), + ] + const part = { rundownId, segmentId } + const instanceId = protectString('newInstance0') + const rundown = createRundown(rundownId, playlistId, 'Test Rundown', 'rundown0') + + test('normal rundown', () => { + const continuedInstances = runAndTidyResult( + previousPartInstance, + { _id: previousPartInstance.segmentId }, + previousPartPieces, + rundown, + { _id: previousPartInstance.segmentId }, + part, + instanceId + ) + expect(continuedInstances).toHaveLength(1) + }) + + test('into scratchpad', () => { + const previousSegment: Pick = { _id: previousPartInstance.segmentId } + const scratchpadSegment: Pick = { + _id: protectString('segment1'), + orphaned: SegmentOrphanedReason.SCRATCHPAD, + } + const continuedInstances = runAndTidyResult( + previousPartInstance, + previousSegment, + previousPartPieces, + rundown, + scratchpadSegment, + part, + instanceId + ) + expect(continuedInstances).toHaveLength(0) + }) + + test('out of scratchpad', () => { + const segment: Pick = { _id: protectString('segment1') } + const scratchpadSegment: Pick = { + _id: previousPartInstance.segmentId, + orphaned: SegmentOrphanedReason.SCRATCHPAD, + } + const continuedInstances = runAndTidyResult( + previousPartInstance, + scratchpadSegment, + previousPartPieces, + rundown, + segment, + part, + instanceId + ) + expect(continuedInstances).toHaveLength(0) + }) + + test('within scratchpad', () => { + const segment: Pick = { _id: previousPartInstance.segmentId } + const scratchpadSegment: Pick = { + _id: previousPartInstance.segmentId, + orphaned: SegmentOrphanedReason.SCRATCHPAD, + } + const continuedInstances = runAndTidyResult( + previousPartInstance, + scratchpadSegment, + previousPartPieces, + rundown, + segment, + part, + instanceId + ) + expect(continuedInstances).toHaveLength(1) + }) + }) }) }) diff --git a/packages/corelib/src/playout/infinites.ts b/packages/corelib/src/playout/infinites.ts index 78d8a2db07..387986315b 100644 --- a/packages/corelib/src/playout/infinites.ts +++ b/packages/corelib/src/playout/infinites.ts @@ -17,6 +17,7 @@ import { assertNever, flatten, getRandomId, groupByToMapFunc, max, normalizeArra import { protectString } from '../protectedString' import _ = require('underscore') import { MongoQuery } from '../mongo' +import { DBSegment, SegmentOrphanedReason } from '../dataModel/Segment' export function buildPiecesStartingInThisPartQuery(part: DBPart): MongoQuery { return { startPartId: part._id } @@ -24,12 +25,12 @@ export function buildPiecesStartingInThisPartQuery(part: DBPart): MongoQuery | null { const fragments = _.compact([ - partsIdsBeforeThisInSegment.length > 0 + partIdsToReceiveOnSegmentEndFrom.length > 0 ? { // same segment, and previous part lifespan: { @@ -43,10 +44,10 @@ export function buildPastInfinitePiecesForThisPartQuery( }, startRundownId: part.rundownId, startSegmentId: part.segmentId, - startPartId: { $in: partsIdsBeforeThisInSegment }, + startPartId: { $in: partIdsToReceiveOnSegmentEndFrom }, } : undefined, - segmentsIdsBeforeThisInRundown.length > 0 + segmentsToReceiveOnRundownEndFrom.length > 0 ? { // same rundown, and previous segment lifespan: { @@ -57,7 +58,7 @@ export function buildPastInfinitePiecesForThisPartQuery( ], }, startRundownId: part.rundownId, - startSegmentId: { $in: segmentsIdsBeforeThisInRundown }, + startSegmentId: { $in: segmentsToReceiveOnRundownEndFrom }, } : undefined, rundownIdsBeforeThisInPlaylist.length > 0 @@ -90,18 +91,29 @@ export function buildPastInfinitePiecesForThisPartQuery( export function getPlayheadTrackingInfinitesForPart( playlistActivationId: RundownPlaylistActivationId, - partsBeforeThisInSegmentSet: Set, - segmentsBeforeThisInRundownSet: Set, - rundownsBeforeThisInPlaylist: RundownId[], + partsToReceiveOnSegmentEndFromSet: Set, + segmentsToReceiveOnRundownEndFromSet: Set, + rundownsToReceiveOnShowStyleEndFrom: RundownId[], rundownsToShowstyles: Map, currentPartInstance: DBPartInstance, + playingSegment: ReadonlyDeep>, currentPartPieceInstances: PieceInstance[], rundown: ReadonlyDeep>, part: DBPart, + segment: ReadonlyDeep>, newInstanceId: PartInstanceId, nextPartIsAfterCurrentPart: boolean, isTemporary: boolean ): PieceInstance[] { + if ( + segment._id !== playingSegment._id && + (segment.orphaned === SegmentOrphanedReason.SCRATCHPAD || + playingSegment.orphaned === SegmentOrphanedReason.SCRATCHPAD) + ) { + // If crossing the boundary between of the scratchpad, don't continue any infinites + return [] + } + const canContinueAdlibOnEnds = nextPartIsAfterCurrentPart interface InfinitePieceSet { [PieceLifespan.OutOnShowStyleEnd]?: PieceInstance @@ -112,7 +124,7 @@ export function getPlayheadTrackingInfinitesForPart( const piecesOnSourceLayers = new Map() const canContinueShowStyleEndInfinites = continueShowStyleEndInfinites( - rundownsBeforeThisInPlaylist, + rundownsToReceiveOnShowStyleEndFrom, rundownsToShowstyles, currentPartInstance.rundownId, rundown @@ -192,12 +204,12 @@ export function getPlayheadTrackingInfinitesForPart( case PieceLifespan.OutOnSegmentEnd: isValid = currentPartInstance.segmentId === part.segmentId && - partsBeforeThisInSegmentSet.has(candidatePiece.piece.startPartId) + partsToReceiveOnSegmentEndFromSet.has(candidatePiece.piece.startPartId) break case PieceLifespan.OutOnRundownEnd: isValid = candidatePiece.rundownId === part.rundownId && - (segmentsBeforeThisInRundownSet.has(currentPartInstance.segmentId) || + (segmentsToReceiveOnRundownEndFromSet.has(currentPartInstance.segmentId) || currentPartInstance.segmentId === part.segmentId) break case PieceLifespan.OutOnShowStyleEnd: @@ -263,9 +275,9 @@ function markPieceInstanceAsContinuation(previousInstance: PieceInstance, instan export function isPiecePotentiallyActiveInPart( previousPartInstance: DBPartInstance | undefined, - partsBeforeThisInSegment: Set, - segmentsBeforeThisInRundown: Set, - rundownsBeforeThisInPlaylist: RundownId[], + partsToReceiveOnSegmentEndFrom: Set, + segmentsToReceiveOnRundownEndFrom: Set, + rundownsToReceiveOnShowStyleEndFrom: RundownId[], rundownsToShowstyles: Map, rundown: ReadonlyDeep>, part: DBPart, @@ -282,14 +294,15 @@ export function isPiecePotentiallyActiveInPart( return false case PieceLifespan.OutOnSegmentEnd: return ( - pieceToCheck.startSegmentId === part.segmentId && partsBeforeThisInSegment.has(pieceToCheck.startPartId) + pieceToCheck.startSegmentId === part.segmentId && + partsToReceiveOnSegmentEndFrom.has(pieceToCheck.startPartId) ) case PieceLifespan.OutOnRundownEnd: if (pieceToCheck.startRundownId === part.rundownId) { if (pieceToCheck.startSegmentId === part.segmentId) { - return partsBeforeThisInSegment.has(pieceToCheck.startPartId) + return partsToReceiveOnSegmentEndFrom.has(pieceToCheck.startPartId) } else { - return segmentsBeforeThisInRundown.has(pieceToCheck.startSegmentId) + return segmentsToReceiveOnRundownEndFrom.has(pieceToCheck.startSegmentId) } } else { return false @@ -303,7 +316,7 @@ export function isPiecePotentiallyActiveInPart( // Predicting what will happen at arbitrary point in the future return ( pieceToCheck.startSegmentId === part.segmentId && - partsBeforeThisInSegment.has(pieceToCheck.startPartId) + partsToReceiveOnSegmentEndFrom.has(pieceToCheck.startPartId) ) } case PieceLifespan.OutOnRundownChange: @@ -315,13 +328,13 @@ export function isPiecePotentiallyActiveInPart( // Predicting what will happen at arbitrary point in the future return ( pieceToCheck.startRundownId === part.rundownId && - segmentsBeforeThisInRundown.has(pieceToCheck.startSegmentId) + segmentsToReceiveOnRundownEndFrom.has(pieceToCheck.startSegmentId) ) } case PieceLifespan.OutOnShowStyleEnd: return previousPartInstance && pieceToCheck.lifespan === PieceLifespan.OutOnShowStyleEnd ? continueShowStyleEndInfinites( - rundownsBeforeThisInPlaylist, + rundownsToReceiveOnShowStyleEndFrom, rundownsToShowstyles, previousPartInstance.rundownId, rundown @@ -340,9 +353,9 @@ export function isPiecePotentiallyActiveInPart( * @param playingPieceInstances The PieceInstances from the current PartInstance * @param rundown The Rundown the Part belongs to * @param part The Part the PartInstance is based on - * @param partsBeforeThisInSegmentSet Set of PartIds that exist in the Segment before the part being processed - * @param segmentsBeforeThisInRundownSet Set of SegmentIds that exist in the Rundown before the part being processed - * @param rundownsBeforeThisInPlaylist Set of RundownIds that exist in the Playlist before the part being processed + * @param partsToReceiveOnSegmentEndFromSet Set of PartIds that exist in the Segment before the part being processed + * @param segmentsToReceiveOnRundownEndFromSet Set of SegmentIds that exist in the Rundown before the part being processed + * @param rundownsToReceiveOnShowStyleEndFrom Set of RundownIds that exist in the Playlist before the part being processed * @param rundownsToShowstyles Lookup of RundownIds in the Playlist, to their ShowStyleBase id * @param possiblePieces Array of Pieces that should be considered for being a PieceInstance in the new PartInstance * @param orderedPartIds Ordered array of all PartId in the Rundown @@ -354,12 +367,14 @@ export function isPiecePotentiallyActiveInPart( export function getPieceInstancesForPart( playlistActivationId: RundownPlaylistActivationId, playingPartInstance: DBPartInstance | undefined, + playingSegment: ReadonlyDeep> | undefined, playingPieceInstances: PieceInstance[] | undefined, rundown: ReadonlyDeep>, + segment: ReadonlyDeep>, part: DBPart, - partsBeforeThisInSegmentSet: Set, - segmentsBeforeThisInRundownSet: Set, - rundownsBeforeThisInPlaylist: RundownId[], + partsToReceiveOnSegmentEndFromSet: Set, + segmentsToReceiveOnRundownEndFromSet: Set, + rundownsToReceiveOnShowStyleEndFrom: RundownId[], rundownsToShowstyles: Map, possiblePieces: Piece[], orderedPartIds: PartId[], @@ -403,9 +418,9 @@ export function getPieceInstancesForPart( ) { const useIt = isPiecePotentiallyActiveInPart( playingPartInstance, - partsBeforeThisInSegmentSet, - segmentsBeforeThisInRundownSet, - rundownsBeforeThisInPlaylist, + partsToReceiveOnSegmentEndFromSet, + segmentsToReceiveOnRundownEndFromSet, + rundownsToReceiveOnShowStyleEndFrom, rundownsToShowstyles, rundown, part, @@ -424,22 +439,25 @@ export function getPieceInstancesForPart( } // OnChange infinites take priority over onEnd, as they travel with the playhead - const infinitesFromPrevious = playingPartInstance - ? getPlayheadTrackingInfinitesForPart( - playlistActivationId, - partsBeforeThisInSegmentSet, - segmentsBeforeThisInRundownSet, - rundownsBeforeThisInPlaylist, - rundownsToShowstyles, - playingPartInstance, - playingPieceInstances || [], - rundown, - part, - newInstanceId, - nextPartIsAfterCurrentPart, - isTemporary - ) - : [] + const infinitesFromPrevious = + playingPartInstance && playingSegment + ? getPlayheadTrackingInfinitesForPart( + playlistActivationId, + partsToReceiveOnSegmentEndFromSet, + segmentsToReceiveOnRundownEndFromSet, + rundownsToReceiveOnShowStyleEndFrom, + rundownsToShowstyles, + playingPartInstance, + playingSegment, + playingPieceInstances || [], + rundown, + part, + segment, + newInstanceId, + nextPartIsAfterCurrentPart, + isTemporary + ) + : [] // Compile the resulting list @@ -546,7 +564,7 @@ export function isCandidateBetterToBeContinued(best: PieceInstance, candidate: P } function continueShowStyleEndInfinites( - rundownsBeforeThisInPlaylist: RundownId[], + rundownsToReceiveOnShowStyleEndFrom: RundownId[], rundownsToShowstyles: Map, previousRundownId: RundownId, rundown: ReadonlyDeep> @@ -556,8 +574,8 @@ function continueShowStyleEndInfinites( canContinueShowStyleEndInfinites = false } else { const targetShowStyle = rundown.showStyleBaseId - canContinueShowStyleEndInfinites = rundownsBeforeThisInPlaylist - .slice(rundownsBeforeThisInPlaylist.indexOf(previousRundownId)) + canContinueShowStyleEndInfinites = rundownsToReceiveOnShowStyleEndFrom + .slice(rundownsToReceiveOnShowStyleEndFrom.indexOf(previousRundownId)) .every((r) => rundownsToShowstyles.get(r) === targetShowStyle) } diff --git a/packages/corelib/src/worker/studio.ts b/packages/corelib/src/worker/studio.ts index f483a214f2..7cf885684a 100644 --- a/packages/corelib/src/worker/studio.ts +++ b/packages/corelib/src/worker/studio.ts @@ -162,6 +162,8 @@ export enum StudioJobs { * Validate the blueprintConfig for the Studio, with the Blueprint validateConfig */ BlueprintValidateConfigForStudio = 'blueprintValidateConfigForStudio', + + ActivateScratchpad = 'activateScratchpad', } export interface RundownPlayoutPropsBase { @@ -277,6 +279,10 @@ export interface BlueprintValidateConfigForStudioResult { }> } +export interface ActivateScratchpadProps extends RundownPlayoutPropsBase { + rundownId: RundownId +} + /** * Set of valid functions, of form: * `id: (data) => return` @@ -322,6 +328,8 @@ export type StudioJobFunc = { [StudioJobs.BlueprintUpgradeForStudio]: () => void [StudioJobs.BlueprintValidateConfigForStudio]: () => BlueprintValidateConfigForStudioResult + + [StudioJobs.ActivateScratchpad]: (data: ActivateScratchpadProps) => void } export function getStudioQueueName(id: StudioId): string { diff --git a/packages/job-worker/src/ingest/cache.ts b/packages/job-worker/src/ingest/cache.ts index 14a7a56bc3..f827182cdb 100644 --- a/packages/job-worker/src/ingest/cache.ts +++ b/packages/job-worker/src/ingest/cache.ts @@ -10,7 +10,7 @@ import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' import { RundownBaselineObj } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineObj' -import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import { DBSegment, SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' import { JobContext } from '../jobs' import { LazyInitialise } from '../lib/lazy' import { CacheBase } from '../cache/CacheBase' @@ -168,6 +168,7 @@ export class CacheForIngest extends CacheBase { return Promise.all([ DbCacheWriteCollection.createFromDatabase(context, context.directCollections.Segments, { rundownId: rundownId, + orphaned: { $ne: SegmentOrphanedReason.SCRATCHPAD }, }), DbCacheWriteCollection.createFromDatabase(context, context.directCollections.Parts, { rundownId: rundownId, diff --git a/packages/job-worker/src/ingest/commit.ts b/packages/job-worker/src/ingest/commit.ts index ccbd713c24..e4cdc27949 100644 --- a/packages/job-worker/src/ingest/commit.ts +++ b/packages/job-worker/src/ingest/commit.ts @@ -224,6 +224,8 @@ export async function CommitIngestOperation( const pSaveIngest = ingestCache.saveAllToDatabase() pSaveIngest.catch(() => null) // Ensure promise isn't reported as unhandled + await validateScratchpad(context, playoutCache) + try { // sync changes to the 'selected' partInstances await syncChangesToPartInstances(context, playoutCache, ingestCache) @@ -848,3 +850,26 @@ async function preserveUnsyncedPlayingSegmentContents( } } } + +async function validateScratchpad(_context: JobContext, cache: CacheForPlayout) { + const scratchpadSegments = cache.Segments.findAll((s) => s.orphaned === SegmentOrphanedReason.SCRATCHPAD) + + // Note: this assumes there will be up to one per rundown + for (const segment of scratchpadSegments) { + // Ensure the _rank is just before the real content + const otherSegmentsInRundown = cache.Segments.findAll( + (s) => s.rundownId === segment.rundownId && s.orphaned !== SegmentOrphanedReason.SCRATCHPAD + ) + const minNormalRank = Math.min(0, ...otherSegmentsInRundown.map((s) => s._rank)) + + cache.Segments.updateOne(segment._id, (segment) => { + const newRank = minNormalRank - 1 + if (segment._rank !== newRank) { + segment._rank = newRank + return segment + } else { + return false + } + }) + } +} diff --git a/packages/job-worker/src/ingest/ingestSegmentJobs.ts b/packages/job-worker/src/ingest/ingestSegmentJobs.ts index bc7f1f37f4..d940a16b7a 100644 --- a/packages/job-worker/src/ingest/ingestSegmentJobs.ts +++ b/packages/job-worker/src/ingest/ingestSegmentJobs.ts @@ -187,6 +187,8 @@ export async function handleRemoveOrphanedSegemnts( // We flag them for deletion again, and they will either be kept if they are somehow playing, or purged if they are not const stillOrphanedSegments = ingestCache.Segments.findAll((s) => !!s.orphaned) + // Note: scratchpad segments are ignored here, as they will never be in the ingestCache + const stillHiddenSegments = stillOrphanedSegments .filter( (s) => s.orphaned === SegmentOrphanedReason.HIDDEN && data.orphanedHiddenSegmentIds.includes(s._id) diff --git a/packages/job-worker/src/ingest/lib.ts b/packages/job-worker/src/ingest/lib.ts index bca5d6ba18..11bec36402 100644 --- a/packages/job-worker/src/ingest/lib.ts +++ b/packages/job-worker/src/ingest/lib.ts @@ -57,6 +57,11 @@ export function canSegmentBeUpdated( return false } + if (segment.orphaned === SegmentOrphanedReason.SCRATCHPAD) { + logger.error(`Ingest cannot update Segment "${segment._id}" which is owned by the Scratchpad.`) + return false + } + return true } diff --git a/packages/job-worker/src/ingest/syncChangesToPartInstance.ts b/packages/job-worker/src/ingest/syncChangesToPartInstance.ts index 21584c5b9e..0a3eabed49 100644 --- a/packages/job-worker/src/ingest/syncChangesToPartInstance.ts +++ b/packages/job-worker/src/ingest/syncChangesToPartInstance.ts @@ -25,6 +25,7 @@ import { convertPieceInstanceToBlueprints, } from '../blueprints/context/lib' import { getRundown } from './lib' +import { validateScratchpartPartInstanceProperties } from '../playout/scratchpad' type PlayStatus = 'previous' | 'current' | 'next' type SyncedInstance = { @@ -210,6 +211,8 @@ export async function syncChangesToPartInstances( p.part.notes = notes return p }) + + validateScratchpartPartInstanceProperties(context, cache, existingPartInstance._id) } if (existingPartInstance._id === cache.Playlist.doc.currentPartInfo?.partInstanceId) { diff --git a/packages/job-worker/src/playout/__tests__/infinites.test.ts b/packages/job-worker/src/playout/__tests__/infinites.test.ts index 660a22690a..ef3715e0f7 100644 --- a/packages/job-worker/src/playout/__tests__/infinites.test.ts +++ b/packages/job-worker/src/playout/__tests__/infinites.test.ts @@ -2,7 +2,7 @@ import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/Rund import { MockJobContext, setupDefaultJobEnvironment } from '../../__mocks__/context' import { ReadonlyDeep, SetRequired } from 'type-fest' import { CacheForPlayout, getOrderedSegmentsAndPartsFromPlayoutCache } from '../cache' -import { canContinueAdlibOnEndInfinites } from '../infinites' +import { candidatePartIsAfterPreviewPartInstance } from '../infinites' import { setupDefaultRundownPlaylist, setupMockShowStyleCompound } from '../../__mocks__/presetCollections' import { getRandomId } from '@sofie-automation/corelib/dist/lib' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' @@ -56,7 +56,7 @@ describe('canContinueAdlibOnEndInfinites', () => { // At beginning expect( - canContinueAdlibOnEndInfinites( + candidatePartIsAfterPreviewPartInstance( context, playlist, orderedPartsAndSegments.segments, @@ -67,7 +67,7 @@ describe('canContinueAdlibOnEndInfinites', () => { // Small gap expect( - canContinueAdlibOnEndInfinites( + candidatePartIsAfterPreviewPartInstance( context, playlist, orderedPartsAndSegments.segments, @@ -78,7 +78,7 @@ describe('canContinueAdlibOnEndInfinites', () => { // At end expect( - canContinueAdlibOnEndInfinites( + candidatePartIsAfterPreviewPartInstance( context, playlist, orderedPartsAndSegments.segments, @@ -92,7 +92,7 @@ describe('canContinueAdlibOnEndInfinites', () => { // Start to end expect( - canContinueAdlibOnEndInfinites( + candidatePartIsAfterPreviewPartInstance( context, playlist, orderedPartsAndSegments.segments, @@ -109,7 +109,7 @@ describe('canContinueAdlibOnEndInfinites', () => { const orderedPartsAndSegments = getOrderedSegmentsAndPartsFromPlayoutCache(cache) expect( - canContinueAdlibOnEndInfinites( + candidatePartIsAfterPreviewPartInstance( context, playlist, orderedPartsAndSegments.segments, @@ -127,7 +127,7 @@ describe('canContinueAdlibOnEndInfinites', () => { // At beginning expect( - canContinueAdlibOnEndInfinites( + candidatePartIsAfterPreviewPartInstance( context, playlist, orderedPartsAndSegments.segments, @@ -138,7 +138,7 @@ describe('canContinueAdlibOnEndInfinites', () => { // At end expect( - canContinueAdlibOnEndInfinites( + candidatePartIsAfterPreviewPartInstance( context, playlist, orderedPartsAndSegments.segments, @@ -152,7 +152,7 @@ describe('canContinueAdlibOnEndInfinites', () => { // Start to end expect( - canContinueAdlibOnEndInfinites( + candidatePartIsAfterPreviewPartInstance( context, playlist, orderedPartsAndSegments.segments, @@ -180,7 +180,7 @@ describe('canContinueAdlibOnEndInfinites', () => { // After first expect( - canContinueAdlibOnEndInfinites( + candidatePartIsAfterPreviewPartInstance( context, playlist, orderedPartsAndSegments.segments, @@ -191,7 +191,7 @@ describe('canContinueAdlibOnEndInfinites', () => { // Before second expect( - canContinueAdlibOnEndInfinites( + candidatePartIsAfterPreviewPartInstance( context, playlist, orderedPartsAndSegments.segments, diff --git a/packages/job-worker/src/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index 81311a6775..882f189b7d 100644 --- a/packages/job-worker/src/playout/adlibAction.ts +++ b/packages/job-worker/src/playout/adlibAction.ts @@ -18,6 +18,7 @@ import { performTakeToNextedPart } from './take' import { ActionUserData } from '@sofie-automation/blueprints-integration' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { logger } from '../logging' +import { validateScratchpartPartInstanceProperties } from './scratchpad' /** * Execute an AdLib Action @@ -188,6 +189,15 @@ async function applyAnyExecutionSideEffects( const nextPartInstanceId = cache.Playlist.doc.nextPartInfo?.partInstanceId if (nextPartInstanceId) { updateExpectedDurationWithPrerollForPartInstance(cache, nextPartInstanceId) + + validateScratchpartPartInstanceProperties(context, cache, nextPartInstanceId) + } + } + + if (actionContext.currentPartState !== ActionPartChange.NONE) { + const currentPartInstanceId = cache.Playlist.doc.currentPartInfo?.partInstanceId + if (currentPartInstanceId) { + validateScratchpartPartInstanceProperties(context, cache, currentPartInstanceId) } } diff --git a/packages/job-worker/src/playout/cache.ts b/packages/job-worker/src/playout/cache.ts index f08fd077a3..a220592329 100644 --- a/packages/job-worker/src/playout/cache.ts +++ b/packages/job-worker/src/playout/cache.ts @@ -8,7 +8,7 @@ import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { ReadonlyDeep } from 'type-fest' import { JobContext } from '../jobs' import { CacheForStudioBase } from '../studio/cache' -import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import { DBSegment, SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' @@ -126,7 +126,7 @@ export class CacheForPlayout extends CacheForPlayoutPreInit implements CacheForS public readonly Timeline: DbCacheWriteOptionalObject - public readonly Segments: DbCacheReadCollection + public readonly Segments: DbCacheWriteCollection public readonly Parts: DbCacheReadCollection public readonly PartInstances: DbCacheWriteCollection public readonly PieceInstances: DbCacheWriteCollection @@ -142,7 +142,7 @@ export class CacheForPlayout extends CacheForPlayoutPreInit implements CacheForS peripheralDevices: DbCacheReadCollection, playlist: DbCacheWriteObject, rundowns: DbCacheReadCollection, - segments: DbCacheReadCollection, + segments: DbCacheWriteCollection, parts: DbCacheReadCollection, partInstances: DbCacheWriteCollection, pieceInstances: DbCacheWriteCollection, @@ -245,7 +245,7 @@ export class CacheForPlayout extends CacheForPlayoutPreInit implements CacheForS rundownIds: RundownId[] ): Promise< [ - DbCacheReadCollection, + DbCacheWriteCollection, DbCacheReadCollection, DbCacheWriteCollection, DbCacheWriteCollection, @@ -289,8 +289,8 @@ export class CacheForPlayout extends CacheForPlayoutPreInit implements CacheForS }, ], } - // TODO - is this correct? If the playlist isnt active do we want any of these? - if (playlist.activationId) partInstancesSelector.playlistActivationId = playlist.activationId + // Filter the PieceInstances to the activationId, if possible + pieceInstancesSelector.playlistActivationId = playlist.activationId || { $exists: false } return DbCacheWriteCollection.createFromDatabase( context, @@ -308,12 +308,22 @@ export class CacheForPlayout extends CacheForPlayoutPreInit implements CacheForS rundownId: { $in: rundownIds }, partInstanceId: { $in: selectedPartInstanceIds }, } - // TODO - is this correct? If the playlist isnt active do we want any of these? - if (playlist.activationId) pieceInstancesSelector.playlistActivationId = playlist.activationId + // Filter the PieceInstances to the activationId, if possible + pieceInstancesSelector.playlistActivationId = playlist.activationId || { $exists: false } const [segments, parts, baselineObjects, ...collections] = await Promise.all([ - DbCacheReadCollection.createFromDatabase(context, context.directCollections.Segments, { - rundownId: { $in: loadRundownIds }, + DbCacheWriteCollection.createFromDatabase(context, context.directCollections.Segments, { + $or: [ + { + // In a different rundown + rundownId: { $in: loadRundownIds }, + }, + { + // Is the scratchpad + rundownId: { $in: rundownIds }, + orphaned: SegmentOrphanedReason.SCRATCHPAD, + }, + ], }), DbCacheReadCollection.createFromDatabase(context, context.directCollections.Parts, { rundownId: { $in: loadRundownIds }, diff --git a/packages/job-worker/src/playout/infinites.ts b/packages/job-worker/src/playout/infinites.ts index a9aeb90a89..a71e784947 100644 --- a/packages/job-worker/src/playout/infinites.ts +++ b/packages/job-worker/src/playout/infinites.ts @@ -26,7 +26,7 @@ import { flatten } from '@sofie-automation/corelib/dist/lib' import _ = require('underscore') import { ReadOnlyCache } from '../cache/CacheBase' import { CacheForIngest } from '../ingest/cache' -import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import { DBSegment, SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' import { sortRundownIDsInPlaylist } from '@sofie-automation/corelib/dist/playout/playlist' import { mongoWhere } from '@sofie-automation/corelib/dist/mongo' @@ -36,7 +36,7 @@ export const DEFINITELY_ENDED_FUTURE_DURATION = 1 * 1000 /** * We can only continue adlib onEnd infinites if we go forwards in the rundown. Any distance backwards will clear them. * */ -export function canContinueAdlibOnEndInfinites( +export function candidatePartIsAfterPreviewPartInstance( _context: JobContext, playlist: ReadonlyDeep, orderedSegments: DBSegment[], @@ -65,8 +65,13 @@ export function canContinueAdlibOnEndInfinites( } } +/** + * Get the ids of parts, segments and rundowns before a given part in the playlist. + * Note: this will return no segments and rundowns if the part is in the scratchpad + */ function getIdsBeforeThisPart(context: JobContext, cache: ReadOnlyCache, nextPart: DBPart) { const span = context.startSpan('getIdsBeforeThisPart') + // Get the normal parts const partsBeforeThisInSegment = cache.Parts.findAll( (p) => p.segmentId === nextPart.segmentId && p._rank < nextPart._rank @@ -77,26 +82,43 @@ function getIdsBeforeThisPart(context: JobContext, cache: ReadOnlyCache p.part)) - const currentSegment = cache.Segments.findOne(nextPart.segmentId) - const segmentsBeforeThisInRundown = currentSegment - ? cache.Segments.findAll((s) => s.rundownId === nextPart.rundownId && s._rank < currentSegment._rank).map( - (p) => p._id - ) - : [] + const partsBeforeThisInSegmentSorted = _.sortBy(partsBeforeThisInSegment, (p) => p._rank).map((p) => p._id) - const sortedRundownIds = sortRundownIDsInPlaylist( - cache.Playlist.doc.rundownIdsInOrder, - cache.Rundowns.findAll(null).map((rd) => rd._id) - ) - const currentRundownIndex = sortedRundownIds.indexOf(nextPart.rundownId) - const rundownsBeforeThisInPlaylist = - currentRundownIndex === -1 ? [] : sortedRundownIds.slice(0, currentRundownIndex) + const nextPartSegment = cache.Segments.findOne(nextPart.segmentId) + if (nextPartSegment?.orphaned === SegmentOrphanedReason.SCRATCHPAD) { + if (span) span.end() + return { + partsToReceiveOnSegmentEndFrom: partsBeforeThisInSegmentSorted, + segmentsToReceiveOnRundownEndFrom: [], + rundownsToReceiveOnShowStyleEndFrom: [], + } + } else { + // Note: In theory we should ignore any scratchpad segments here, but they will never produce any planned `Pieces`, only `PieceInstances` - if (span) span.end() - return { - partsBeforeThisInSegment: _.sortBy(partsBeforeThisInSegment, (p) => p._rank).map((p) => p._id), - segmentsBeforeThisInRundown, - rundownsBeforeThisInPlaylist, + const currentSegment = cache.Segments.findOne(nextPart.segmentId) + const segmentsToReceiveOnRundownEndFrom = currentSegment + ? cache.Segments.findAll( + (s) => + s.rundownId === nextPart.rundownId && + s._rank < currentSegment._rank && + s.orphaned !== SegmentOrphanedReason.SCRATCHPAD + ).map((p) => p._id) + : [] + + const sortedRundownIds = sortRundownIDsInPlaylist( + cache.Playlist.doc.rundownIdsInOrder, + cache.Rundowns.findAll(null).map((rd) => rd._id) + ) + const currentRundownIndex = sortedRundownIds.indexOf(nextPart.rundownId) + const rundownsToReceiveOnShowStyleEndFrom = + currentRundownIndex === -1 ? [] : sortedRundownIds.slice(0, currentRundownIndex) + + if (span) span.end() + return { + partsToReceiveOnSegmentEndFrom: partsBeforeThisInSegmentSorted, + segmentsToReceiveOnRundownEndFrom, + rundownsToReceiveOnShowStyleEndFrom, + } } } @@ -128,15 +150,15 @@ export async function fetchPiecesThatMayBeActiveForPart( ) // Figure out the ids of everything else we will have to search through - const { partsBeforeThisInSegment, segmentsBeforeThisInRundown, rundownsBeforeThisInPlaylist } = + const { partsToReceiveOnSegmentEndFrom, segmentsToReceiveOnRundownEndFrom, rundownsToReceiveOnShowStyleEndFrom } = getIdsBeforeThisPart(context, cache, part) if (unsavedIngestCache?.RundownId === part.rundownId) { // Find pieces for the current rundown const thisRundownPieceQuery = buildPastInfinitePiecesForThisPartQuery( part, - partsBeforeThisInSegment, - segmentsBeforeThisInRundown, + partsToReceiveOnSegmentEndFrom, + segmentsToReceiveOnRundownEndFrom, [] // other rundowns don't exist in the ingestCache ) if (thisRundownPieceQuery) { @@ -148,7 +170,7 @@ export async function fetchPiecesThatMayBeActiveForPart( part, [], // Only applies to the current rundown [], // Only applies to the current rundown - rundownsBeforeThisInPlaylist + rundownsToReceiveOnShowStyleEndFrom ) if (previousRundownPieceQuery) { piecePromises.push(context.directCollections.Pieces.findFetch(previousRundownPieceQuery)) @@ -157,9 +179,9 @@ export async function fetchPiecesThatMayBeActiveForPart( // No cache, so we can do a single query to the db for it all const infinitePiecesQuery = buildPastInfinitePiecesForThisPartQuery( part, - partsBeforeThisInSegment, - segmentsBeforeThisInRundown, - rundownsBeforeThisInPlaylist + partsToReceiveOnSegmentEndFrom, + segmentsToReceiveOnRundownEndFrom, + rundownsToReceiveOnShowStyleEndFrom ) if (infinitePiecesQuery) { piecePromises.push(context.directCollections.Pieces.findFetch(infinitePiecesQuery)) @@ -186,17 +208,26 @@ export async function syncPlayheadInfinitesForNextPartInstance( const playlist = cache.Playlist.doc if (!playlist.activationId) throw new Error(`RundownPlaylist "${playlist._id}" is not active`) - const { partsBeforeThisInSegment, segmentsBeforeThisInRundown, rundownsBeforeThisInPlaylist } = - getIdsBeforeThisPart(context, cache, nextPartInstance.part) + const { + partsToReceiveOnSegmentEndFrom, + segmentsToReceiveOnRundownEndFrom, + rundownsToReceiveOnShowStyleEndFrom, + } = getIdsBeforeThisPart(context, cache, nextPartInstance.part) const rundown = cache.Rundowns.findOne(currentPartInstance.rundownId) if (!rundown) throw new Error(`Rundown "${currentPartInstance.rundownId}" not found!`) + const currentSegment = cache.Segments.findOne(currentPartInstance.segmentId) + if (!currentSegment) throw new Error(`Segment "${currentPartInstance.segmentId}" not found!`) + + const nextSegment = cache.Segments.findOne(nextPartInstance.segmentId) + if (!nextSegment) throw new Error(`Segment "${nextPartInstance.segmentId}" not found!`) + const showStyleBase = await context.getShowStyleBase(rundown.showStyleBaseId) const orderedPartsAndSegments = getOrderedSegmentsAndPartsFromPlayoutCache(cache) - const canContinueAdlibOnEnds = canContinueAdlibOnEndInfinites( + const nextPartIsAfterCurrentPart = candidatePartIsAfterPreviewPartInstance( context, playlist, orderedPartsAndSegments.segments, @@ -218,16 +249,18 @@ export async function syncPlayheadInfinitesForNextPartInstance( const infinites = libgetPlayheadTrackingInfinitesForPart( playlist.activationId, - new Set(partsBeforeThisInSegment), - new Set(segmentsBeforeThisInRundown), - rundownsBeforeThisInPlaylist, + new Set(partsToReceiveOnSegmentEndFrom), + new Set(segmentsToReceiveOnRundownEndFrom), + rundownsToReceiveOnShowStyleEndFrom, rundownIdsToShowstyleIds, currentPartInstance, + currentSegment, prunedPieceInstances, rundown, nextPartInstance.part, + nextSegment, nextPartInstance._id, - canContinueAdlibOnEnds, + nextPartIsAfterCurrentPart, false ) @@ -262,7 +295,7 @@ export function getPieceInstancesForPart( newInstanceId: PartInstanceId ): PieceInstance[] { const span = context.startSpan('getPieceInstancesForPart') - const { partsBeforeThisInSegment, segmentsBeforeThisInRundown, rundownsBeforeThisInPlaylist } = + const { partsToReceiveOnSegmentEndFrom, segmentsToReceiveOnRundownEndFrom, rundownsToReceiveOnShowStyleEndFrom } = getIdsBeforeThisPart(context, cache, part) const playlist = cache.Playlist.doc @@ -273,7 +306,7 @@ export function getPieceInstancesForPart( ? cache.PieceInstances.findAll((p) => p.partInstanceId === playingPartInstance._id) : [] - const canContinueAdlibOnEnds = canContinueAdlibOnEndInfinites( + const nextPartIsAfterCurrentPart = candidatePartIsAfterPreviewPartInstance( context, playlist, orderedPartsAndSegments.segments, @@ -283,20 +316,29 @@ export function getPieceInstancesForPart( const rundownIdsToShowstyleIds = getShowStyleIdsRundownMappingFromCache(cache) + const playingSegment = playingPartInstance && cache.Segments.findOne(playingPartInstance.segmentId) + if (playingPartInstance && !playingSegment) + throw new Error(`Segment "${playingPartInstance?.segmentId}" not found!`) + + const segment = cache.Segments.findOne(part.segmentId) + if (!segment) throw new Error(`Segment "${part.segmentId}" not found!`) + const res = libgetPieceInstancesForPart( playlist.activationId, playingPartInstance, + playingSegment, playingPieceInstances, rundown, + segment, part, - new Set(partsBeforeThisInSegment), - new Set(segmentsBeforeThisInRundown), - rundownsBeforeThisInPlaylist, + new Set(partsToReceiveOnSegmentEndFrom), + new Set(segmentsToReceiveOnRundownEndFrom), + rundownsToReceiveOnShowStyleEndFrom, rundownIdsToShowstyleIds, possiblePieces, orderedPartsAndSegments.parts.map((p) => p._id), newInstanceId, - canContinueAdlibOnEnds, + nextPartIsAfterCurrentPart, false ) if (span) span.end() diff --git a/packages/job-worker/src/playout/lib.ts b/packages/job-worker/src/playout/lib.ts index c1cfbd3995..a915c93096 100644 --- a/packages/job-worker/src/playout/lib.ts +++ b/packages/job-worker/src/playout/lib.ts @@ -15,6 +15,7 @@ import { mongoWhere } from '@sofie-automation/corelib/dist/mongo' import _ = require('underscore') import { setNextPart } from './setNext' import { selectNextPart } from './selectNextPart' +import { SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' /** * Reset the rundownPlaylist (all of the rundowns within the playlist): @@ -28,6 +29,9 @@ export async function resetRundownPlaylist(context: JobContext, cache: CacheForP removePartInstancesWithPieceInstances(context, cache, { rehearsal: true }) resetPartInstancesWithPieceInstances(context, cache) + // Remove the scratchpad + cache.Segments.remove((segment) => segment.orphaned === SegmentOrphanedReason.SCRATCHPAD) + cache.Playlist.update((p) => { p.previousPartInfo = null p.currentPartInfo = null diff --git a/packages/job-worker/src/playout/moveNextPart.ts b/packages/job-worker/src/playout/moveNextPart.ts index da66044fd4..a3a998cbe9 100644 --- a/packages/job-worker/src/playout/moveNextPart.ts +++ b/packages/job-worker/src/playout/moveNextPart.ts @@ -6,6 +6,7 @@ import { CacheForPlayout, getOrderedSegmentsAndPartsFromPlayoutCache, getSelecte import { sortPartsInSortedSegments } from '@sofie-automation/corelib/dist/playout/playlist' import { setNextPartFromPart } from './setNext' import { logger } from '../logging' +import { SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' export async function moveNextPart( context: JobContext, @@ -26,7 +27,9 @@ export async function moveNextPart( if (segmentDelta) { // Ignores horizontalDelta - const considerSegments = rawSegments.filter((s) => s._id === refPart.segmentId || !s.isHidden) + const considerSegments = rawSegments.filter( + (s) => s._id === refPart.segmentId || !s.isHidden || s.orphaned === SegmentOrphanedReason.SCRATCHPAD + ) const refSegmentIndex = considerSegments.findIndex((s) => s._id === refPart.segmentId) if (refSegmentIndex === -1) throw new Error(`Segment "${refPart.segmentId}" not found!`) diff --git a/packages/job-worker/src/playout/scratchpad.ts b/packages/job-worker/src/playout/scratchpad.ts new file mode 100644 index 0000000000..2cf1e78eb9 --- /dev/null +++ b/packages/job-worker/src/playout/scratchpad.ts @@ -0,0 +1,124 @@ +import { DBSegment, SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' +import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/error' +import { getRandomId } from '@sofie-automation/corelib/dist/lib' +import { ActivateScratchpadProps } from '@sofie-automation/corelib/dist/worker/studio' +import { literal } from '@sofie-automation/shared-lib/dist/lib/lib' +import { getCurrentTime } from '../lib' +import { JobContext } from '../jobs' +import { runJobWithPlayoutCache } from './lock' +import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' +import { performTakeToNextedPart } from './take' +import { PartInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { CacheForPlayout } from './cache' + +export async function handleActivateScratchpad(context: JobContext, data: ActivateScratchpadProps): Promise { + if (!context.studio.settings.allowScratchpad) throw UserError.create(UserErrorMessage.ScratchpadNotAllowed) + + return runJobWithPlayoutCache( + context, + data, + async (cache) => { + const playlist = cache.Playlist.doc + if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown) + + if (playlist.currentPartInfo) throw UserError.create(UserErrorMessage.RundownAlreadyActive) + }, + async (cache) => { + let playlist = cache.Playlist.doc + if (!playlist.activationId) throw new Error(`Playlist has no activationId!`) + + const rundown = cache.Rundowns.findOne(data.rundownId) + if (!rundown) throw new Error(`Rundown "${data.rundownId}" not found!`) + + const segment = cache.Segments.findOne( + (s) => s.orphaned === SegmentOrphanedReason.SCRATCHPAD && s.rundownId === data.rundownId + ) + if (segment) throw UserError.create(UserErrorMessage.ScratchpadAlreadyActive) + + const minSegmentRank = Math.min(0, ...cache.Segments.findAll(null).map((s) => s._rank)) + + const segmentId = cache.Segments.insert( + literal({ + _id: getRandomId(), + _rank: minSegmentRank - 1, + externalId: '__scratchpad__', + externalModified: getCurrentTime(), + rundownId: data.rundownId, + orphaned: SegmentOrphanedReason.SCRATCHPAD, + name: '', + }) + ) + + const newPartInstance: DBPartInstance = { + _id: getRandomId(), + rundownId: data.rundownId, + segmentId: segmentId, + playlistActivationId: playlist.activationId, + segmentPlayoutId: getRandomId(), + takeCount: 1, + rehearsal: !!playlist.rehearsal, + orphaned: 'adlib-part', + part: { + _id: getRandomId(), + _rank: 0, + externalId: '', + rundownId: data.rundownId, + segmentId: segmentId, + title: 'Scratchpad', + expectedDuration: 0, + expectedDurationWithPreroll: 0, // Filled in later + untimed: true, + }, + } + cache.PartInstances.insert(newPartInstance) + + // Set the part as next + cache.Playlist.update((playlist) => { + playlist.nextPartInfo = { + partInstanceId: newPartInstance._id, + rundownId: newPartInstance.rundownId, + manuallySelected: true, + consumesNextSegmentId: false, + } + + return playlist + }) + playlist = cache.Playlist.doc + + // Take into the newly created Part + await performTakeToNextedPart(context, cache, getCurrentTime()) + } + ) +} + +/** + * Validate and cleanup a PartInstance being added to a SCRATCHPAD segment. + * If PartInstance is not in the scratchpad, do nothing + */ +export function validateScratchpartPartInstanceProperties( + _context: JobContext, + cache: CacheForPlayout, + partInstanceId: PartInstanceId +): void { + const partInstance = cache.PartInstances.findOne(partInstanceId) + if (!partInstance) return + + const segment = cache.Segments.findOne(partInstance.segmentId) + if (!segment) + throw new Error(`Failed to find Segment "${partInstance.segmentId}" for PartInstance "${partInstance._id}"`) + + // Check if this applies + if (segment.orphaned !== SegmentOrphanedReason.SCRATCHPAD) return + + cache.PartInstances.updateOne(partInstance._id, (partInstance) => { + partInstance.orphaned = 'adlib-part' + + // Autonext isn't allowed to begin with, to avoid accidentally exiting the scratchpad + delete partInstance.part.autoNext + + // Force this to not affect rundown timing + partInstance.part.untimed = true + + return partInstance + }) +} diff --git a/packages/job-worker/src/playout/setNext.ts b/packages/job-worker/src/playout/setNext.ts index 89b904a1d0..ded144ce98 100644 --- a/packages/job-worker/src/playout/setNext.ts +++ b/packages/job-worker/src/playout/setNext.ts @@ -1,4 +1,4 @@ -import { getRandomId, literal } from '@sofie-automation/corelib/dist/lib' +import { assertNever, getRandomId, literal } from '@sofie-automation/corelib/dist/lib' import { DBSegment, SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart, isPartPlayable } from '@sofie-automation/corelib/dist/dataModel/Part' import { JobContext } from '../jobs' @@ -299,6 +299,15 @@ async function cleanupOrphanedItems(context: JobContext, cache: CacheForPlayout) rundownSegments.hidden.push(segment._id) break } + case SegmentOrphanedReason.SCRATCHPAD: + // Ignore, as these are owned by playout not ingest + break + case undefined: + // Not orphaned + break + default: + assertNever(segment.orphaned) + break } } } @@ -355,6 +364,9 @@ export async function setNextSegment( ): Promise { const span = context.startSpan('setNextSegment') if (nextSegment) { + if (nextSegment.orphaned === SegmentOrphanedReason.SCRATCHPAD) + throw new Error(`Segment "${nextSegment._id}" is a scratchpad, and cannot be nexted!`) + // Just run so that errors will be thrown if something wrong: const partsInSegment = sortPartsInSortedSegments( cache.Parts.findAll((p) => p.segmentId === nextSegment._id), diff --git a/packages/job-worker/src/workers/studio/jobs.ts b/packages/job-worker/src/workers/studio/jobs.ts index 9e0f7f6926..aee66745bf 100644 --- a/packages/job-worker/src/workers/studio/jobs.ts +++ b/packages/job-worker/src/workers/studio/jobs.ts @@ -34,6 +34,7 @@ import { handleBlueprintUpgradeForStudio, handleBlueprintValidateConfigForStudio import { handleTimelineTriggerTime, handleOnPlayoutPlaybackChanged } from '../../playout/timings' import { handleExecuteAdlibAction } from '../../playout/adlibAction' import { handleTakeNextPart } from '../../playout/take' +import { handleActivateScratchpad } from '../../playout/scratchpad' type ExecutableFunction = ( context: JobContext, @@ -85,4 +86,6 @@ export const studioJobHandlers: StudioJobHandlers = { [StudioJobs.BlueprintUpgradeForStudio]: handleBlueprintUpgradeForStudio, [StudioJobs.BlueprintValidateConfigForStudio]: handleBlueprintValidateConfigForStudio, + + [StudioJobs.ActivateScratchpad]: handleActivateScratchpad, } diff --git a/packages/shared-lib/src/core/model/ShowStyle.ts b/packages/shared-lib/src/core/model/ShowStyle.ts index 9f791c9f24..bf213adcb0 100644 --- a/packages/shared-lib/src/core/model/ShowStyle.ts +++ b/packages/shared-lib/src/core/model/ShowStyle.ts @@ -92,6 +92,7 @@ export enum PlayoutActions { resetRundownPlaylist = 'resetRundownPlaylist', reloadRundownPlaylistData = 'reloadRundownPlaylistData', disableNextPiece = 'disableNextPiece', + activateScratchpadMode = 'activateScratchpadMode', } export enum ClientActions { From 6d77853170b9da0db69c54ce0d63f4a6a874df95 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Mon, 10 Jul 2023 15:47:13 +0200 Subject: [PATCH 050/479] feat: implement partInstance-based timing calculator input --- .../lib/__tests__/rundownTiming.test.ts | 50 +- meteor/client/lib/rundownTiming.ts | 64 +- meteor/client/ui/RundownView.tsx | 7 +- .../RundownTiming/RundownTimingProvider.tsx | 225 ++++--- .../SegmentContainer/withResolvedSegment.ts | 6 + .../SegmentScratchpad/SegmentScratchpad.scss | 9 + .../SegmentScratchpad/SegmentScratchpad.tsx | 560 ++++++++++++++++++ .../SegmentScratchpadContainer.tsx | 265 +++++++++ .../SegmentStoryboardContainer.tsx | 2 - packages/corelib/src/playout/playlist.ts | 24 + 10 files changed, 1051 insertions(+), 161 deletions(-) create mode 100644 meteor/client/ui/SegmentScratchpad/SegmentScratchpad.scss create mode 100644 meteor/client/ui/SegmentScratchpad/SegmentScratchpad.tsx create mode 100644 meteor/client/ui/SegmentScratchpad/SegmentScratchpadContainer.tsx diff --git a/meteor/client/lib/__tests__/rundownTiming.test.ts b/meteor/client/lib/__tests__/rundownTiming.test.ts index 31ba885420..78e5aa014d 100644 --- a/meteor/client/lib/__tests__/rundownTiming.test.ts +++ b/meteor/client/lib/__tests__/rundownTiming.test.ts @@ -97,11 +97,15 @@ function makeMockRundown(id: string, playlist: RundownPlaylist) { }) } +function convertPartsToPartInstances(parts: Part[]): PartInstance[] { + return parts.map((part) => wrapPartToTemporaryInstance(protectString(''), part)) +} + describe('rundown Timing Calculator', () => { it('Provides output for empty playlist', () => { const timing = new RundownTimingCalculator() const playlist: RundownPlaylist = makeMockPlaylist() - const parts: Part[] = [] + const partInstances: PartInstance[] = [] const segmentsMap: Map = new Map() const partInstancesMap: Map = new Map() const result = timing.updateDurations( @@ -110,7 +114,7 @@ describe('rundown Timing Calculator', () => { playlist, [], undefined, - parts, + partInstances, partInstancesMap, new Map(), segmentsMap, @@ -164,6 +168,7 @@ describe('rundown Timing Calculator', () => { parts.push(makeMockPart('part2', 0, rundownId, segmentId1, { expectedDuration: 1000 })) parts.push(makeMockPart('part3', 0, rundownId, segmentId2, { expectedDuration: 1000 })) parts.push(makeMockPart('part4', 0, rundownId, segmentId2, { expectedDuration: 1000 })) + const partInstances = convertPartsToPartInstances(parts) const partInstancesMap: Map = new Map() const rundown = makeMockRundown(rundownId, playlist) const rundowns = [rundown] @@ -173,7 +178,7 @@ describe('rundown Timing Calculator', () => { playlist, rundowns, undefined, - parts, + partInstances, partInstancesMap, new Map(), segmentsMap, @@ -266,6 +271,7 @@ describe('rundown Timing Calculator', () => { parts.push(makeMockPart('part2', 0, rundownId, segmentId1, { expectedDuration: 1000 })) parts.push(makeMockPart('part3', 0, rundownId, segmentId2, { expectedDuration: 1000 })) parts.push(makeMockPart('part4', 0, rundownId, segmentId2, { expectedDuration: 1000 })) + const partInstances = convertPartsToPartInstances(parts) const partInstancesMap: Map = new Map() const rundown = makeMockRundown(rundownId, playlist) const rundowns = [rundown] @@ -275,7 +281,7 @@ describe('rundown Timing Calculator', () => { playlist, rundowns, undefined, - parts, + partInstances, partInstancesMap, new Map(), segmentsMap, @@ -369,6 +375,7 @@ describe('rundown Timing Calculator', () => { parts.push(makeMockPart('part2', 0, rundownId1, segmentId1, { expectedDuration: 1000 })) parts.push(makeMockPart('part3', 0, rundownId2, segmentId2, { expectedDuration: 1000 })) parts.push(makeMockPart('part4', 0, rundownId2, segmentId2, { expectedDuration: 1000 })) + const partInstances = convertPartsToPartInstances(parts) const partInstancesMap: Map = new Map() const rundown1 = makeMockRundown(rundownId1, playlist) const rundown2 = makeMockRundown(rundownId1, playlist) @@ -379,7 +386,7 @@ describe('rundown Timing Calculator', () => { playlist, rundowns, undefined, - parts, + partInstances, partInstancesMap, new Map(), segmentsMap, @@ -498,6 +505,7 @@ describe('rundown Timing Calculator', () => { displayDurationGroup: 'test', }) ) + const partInstances = convertPartsToPartInstances(parts) const partInstancesMap: Map = new Map() const rundown = makeMockRundown(rundownId1, playlist) const rundowns = [rundown] @@ -507,7 +515,7 @@ describe('rundown Timing Calculator', () => { playlist, rundowns, undefined, - parts, + partInstances, partInstancesMap, new Map(), segmentsMap, @@ -601,6 +609,7 @@ describe('rundown Timing Calculator', () => { parts.push(makeMockPart('part2', 0, rundownId, segmentId1, { expectedDuration: 1000 })) parts.push(makeMockPart('part3', 0, rundownId, segmentId2, { expectedDuration: 1000 })) parts.push(makeMockPart('part4', 0, rundownId, segmentId2, { expectedDuration: 1000 })) + const partInstances = convertPartsToPartInstances(parts) const partInstancesMap: Map = new Map() const rundown = makeMockRundown(rundownId, playlist) const rundowns = [rundown] @@ -610,7 +619,7 @@ describe('rundown Timing Calculator', () => { playlist, rundowns, undefined, - parts, + partInstances, partInstancesMap, new Map(), segmentsMap, @@ -732,6 +741,7 @@ describe('rundown Timing Calculator', () => { expectedDuration: 1000, }) ) + const partInstances = convertPartsToPartInstances(parts) const partInstancesMap: Map = new Map() const rundown = makeMockRundown(rundownId1, playlist) const rundowns = [rundown] @@ -741,7 +751,7 @@ describe('rundown Timing Calculator', () => { playlist, rundowns, undefined, - parts, + partInstances, partInstancesMap, new Map(), segmentsMap, @@ -857,6 +867,7 @@ describe('rundown Timing Calculator', () => { }) ) parts.push(makeMockPart('part4', 0, rundownId1, segmentId2, { expectedDuration: 1000 })) + const partInstances = convertPartsToPartInstances(parts) const partInstancesMap: Map = new Map() const rundown = makeMockRundown(rundownId1, playlist) const rundowns = [rundown] @@ -866,7 +877,7 @@ describe('rundown Timing Calculator', () => { playlist, rundowns, undefined, - parts, + partInstances, partInstancesMap, new Map(), segmentsMap, @@ -962,6 +973,7 @@ describe('rundown Timing Calculator', () => { parts.push(makeMockPart('part2', 0, rundownId, segmentId1, { expectedDuration: 1000 })) parts.push(makeMockPart('part3', 0, rundownId, segmentId2, { expectedDuration: 1000 })) parts.push(makeMockPart('part4', 0, rundownId, segmentId2, { expectedDuration: 1000 })) + const partInstances = convertPartsToPartInstances(parts) const piecesMap: Map = new Map() piecesMap.set(protectString('part1'), [ literal({ @@ -990,7 +1002,7 @@ describe('rundown Timing Calculator', () => { playlist, rundowns, undefined, - parts, + partInstances, partInstancesMap, piecesMap, segmentsMap, @@ -1106,6 +1118,7 @@ describe('rundown Timing Calculator', () => { return [part._id, wrapPartToTemporaryInstance(protectString('active'), part)] }) ) + const partInstances = Array.from(partInstancesMap.values()) partInstancesMap.get(parts[0]._id)!.timings = { duration: 1000, take: 0, @@ -1138,7 +1151,7 @@ describe('rundown Timing Calculator', () => { playlist, rundowns, rundown, - parts, + partInstances, partInstancesMap, new Map(), segmentsMap, @@ -1255,6 +1268,7 @@ describe('rundown Timing Calculator', () => { return [part._id, wrapPartToTemporaryInstance(protectString('active'), part)] }) ) + const partInstances = Array.from(partInstancesMap.values()) partInstancesMap.get(parts[0]._id)!.timings = { duration: 1000, take: 0, @@ -1287,7 +1301,7 @@ describe('rundown Timing Calculator', () => { playlist, rundowns, rundown, - parts, + partInstances, partInstancesMap, new Map(), segmentsMap, @@ -1404,6 +1418,7 @@ describe('rundown Timing Calculator', () => { return [part._id, wrapPartToTemporaryInstance(protectString('active'), part)] }) ) + const partInstances = Array.from(partInstancesMap.values()) partInstancesMap.get(parts[0]._id)!.timings = { duration: 1000, take: 0, @@ -1442,7 +1457,7 @@ describe('rundown Timing Calculator', () => { playlist, rundowns, rundown, - parts, + partInstances, partInstancesMap, new Map(), segmentsMap, @@ -1569,6 +1584,7 @@ describe('rundown Timing Calculator', () => { take: 1000, plannedStartedPlayback: 1000, } + const partInstances = Array.from(partInstancesMap.values()) const currentPartInstanceId = partInstancesMap.get(parts[1]._id)!._id const nextPartInstanceId = partInstancesMap.get(parts[2]._id)!._id playlist.currentPartInfo = { @@ -1591,7 +1607,7 @@ describe('rundown Timing Calculator', () => { playlist, rundowns, rundown, - parts, + partInstances, partInstancesMap, new Map(), segmentsMap, @@ -1708,6 +1724,7 @@ describe('rundown Timing Calculator', () => { return [part._id, wrapPartToTemporaryInstance(protectString('active'), part)] }) ) + const partInstances = Array.from(partInstancesMap.values()) partInstancesMap.get(parts[0]._id)!.timings = { duration: 1000, take: 0, @@ -1740,7 +1757,7 @@ describe('rundown Timing Calculator', () => { playlist, rundowns, rundown, - parts, + partInstances, partInstancesMap, new Map(), segmentsMap, @@ -1873,6 +1890,7 @@ describe('rundown Timing Calculator', () => { take: 2000, plannedStartedPlayback: 2000, } + const partInstances = Array.from(partInstancesMap.values()) const currentPartInstanceId = partInstancesMap.get(parts[2]._id)!._id const nextPartInstanceId = partInstancesMap.get(parts[3]._id)!._id playlist.currentPartInfo = { @@ -1895,7 +1913,7 @@ describe('rundown Timing Calculator', () => { playlist, rundowns, rundown, - parts, + partInstances, partInstancesMap, new Map(), segmentsMap, diff --git a/meteor/client/lib/rundownTiming.ts b/meteor/client/lib/rundownTiming.ts index 28f8430925..b7f954860b 100644 --- a/meteor/client/lib/rundownTiming.ts +++ b/meteor/client/lib/rundownTiming.ts @@ -18,13 +18,8 @@ import { calculatePartInstanceExpectedDurationWithPreroll, CalculateTimingsPiece, } from '@sofie-automation/corelib/dist/playout/timings' -import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' -import { - findPartInstanceInMapOrWrapToTemporary, - PartInstance, - wrapPartToTemporaryInstance, -} from '../../lib/collections/PartInstances' -import { Part } from '../../lib/collections/Parts' +import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' +import { PartInstance } from '../../lib/collections/PartInstances' import { DBRundownPlaylist, RundownPlaylist } from '../../lib/collections/RundownPlaylists' import { getCurrentTime, objectFromEntries } from '../../lib/lib' import { Settings } from '../../lib/Settings' @@ -39,6 +34,8 @@ interface BreakProps { breakIsLastRundown: boolean } +type CalculateTimingsPartInstance = Pick + /** * This is a class for calculating timings in a Rundown playlist used by RundownTimingProvider. * @@ -46,8 +43,6 @@ interface BreakProps { * @class RundownTimingCalculator */ export class RundownTimingCalculator { - private temporaryPartInstances: Map = new Map() - private linearParts: Array<[PartId, number | null]> = [] // we need to keep the nextSegmentId here for the brief moment when the next partinstance can't be found @@ -85,9 +80,10 @@ export class RundownTimingCalculator { * @param {(RundownPlaylist | undefined)} playlist * @param {Rundown[]} rundowns * @param {(Rundown | undefined)} currentRundown - * @param {Part[]} parts - * @param {Map} partInstancesMap + * @param {CalculateTimingsPartInstance[]} partInstances + * @param {Map} partInstancesMap * @param {number} [defaultDuration] + * @param {CalculateTimingsPartInstance[]} segmentEntryPartInstances * @return {*} {RundownTimingContext} * @memberof RundownTimingCalculator */ @@ -97,8 +93,8 @@ export class RundownTimingCalculator { playlist: RundownPlaylist | undefined, rundowns: Rundown[], currentRundown: Rundown | undefined, - parts: Part[], - partInstancesMap: Map, + partInstances: CalculateTimingsPartInstance[], + partInstancesMap: Map, pieces: Map, segmentsMap: Map, /** Fallback duration for Parts that have no as-played duration of their own. */ @@ -109,7 +105,7 @@ export class RundownTimingCalculator { * the currentPartInstance. * * This is being used for calculating Segment Duration Budget */ - segmentEntryPartInstances: PartInstance[] + segmentEntryPartInstances: CalculateTimingsPartInstance[] ): RundownTimingContext { let totalRundownDuration = 0 let remainingRundownDuration = 0 @@ -154,7 +150,8 @@ export class RundownTimingCalculator { this.nextSegmentId = undefined } - parts.forEach((origPart) => { + partInstances.forEach((partInstance) => { + const origPart = partInstance.part if (origPart.budgetDuration !== undefined) { const segmentId = unprotectString(origPart.segmentId) if (this.segmentBudgetDurations[segmentId] !== undefined) { @@ -171,8 +168,7 @@ export class RundownTimingCalculator { partInstance.timings?.reportedStartedPlayback }) - parts.forEach((origPart, itIndex) => { - const partInstance = this.getPartInstanceOrGetCachedTemp(partInstancesMap, origPart) + partInstances.forEach((partInstance, itIndex) => { const piecesForPart = pieces.get(partInstance.part._id) ?? [] if (partInstance.segmentId !== lastSegmentId) { @@ -247,8 +243,9 @@ export class RundownTimingCalculator { // either this is not the first element of the displayDurationGroup (this.displayDurationGroups[partInstance.part.displayDurationGroup] !== undefined || // or there is a following member of this displayDurationGroup - (parts[itIndex + 1] && - parts[itIndex + 1].displayDurationGroup === partInstance.part.displayDurationGroup)) && + (partInstances[itIndex + 1] && + partInstances[itIndex + 1].part.displayDurationGroup === + partInstance.part.displayDurationGroup)) && !partInstance.part.floated && !partIsUntimed ) { @@ -601,8 +598,8 @@ export class RundownTimingCalculator { let remainingTimeOnCurrentPart: number | undefined = undefined let currentPartWillAutoNext = false if (currentAIndex >= 0) { - const currentLivePart = parts[currentAIndex] - const currentLivePartInstance = findPartInstanceInMapOrWrapToTemporary(partInstancesMap, currentLivePart) + const currentLivePartInstance = partInstances[currentAIndex] + const currentLivePart = currentLivePartInstance.part const piecesForPart = pieces.get(currentLivePartInstance.part._id) ?? [] const lastStartedPlayback = currentLivePartInstance.timings?.plannedStartedPlayback @@ -654,27 +651,6 @@ export class RundownTimingCalculator { }) } - clearTempPartInstances(): void { - this.temporaryPartInstances.clear() - } - - private getPartInstanceOrGetCachedTemp(partInstancesMap: Map, part: Part): PartInstance { - const origPartId = part._id - const partInstance = partInstancesMap.get(origPartId) - if (partInstance !== undefined) { - return partInstance - } else { - let tempPartInstance = this.temporaryPartInstances.get(origPartId) - if (tempPartInstance !== undefined) { - return tempPartInstance - } else { - tempPartInstance = wrapPartToTemporaryInstance(protectString(''), part) - this.temporaryPartInstances.set(origPartId, tempPartInstance) - return tempPartInstance - } - } - } - private getRundownsBeforeNextBreak( orderedRundowns: Rundown[], currentRundown: Rundown | undefined @@ -855,7 +831,7 @@ export function getPlaylistTimingDiff( } function ensureMinimumDefaultDurationIfNotAuto( - partInstance: PartInstance, + partInstance: CalculateTimingsPartInstance, incomingDuration: number | undefined, defaultDuration: number ): number { @@ -875,7 +851,7 @@ function ensureMinimumDefaultDurationIfNotAuto( */ function getSegmentRundownAnchorFromPart( partId: PartId, - partInstancesMap: Map, + partInstancesMap: Map, segmentsMap: Map, now: number ): number | undefined { diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx index f7c8c0e969..940d9e7ab2 100644 --- a/meteor/client/ui/RundownView.tsx +++ b/meteor/client/ui/RundownView.tsx @@ -17,7 +17,7 @@ import Tooltip from 'rc-tooltip' import { NavLink, Route, Prompt } from 'react-router-dom' import { RundownPlaylist } from '../../lib/collections/RundownPlaylists' import { Rundown } from '../../lib/collections/Rundowns' -import { DBSegment, Segment } from '../../lib/collections/Segments' +import { DBSegment, Segment, SegmentOrphanedReason } from '../../lib/collections/Segments' import { StudioRouteSet } from '../../lib/collections/Studios' import { Part } from '../../lib/collections/Parts' import { ContextMenu, MenuItem, ContextMenuTrigger } from '@jstarpl/react-contextmenu' @@ -142,6 +142,7 @@ import { } from '../collections' import { UIShowStyleBase } from '../../lib/api/showStyles' import { RundownPlaylistCollectionUtil } from '../../lib/collections/rundownPlaylistUtil' +import { SegmentScratchpadContainer } from './SegmentScratchpad/SegmentScratchpadContainer' export const MAGIC_TIME_SCALE_FACTOR = 0.03 @@ -2548,6 +2549,10 @@ export const RundownView = translateWithTracker(( showDurationSourceLayers: showDurationSourceLayers, } + if (segment.orphaned === SegmentOrphanedReason.SCRATCHPAD) { + return + } + switch (displayMode) { case SegmentViewMode.Storyboard: return diff --git a/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx b/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx index 5f9d1aa180..56222134fe 100644 --- a/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx +++ b/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx @@ -2,19 +2,17 @@ import React, { PropsWithChildren } from 'react' import { Meteor } from 'meteor/meteor' import * as PropTypes from 'prop-types' import { withTracker } from '../../../lib/ReactMeteorData/react-meteor-data' -import { Part } from '../../../../lib/collections/Parts' -import { getCurrentTime } from '../../../../lib/lib' +import { getCurrentTime, protectString } from '../../../../lib/lib' import { MeteorReactComponent } from '../../../lib/MeteorReactComponent' import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists' -import { PartInstance } from '../../../../lib/collections/PartInstances' +import { PartInstance, wrapPartToTemporaryInstance } from '../../../../lib/collections/PartInstances' import { RundownTiming, TimeEventArgs } from './RundownTiming' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { RundownTimingCalculator, RundownTimingContext } from '../../../lib/rundownTiming' -import { PartId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { PartInstances } from '../../../collections' +import { PartId, PartInstanceId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { RundownPlaylistCollectionUtil } from '../../../../lib/collections/rundownPlaylistUtil' -import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' +import { sortPartInstancesInSortedSegments } from '@sofie-automation/corelib/dist/playout/playlist' import { CalculateTimingsPiece } from '@sofie-automation/corelib/dist/playout/timings' const TIMING_DEFAULT_REFRESH_INTERVAL = 1000 / 60 // the interval for high-resolution events (timeupdateHR) @@ -47,14 +45,19 @@ interface IRundownTimingProviderState {} interface IRundownTimingProviderTrackedProps { rundowns: Array currentRundown: Rundown | undefined - parts: Array - partInstancesMap: Map + partInstances: Array + partInstancesMap: Map pieces: Map - segmentEntryPartInstances: PartInstance[] + segmentEntryPartInstances: MinimalPartInstance[] segments: DBSegment[] segmentsMap: Map } +type MinimalPartInstance = Pick< + PartInstance, + '_id' | 'rundownId' | 'segmentId' | 'segmentPlayoutId' | 'takeCount' | 'part' +> + /** * RundownTimingProvider is a container component that provides a timing context to all child elements. * It allows calculating a single @@ -65,102 +68,103 @@ export const RundownTimingProvider = withTracker< PropsWithChildren, IRundownTimingProviderState, IRundownTimingProviderTrackedProps ->((props) => { - let rundowns: Array = [] - let parts: Array = [] - let segments: Array = [] - const partInstancesMap = new Map() - let pieces: Map = new Map() - let currentRundown: Rundown | undefined - const segmentEntryPartInstances: PartInstance[] = [] - if (props.playlist) { - rundowns = RundownPlaylistCollectionUtil.getRundownsOrdered(props.playlist) - const { parts: incomingParts, segments: incomingSegments } = RundownPlaylistCollectionUtil.getSegmentsAndPartsSync( - props.playlist - ) - parts = incomingParts - segments = incomingSegments - const partInstances = RundownPlaylistCollectionUtil.getActivePartInstances(props.playlist) +>(({ playlist }) => { + if (!playlist) { + return { + rundowns: [], + currentRundown: undefined, + partInstances: [], + partInstancesMap: new Map(), + pieces: new Map(), + segmentEntryPartInstances: [], + segments: [], + segmentsMap: new Map(), + } + } - const currentPartInstance = partInstances.find((p) => p._id === props.playlist?.currentPartInfo?.partInstanceId) - const previousPartInstance = partInstances.find((p) => p._id === props.playlist?.previousPartInfo?.partInstanceId) + // TODO: Remove + console.time('rundownTiming') - currentRundown = currentPartInstance ? rundowns.find((r) => r._id === currentPartInstance.rundownId) : rundowns[0] - // These are needed to retrieve the start time of a segment for calculating the remaining budget, in case the first partInstance was removed + const partInstancesMap = new Map() + const segmentEntryPartInstances: MinimalPartInstance[] = [] - const firstPartInstanceInCurrentSegmentPlay = + const rundowns = RundownPlaylistCollectionUtil.getRundownsOrdered(playlist) + const segments = RundownPlaylistCollectionUtil.getSegments(playlist) + const segmentsMap = new Map(segments.map((segment) => [segment._id, segment])) + const unorderedParts = RundownPlaylistCollectionUtil.getUnorderedParts(playlist) + const activePartInstances = RundownPlaylistCollectionUtil.getActivePartInstances(playlist, undefined, { + projection: { + _id: 1, + rundownId: 1, + segmentId: 1, + segmentPlayoutId: 1, + takeCount: 1, + part: 1, + }, + }) as Array> + + const { currentPartInstance, previousPartInstance } = findCurrentAndPreviousPartInstance( + activePartInstances, + playlist.currentPartInfo?.partInstanceId, + playlist.previousPartInfo?.partInstanceId + ) + + const currentRundown = currentPartInstance + ? rundowns.find((r) => r._id === currentPartInstance.rundownId) + : rundowns[0] + // These are needed to retrieve the start time of a segment for calculating the remaining budget, in case the first partInstance was removed + + let firstPartInstanceInCurrentSegmentPlay: MinimalPartInstance | undefined + let firstPartInstanceInCurrentSegmentPlayPartInstanceTakeCount = Number.POSITIVE_INFINITY + let firstPartInstanceInPreviousSegmentPlay: MinimalPartInstance | undefined + let firstPartInstanceInPreviousSegmentPlayPartInstanceTakeCount = Number.POSITIVE_INFINITY + + let partInstances: MinimalPartInstance[] = [] + + const allPartIds: Set = new Set() + + for (const partInstance of activePartInstances) { + if ( currentPartInstance && - PartInstances.findOne( - { - rundownId: currentPartInstance.rundownId, - segmentPlayoutId: currentPartInstance.segmentPlayoutId, - }, - { sort: { takeCount: 1 } } - ) - if (firstPartInstanceInCurrentSegmentPlay) segmentEntryPartInstances.push(firstPartInstanceInCurrentSegmentPlay) - - const firstPartInstanceInPreviousSegmentPlay = + partInstance.segmentPlayoutId === currentPartInstance.segmentPlayoutId && + partInstance.takeCount < firstPartInstanceInCurrentSegmentPlayPartInstanceTakeCount + ) { + firstPartInstanceInCurrentSegmentPlay = partInstance + firstPartInstanceInCurrentSegmentPlayPartInstanceTakeCount = partInstance.takeCount + } + if ( previousPartInstance && - previousPartInstance.segmentPlayoutId !== currentPartInstance?.segmentPlayoutId && - PartInstances.findOne( - { - rundownId: previousPartInstance.rundownId, - segmentPlayoutId: previousPartInstance.segmentPlayoutId, - }, - { sort: { takeCount: 1 } } - ) - if (firstPartInstanceInPreviousSegmentPlay) segmentEntryPartInstances.push(firstPartInstanceInPreviousSegmentPlay) - - partInstances.forEach((partInstance) => { - partInstancesMap.set(partInstance.part._id, partInstance) - - // if the part is orphaned, we need to inject it's part into the incoming parts in the correct position - if (partInstance.orphaned) { - let foundSegment = false - let insertBefore: number | null = null - for (let i = 0; i < parts.length; i++) { - if (parts[i].segmentId === partInstance.segmentId) { - // mark that we have found parts from the segment we're looking for - foundSegment = true - - if (parts[i]._id === partInstance.part._id) { - // the PartInstance is orphaned, but there's still the underlying part in the collection - // let's skip for now. - // this needs to be updated at some time since it should be treated as a different part at - // this point. - break - } else if (parts[i]._rank > partInstance.part._rank) { - // we have found a part with a rank greater than the rank of the orphaned PartInstance - insertBefore = i - break - } - } else if (foundSegment && parts[i].segmentId !== partInstance.segmentId) { - // we have found parts from the segment we're looking for, but none of them had a rank - // greater than the rank of the orphaned PartInstance. Lets insert the part before the first - // part of the next segment - insertBefore = i - break - } - } - - if (insertBefore !== null) { - parts.splice(insertBefore, 0, partInstance.part) - } else if (foundSegment && partInstance.orphaned === 'adlib-part') { - // Part is right at the end of the rundown - parts.push(partInstance.part) - } - } - }) + partInstance.segmentPlayoutId === previousPartInstance.segmentPlayoutId && + partInstance.takeCount < firstPartInstanceInPreviousSegmentPlayPartInstanceTakeCount + ) { + firstPartInstanceInPreviousSegmentPlay = partInstance + firstPartInstanceInPreviousSegmentPlayPartInstanceTakeCount = partInstance.takeCount + } - pieces = RundownPlaylistCollectionUtil.getPiecesForParts(parts.map((p) => p._id)) + allPartIds.add(partInstance.part._id) } - const segmentsMap = new Map(segments.map((segment) => [segment._id, segment])) + partInstances = activePartInstances + + for (const part of unorderedParts) { + if (allPartIds.has(part._id)) continue + partInstances.push(wrapPartToTemporaryInstance(playlist.activationId ?? protectString(''), part)) + } + + partInstances = sortPartInstancesInSortedSegments(partInstances, segments) + + if (firstPartInstanceInCurrentSegmentPlay) segmentEntryPartInstances.push(firstPartInstanceInCurrentSegmentPlay) + if (firstPartInstanceInPreviousSegmentPlay) segmentEntryPartInstances.push(firstPartInstanceInPreviousSegmentPlay) + + const pieces = RundownPlaylistCollectionUtil.getPiecesForParts(Array.from(allPartIds.values())) + + // TODO: Remove + console.timeEnd('rundownTiming') return { rundowns, currentRundown, - parts, + partInstances, partInstancesMap, pieces, segmentEntryPartInstances, @@ -250,12 +254,10 @@ export const RundownTimingProvider = withTracker< this.refreshTimer = Meteor.setInterval(this.onRefreshTimer, this.refreshTimerInterval) } if ( - prevProps.parts !== this.props.parts || + prevProps.partInstances !== this.props.partInstances || prevProps.playlist?.nextPartInfo?.partInstanceId !== this.props.playlist?.nextPartInfo?.partInstanceId || prevProps.playlist?.currentPartInfo?.partInstanceId !== this.props.playlist?.currentPartInfo?.partInstanceId ) { - // empty the temporary Part Instances cache - this.timingCalculator.clearTempPartInstances() this.refreshDecimator = 0 // Force LR update this.lastSyncedTime = 0 // Force synced update this.onRefreshTimer() @@ -303,7 +305,7 @@ export const RundownTimingProvider = withTracker< playlist, rundowns, currentRundown, - parts, + partInstances, partInstancesMap, pieces, segmentsMap, @@ -315,7 +317,7 @@ export const RundownTimingProvider = withTracker< playlist, rundowns, currentRundown, - parts, + partInstances, partInstancesMap, pieces, segmentsMap, @@ -334,3 +336,30 @@ export const RundownTimingProvider = withTracker< } } ) + +function findCurrentAndPreviousPartInstance( + activePartInstances: MinimalPartInstance[], + currentPartInstanceId: PartInstanceId | undefined, + previousPartInstanceId: PartInstanceId | undefined +) { + let currentPartInstance: MinimalPartInstance | undefined + let previousPartInstance: MinimalPartInstance | undefined + // the activePartInstances are usually sorted ascending by takeCount, so it makes sense to start + // at the end of the array, since that's where the latest PartInstances generally will be + for (let i = activePartInstances.length - 1; i >= 0; i--) { + const partInstance = activePartInstances[i] + if (partInstance._id === currentPartInstanceId) currentPartInstance = partInstance + if (partInstance._id === previousPartInstanceId) previousPartInstance = partInstance + // we've found what we were looking for, we can stop + if ( + (currentPartInstance || currentPartInstanceId === undefined) && + (previousPartInstance || previousPartInstanceId === undefined) + ) + break + } + + return { + currentPartInstance, + previousPartInstance, + } +} diff --git a/meteor/client/ui/SegmentContainer/withResolvedSegment.ts b/meteor/client/ui/SegmentContainer/withResolvedSegment.ts index 73783f38c9..54a59bab8f 100644 --- a/meteor/client/ui/SegmentContainer/withResolvedSegment.ts +++ b/meteor/client/ui/SegmentContainer/withResolvedSegment.ts @@ -38,6 +38,7 @@ import { RundownPlaylistCollectionUtil } from '../../../lib/collections/rundownP import { CalculateTimingsPiece } from '@sofie-automation/corelib/dist/playout/timings' import { ReadonlyDeep } from 'type-fest' import { PieceContentStatusObj } from '../../../lib/mediaObjects' +import { SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' export interface SegmentUi extends SegmentExtended { /** Output layers available in the installation used by this segment */ @@ -112,6 +113,7 @@ export interface ITrackedProps { hasRemoteItems: boolean hasGuestItems: boolean hasAlreadyPlayed: boolean + isScratchpad: boolean lastValidPartIndex: number | undefined budgetDuration: number | undefined displayLiveLineCounter: boolean @@ -143,6 +145,7 @@ export function withResolvedSegment( budgetDuration: undefined, displayLiveLineCounter: true, showCountdownToSegment: true, + isScratchpad: false, } } @@ -294,6 +297,8 @@ export function withResolvedSegment( ) } + const isScratchpad = segment.orphaned === SegmentOrphanedReason.SCRATCHPAD + return { segmentui: o.segmentExtended, parts: o.parts, @@ -306,6 +311,7 @@ export function withResolvedSegment( budgetDuration, displayLiveLineCounter, showCountdownToSegment, + isScratchpad, } }, (data: ITrackedProps, props: IProps, nextProps: IProps): boolean => { diff --git a/meteor/client/ui/SegmentScratchpad/SegmentScratchpad.scss b/meteor/client/ui/SegmentScratchpad/SegmentScratchpad.scss new file mode 100644 index 0000000000..fb336d1fe2 --- /dev/null +++ b/meteor/client/ui/SegmentScratchpad/SegmentScratchpad.scss @@ -0,0 +1,9 @@ +$segment-scratchpad-header-color: #fff; + +.segment-timeline.segment-scratchpad { + .segment-timeline__title { + background: $segment-scratchpad-header-color; + color: #000; + text-shadow: none; + } +} \ No newline at end of file diff --git a/meteor/client/ui/SegmentScratchpad/SegmentScratchpad.tsx b/meteor/client/ui/SegmentScratchpad/SegmentScratchpad.tsx new file mode 100644 index 0000000000..8d418f953f --- /dev/null +++ b/meteor/client/ui/SegmentScratchpad/SegmentScratchpad.tsx @@ -0,0 +1,560 @@ +import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' +import { NoteSeverity } from '@sofie-automation/blueprints-integration' +import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists' +import { IContextMenuContext } from '../RundownView' +import { IOutputLayerUi, PartUi, PieceUi, SegmentNoteCounts, SegmentUi } from '../SegmentContainer/withResolvedSegment' +import { ContextMenuTrigger } from '@jstarpl/react-contextmenu' +import { CriticalIconSmall, WarningIconSmall } from '../../lib/ui/icons/notifications' +import { contextMenuHoldToDisplayTime, useCombinedRefs } from '../../lib/lib' +import { isPartPlayable } from '../../../lib/collections/Parts' +import { useTranslation } from 'react-i18next' +import { literal, unprotectString } from '../../../lib/lib' +import { lockPointer, unlockPointer } from '../../lib/viewPort' +import { StoryboardPart } from '../SegmentStoryboard/StoryboardPart' +import classNames from 'classnames' +import RundownViewEventBus, { + GoToPartEvent, + GoToPartInstanceEvent, + RundownViewEvents, +} from '../../../lib/api/triggers/RundownViewEventBus' +import { getElementWidth } from '../../utils/dimensions' +import { HOVER_TIMEOUT } from '../Shelf/DashboardPieceButton' +import { Meteor } from 'meteor/meteor' +import { hidePointerLockCursor, showPointerLockCursor } from '../../lib/PointerLockCursor' +import { OptionalVelocityComponent } from '../../lib/utilComponents' +import { filterSecondarySourceLayers } from '../SegmentStoryboard/StoryboardPartSecondaryPieces/StoryboardPartSecondaryPieces' +import { UIStudio } from '../../../lib/api/studios' +import { PartId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { CalculateTimingsPiece } from '@sofie-automation/corelib/dist/playout/timings' + +interface IProps { + id: string + key: string + segment: SegmentUi + playlist: RundownPlaylist + studio: UIStudio + parts: Array + pieces: Map + segmentNoteCounts: SegmentNoteCounts + hasAlreadyPlayed: boolean + hasGuestItems: boolean + hasRemoteItems: boolean + isLiveSegment: boolean + isNextSegment: boolean + isQueuedSegment: boolean + followLiveLine: boolean + liveLineHistorySize: number + displayLiveLineCounter: boolean + currentPartWillAutoNext: boolean + onScroll: (scrollLeft: number, event: any) => void + onFollowLiveLine?: (state: boolean, event: any) => void + onShowEntireSegment?: (event: any) => void + onContextMenu?: (contextMenuContext: IContextMenuContext) => void + onItemClick?: (piece: PieceUi, e: React.MouseEvent) => void + onItemDoubleClick?: (item: PieceUi, e: React.MouseEvent) => void + onHeaderNoteClick?: (segmentId: SegmentId, level: NoteSeverity) => void + isLastSegment: boolean + lastValidPartIndex: number | undefined + budgetDuration?: number + showCountdownToSegment: boolean + fixedSegmentDuration: boolean | undefined + subscriptionsReady: boolean +} + +const PART_WIDTH = 166 // Must match SCSS: $segment-storyboard-part-width +const PART_LIST_LEAD_IN = 0 // Must match SCSS: .segment-storyboard__part-list(padding-left) +const PART_SHADE_WIDTH = 100 + +export const SegmentScratchpad = React.memo( + React.forwardRef(function SegmentScratchpad(props: IProps, ref) { + const innerRef = useRef(null) + const combinedRef = useCombinedRefs(null, ref, innerRef) + const listRef = useRef(null) + const [listWidth, setListWidth] = useState(0) + const [scrollLeft, setScrollLeft] = useState(0) + const [grabbed, setGrabbed] = useState<{ clientX: number; clientY: number } | null>(null) + const [touched, setTouched] = useState<{ clientX: number; clientY: number } | null>(null) + const [animateScrollLeft, setAnimateScrollLeft] = useState(true) + const { t } = useTranslation() + const [squishedHover, setSquishedHover] = useState(null) + const squishedHoverTimeout = useRef(null) + + const criticalNotes = props.segmentNoteCounts.criticial + const warningNotes = props.segmentNoteCounts.warning + + const getSegmentContext = (_props) => { + const ctx = literal({ + segment: props.segment, + part: props.parts.find((p) => isPartPlayable(p.instance.part)) || null, + }) + + if (props.onContextMenu && typeof props.onContextMenu === 'function') { + props.onContextMenu(ctx) + } + + return ctx + } + + let nextPartIndex = -1 + let currentPartIndex = -1 + + const parts: JSX.Element[] = [] + const squishedParts: JSX.Element[] = [] + + const renderedParts = props.parts.filter((part) => !(part.instance.part.invalid && part.instance.part.gap)) + + const lastValidPartId = props.parts[props.lastValidPartIndex ?? props.parts.length - 1]?.instance._id + + const maxScrollLeft = PART_WIDTH * (renderedParts.length - 1) - PART_LIST_LEAD_IN / 2 + + let fittingParts = 1 + let spaceLeft = 0 + let modifier = 2 + let squishedPartsNum = renderedParts.length + while (fittingParts < renderedParts.length && spaceLeft < PART_WIDTH * 1.25 && squishedPartsNum > 1) { + fittingParts = Math.ceil((listWidth + scrollLeft - PART_LIST_LEAD_IN) / PART_WIDTH) - modifier + spaceLeft = listWidth + scrollLeft - PART_LIST_LEAD_IN - fittingParts * PART_WIDTH + squishedPartsNum = Math.max(0, renderedParts.length - fittingParts) + modifier++ + + if (fittingParts <= 1) { + // we must at least fit a single part in the Segment, we'll just overflow beyond that point + fittingParts = 1 + spaceLeft = listWidth - PART_LIST_LEAD_IN - fittingParts * PART_WIDTH + squishedPartsNum = Math.max(0, renderedParts.length - fittingParts) + break + } + } + + const squishedPartCardStride = + squishedPartsNum > 1 ? Math.max(4, (spaceLeft - PART_WIDTH) / (squishedPartsNum - 1)) : null + + const playlistHasNextPart = !!props.playlist.nextPartInfo + const playlistIsLooping = props.playlist.loop + + renderedParts.forEach((part, index) => { + const isLivePart = part.instance._id === props.playlist.currentPartInfo?.partInstanceId + const isNextPart = part.instance._id === props.playlist.nextPartInfo?.partInstanceId + + if (isLivePart) currentPartIndex = index + if (isNextPart) nextPartIndex = index + + if (part.instance.part.invalid && part.instance.part.gap) return null + + const needsToBeSquished = index > fittingParts - 1 + const partComponent = ( + 0 + ? 'background' + : undefined + : squishedHover === index + ? 'hover' + : undefined + } + style={ + needsToBeSquished && squishedPartCardStride + ? { + transform: `translate(${(index - fittingParts) * squishedPartCardStride}px, 0)`, + zIndex: + squishedHover !== null + ? index <= squishedHover + ? renderedParts.length + index + : renderedParts.length - index + : undefined, + } + : undefined + } + onHoverOver={() => needsToBeSquished && setSquishedHover(index)} + /> + ) + + if (needsToBeSquished) { + squishedParts.unshift(partComponent) + } else { + parts.push(partComponent) + } + }) + + useEffect(() => { + if (!props.followLiveLine) return + + if (nextPartIndex >= 0 && currentPartIndex >= 0) { + setScrollLeft(Math.max(0, currentPartIndex * PART_WIDTH - props.liveLineHistorySize)) + } else if (nextPartIndex >= 0) { + setScrollLeft(Math.max(0, nextPartIndex * PART_WIDTH - props.liveLineHistorySize)) + } else if (currentPartIndex >= 0) { + setScrollLeft(Math.max(0, currentPartIndex * PART_WIDTH - props.liveLineHistorySize)) + } + }, [currentPartIndex, nextPartIndex, props.followLiveLine]) + + const onRewindSegment = useCallback(() => { + if (!props.isLiveSegment) { + setScrollLeft(0) + } + }, []) + + const onGoToPart = useCallback( + (e: GoToPartEvent) => { + const idx = renderedParts.findIndex((partInstance) => e.partId === partInstance.partId) + if (idx >= 0) { + setScrollLeft(idx * PART_WIDTH - props.liveLineHistorySize) + } + }, + [renderedParts, props.followLiveLine] + ) + + const onGoToPartInstance = useCallback( + (e: GoToPartInstanceEvent) => { + const idx = renderedParts.findIndex((partInstance) => partInstance.instance._id === e.partInstanceId) + if (idx >= 0) { + setScrollLeft(idx * PART_WIDTH - props.liveLineHistorySize) + } + }, + [renderedParts, props.followLiveLine] + ) + + useEffect(() => { + RundownViewEventBus.on(RundownViewEvents.REWIND_SEGMENTS, onRewindSegment) + RundownViewEventBus.on(RundownViewEvents.GO_TO_PART, onGoToPart) + RundownViewEventBus.on(RundownViewEvents.GO_TO_PART_INSTANCE, onGoToPartInstance) + + return () => { + RundownViewEventBus.off(RundownViewEvents.REWIND_SEGMENTS, onRewindSegment) + RundownViewEventBus.off(RundownViewEvents.GO_TO_PART, onGoToPart) + RundownViewEventBus.off(RundownViewEvents.GO_TO_PART_INSTANCE, onGoToPartInstance) + } + }, [onRewindSegment, onGoToPart, onGoToPartInstance]) + + useLayoutEffect(() => { + if (!listRef.current) return + + const width = getElementWidth(listRef.current) + if (width >= 0) { + setListWidth(width) + } + + const resizeObserver = new ResizeObserver((e) => { + if (e[0].target === listRef.current) { + const width = Math.floor(e[0].contentRect.width || 0) + setListWidth(width) + } + }) + + resizeObserver.observe(listRef.current) + + return () => { + resizeObserver.disconnect() + } + }, [listRef.current]) + + const onSquishedPointerEnter = (e: React.PointerEvent) => { + if (e.pointerType === 'mouse') { + squishedHoverTimeout.current = Meteor.setTimeout(() => setSquishedHover(null), HOVER_TIMEOUT) + } + } + + const clearSquishedHoverTimeout = () => { + if (squishedHoverTimeout.current) { + Meteor.clearTimeout(squishedHoverTimeout.current) + squishedHoverTimeout.current = null + } + } + + const onSquishedPointerLeave = (e: React.PointerEvent) => { + if (e.pointerType === 'mouse') { + clearSquishedHoverTimeout() + setSquishedHover(null) + } + } + + const onSquishedPointerMove = (e: React.PointerEvent) => { + if (e.pointerType === 'mouse') { + clearSquishedHoverTimeout() + squishedHoverTimeout.current = Meteor.setTimeout(() => setSquishedHover(null), HOVER_TIMEOUT) + } + } + + useEffect(() => { + return () => clearSquishedHoverTimeout() + }, []) + + const onListPointerDown = (e: React.PointerEvent) => { + if (e.pointerType === 'mouse' && e.buttons === 1) { + setGrabbed({ + clientX: e.clientX, + clientY: e.clientY, + }) + setAnimateScrollLeft(false) + } else if ((e.pointerType === 'touch' && e.isPrimary) || (e.pointerType === 'pen' && e.isPrimary)) { + setTouched({ + clientX: e.clientX, + clientY: e.clientY, + }) + setAnimateScrollLeft(false) + } + } + + const onSegmentWheel = (e: WheelEvent) => { + let scrollDelta = 0 + if ( + (!e.ctrlKey && e.altKey && !e.metaKey && !e.shiftKey) || + (e.ctrlKey && !e.metaKey && !e.shiftKey && e.altKey) + ) { + // this.props.onScroll(Math.max(0, this.props.scrollLeft + e.deltaY / this.props.timeScale), e) + scrollDelta = e.deltaY * -1 + e.preventDefault() + } else if (!e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey) { + // no modifier + if (e.deltaX !== 0) { + // this.props.onScroll(Math.max(0, this.props.scrollLeft + e.deltaX / this.props.timeScale), e) + scrollDelta = e.deltaX * -1 + e.preventDefault() + } + } + + if (scrollDelta !== 0) { + setScrollLeft((value) => { + const newScrollLeft = Math.max(0, Math.min(value - scrollDelta, maxScrollLeft)) + props.onScroll(newScrollLeft, e) + return newScrollLeft + }) + } + } + + useEffect(() => { + if (!grabbed) return + + const onListPointerRelease = () => { + setGrabbed(null) + setAnimateScrollLeft(true) + } + const onPointerLockChange = () => { + if (!document.pointerLockElement) { + setGrabbed(null) + setAnimateScrollLeft(true) + } + } + const onPointerMove = (e: PointerEvent) => { + setScrollLeft((value) => { + const newScrollLeft = Math.max(0, Math.min(value - e.movementX, maxScrollLeft)) + props.onScroll(newScrollLeft, e) + return newScrollLeft + }) + } + + document.addEventListener('pointerup', onListPointerRelease) + document.addEventListener('pointercancel', onListPointerRelease) + document.addEventListener('pointerlockchange', onPointerLockChange) + document.addEventListener('pointerlockerror', onListPointerRelease) + document.addEventListener('pointermove', onPointerMove) + lockPointer() + showPointerLockCursor(grabbed.clientX, grabbed.clientY) + + return () => { + unlockPointer() + hidePointerLockCursor() + document.removeEventListener('pointerup', onListPointerRelease) + document.removeEventListener('pointercancel', onListPointerRelease) + document.removeEventListener('pointerlockchange', onPointerLockChange) + document.removeEventListener('pointerlockerror', onListPointerRelease) + document.removeEventListener('pointermove', onPointerMove) + } + }, [grabbed, renderedParts.length, props.onScroll]) + + useEffect(() => { + if (!touched) return + + const startingScrollLeft = scrollLeft + + const onListTouchRelease = () => { + setTouched(null) + setAnimateScrollLeft(true) + } + const onTouchMove = (e: TouchEvent) => { + e.preventDefault() + setScrollLeft(() => { + const newScrollLeft = Math.max( + 0, + Math.min(startingScrollLeft + (touched.clientX - e.touches[0].clientX), maxScrollLeft) + ) + props.onScroll(newScrollLeft, e) + return newScrollLeft + }) + } + + document.addEventListener('touchend', onListTouchRelease) + document.addEventListener('touchcancel', onListTouchRelease) + document.addEventListener('touchmove', onTouchMove, { + passive: false, + }) + + return () => { + document.removeEventListener('touchend', onListTouchRelease) + document.removeEventListener('touchcancel', onListTouchRelease) + document.removeEventListener('touchmove', onTouchMove) + } + }, [touched, renderedParts.length, props.onScroll]) + + useLayoutEffect(() => { + const segment = innerRef.current + if (!segment) return + + segment.addEventListener('wheel', onSegmentWheel, { + passive: false, + }) + + return () => { + segment.removeEventListener('wheel', onSegmentWheel) + } + }, [innerRef.current]) + + return ( +
    0, + // 'invert-flash': highlight, + // 'time-of-day-countdowns': useTimeOfDayCountdowns, + })} + data-obj-id={props.segment._id} + ref={combinedRef} + role="region" + aria-labelledby={`segment-name-${props.segment._id}`} + aria-roledescription={t('segment')} + > + +

    + {t('Scratchpad')} +

    + {(criticalNotes > 0 || warningNotes > 0) && ( +
    + {criticalNotes > 0 && ( +
    + props.onHeaderNoteClick && props.onHeaderNoteClick(props.segment._id, NoteSeverity.ERROR) + } + aria-label={t('Critical problems')} + > + +
    {criticalNotes}
    +
    + )} + {warningNotes > 0 && ( +
    + props.onHeaderNoteClick && props.onHeaderNoteClick(props.segment._id, NoteSeverity.WARNING) + } + aria-label={t('Warnings')} + > + +
    {warningNotes}
    +
    + )} +
    + )} +
    +
    + {Object.values(props.segment.outputLayers) + .filter((outputGroup) => outputGroup.used) + .map((outputGroup) => ( +
    + {filterSecondarySourceLayers(outputGroup.sourceLayers).map((sourceLayer) => + sourceLayer.pieces.length > 0 ? ( +
    + {sourceLayer.name} +
    + ) : null + )} +
    + ))} +
    +
    + +
    + {parts} +
    + {squishedParts} +
    +
    +
    +
    +
    +
    + ) + }) +) + +SegmentScratchpad.displayName = 'SegmentStoryboard' diff --git a/meteor/client/ui/SegmentScratchpad/SegmentScratchpadContainer.tsx b/meteor/client/ui/SegmentScratchpad/SegmentScratchpadContainer.tsx new file mode 100644 index 0000000000..f87101e409 --- /dev/null +++ b/meteor/client/ui/SegmentScratchpad/SegmentScratchpadContainer.tsx @@ -0,0 +1,265 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react' +import { PieceLifespan } from '@sofie-automation/blueprints-integration' +import { meteorSubscribe, PubSub } from '../../../lib/api/pubsub' +import { useSubscription, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' +import { + // PartUi, + withResolvedSegment, + IProps as IResolvedSegmentProps, + ITrackedProps as ITrackedResolvedSegmentProps, +} from '../SegmentContainer/withResolvedSegment' +import { SpeechSynthesiser } from '../../lib/speechSynthesis' +import { SegmentScratchpad } from './SegmentScratchpad' +import { unprotectString } from '../../../lib/lib' +import { LIVELINE_HISTORY_SIZE as TIMELINE_LIVELINE_HISTORY_SIZE } from '../SegmentTimeline/SegmentTimelineContainer' +import { PartInstances, Parts, Segments } from '../../collections' +import { literal } from '@sofie-automation/shared-lib/dist/lib/lib' +import { MongoFieldSpecifierOnes } from '@sofie-automation/corelib/dist/mongo' +import { PartInstance } from '../../../lib/collections/PartInstances' + +export const LIVELINE_HISTORY_SIZE = TIMELINE_LIVELINE_HISTORY_SIZE + +interface IProps extends IResolvedSegmentProps { + id: string +} + +export const SegmentScratchpadContainer = withResolvedSegment(function SegmentScratchpadContainer({ + rundownId, + rundownIdsBefore, + segmentId, + segmentsIdsBefore, + ...props +}: IProps & ITrackedResolvedSegmentProps) { + const partIds = useTracker( + () => + Parts.find( + { + segmentId, + }, + { + fields: { + _id: 1, + }, + } + ).map((part) => part._id), + [segmentId] + ) + + const piecesReady = useSubscription(PubSub.pieces, { + startRundownId: rundownId, + startPartId: { + $in: partIds, + }, + }) + + const partInstanceIds = useTracker( + () => + PartInstances.find( + { + segmentId: segmentId, + reset: { + $ne: true, + }, + }, + { + fields: { + _id: 1, + part: 1, + }, + } + ).map((instance) => instance._id), + [segmentId] + ) + + const pieceInstancesReady = useSubscription(PubSub.pieceInstances, { + rundownId: rundownId, + partInstanceId: { + $in: partInstanceIds, + }, + reset: { + $ne: true, + }, + }) + + useTracker(() => { + const segment = Segments.findOne(segmentId, { + fields: { + rundownId: 1, + _rank: 1, + }, + }) + segment && + meteorSubscribe(PubSub.pieces, { + invalid: { + $ne: true, + }, + $or: [ + // same rundown, and previous segment + { + startRundownId: rundownId, + startSegmentId: { $in: Array.from(segmentsIdsBefore.values()) }, + lifespan: { + $in: [PieceLifespan.OutOnRundownEnd, PieceLifespan.OutOnRundownChange, PieceLifespan.OutOnShowStyleEnd], + }, + }, + // Previous rundown + { + startRundownId: { $in: Array.from(rundownIdsBefore.values()) }, + lifespan: { + $in: [PieceLifespan.OutOnShowStyleEnd], + }, + }, + ], + }) + }, [segmentId, rundownId, segmentsIdsBefore.values(), rundownIdsBefore.values()]) + + const isLiveSegment = useTracker( + () => { + if (!props.playlist.currentPartInfo || !props.playlist.activationId) { + return false + } + + const currentPartInstance = PartInstances.findOne(props.playlist.currentPartInfo.partInstanceId) + if (!currentPartInstance) { + return false + } + + return currentPartInstance.segmentId === segmentId + }, + [segmentId, props.playlist.activationId, props.playlist.currentPartInfo?.partInstanceId], + false + ) + + const isNextSegment = useTracker( + () => { + if (!props.playlist.nextPartInfo || !props.playlist.activationId) { + return false + } + + const partInstance = PartInstances.findOne(props.playlist.nextPartInfo.partInstanceId, { + fields: literal>({ + segmentId: 1, + //@ts-expect-error typescript doesnt like it + 'part._id': 1, + }), + }) + if (!partInstance) { + return false + } + + return partInstance.segmentId === segmentId + }, + [segmentId, props.playlist.activationId, props.playlist.nextPartInfo?.partInstanceId], + false + ) + + const currentPartWillAutoNext = useTracker( + () => { + if (!props.playlist.currentPartInfo || !props.playlist.activationId) { + return false + } + + const currentPartInstance = PartInstances.findOne(props.playlist.currentPartInfo.partInstanceId, { + fields: { + //@ts-expect-error deep property + 'part.autoNext': 1, + 'part.expectedDuration': 1, + }, + }) + if (!currentPartInstance) { + return false + } + + return !!(currentPartInstance.part.autoNext && currentPartInstance.part.expectedDuration) + }, + [segmentId, props.playlist.activationId, props.playlist.currentPartInfo?.partInstanceId], + false + ) + + useEffect(() => { + SpeechSynthesiser.init() + }, []) + + const segmentRef = useRef(null) + + const subscriptionsReady = piecesReady && pieceInstancesReady + + // We are only interested in when subscriptionsReady turns *true* the first time. It can turn false later + // and then back to true (when re-subscribing, say when you re-next a Part), but we're not interested in those + // cases and it's a "false" signal for us. + const [initialSubscriptionsReady, setInitialSubscriptionsReady] = useState(subscriptionsReady) + + // The following set up is supposed to avoid a flash of "empty" parts while Pieces and PieceInstances are streaming in. + // One could think that it would be enough to wait for "subscriptionsReady" to turn to *true*, but one would be wrong. + // There _could_ be a short amount of time when the subscriptions are ready, but the trackers haven't + // finished autorunning, and then `withResolvedSegment` needs some time to re-resolve the entire Segment. + // In that window of time it is possible that `withResolvedSegment` will actually return a Segment with just + // rundown-spanning infinites, therefore we need to filter them out as well. + // Ultimately, it is possible that the `firstNonInvalidPart` is in fact empty, so we need to set up a Timeout + // so that we show the Part as-is. + const firstNonInvalidPart = props.parts.find((part) => !part.instance.part.invalid) + useEffect(() => { + if (subscriptionsReady === true) { + if ( + firstNonInvalidPart?.pieces !== undefined && + firstNonInvalidPart?.pieces.filter((piece) => !piece.hasOriginInPreceedingPart).length > 0 + ) { + setInitialSubscriptionsReady(subscriptionsReady) + } else { + // we can't wait for the pieces to appear forever + const timeout = setTimeout(() => { + setInitialSubscriptionsReady(subscriptionsReady) + }, 1000) + + return () => { + clearTimeout(timeout) + } + } + } + }, [subscriptionsReady, firstNonInvalidPart?.pieces.length]) + + const onScroll = useCallback(() => { + if (isLiveSegment) { + if (props.onSegmentScroll) props.onSegmentScroll() + } + }, [props.onSegmentScroll, isLiveSegment]) + + if (props.segmentui === undefined || props.segmentui.isHidden) { + return null + } + + return ( + + ) +}) diff --git a/meteor/client/ui/SegmentStoryboard/SegmentStoryboardContainer.tsx b/meteor/client/ui/SegmentStoryboard/SegmentStoryboardContainer.tsx index 2583062788..e0a6fe9b03 100644 --- a/meteor/client/ui/SegmentStoryboard/SegmentStoryboardContainer.tsx +++ b/meteor/client/ui/SegmentStoryboard/SegmentStoryboardContainer.tsx @@ -252,8 +252,6 @@ export const SegmentStoryboardContainer = withResolvedSegment(function S liveLineHistorySize={LIVELINE_HISTORY_SIZE} displayLiveLineCounter={props.displayLiveLineCounter} onContextMenu={props.onContextMenu} - // onFollowLiveLine={this.onFollowLiveLine} - // onShowEntireSegment={this.onShowEntireSegment} onScroll={onScroll} isLastSegment={props.isLastSegment} lastValidPartIndex={props.lastValidPartIndex} diff --git a/packages/corelib/src/playout/playlist.ts b/packages/corelib/src/playout/playlist.ts index 3b0241a110..bff80f5dfe 100644 --- a/packages/corelib/src/playout/playlist.ts +++ b/packages/corelib/src/playout/playlist.ts @@ -1,5 +1,6 @@ import { DBSegment } from '../dataModel/Segment' import { DBPart } from '../dataModel/Part' +import { DBPartInstance } from '../dataModel/PartInstance' import { RundownId, SegmentId } from '../dataModel/Ids' import { ReadonlyDeep } from 'type-fest' @@ -47,6 +48,29 @@ export function sortPartsInSortedSegments

    & { + part: Pick +} +export function sortPartInstancesInSortedSegments

    ( + partInstances: P[], + sortedSegments: Array> +): P[] { + const segmentRanks = new Map() + for (let i = 0; i < sortedSegments.length; i++) { + segmentRanks.set(sortedSegments[i]._id, i) + } + + return partInstances.sort((a, b) => { + if (a.segmentId === b.segmentId) { + return a.part._rank - b.part._rank + } else { + const segA = segmentRanks.get(a.segmentId) ?? Number.POSITIVE_INFINITY + const segB = segmentRanks.get(b.segmentId) ?? Number.POSITIVE_INFINITY + return segA - segB + } + }) +} + /** * Sort an array of RundownIds based on a reference list * @param sortedPossibleIds The already sorted ids. This may be missing some of the unsorted ones From 7362b5216d89b8ed6061448ac73b30393c8caa57 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Tue, 11 Jul 2023 16:39:58 +0200 Subject: [PATCH 051/479] feat: have withResolvedSegment return Parts in correct order --- meteor/client/lib/rundown.ts | 1 - .../RundownTiming/RundownTimingProvider.tsx | 17 ++++++------ .../SegmentContainer/withResolvedSegment.ts | 2 +- meteor/lib/Rundown.ts | 26 +++++++++---------- packages/corelib/src/playout/playlist.ts | 4 +-- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/meteor/client/lib/rundown.ts b/meteor/client/lib/rundown.ts index ebdf0636a8..9b399b7400 100644 --- a/meteor/client/lib/rundown.ts +++ b/meteor/client/lib/rundown.ts @@ -363,7 +363,6 @@ export namespace RundownUtils { fields: { isTaken: 0, previousPartEndState: 0, - takeCount: 0, }, } )[0] as { segment: Segment; partInstances: PartInstanceLimited[] } | undefined diff --git a/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx b/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx index 56222134fe..d519b301c4 100644 --- a/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx +++ b/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx @@ -55,7 +55,7 @@ interface IRundownTimingProviderTrackedProps { type MinimalPartInstance = Pick< PartInstance, - '_id' | 'rundownId' | 'segmentId' | 'segmentPlayoutId' | 'takeCount' | 'part' + '_id' | 'rundownId' | 'segmentId' | 'segmentPlayoutId' | 'takeCount' | 'part' | 'timings' | 'orphaned' > /** @@ -82,9 +82,6 @@ export const RundownTimingProvider = withTracker< } } - // TODO: Remove - console.time('rundownTiming') - const partInstancesMap = new Map() const segmentEntryPartInstances: MinimalPartInstance[] = [] @@ -100,8 +97,15 @@ export const RundownTimingProvider = withTracker< segmentPlayoutId: 1, takeCount: 1, part: 1, + timings: 1, + orphaned: 1, }, - }) as Array> + }) as Array< + Pick< + PartInstance, + '_id' | 'rundownId' | 'segmentId' | 'segmentPlayoutId' | 'takeCount' | 'part' | 'timings' | 'orphaned' + > + > const { currentPartInstance, previousPartInstance } = findCurrentAndPreviousPartInstance( activePartInstances, @@ -158,9 +162,6 @@ export const RundownTimingProvider = withTracker< const pieces = RundownPlaylistCollectionUtil.getPiecesForParts(Array.from(allPartIds.values())) - // TODO: Remove - console.timeEnd('rundownTiming') - return { rundowns, currentRundown, diff --git a/meteor/client/ui/SegmentContainer/withResolvedSegment.ts b/meteor/client/ui/SegmentContainer/withResolvedSegment.ts index 54a59bab8f..a83a62fa56 100644 --- a/meteor/client/ui/SegmentContainer/withResolvedSegment.ts +++ b/meteor/client/ui/SegmentContainer/withResolvedSegment.ts @@ -185,7 +185,7 @@ export function withResolvedSegment( } ).parts as Pick[] ).map((part) => part._id), - 'playlist.getAllOrderedParts', + 'playlist.getSegmentsAndPartsSync', props.playlist._id ), memoizedIsolatedAutorun( diff --git a/meteor/lib/Rundown.ts b/meteor/lib/Rundown.ts index dcae64c6b9..f87fc6831d 100644 --- a/meteor/lib/Rundown.ts +++ b/meteor/lib/Rundown.ts @@ -1,4 +1,3 @@ -import * as _ from 'underscore' import { Piece } from './collections/Pieces' import { IOutputLayer, ISourceLayer, ITranslatableMessage } from '@sofie-automation/blueprints-integration' import { DBSegment, Segment, SegmentOrphanedReason } from './collections/Segments' @@ -42,7 +41,7 @@ export interface SegmentExtended extends DBSegment { } } -export type PartInstanceLimited = Omit +export type PartInstanceLimited = Omit export interface PartExtended { partId: PartId @@ -306,21 +305,22 @@ export function getSegmentsWithPartInstances( } else if (segmentParts.length === 0) { return { segment, - partInstances: _.sortBy(segmentPartInstances, (p) => p.part._rank), + partInstances: segmentPartInstances.sort( + (a, b) => a.part._rank - b.part._rank || a.takeCount - b.takeCount + ), } } else { - const partInstanceMap = new Map() - for (const part of segmentParts) - partInstanceMap.set(part._id, wrapPartToTemporaryInstance(playlistActivationId, part)) + const partIds: Set = new Set() for (const partInstance of segmentPartInstances) { - // Check what we already have in the map for this PartId. If the map returns the currentPartInstance then we keep that, otherwise replace with this partInstance - const currentValue = partInstanceMap.get(partInstance.part._id) - if (!currentValue || currentValue._id !== playlist.currentPartInfo?.partInstanceId) { - partInstanceMap.set(partInstance.part._id, partInstance) - } + partIds.add(partInstance.part._id) } - - const allPartInstances = _.sortBy(Array.from(partInstanceMap.values()), (p) => p.part._rank) + for (const part of segmentParts) { + if (partIds.has(part._id)) continue + segmentPartInstances.push(wrapPartToTemporaryInstance(playlistActivationId, part)) + } + const allPartInstances = segmentPartInstances.sort( + (a, b) => a.part._rank - b.part._rank || a.takeCount - b.takeCount + ) return { segment, diff --git a/packages/corelib/src/playout/playlist.ts b/packages/corelib/src/playout/playlist.ts index bff80f5dfe..dd53fa50f6 100644 --- a/packages/corelib/src/playout/playlist.ts +++ b/packages/corelib/src/playout/playlist.ts @@ -48,7 +48,7 @@ export function sortPartsInSortedSegments

    & { +type SortableDBPartInstance = Pick & { part: Pick } export function sortPartInstancesInSortedSegments

    ( @@ -62,7 +62,7 @@ export function sortPartInstancesInSortedSegments

    { if (a.segmentId === b.segmentId) { - return a.part._rank - b.part._rank + return a.part._rank - b.part._rank || a.takeCount - b.takeCount } else { const segA = segmentRanks.get(a.segmentId) ?? Number.POSITIVE_INFINITY const segB = segmentRanks.get(b.segmentId) ?? Number.POSITIVE_INFINITY From 91c07c8410039c0527f3b09f11a5b9e64cc7737f Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Fri, 14 Jul 2023 22:45:00 +0200 Subject: [PATCH 052/479] feat: have both part and partInstance timings in rundownTiming context --- meteor/client/lib/rundownTiming.ts | 72 ++++++++++++------- .../client/ui/ClockView/CameraScreen/Part.tsx | 5 +- .../RundownTiming/PartDuration.tsx | 8 +-- .../RundownTiming/RundownTimingProvider.tsx | 13 +++- .../RundownTiming/SegmentDuration.tsx | 3 +- .../ui/SegmentList/LinePartTimeline.tsx | 3 +- .../client/ui/SegmentList/OvertimeShadow.tsx | 10 ++- .../ui/SegmentList/utils/LiveLineIsPast.tsx | 10 ++- .../Parts/FlattenedSourceLayers.tsx | 2 +- .../ui/SegmentTimeline/Parts/OutputGroup.tsx | 6 +- .../Parts/SegmentTimelinePart.tsx | 59 +++++++-------- .../ui/SegmentTimeline/Parts/SourceLayer.tsx | 4 +- .../Renderers/CustomLayerItemRenderer.tsx | 2 +- .../Renderers/VTSourceRenderer.tsx | 2 +- .../ui/SegmentTimeline/SegmentContextMenu.tsx | 2 +- .../ui/SegmentTimeline/SegmentTimeline.tsx | 19 ++--- .../SegmentTimelineContainer.tsx | 23 +++--- .../SegmentTimelineSmallPartFlag.tsx | 14 ++-- .../ui/SegmentTimeline/SourceLayerItem.tsx | 2 +- 19 files changed, 144 insertions(+), 115 deletions(-) diff --git a/meteor/client/lib/rundownTiming.ts b/meteor/client/lib/rundownTiming.ts index b7f954860b..f8562a2299 100644 --- a/meteor/client/lib/rundownTiming.ts +++ b/meteor/client/lib/rundownTiming.ts @@ -25,6 +25,7 @@ import { getCurrentTime, objectFromEntries } from '../../lib/lib' import { Settings } from '../../lib/Settings' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' // Minimum duration that a part can be assigned. Used by gap parts to allow them to "compress" to indicate time running out. const MINIMAL_NONZERO_DURATION = 1 @@ -34,7 +35,12 @@ interface BreakProps { breakIsLastRundown: boolean } -type CalculateTimingsPartInstance = Pick +type CalculateTimingsPartInstance = Pick< + PartInstance, + '_id' | 'isTemporary' | 'segmentId' | 'orphaned' | 'timings' | 'part' +> + +export type TimingId = string /** * This is a class for calculating timings in a Rundown playlist used by RundownTimingProvider. @@ -52,13 +58,13 @@ export class RundownTimingCalculator { // Note, that these objects are created when an instance is created and are reused for the lifetime // of the component. This is to avoid GC running all the time on discarded objects. // Only the RundownTimingContext object is unique for a call to `updateDurations`. - private partDurations: Record = {} - private partExpectedDurations: Record = {} - private partPlayed: Record = {} - private partStartsAt: Record = {} - private partDisplayStartsAt: Record = {} - private partDisplayDurations: Record = {} - private partDisplayDurationsNoPlayback: Record = {} + private partDurations: Record = {} + private partExpectedDurations: Record = {} + private partPlayed: Record = {} + private partStartsAt: Record = {} + private partDisplayStartsAt: Record = {} + private partDisplayDurations: Record = {} + private partDisplayDurationsNoPlayback: Record = {} private displayDurationGroups: Record = {} private segmentBudgetDurations: Record = {} private segmentStartedPlayback: Record = {} @@ -169,7 +175,10 @@ export class RundownTimingCalculator { }) partInstances.forEach((partInstance, itIndex) => { - const piecesForPart = pieces.get(partInstance.part._id) ?? [] + const partId = partInstance.part._id + const partInstanceId = !partInstance.isTemporary ? partInstance._id : null + const partInstanceOrPartId = unprotectString(partInstanceId ?? partId) + const piecesForPart = pieces.get(partId) ?? [] if (partInstance.segmentId !== lastSegmentId) { this.untimedSegments.add(partInstance.segmentId) @@ -182,7 +191,7 @@ export class RundownTimingCalculator { } // add piece to accumulator - const aIndex = this.linearParts.push([partInstance.part._id, waitAccumulator]) - 1 + const aIndex = this.linearParts.push([partId, waitAccumulator]) - 1 // if this is next Part, clear previous countdowns and clear accumulator if (playlist.nextPartInfo?.partInstanceId === partInstance._id) { @@ -290,7 +299,7 @@ export class RundownTimingCalculator { : calculatePartInstanceExpectedDurationWithPreroll(partInstance, piecesForPart)) || defaultDuration partDisplayDuration = Math.max(partDisplayDurationNoPlayback, now - lastStartedPlayback) - this.partPlayed[unprotectString(partInstance.part._id)] = now - lastStartedPlayback + this.partPlayed[partInstanceOrPartId] = now - lastStartedPlayback const segmentStartedPlayback = this.segmentStartedPlayback[unprotectString(partInstance.segmentId)] || lastStartedPlayback @@ -330,8 +339,7 @@ export class RundownTimingCalculator { ) ) partDisplayDuration = partDisplayDurationNoPlayback - this.partPlayed[unprotectString(partInstance.part._id)] = - (partInstance.timings?.duration || 0) - playOffset + this.partPlayed[partInstanceOrPartId] = (partInstance.timings?.duration || 0) - playOffset } // asPlayed is the actual duration so far and expected durations in unplayed lines. @@ -406,7 +414,7 @@ export class RundownTimingCalculator { // Handle invalid parts by overriding the values to preset values for Invalid parts if (partInstance.part.invalid && !partInstance.part.gap) { partDisplayDuration = defaultDuration - this.partPlayed[unprotectString(partInstance.part._id)] = 0 + this.partPlayed[partInstanceOrPartId] = 0 } if ( @@ -421,15 +429,14 @@ export class RundownTimingCalculator { this.displayDurationGroups[partInstance.part.displayDurationGroup] - partDisplayDuration } - const partInstancePartId = unprotectString(partInstance.part._id) - this.partExpectedDurations[partInstancePartId] = partExpectedDuration - this.partStartsAt[partInstancePartId] = startsAtAccumulator - this.partDisplayStartsAt[partInstancePartId] = displayStartsAtAccumulator - this.partDurations[partInstancePartId] = partDuration - this.partDisplayDurations[partInstancePartId] = partDisplayDuration - this.partDisplayDurationsNoPlayback[partInstancePartId] = partDisplayDurationNoPlayback - startsAtAccumulator += this.partDurations[partInstancePartId] - displayStartsAtAccumulator += this.partDisplayDurations[partInstancePartId] + this.partExpectedDurations[partInstanceOrPartId] = partExpectedDuration + this.partStartsAt[partInstanceOrPartId] = startsAtAccumulator + this.partDisplayStartsAt[partInstanceOrPartId] = displayStartsAtAccumulator + this.partDurations[partInstanceOrPartId] = partDuration + this.partDisplayDurations[partInstanceOrPartId] = partDisplayDuration + this.partDisplayDurationsNoPlayback[partInstanceOrPartId] = partDisplayDurationNoPlayback + startsAtAccumulator += this.partDurations[partInstanceOrPartId] + displayStartsAtAccumulator += this.partDisplayDurations[partInstanceOrPartId] // waitAccumulator is used to calculate the countdowns for Parts relative to the current Part // always add the full duration, in case by some manual intervention this segment should play twice @@ -707,7 +714,7 @@ export interface RundownTimingContext { rundownExpectedDurations?: Record /** This is the complete duration of each rundown: as planned for the unplayed content, and as-run for the played-out, but ignoring unplayed/unplayable parts in order */ rundownAsPlayedDurations?: Record - /** this is the countdown to each of the parts relative to the current on air part. */ + /** this is the countdown to each of the parts relative to the current on air part. This allways uses PartId's as the index */ partCountdown?: Record /** The calculated durations of each of the Parts: as-planned/as-run depending on state. */ partDurations?: Record @@ -778,6 +785,23 @@ export function computeSegmentDuration( }, 0) } +export function getPartInstanceTimingId( + partInstance: Pick & { part: Pick } +): TimingId { + return !partInstance.isTemporary ? unprotectString(partInstance._id) : unprotectString(partInstance.part._id) +} + +export function getPartInstanceTimingValue( + values: Record | undefined, + partInstance: Pick & { part: Pick } +): number { + if (!values) return Number.NaN + if (partInstance.isTemporary) { + return values[unprotectString(partInstance.part._id)] ?? Number.NaN + } + return values[unprotectString(partInstance._id)] ?? values[unprotectString(partInstance.part._id)] ?? Number.NaN +} + export function getPlaylistTimingDiff( playlist: Pick, timingContext: RundownTimingContext diff --git a/meteor/client/ui/ClockView/CameraScreen/Part.tsx b/meteor/client/ui/ClockView/CameraScreen/Part.tsx index 0ba445dd66..39a2273d7d 100644 --- a/meteor/client/ui/ClockView/CameraScreen/Part.tsx +++ b/meteor/client/ui/ClockView/CameraScreen/Part.tsx @@ -5,6 +5,7 @@ import { AreaZoom } from '.' import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists' import { PieceExtended } from '../../../../lib/Rundown' import { getAllowSpeaking, getAllowVibrating } from '../../../lib/localStorage' +import { getPartInstanceTimingId } from '../../../lib/rundownTiming' import { AutoNextStatus } from '../../RundownView/RundownTiming/AutoNextStatus' import { CurrentPartRemaining } from '../../RundownView/RundownTiming/CurrentPartRemaining' import { PartCountdown } from '../../RundownView/RundownTiming/PartCountdown' @@ -29,8 +30,8 @@ export const Part = withTiming({ let left = timingDurations.partCountdown?.[unprotectString(part.partId)] ?? - 0 - (timingDurations.partPlayed?.[unprotectString(part.partId)] ?? 0) - let width: number | null = timingDurations.partDisplayDurations?.[unprotectString(part.partId)] ?? 0 + 0 - (timingDurations.partPlayed?.[getPartInstanceTimingId(part.instance)] ?? 0) + let width: number | null = timingDurations.partDisplayDurations?.[getPartInstanceTimingId(part.instance)] ?? 0 if (isLive) { left = 0 diff --git a/meteor/client/ui/RundownView/RundownTiming/PartDuration.tsx b/meteor/client/ui/RundownView/RundownTiming/PartDuration.tsx index f887d815dc..c5f93056d8 100644 --- a/meteor/client/ui/RundownView/RundownTiming/PartDuration.tsx +++ b/meteor/client/ui/RundownView/RundownTiming/PartDuration.tsx @@ -1,9 +1,9 @@ import ClassNames from 'classnames' import React, { ReactNode } from 'react' import { withTiming, WithTiming } from './withTiming' -import { unprotectString } from '../../../../lib/lib' import { RundownUtils } from '../../../lib/rundown' import { PartUi } from '../../SegmentTimeline/SegmentTimelineContainer' +import { getPartInstanceTimingId } from '../../../lib/rundownTiming' interface IPartDurationProps { part: PartUi @@ -22,7 +22,7 @@ interface IPartDurationProps { */ export const PartDisplayDuration = withTiming((props) => ({ filter: (context) => { - return context.partExpectedDurations && context.partExpectedDurations[unprotectString(props.part.partId)] + return context.partExpectedDurations && context.partExpectedDurations[getPartInstanceTimingId(props.part.instance)] }, }))(function PartDisplayDuration(props: WithTiming) { let duration: number | undefined = undefined @@ -36,8 +36,8 @@ export const PartDisplayDuration = withTiming((props) => budget = part.instance.orphaned || part.instance.part.untimed ? 0 - : partExpectedDurations[unprotectString(part.partId)] || 0 - playedOut = (!part.instance.part.untimed ? partPlayed[unprotectString(part.partId)] : 0) || 0 + : partExpectedDurations[getPartInstanceTimingId(part.instance)] || 0 + playedOut = (!part.instance.part.untimed ? partPlayed[getPartInstanceTimingId(part.instance)] : 0) || 0 } duration = budget - playedOut diff --git a/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx b/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx index d519b301c4..f51d0fab1b 100644 --- a/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx +++ b/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx @@ -55,7 +55,7 @@ interface IRundownTimingProviderTrackedProps { type MinimalPartInstance = Pick< PartInstance, - '_id' | 'rundownId' | 'segmentId' | 'segmentPlayoutId' | 'takeCount' | 'part' | 'timings' | 'orphaned' + '_id' | 'isTemporary' | 'rundownId' | 'segmentId' | 'segmentPlayoutId' | 'takeCount' | 'part' | 'timings' | 'orphaned' > /** @@ -94,6 +94,7 @@ export const RundownTimingProvider = withTracker< _id: 1, rundownId: 1, segmentId: 1, + isTemporary: 1, segmentPlayoutId: 1, takeCount: 1, part: 1, @@ -103,7 +104,15 @@ export const RundownTimingProvider = withTracker< }) as Array< Pick< PartInstance, - '_id' | 'rundownId' | 'segmentId' | 'segmentPlayoutId' | 'takeCount' | 'part' | 'timings' | 'orphaned' + | '_id' + | 'rundownId' + | 'segmentId' + | 'isTemporary' + | 'segmentPlayoutId' + | 'takeCount' + | 'part' + | 'timings' + | 'orphaned' > > diff --git a/meteor/client/ui/RundownView/RundownTiming/SegmentDuration.tsx b/meteor/client/ui/RundownView/RundownTiming/SegmentDuration.tsx index cc42683560..5beb44330e 100644 --- a/meteor/client/ui/RundownView/RundownTiming/SegmentDuration.tsx +++ b/meteor/client/ui/RundownView/RundownTiming/SegmentDuration.tsx @@ -9,6 +9,7 @@ import { CalculateTimingsPiece, } from '@sofie-automation/corelib/dist/playout/timings' import { PartId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { getPartInstanceTimingId } from '../../../lib/rundownTiming' interface ISegmentDurationProps { segmentId: SegmentId @@ -53,7 +54,7 @@ export const SegmentDuration = withTiming()(function }) } props.parts.forEach((part) => { - playedOut += (!part.instance.part.untimed ? partPlayed[unprotectString(part.instance.part._id)] : 0) || 0 + playedOut += (!part.instance.part.untimed ? partPlayed[getPartInstanceTimingId(part.instance)] : 0) || 0 }) } diff --git a/meteor/client/ui/SegmentList/LinePartTimeline.tsx b/meteor/client/ui/SegmentList/LinePartTimeline.tsx index 26e8818694..21eae3edbc 100644 --- a/meteor/client/ui/SegmentList/LinePartTimeline.tsx +++ b/meteor/client/ui/SegmentList/LinePartTimeline.tsx @@ -13,6 +13,7 @@ import { PartAutoNextMarker } from './PartAutoNextMarker' import { PieceUi } from '../SegmentContainer/withResolvedSegment' import StudioContext from '../RundownView/StudioContext' import { InvalidPartCover } from '../SegmentTimeline/Parts/InvalidPartCover' +import { getPartInstanceTimingId } from '../../lib/rundownTiming' const TIMELINE_DEFAULT_BASE = 30 * 1000 @@ -141,7 +142,7 @@ export const LinePartTimeline: React.FC = function LinePartTimeline({ {transitionPiece && } {!willAutoNextOut && !isInvalid && ( ((props) => ({ - filter: (data) => data.partPlayed?.[unprotectString(props.partId)], + filter: (data) => data.partPlayed?.[props.partInstanceTimingId], dataResolution: TimingDataResolution.High, tickResolution: TimingTickResolution.High, }))(function OvertimeShadow({ - partId, + partInstanceTimingId, timingDurations, timelineBase, mainSourceEnd, @@ -48,7 +46,7 @@ export const OvertimeShadow = withTiming((props) => ({ isLive, hasAlreadyPlayed, }: WithTiming) { - const livePosition = timingDurations.partPlayed?.[unprotectString(partId)] ?? 0 + const livePosition = timingDurations.partPlayed?.[partInstanceTimingId] ?? 0 const contentVsPartDiff = mainSourceEnd - partRenderedDuration const toFreezeFrame = diff --git a/meteor/client/ui/SegmentList/utils/LiveLineIsPast.tsx b/meteor/client/ui/SegmentList/utils/LiveLineIsPast.tsx index d2ef3fdf38..262b562489 100644 --- a/meteor/client/ui/SegmentList/utils/LiveLineIsPast.tsx +++ b/meteor/client/ui/SegmentList/utils/LiveLineIsPast.tsx @@ -1,15 +1,13 @@ import React from 'react' import { RundownTimingConsumer } from '../../RundownView/RundownTiming/RundownTimingConsumer' import { TimingDataResolution, TimingTickResolution } from '../../RundownView/RundownTiming/withTiming' -import { PartId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' export const LiveLineIsPast = React.memo(function LiveLineIsPast({ - partId, + partTimingId, time, children, }: { - partId: PartId + partTimingId: string time: number children?: (isPast: boolean) => JSX.Element | null }) { @@ -17,10 +15,10 @@ export const LiveLineIsPast = React.memo(function LiveLineIsPast({ data.partPlayed?.[unprotectString(partId)]} + filter={(data) => data.partPlayed?.[partTimingId]} > {(timingContext) => { - const livePosition = timingContext.partPlayed?.[unprotectString(partId)] ?? 0 + const livePosition = timingContext.partPlayed?.[partTimingId] ?? 0 return children ? children(livePosition > time) : null }} diff --git a/meteor/client/ui/SegmentTimeline/Parts/FlattenedSourceLayers.tsx b/meteor/client/ui/SegmentTimeline/Parts/FlattenedSourceLayers.tsx index 62b37ea0af..f2d2730812 100644 --- a/meteor/client/ui/SegmentTimeline/Parts/FlattenedSourceLayers.tsx +++ b/meteor/client/ui/SegmentTimeline/Parts/FlattenedSourceLayers.tsx @@ -75,7 +75,7 @@ export function FlattenedSourceLayers(props: IFlattenedSourceLayerProps): JSX.El part={props.part} partStartsAt={props.startsAt} partDuration={props.duration} - partExpectedDuration={props.expectedDuration} + partDisplayDuration={props.displayDuration} timeScale={props.timeScale} autoNextPart={props.autoNextPart} liveLinePadding={props.liveLinePadding} diff --git a/meteor/client/ui/SegmentTimeline/Parts/OutputGroup.tsx b/meteor/client/ui/SegmentTimeline/Parts/OutputGroup.tsx index 4468ab07c6..9b4aa3492f 100644 --- a/meteor/client/ui/SegmentTimeline/Parts/OutputGroup.tsx +++ b/meteor/client/ui/SegmentTimeline/Parts/OutputGroup.tsx @@ -22,7 +22,7 @@ interface IOutputGroupProps { pieces: CalculateTimingsPiece[] startsAt: number duration: number - expectedDuration: number + displayDuration: number timeScale: number collapsedOutputs: { [key: string]: boolean @@ -73,7 +73,7 @@ export function OutputGroup(props: IOutputGroupProps): JSX.Element { pieces={props.pieces} startsAt={props.startsAt} duration={props.duration} - expectedDuration={props.expectedDuration} + displayDuration={props.displayDuration} timeScale={props.timeScale} autoNextPart={props.autoNextPart} liveLinePadding={props.liveLinePadding} @@ -109,7 +109,7 @@ export function OutputGroup(props: IOutputGroupProps): JSX.Element { pieces={props.pieces} startsAt={props.startsAt} duration={props.duration} - expectedDuration={props.expectedDuration} + displayDuration={props.displayDuration} timeScale={props.timeScale} autoNextPart={props.autoNextPart} liveLinePadding={props.liveLinePadding} diff --git a/meteor/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx b/meteor/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx index e8b9b73b6e..0e0abbe24e 100644 --- a/meteor/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx +++ b/meteor/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx @@ -14,7 +14,7 @@ import { import { RundownTiming } from '../../RundownView/RundownTiming/RundownTiming' import { RundownUtils } from '../../../lib/rundown' -import { getCurrentTime, unprotectString } from '../../../../lib/lib' +import { getCurrentTime } from '../../../../lib/lib' import { DEBUG_MODE } from '../SegmentTimelineDebugMode' import { Translated } from '../../../lib/ReactMeteorData/ReactMeteorData' @@ -29,7 +29,7 @@ import { LoopingIcon } from '../../../lib/ui/icons/looping' import { SegmentEnd } from '../../../lib/ui/icons/segment' import { getShowHiddenSourceLayers } from '../../../lib/localStorage' import { Part } from '../../../../lib/collections/Parts' -import { RundownTimingContext } from '../../../lib/rundownTiming' +import { getPartInstanceTimingId, getPartInstanceTimingValue, RundownTimingContext } from '../../../lib/rundownTiming' import { OutputGroup } from './OutputGroup' import { InvalidPartCover } from './InvalidPartCover' import { ISourceLayer } from '@sofie-automation/blueprints-integration' @@ -129,7 +129,7 @@ export class SegmentTimelinePartClass extends React.Component): number { - return ( - props.part.instance.timings?.duration || - (props.timingDurations.partDisplayDurations && - props.timingDurations.partDisplayDurations[unprotectString(props.part.instance.part._id)]) || - props.part.renderedDuration || - 0 - ) - } - static getPartDuration( props: WithTiming, liveDuration: number, @@ -382,14 +371,14 @@ export class SegmentTimelinePartClass extends React.Component): number { @@ -398,20 +387,17 @@ export class SegmentTimelinePartClass extends React.Component @@ -794,14 +783,14 @@ export const SegmentTimelinePart = withTranslation()( filter: (durations: RundownTimingContext) => { durations = durations || {} - const partId = unprotectString(props.part.instance.part._id) + const timingId = getPartInstanceTimingId(props.part.instance) const firstPartInSegmentId = props.firstPartInSegment - ? unprotectString(props.firstPartInSegment.instance.part._id) + ? getPartInstanceTimingId(props.firstPartInSegment.instance) : undefined return [ - (durations.partDurations || {})[partId], - (durations.partDisplayStartsAt || {})[partId], - (durations.partDisplayDurations || {})[partId], + (durations.partDurations || {})[timingId], + (durations.partDisplayStartsAt || {})[timingId], + (durations.partDisplayDurations || {})[timingId], firstPartInSegmentId ? (durations.partDisplayStartsAt || {})[firstPartInSegmentId] : undefined, firstPartInSegmentId ? (durations.partDisplayDurations || {})[firstPartInSegmentId] : undefined, ] diff --git a/meteor/client/ui/SegmentTimeline/Parts/SourceLayer.tsx b/meteor/client/ui/SegmentTimeline/Parts/SourceLayer.tsx index 3cdf8257fe..0fc4c6e1aa 100644 --- a/meteor/client/ui/SegmentTimeline/Parts/SourceLayer.tsx +++ b/meteor/client/ui/SegmentTimeline/Parts/SourceLayer.tsx @@ -22,7 +22,7 @@ export interface ISourceLayerPropsBase { pieces: CalculateTimingsPiece[] startsAt: number duration: number - expectedDuration: number + displayDuration: number timeScale: number isLiveLine: boolean isNextLine: boolean @@ -122,7 +122,7 @@ export function SourceLayer(props: ISourceLayerProps): JSX.Element { pieces={props.pieces} partStartsAt={props.startsAt} partDuration={props.duration} - partExpectedDuration={props.expectedDuration} + partDisplayDuration={props.displayDuration} timeScale={props.timeScale} autoNextPart={props.autoNextPart} liveLinePadding={props.liveLinePadding} diff --git a/meteor/client/ui/SegmentTimeline/Renderers/CustomLayerItemRenderer.tsx b/meteor/client/ui/SegmentTimeline/Renderers/CustomLayerItemRenderer.tsx index e1d40af4e0..e9f68f8659 100644 --- a/meteor/client/ui/SegmentTimeline/Renderers/CustomLayerItemRenderer.tsx +++ b/meteor/client/ui/SegmentTimeline/Renderers/CustomLayerItemRenderer.tsx @@ -22,7 +22,7 @@ export interface ICustomLayerItemProps { isLiveLine: boolean partStartsAt: number partDuration: number // 0 if unknown - partExpectedDuration: number + partDisplayDuration: number piece: PieceUi timeScale: number onFollowLiveLine?: (state: boolean, event: any) => void diff --git a/meteor/client/ui/SegmentTimeline/Renderers/VTSourceRenderer.tsx b/meteor/client/ui/SegmentTimeline/Renderers/VTSourceRenderer.tsx index 286ff1c978..f3575ffe36 100644 --- a/meteor/client/ui/SegmentTimeline/Renderers/VTSourceRenderer.tsx +++ b/meteor/client/ui/SegmentTimeline/Renderers/VTSourceRenderer.tsx @@ -355,7 +355,7 @@ export class VTSourceRendererBase extends CustomLayerItemRenderer 500)) ) { let endOfContentAt: number = vtContent.sourceDuration + (vtContent.postrollDuration || 0) diff --git a/meteor/client/ui/SegmentTimeline/SegmentContextMenu.tsx b/meteor/client/ui/SegmentTimeline/SegmentContextMenu.tsx index c36f23584d..1a778fe6ee 100644 --- a/meteor/client/ui/SegmentTimeline/SegmentContextMenu.tsx +++ b/meteor/client/ui/SegmentTimeline/SegmentContextMenu.tsx @@ -74,7 +74,7 @@ export const SegmentContextMenu = withTranslation()( <> this.props.onSetNext(part.instance.part, e)} - disabled={isCurrentPart || !!part.instance.orphaned || !canSetAsNext} + disabled={!!part.instance.orphaned || !canSetAsNext} > Next') }}> {startsAt !== null && diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx index ad5aa647ec..992b6e0af3 100644 --- a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx +++ b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx @@ -33,7 +33,7 @@ import { wrapPartToTemporaryInstance } from '../../../lib/collections/PartInstan import { SegmentTimelineSmallPartFlag } from './SmallParts/SegmentTimelineSmallPartFlag' import { UIStateStorage } from '../../lib/UIStateStorage' -import { RundownTimingContext } from '../../lib/rundownTiming' +import { getPartInstanceTimingId, RundownTimingContext } from '../../lib/rundownTiming' import { IOutputLayer, ISourceLayer, NoteSeverity } from '@sofie-automation/blueprints-integration' import { SegmentTimelineZoomButtons } from './SegmentTimelineZoomButtons' import { SegmentViewMode } from '../SegmentContainer/SegmentViewModes' @@ -169,11 +169,12 @@ const SegmentTimelineZoom = class SegmentTimelineZoom extends React.Component< let total = 0 if (this.context && this.context.durations) { const durations = this.context.durations as RundownTimingContext - this.props.parts.forEach((item) => { + this.props.parts.forEach((partExtended) => { // total += durations.partDurations ? durations.partDurations[item._id] : (item.duration || item.renderedDuration || 1) + const partInstanceTimingId = getPartInstanceTimingId(partExtended.instance) const duration = Math.max( - item.instance.timings?.duration || item.renderedDuration || 0, - (durations.partDisplayDurations && durations.partDisplayDurations[unprotectString(item.instance.part._id)]) || + partExtended.instance.timings?.duration || partExtended.renderedDuration || 0, + (durations.partDisplayDurations && durations.partDisplayDurations[partInstanceTimingId]) || Settings.defaultDisplayDuration ) total += duration @@ -736,7 +737,7 @@ export class SegmentTimelineClass extends React.Component part.instance._id === props.playlist.currentPartInfo?.partInstanceId ) - const livePartId = unprotectString(livePart?.instance.part._id) + const livePartId = livePart ? getPartInstanceTimingId(livePart.instance) : undefined return [ livePartId ? (durations.partDisplayStartsAt || {})[livePartId] : undefined, livePartId ? (durations.partDisplayDurations || {})[livePartId] : undefined, diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx index 2fbcecd293..6e58f5b537 100644 --- a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx +++ b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx @@ -29,7 +29,7 @@ import { ITrackedProps, IOutputLayerUi, } from '../SegmentContainer/withResolvedSegment' -import { computeSegmentDuration, RundownTimingContext } from '../../lib/rundownTiming' +import { computeSegmentDuration, getPartInstanceTimingId, RundownTimingContext } from '../../lib/rundownTiming' import { RundownViewShelf } from '../RundownView/RundownViewShelf' import { PartInstanceId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PartInstances, Parts, Segments } from '../../collections' @@ -278,14 +278,21 @@ export const SegmentTimelineContainer = withResolvedSegment( this.stopLive() } + const currentNextPartInstanceTimingId = currentNextPart + ? getPartInstanceTimingId(currentNextPart?.instance) + : undefined + const firstPartInstanceTimingId = this.props.parts[0] + ? getPartInstanceTimingId(this.props.parts[0].instance) + : undefined + // Setting the correct scroll position on parts when setting is next const nextPartDisplayStartsAt = - (currentNextPart && this.context.durations?.partDisplayStartsAt?.[unprotectString(currentNextPart.partId)]) ?? 0 + (currentNextPartInstanceTimingId && + this.context.durations?.partDisplayStartsAt?.[currentNextPartInstanceTimingId]) || + 0 const partOffset = nextPartDisplayStartsAt - - (this.props.parts.length > 0 - ? this.context.durations?.partDisplayStartsAt?.[unprotectString(this.props.parts[0].instance.part._id)] ?? 0 - : 0) + (firstPartInstanceTimingId ? this.context.durations?.partDisplayStartsAt?.[firstPartInstanceTimingId] ?? 0 : 0) const nextPartIdOrOffsetHasChanged = currentNextPart && this.props.playlist.nextPartInfo && @@ -554,12 +561,10 @@ export const SegmentTimelineContainer = withResolvedSegment( this.setState((state) => { if (state.isLiveSegment && state.currentLivePart) { const currentLivePartInstance = state.currentLivePart.instance - const currentLivePart = currentLivePartInstance.part const partOffset = - (this.context.durations?.partDisplayStartsAt?.[unprotectString(currentLivePart._id)] || 0) - - (this.context.durations?.partDisplayStartsAt?.[unprotectString(this.props.parts[0]?.instance.part._id)] || - 0) + (this.context.durations?.partDisplayStartsAt?.[getPartInstanceTimingId(currentLivePartInstance)] || 0) - + (this.context.durations?.partDisplayStartsAt?.[getPartInstanceTimingId(this.props.parts[0]?.instance)] || 0) let isExpectedToPlay = !!currentLivePartInstance.timings?.plannedStartedPlayback const lastTake = currentLivePartInstance.timings?.take diff --git a/meteor/client/ui/SegmentTimeline/SmallParts/SegmentTimelineSmallPartFlag.tsx b/meteor/client/ui/SegmentTimeline/SmallParts/SegmentTimelineSmallPartFlag.tsx index 9ab2f1e567..e59ecc1a73 100644 --- a/meteor/client/ui/SegmentTimeline/SmallParts/SegmentTimelineSmallPartFlag.tsx +++ b/meteor/client/ui/SegmentTimeline/SmallParts/SegmentTimelineSmallPartFlag.tsx @@ -12,13 +12,15 @@ import { CalculateTimingsPiece } from '@sofie-automation/corelib/dist/playout/ti import { PartId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { TimingDataResolution, TimingTickResolution, withTiming } from '../../RundownView/RundownTiming/withTiming' import { SegmentTimelinePartClass } from '../Parts/SegmentTimelinePart' +import { PartExtended } from '../../../../lib/Rundown' +import { getPartInstanceTimingId } from '../../../lib/rundownTiming' export const SegmentTimelineSmallPartFlag = withTiming< { parts: [PartUi, number, number][] pieces: Map followingPart: PartUi | undefined - firstPartInSegmentId: PartId + firstPartInSegment: PartExtended sourceLayers: { [key: string]: ISourceLayer } @@ -48,8 +50,8 @@ export const SegmentTimelineSmallPartFlag = withTiming< dataResolution: TimingDataResolution.High, tickResolution: TimingTickResolution.High, filter: (timings) => [ - timings?.partDisplayStartsAt?.[unprotectString(props.firstPartInSegmentId)], - timings?.partDisplayStartsAt?.[unprotectString(props.parts[0][0].partId)], + timings?.partDisplayStartsAt?.[getPartInstanceTimingId(props.firstPartInSegment.instance)], + timings?.partDisplayStartsAt?.[getPartInstanceTimingId(props.parts[0][0].instance)], ], }))( ({ @@ -58,7 +60,7 @@ export const SegmentTimelineSmallPartFlag = withTiming< followingPart, sourceLayers, timeToPixelRatio, - firstPartInSegmentId, + firstPartInSegment, segment, playlist, @@ -113,8 +115,8 @@ export const SegmentTimelineSmallPartFlag = withTiming< ]) const firstPartDisplayStartsAt = - (timingDurations.partDisplayStartsAt?.[unprotectString(parts[0][0].partId)] ?? 0) - - (timingDurations.partDisplayStartsAt?.[unprotectString(firstPartInSegmentId)] ?? 0) + (timingDurations.partDisplayStartsAt?.[getPartInstanceTimingId(parts[0][0].instance)] ?? 0) - + (timingDurations.partDisplayStartsAt?.[getPartInstanceTimingId(firstPartInSegment.instance)] ?? 0) const pixelOffsetPosition = (firstPartDisplayStartsAt + futureShadePaddingTime) * timeToPixelRatio diff --git a/meteor/client/ui/SegmentTimeline/SourceLayerItem.tsx b/meteor/client/ui/SegmentTimeline/SourceLayerItem.tsx index ab64e8fdb4..d75e66a12a 100644 --- a/meteor/client/ui/SegmentTimeline/SourceLayerItem.tsx +++ b/meteor/client/ui/SegmentTimeline/SourceLayerItem.tsx @@ -35,7 +35,7 @@ export interface ISourceLayerItemProps { /** Part definite duration (generally set after part is played) */ partDuration: number /** Part expected duration (before playout) */ - partExpectedDuration: number + partDisplayDuration: number /** The piece being rendered in this layer */ piece: PieceUi /** Pieces belonging to the Part */ From d5ed171f8ab99ddea16707c7a34e51dae0f71ea8 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Tue, 18 Jul 2023 11:38:28 +0200 Subject: [PATCH 053/479] fix(CameraView): broken countdowns after RundownTimingCalculator modifications --- meteor/client/lib/rundownTiming.ts | 11 +++++++---- meteor/client/ui/ClockView/CameraScreen/Part.tsx | 8 ++++---- .../SegmentTimeline/Parts/SegmentTimelinePart.tsx | 14 +++++++++----- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/meteor/client/lib/rundownTiming.ts b/meteor/client/lib/rundownTiming.ts index f8562a2299..82e8aa97a3 100644 --- a/meteor/client/lib/rundownTiming.ts +++ b/meteor/client/lib/rundownTiming.ts @@ -791,15 +791,18 @@ export function getPartInstanceTimingId( return !partInstance.isTemporary ? unprotectString(partInstance._id) : unprotectString(partInstance.part._id) } +/** + * Get a timing value from a timing context map for a given (temporary) PartInstance. Will return `null` if not found. + */ export function getPartInstanceTimingValue( values: Record | undefined, partInstance: Pick & { part: Pick } -): number { - if (!values) return Number.NaN +): number | null { + if (!values) return null if (partInstance.isTemporary) { - return values[unprotectString(partInstance.part._id)] ?? Number.NaN + return values[unprotectString(partInstance.part._id)] ?? null } - return values[unprotectString(partInstance._id)] ?? values[unprotectString(partInstance.part._id)] ?? Number.NaN + return values[unprotectString(partInstance._id)] ?? values[unprotectString(partInstance.part._id)] ?? null } export function getPlaylistTimingDiff( diff --git a/meteor/client/ui/ClockView/CameraScreen/Part.tsx b/meteor/client/ui/ClockView/CameraScreen/Part.tsx index 39a2273d7d..3873ba7608 100644 --- a/meteor/client/ui/ClockView/CameraScreen/Part.tsx +++ b/meteor/client/ui/ClockView/CameraScreen/Part.tsx @@ -5,7 +5,7 @@ import { AreaZoom } from '.' import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists' import { PieceExtended } from '../../../../lib/Rundown' import { getAllowSpeaking, getAllowVibrating } from '../../../lib/localStorage' -import { getPartInstanceTimingId } from '../../../lib/rundownTiming' +import { getPartInstanceTimingValue } from '../../../lib/rundownTiming' import { AutoNextStatus } from '../../RundownView/RundownTiming/AutoNextStatus' import { CurrentPartRemaining } from '../../RundownView/RundownTiming/CurrentPartRemaining' import { PartCountdown } from '../../RundownView/RundownTiming/PartCountdown' @@ -29,9 +29,9 @@ export const Part = withTiming({ const areaZoom = useContext(AreaZoom) let left = - timingDurations.partCountdown?.[unprotectString(part.partId)] ?? - 0 - (timingDurations.partPlayed?.[getPartInstanceTimingId(part.instance)] ?? 0) - let width: number | null = timingDurations.partDisplayDurations?.[getPartInstanceTimingId(part.instance)] ?? 0 + (timingDurations.partCountdown?.[unprotectString(part.partId)] ?? 0) - + (getPartInstanceTimingValue(timingDurations.partPlayed, part.instance) || 0) + let width: number | null = getPartInstanceTimingValue(timingDurations.partDisplayDurations, part.instance) ?? null if (isLive) { left = 0 diff --git a/meteor/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx b/meteor/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx index 0e0abbe24e..758802a6af 100644 --- a/meteor/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx +++ b/meteor/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx @@ -387,17 +387,21 @@ export class SegmentTimelinePartClass extends React.Component Date: Tue, 18 Jul 2023 13:23:11 +0200 Subject: [PATCH 054/479] fix: displayDurationGroup calculations --- .../lib/__tests__/rundownTiming.test.ts | 569 ++++++++++++++---- meteor/client/lib/rundownTiming.ts | 9 +- 2 files changed, 449 insertions(+), 129 deletions(-) diff --git a/meteor/client/lib/__tests__/rundownTiming.test.ts b/meteor/client/lib/__tests__/rundownTiming.test.ts index 78e5aa014d..700ee1d604 100644 --- a/meteor/client/lib/__tests__/rundownTiming.test.ts +++ b/meteor/client/lib/__tests__/rundownTiming.test.ts @@ -462,131 +462,447 @@ describe('rundown Timing Calculator', () => { ) }) - it('Handles display duration groups', () => { - const timing = new RundownTimingCalculator() - const playlist: RundownPlaylist = makeMockPlaylist() - playlist.timing = { - type: 'forward-time' as any, - expectedStart: 0, - expectedDuration: 40000, - } - const rundownId1 = 'rundown1' - const segmentId1 = 'segment1' - const segmentId2 = 'segment2' - const segmentsMap: Map = new Map() - segmentsMap.set(protectString(segmentId1), makeMockSegment(segmentId1, 0, rundownId1)) - segmentsMap.set(protectString(segmentId2), makeMockSegment(segmentId2, 0, rundownId1)) - const parts: Part[] = [] - parts.push( - makeMockPart('part1', 0, rundownId1, segmentId1, { - expectedDuration: 1000, - displayDuration: 2000, - displayDurationGroup: 'test', - }) - ) - parts.push( - makeMockPart('part2', 0, rundownId1, segmentId1, { - expectedDuration: 1000, - displayDuration: 3000, - displayDurationGroup: 'test', - }) - ) - parts.push( - makeMockPart('part3', 0, rundownId1, segmentId2, { - expectedDuration: 1000, - displayDuration: 4000, - displayDurationGroup: 'test', - }) - ) - parts.push( - makeMockPart('part4', 0, rundownId1, segmentId2, { - expectedDuration: 1000, - displayDuration: 5000, - displayDurationGroup: 'test', - }) - ) - const partInstances = convertPartsToPartInstances(parts) - const partInstancesMap: Map = new Map() - const rundown = makeMockRundown(rundownId1, playlist) - const rundowns = [rundown] - const result = timing.updateDurations( - 0, - false, - playlist, - rundowns, - undefined, - partInstances, - partInstancesMap, - new Map(), - segmentsMap, - DEFAULT_DURATION, - [] - ) - expect(result).toEqual( - literal({ - currentPartInstanceId: null, - isLowResolution: false, - asDisplayedPlaylistDuration: 4000, - asPlayedPlaylistDuration: 4000, - currentPartWillAutoNext: false, - currentTime: 0, - rundownExpectedDurations: { - [rundownId1]: 4000, - }, - rundownAsPlayedDurations: { - [rundownId1]: 4000, - }, - partCountdown: { - part1: 0, - part2: 2000, - part3: 5000, - part4: 9000, - }, - partDisplayDurations: { - part1: 2000, - part2: 3000, - part3: 4000, - part4: 5000, - }, - partDisplayStartsAt: { - part1: 0, - part2: 2000, - part3: 5000, - part4: 9000, - }, - partDurations: { - part1: 1000, - part2: 1000, - part3: 1000, - part4: 1000, - }, - partExpectedDurations: { - part1: 1000, - part2: 1000, - part3: 1000, - part4: 1000, - }, - partPlayed: { - part1: 0, - part2: 0, - part3: 0, - part4: 0, - }, - partStartsAt: { - part1: 0, - part2: 1000, - part3: 2000, - part4: 3000, - }, - remainingPlaylistDuration: 4000, - totalPlaylistDuration: 4000, - breakIsLastRundown: undefined, - remainingTimeOnCurrentPart: undefined, - rundownsBeforeNextBreak: undefined, - segmentBudgetDurations: {}, - segmentStartedPlayback: {}, - }) - ) + describe('Display duration groups', () => { + it('Handles groups when not playing', () => { + const timing = new RundownTimingCalculator() + const playlist: RundownPlaylist = makeMockPlaylist() + playlist.timing = { + type: 'forward-time' as any, + expectedStart: 0, + expectedDuration: 40000, + } + const rundownId1 = 'rundown1' + const segmentId1 = 'segment1' + const segmentId2 = 'segment2' + const segmentsMap: Map = new Map() + segmentsMap.set(protectString(segmentId1), makeMockSegment(segmentId1, 0, rundownId1)) + segmentsMap.set(protectString(segmentId2), makeMockSegment(segmentId2, 0, rundownId1)) + const parts: Part[] = [] + parts.push( + makeMockPart('part1', 0, rundownId1, segmentId1, { + expectedDuration: 1000, + displayDuration: 2000, + displayDurationGroup: 'test', + }) + ) + parts.push( + makeMockPart('part2', 0, rundownId1, segmentId1, { + expectedDuration: 1000, + displayDuration: 3000, + displayDurationGroup: 'test', + }) + ) + parts.push( + makeMockPart('part3', 0, rundownId1, segmentId2, { + expectedDuration: 1000, + displayDuration: 4000, + displayDurationGroup: 'test', + }) + ) + parts.push( + makeMockPart('part4', 0, rundownId1, segmentId2, { + expectedDuration: 1000, + displayDuration: 5000, + displayDurationGroup: 'test', + }) + ) + const partInstances = convertPartsToPartInstances(parts) + const partInstancesMap: Map = new Map() + const rundown = makeMockRundown(rundownId1, playlist) + const rundowns = [rundown] + const result = timing.updateDurations( + 0, + false, + playlist, + rundowns, + undefined, + partInstances, + partInstancesMap, + new Map(), + segmentsMap, + DEFAULT_DURATION, + [] + ) + expect(result).toEqual( + literal({ + currentPartInstanceId: null, + isLowResolution: false, + asDisplayedPlaylistDuration: 4000, + asPlayedPlaylistDuration: 4000, + currentPartWillAutoNext: false, + currentTime: 0, + rundownExpectedDurations: { + [rundownId1]: 4000, + }, + rundownAsPlayedDurations: { + [rundownId1]: 4000, + }, + partCountdown: { + part1: 0, + part2: 2000, + part3: 5000, + part4: 9000, + }, + partDisplayDurations: { + part1: 2000, + part2: 3000, + part3: 4000, + part4: 5000, + }, + partDisplayStartsAt: { + part1: 0, + part2: 2000, + part3: 5000, + part4: 9000, + }, + partDurations: { + part1: 1000, + part2: 1000, + part3: 1000, + part4: 1000, + }, + partExpectedDurations: { + part1: 1000, + part2: 1000, + part3: 1000, + part4: 1000, + }, + partPlayed: { + part1: 0, + part2: 0, + part3: 0, + part4: 0, + }, + partStartsAt: { + part1: 0, + part2: 1000, + part3: 2000, + part4: 3000, + }, + remainingPlaylistDuration: 4000, + totalPlaylistDuration: 4000, + breakIsLastRundown: undefined, + remainingTimeOnCurrentPart: undefined, + rundownsBeforeNextBreak: undefined, + segmentBudgetDurations: {}, + segmentStartedPlayback: {}, + }) + ) + }) + + it('Handles groups when playing', () => { + const timing = new RundownTimingCalculator() + const playlist: RundownPlaylist = makeMockPlaylist() + playlist.timing = { + type: 'forward-time' as any, + expectedStart: 0, + expectedDuration: 40000, + } + const rundownId1 = 'rundown1' + const segmentId1 = 'segment1' + const segmentsMap: Map = new Map() + segmentsMap.set(protectString(segmentId1), makeMockSegment(segmentId1, 0, rundownId1)) + const parts: Part[] = [] + parts.push( + makeMockPart('part1', 0, rundownId1, segmentId1, { + expectedDuration: 1000, + }) + ) + parts.push( + makeMockPart('part2', 0, rundownId1, segmentId1, { + expectedDuration: 5000, + displayDuration: 1000, + displayDurationGroup: 'test', + }) + ) + parts.push( + makeMockPart('part3', 0, rundownId1, segmentId1, { + displayDurationGroup: 'test', + }) + ) + parts.push( + makeMockPart('part4', 0, rundownId1, segmentId1, { + expectedDuration: 1000, + }) + ) + const partInstancesMap: Map = new Map( + parts.map((part) => { + return [part._id, wrapPartToTemporaryInstance(protectString('active'), part)] + }) + ) + const partInstances = Array.from(partInstancesMap.values()) + partInstancesMap.get(parts[0]._id)!.timings = { + // part1 + duration: 1000, + take: 0, + plannedStartedPlayback: 0, + plannedStoppedPlayback: 1000, + } + partInstancesMap.get(parts[1]._id)!.timings = { + // part2 + duration: 2000, + take: 1000, + plannedStartedPlayback: 1000, + plannedStoppedPlayback: 3000, + } + partInstancesMap.get(parts[2]._id)!.timings = { + // part3 + take: 3000, + plannedStartedPlayback: 3000, + } + const currentPartInstanceId = partInstancesMap.get(parts[2]._id)!._id + const nextPartInstanceId = partInstancesMap.get(parts[3]._id)!._id + playlist.currentPartInfo = { + partInstanceId: currentPartInstanceId, + rundownId: protectString(rundownId1), + manuallySelected: false, + consumesNextSegmentId: false, + } + playlist.nextPartInfo = { + partInstanceId: nextPartInstanceId, + rundownId: protectString(rundownId1), + manuallySelected: false, + consumesNextSegmentId: false, + } + const rundown = makeMockRundown(rundownId1, playlist) + const rundowns = [rundown] + const result = timing.updateDurations( + 3500, + false, + playlist, + rundowns, + rundown, + partInstances, + partInstancesMap, + new Map(), + segmentsMap, + DEFAULT_DURATION, + [] + ) + expect(result).toEqual( + literal({ + currentPartInstanceId: currentPartInstanceId, + isLowResolution: false, + asDisplayedPlaylistDuration: 7000, + asPlayedPlaylistDuration: 7000, + currentPartWillAutoNext: false, + currentTime: 3500, + rundownExpectedDurations: { + [rundownId1]: 7000, + }, + rundownAsPlayedDurations: { + [rundownId1]: 7000, + }, + partCountdown: { + part1: null, + part2: null, + part3: null, + part4: 2500, + }, + partDisplayDurations: { + part1: 1000, + part2: 2000, + part3: 3000, + part4: 1000, + }, + partDisplayStartsAt: { + part1: 0, + part2: 1000, + part3: 3000, + part4: 6000, + }, + partDurations: { + part1: 1000, + part2: 2000, + part3: 500, + part4: 1000, + }, + partExpectedDurations: { + part1: 1000, + part2: 5000, + part3: 3000, + part4: 1000, + }, + partPlayed: { + part1: 1000, + part2: 2000, + part3: 500, + part4: 0, + }, + partStartsAt: { + part1: 0, + part2: 1000, + part3: 3000, + part4: 3500, + }, + remainingPlaylistDuration: 3500, + totalPlaylistDuration: 7000, + breakIsLastRundown: false, + remainingTimeOnCurrentPart: 2500, + rundownsBeforeNextBreak: [], + segmentBudgetDurations: {}, + segmentStartedPlayback: {}, + nextRundownAnchor: undefined, + }) + ) + }) + + it("Handles groups when playing outside of displayDurationGroup's budget", () => { + const timing = new RundownTimingCalculator() + const playlist: RundownPlaylist = makeMockPlaylist() + playlist.timing = { + type: 'forward-time' as any, + expectedStart: 0, + expectedDuration: 40000, + } + const rundownId1 = 'rundown1' + const segmentId1 = 'segment1' + const segmentsMap: Map = new Map() + segmentsMap.set(protectString(segmentId1), makeMockSegment(segmentId1, 0, rundownId1)) + const parts: Part[] = [] + parts.push( + makeMockPart('part1', 0, rundownId1, segmentId1, { + expectedDuration: 1000, + }) + ) + parts.push( + makeMockPart('part2', 0, rundownId1, segmentId1, { + expectedDuration: 5000, + displayDuration: 1000, + displayDurationGroup: 'test', + }) + ) + parts.push( + makeMockPart('part3', 0, rundownId1, segmentId1, { + displayDurationGroup: 'test', + }) + ) + parts.push( + makeMockPart('part4', 0, rundownId1, segmentId1, { + expectedDuration: 1000, + }) + ) + const partInstancesMap: Map = new Map( + parts.map((part) => { + return [part._id, wrapPartToTemporaryInstance(protectString('active'), part)] + }) + ) + const partInstances = Array.from(partInstancesMap.values()) + partInstancesMap.get(parts[0]._id)!.timings = { + // part1 + duration: 1000, + take: 0, + plannedStartedPlayback: 0, + plannedStoppedPlayback: 1000, + } + partInstancesMap.get(parts[1]._id)!.timings = { + // part2 + duration: 2000, + take: 1000, + plannedStartedPlayback: 1000, + plannedStoppedPlayback: 3000, + } + partInstancesMap.get(parts[2]._id)!.timings = { + // part3 + take: 3000, + plannedStartedPlayback: 3000, + } + const currentPartInstanceId = partInstancesMap.get(parts[2]._id)!._id + const nextPartInstanceId = partInstancesMap.get(parts[3]._id)!._id + playlist.currentPartInfo = { + partInstanceId: currentPartInstanceId, + rundownId: protectString(rundownId1), + manuallySelected: false, + consumesNextSegmentId: false, + } + playlist.nextPartInfo = { + partInstanceId: nextPartInstanceId, + rundownId: protectString(rundownId1), + manuallySelected: false, + consumesNextSegmentId: false, + } + const rundown = makeMockRundown(rundownId1, playlist) + const rundowns = [rundown] + const result = timing.updateDurations( + 10000, + false, + playlist, + rundowns, + rundown, + partInstances, + partInstancesMap, + new Map(), + segmentsMap, + DEFAULT_DURATION, + [] + ) + expect(result).toEqual( + literal({ + currentPartInstanceId: currentPartInstanceId, + isLowResolution: false, + asDisplayedPlaylistDuration: 11000, + asPlayedPlaylistDuration: 11000, + currentPartWillAutoNext: false, + currentTime: 10000, + rundownExpectedDurations: { + [rundownId1]: 7000, + }, + rundownAsPlayedDurations: { + [rundownId1]: 11000, + }, + partCountdown: { + part1: null, + part2: null, + part3: null, + part4: 0, + }, + partDisplayDurations: { + part1: 1000, + part2: 2000, + part3: 7000, + part4: 1000, + }, + partDisplayStartsAt: { + part1: 0, + part2: 1000, + part3: 3000, + part4: 10000, + }, + partDurations: { + part1: 1000, + part2: 2000, + part3: 7000, + part4: 1000, + }, + partExpectedDurations: { + part1: 1000, + part2: 5000, + part3: 3000, + part4: 1000, + }, + partPlayed: { + part1: 1000, + part2: 2000, + part3: 7000, + part4: 0, + }, + partStartsAt: { + part1: 0, + part2: 1000, + part3: 3000, + part4: 10000, + }, + remainingPlaylistDuration: 1000, + totalPlaylistDuration: 7000, + breakIsLastRundown: false, + remainingTimeOnCurrentPart: -4000, + rundownsBeforeNextBreak: [], + segmentBudgetDurations: {}, + segmentStartedPlayback: {}, + nextRundownAnchor: undefined, + }) + ) + }) }) describe('Non-zero default Part duration', () => { @@ -1018,7 +1334,7 @@ describe('rundown Timing Calculator', () => { currentPartWillAutoNext: false, currentTime: 0, rundownExpectedDurations: { - [rundownId]: 9240, + [rundownId]: 4000, }, rundownAsPlayedDurations: { [rundownId]: 9240, @@ -1072,6 +1388,7 @@ describe('rundown Timing Calculator', () => { rundownsBeforeNextBreak: undefined, segmentBudgetDurations: {}, segmentStartedPlayback: {}, + nextRundownAnchor: undefined, }) ) }) diff --git a/meteor/client/lib/rundownTiming.ts b/meteor/client/lib/rundownTiming.ts index 82e8aa97a3..b0e5b0e299 100644 --- a/meteor/client/lib/rundownTiming.ts +++ b/meteor/client/lib/rundownTiming.ts @@ -489,9 +489,11 @@ export class RundownTimingCalculator { } if (!rundownExpectedDurations[unprotectString(partInstance.part.rundownId)]) { - rundownExpectedDurations[unprotectString(partInstance.part.rundownId)] = partExpectedDuration + rundownExpectedDurations[unprotectString(partInstance.part.rundownId)] = + partInstance.part.expectedDuration ?? 0 } else { - rundownExpectedDurations[unprotectString(partInstance.part.rundownId)] += partExpectedDuration + rundownExpectedDurations[unprotectString(partInstance.part.rundownId)] += + partInstance.part.expectedDuration ?? 0 } }) @@ -620,7 +622,8 @@ export class RundownTimingCalculator { (currentLivePart.expectedDuration === undefined || currentLivePart.expectedDuration === 0) ) { onAirPartDuration = - this.partDisplayDurationsNoPlayback[unprotectString(currentLivePart._id)] || onAirPartDuration + getPartInstanceTimingValue(this.partDisplayDurationsNoPlayback, currentLivePartInstance) ?? + onAirPartDuration } remainingTimeOnCurrentPart = From 628709fc9de09bb5aa48b69b3c6e41f48b4a3ad6 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Tue, 15 Aug 2023 11:26:52 +0200 Subject: [PATCH 055/479] chore: Fixed typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9f30817d74..b86061443b 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Follow these instructions to start up Sofie Core in development mode. (For produ - Install [Meteor](https://www.meteor.com/install) (`npm install --global meteor`) - Install [Yarn](https://yarnpkg.com) (`npm install --global corepack && corepack enable`) -### Quick-start: +### Quick Start: ```bash git clone -b master https://github.com/nrkno/sofie-core.git From 8535da3c8cfa644b14e4a31f3a1ff10c11ad69e0 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 16 Aug 2023 11:13:13 +0100 Subject: [PATCH 056/479] chore: update tsr --- meteor/yarn.lock | 10 +++++----- packages/playout-gateway/package.json | 2 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 23 ++++++++++++----------- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/meteor/yarn.lock b/meteor/yarn.lock index b952415c1f..a0c4ad71d6 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1685,7 +1685,7 @@ __metadata: resolution: "@sofie-automation/shared-lib@portal:../packages/shared-lib::locator=automation-core%40workspace%3A." dependencies: "@mos-connection/model": ^3.0.4 - timeline-state-resolver-types: 9.0.0-release50.5 + timeline-state-resolver-types: 9.1.0-nightly-release51-20230816-095601-dcd9f0262.0 tslib: ^2.6.0 type-fest: ^3.10.0 languageName: node @@ -12452,12 +12452,12 @@ __metadata: languageName: node linkType: hard -"timeline-state-resolver-types@npm:9.0.0-release50.5": - version: 9.0.0-release50.5 - resolution: "timeline-state-resolver-types@npm:9.0.0-release50.5" +"timeline-state-resolver-types@npm:9.1.0-nightly-release51-20230816-095601-dcd9f0262.0": + version: 9.1.0-nightly-release51-20230816-095601-dcd9f0262.0 + resolution: "timeline-state-resolver-types@npm:9.1.0-nightly-release51-20230816-095601-dcd9f0262.0" dependencies: tslib: ^2.5.1 - checksum: bb333705c2aaccc70698333e8d13b9b95d58a5767402270b9181fee7da22fc0732d95b950e77f24f1429c5e7c789e9604fd66f0e13ea52d801320f69337ecf1a + checksum: 5d962b6c4dcbc34f507dd8023e5775fad6f801e2ec172ca7b62376fa9e2e541b9ec6242a1a5122ae96b874755917c68ee2322df4ed5f6f97483fb48fcdf55181 languageName: node linkType: hard diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index 09568af009..b1034d3bbe 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -60,7 +60,7 @@ "@sofie-automation/shared-lib": "1.51.0-in-development", "debug": "^4.3.4", "influx": "^5.9.3", - "timeline-state-resolver": "9.0.0-release50.5", + "timeline-state-resolver": "9.1.0-nightly-release51-20230816-095601-dcd9f0262.0", "tslib": "^2.6.0", "underscore": "^1.13.6", "winston": "^3.9.0" diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index b1129113e1..b0f4f52ac8 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -39,7 +39,7 @@ ], "dependencies": { "@mos-connection/model": "^3.0.4", - "timeline-state-resolver-types": "9.0.0-release50.5", + "timeline-state-resolver-types": "9.1.0-nightly-release51-20230816-095601-dcd9f0262.0", "tslib": "^2.6.0", "type-fest": "^3.10.0" }, diff --git a/packages/yarn.lock b/packages/yarn.lock index f93f63f21a..1f5212453d 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -6176,7 +6176,7 @@ __metadata: resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: "@mos-connection/model": ^3.0.4 - timeline-state-resolver-types: 9.0.0-release50.5 + timeline-state-resolver-types: 9.1.0-nightly-release51-20230816-095601-dcd9f0262.0 tslib: ^2.6.0 type-fest: ^3.10.0 languageName: unknown @@ -19613,7 +19613,7 @@ asn1@evs-broadcast/node-asn1: "@sofie-automation/shared-lib": 1.51.0-in-development debug: ^4.3.4 influx: ^5.9.3 - timeline-state-resolver: 9.0.0-release50.5 + timeline-state-resolver: 9.1.0-nightly-release51-20230816-095601-dcd9f0262.0 tslib: ^2.6.0 underscore: ^1.13.6 winston: ^3.9.0 @@ -23353,22 +23353,23 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"timeline-state-resolver-types@npm:9.0.0-release50.5": - version: 9.0.0-release50.5 - resolution: "timeline-state-resolver-types@npm:9.0.0-release50.5" +"timeline-state-resolver-types@npm:9.1.0-nightly-release51-20230816-095601-dcd9f0262.0": + version: 9.1.0-nightly-release51-20230816-095601-dcd9f0262.0 + resolution: "timeline-state-resolver-types@npm:9.1.0-nightly-release51-20230816-095601-dcd9f0262.0" dependencies: tslib: ^2.5.1 - checksum: bb333705c2aaccc70698333e8d13b9b95d58a5767402270b9181fee7da22fc0732d95b950e77f24f1429c5e7c789e9604fd66f0e13ea52d801320f69337ecf1a + checksum: 5d962b6c4dcbc34f507dd8023e5775fad6f801e2ec172ca7b62376fa9e2e541b9ec6242a1a5122ae96b874755917c68ee2322df4ed5f6f97483fb48fcdf55181 languageName: node linkType: hard -"timeline-state-resolver@npm:9.0.0-release50.5": - version: 9.0.0-release50.5 - resolution: "timeline-state-resolver@npm:9.0.0-release50.5" +"timeline-state-resolver@npm:9.1.0-nightly-release51-20230816-095601-dcd9f0262.0": + version: 9.1.0-nightly-release51-20230816-095601-dcd9f0262.0 + resolution: "timeline-state-resolver@npm:9.1.0-nightly-release51-20230816-095601-dcd9f0262.0" dependencies: "@tv2media/v-connection": ^7.3.0 atem-connection: 2.4.0 atem-state: ^0.12.2 + cacheable-lookup: ^5.0.3 casparcg-connection: ^6.0.3 casparcg-state: ^3.0.2 debug: ^4.3.4 @@ -23386,7 +23387,7 @@ asn1@evs-broadcast/node-asn1: sprintf-js: ^1.1.2 superfly-timeline: ^8.3.1 threadedclass: ^1.2.1 - timeline-state-resolver-types: 9.0.0-release50.5 + timeline-state-resolver-types: 9.1.0-nightly-release51-20230816-095601-dcd9f0262.0 tslib: ^2.5.1 tv-automation-quantel-gateway-client: ^3.1.11-agent-debug-20230117-160455-c2c4ee4.0 underscore: ^1.13.6 @@ -23394,7 +23395,7 @@ asn1@evs-broadcast/node-asn1: utf-8-validate: ^5.0.10 ws: ^7.5.9 xml-js: ^1.6.11 - checksum: 97517f91d36d0ebc3fd9ec4b30fa6c4a5a078702fc0a0161708bfbd91dbb18cd8d9a194366d0118ea4f450eb3b0e74c5325ed284a7a51736e07a11cdbea2aeba + checksum: a439131d4df7a64c7b19480df7dfff439dbba876a1d5b204dc55fb70169aa22415c786c52ab4d45801135718c71df0844d107565c538feb09e7fd779e5445669 languageName: node linkType: hard From 6ee928230463eedfe496cc5f6c3951e72e1ef744 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Thu, 17 Aug 2023 13:32:02 +0200 Subject: [PATCH 057/479] feat: change sort icons --- meteor/client/lib/ui/icons/sorting.tsx | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/meteor/client/lib/ui/icons/sorting.tsx b/meteor/client/lib/ui/icons/sorting.tsx index aa993f7e1b..b7a45a7747 100644 --- a/meteor/client/lib/ui/icons/sorting.tsx +++ b/meteor/client/lib/ui/icons/sorting.tsx @@ -2,13 +2,9 @@ import React from 'react' export function SortDescending(): JSX.Element { return ( - - + @@ -19,11 +15,7 @@ export function SortAscending(): JSX.Element { return ( - From 4e77241080b7e20320607ea74e4566ca4a76970b Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Thu, 17 Aug 2023 14:26:31 +0200 Subject: [PATCH 058/479] feat: reorder elements in Media Status lists --- .../MediaStatusPopUpHeader.tsx | 2 +- .../MediaStatusPopUpItem.scss | 12 +++++-- .../MediaStatusPopUp/MediaStatusPopUpItem.tsx | 33 ++++++++++++++----- .../ui/RundownView/MediaStatusPopUp/index.tsx | 1 + meteor/client/ui/Status.tsx | 2 +- .../Status/media-status/MediaStatusList.scss | 2 +- .../media-status/MediaStatusListHeader.tsx | 8 +++-- .../media-status/MediaStatusListItem.scss | 7 +++- .../media-status/MediaStatusListItem.tsx | 8 ++--- 9 files changed, 55 insertions(+), 20 deletions(-) diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader.tsx b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader.tsx index 3bbf41ffa6..39a26c4cc9 100644 --- a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader.tsx +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader.tsx @@ -57,6 +57,7 @@ export function MediaStatusPopUpHeader({ {t('On Air In')} + changeSortOrder('sourceLayer', order)} /> -

    {sourceLayerName}
    -

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

) diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx b/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx index 8eaa41f525..da229ca975 100644 --- a/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx @@ -125,6 +125,7 @@ export function MediaStatusPopUp({ playlistId }: IProps): JSX.Element { > { - + diff --git a/meteor/client/ui/Status/media-status/MediaStatusList.scss b/meteor/client/ui/Status/media-status/MediaStatusList.scss index 9d755d7723..2bdbf3a60d 100644 --- a/meteor/client/ui/Status/media-status/MediaStatusList.scss +++ b/meteor/client/ui/Status/media-status/MediaStatusList.scss @@ -14,7 +14,7 @@ float: right; position: relative; - margin-top: 0.5em; + margin-top: 2em; .media-status-table-search__search-input { &::placeholder { diff --git a/meteor/client/ui/Status/media-status/MediaStatusListHeader.tsx b/meteor/client/ui/Status/media-status/MediaStatusListHeader.tsx index dc99b28699..05a9ff4a02 100644 --- a/meteor/client/ui/Status/media-status/MediaStatusListHeader.tsx +++ b/meteor/client/ui/Status/media-status/MediaStatusListHeader.tsx @@ -1,4 +1,5 @@ import React from 'react' +import classNames from 'classnames' import { useTranslation } from 'react-i18next' import { SortOrderButton } from '../../MediaStatus/SortOrderButton' @@ -25,10 +26,14 @@ export function MediaStatusListHeader({ + - + @@ -57,10 +61,6 @@ export function MediaStatusListItem({
{sourceLayerName}
-
From dd44cafdd746ba0a9760390369a0cf793cc1c080 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 17 Aug 2023 14:08:36 +0100 Subject: [PATCH 059/479] chore: reduce duplicated call to `extendIngestRundownCore` --- packages/job-worker/src/ingest/generationRundown.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/job-worker/src/ingest/generationRundown.ts b/packages/job-worker/src/ingest/generationRundown.ts index 31788d2279..bf2ab0b4f8 100644 --- a/packages/job-worker/src/ingest/generationRundown.ts +++ b/packages/job-worker/src/ingest/generationRundown.ts @@ -31,7 +31,7 @@ import { SelectedShowStyleVariant, selectShowStyleVariant } from './selectShowSt import { getExternalNRCSName, PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { updateBaselineExpectedPackagesOnRundown } from './expectedPackages' import { ReadonlyDeep } from 'type-fest' -import { BlueprintResultRundown } from '@sofie-automation/blueprints-integration' +import { BlueprintResultRundown, ExtendedIngestRundown } from '@sofie-automation/blueprints-integration' import { wrapTranslatableMessageFromBlueprints } from '@sofie-automation/corelib/dist/TranslatableMessage' import { ReadOnlyCache } from '../cache/CacheBase' import { convertRundownToBlueprintSegmentRundown } from '../blueprints/context/lib' @@ -96,7 +96,7 @@ export async function updateRundownFromIngestData( const rundownData = await getRundownFromIngestData( context, cache, - ingestRundown, + extendedIngestRundown, pPeripheralDevice, showStyle, showStyleBlueprint, @@ -194,7 +194,7 @@ export async function updateRundownMetadataFromIngestData( const rundownData = await getRundownFromIngestData( context, cache, - ingestRundown, + extendedIngestRundown, pPeripheralDevice, showStyle, showStyleBlueprint, @@ -261,14 +261,12 @@ export async function updateRundownMetadataFromIngestData( export async function getRundownFromIngestData( context: JobContext, cache: ReadOnlyCache, - ingestRundown: LocalIngestRundown, + extendedIngestRundown: ExtendedIngestRundown, pPeripheralDevice: Promise | undefined, showStyle: SelectedShowStyleVariant, showStyleBlueprint: ReadonlyDeep, allRundownWatchedPackages: WatchedPackagesHelper ): Promise<{ dbRundownData: DBRundown; rundownRes: BlueprintResultRundown } | null> { - const extendedIngestRundown = extendIngestRundownCore(ingestRundown, cache.Rundown.doc) - const rundownBaselinePackages = allRundownWatchedPackages.filter( context, (pkg) => @@ -345,7 +343,7 @@ export async function getRundownFromIngestData( ...rundownRes.rundown, notes: rundownNotes, _id: cache.RundownId, - externalId: ingestRundown.externalId, + externalId: extendedIngestRundown.externalId, organizationId: context.studio.organizationId, studioId: context.studio._id, showStyleVariantId: showStyle.variant._id, From 15ff3eef1760066f6f0a648f441fdd36f37e418b Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Fri, 18 Aug 2023 09:06:39 +0200 Subject: [PATCH 060/479] fix: update timeline to v9 --- meteor/client/ui/TestTools/Timeline.tsx | 41 ++++++++--------- meteor/package.json | 2 +- meteor/yarn.lock | 27 ++++++----- packages/job-worker/package.json | 2 +- .../src/playout/timeline/generate.ts | 2 +- packages/playout-gateway/package.json | 2 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 46 +++++++++++-------- 8 files changed, 63 insertions(+), 61 deletions(-) diff --git a/meteor/client/ui/TestTools/Timeline.tsx b/meteor/client/ui/TestTools/Timeline.tsx index b72671c950..aca9cefec0 100644 --- a/meteor/client/ui/TestTools/Timeline.tsx +++ b/meteor/client/ui/TestTools/Timeline.tsx @@ -6,12 +6,12 @@ import { applyToArray, clone, normalizeArray, protectString } from '../../../lib import { CustomCollectionName, PubSub } from '../../../lib/api/pubsub' import { TimelineState, - Resolver, ResolvedTimelineObjectInstance, ResolvedTimeline, ResolvedTimelineObject, - ResolvedStates, TimelineObjectInstance, + resolveTimeline, + getResolvedState, } from 'superfly-timeline' import { TimelineContentObject, transformTimeline } from '@sofie-automation/corelib/dist/playout/timeline' import { useCurrentTime } from '../../lib/lib' @@ -58,7 +58,7 @@ function ComponentTimelineSimulate({ studioId }: ITimelineSimulateProps) { const now = useCurrentTime() const tlComplete = useTracker(() => StudioTimeline.findOne(studioId), [studioId]) - const [resolvedTl, errorMsgResolve] = useMemo(() => { + const [resolvedTimeline, errorMsgResolve] = useMemo(() => { try { const timelineObj = tlComplete && deserializeTimelineBlob(tlComplete.timelineBlob) console.log('regen timeline', tlComplete?.timelineHash, tlComplete?.generated) @@ -102,38 +102,31 @@ function ComponentTimelineSimulate({ studioId }: ITimelineSimulateProps) { } // TODO - dont repeat unless changed - const tl = Resolver.resolveTimeline(timeline as any, { time: tlComplete?.generated || now }) + const tl = resolveTimeline(timeline as any, { time: now }) return [tl, undefined] } catch (e) { return [undefined, `Failed to resolveTimeline:\n${e}`] } }, [tlComplete, now]) - const [allStates, errorMsgStates] = useMemo(() => { - try { - const states = resolvedTl ? Resolver.resolveAllStates(resolvedTl) : undefined - return [states, undefined] - } catch (e) { - return [undefined, `Failed to resolveAllStates:\n${e}`] - } - }, [resolvedTl, now]) - return (
- {errorMsgResolve ?

{errorMsgResolve}

: ''} -

Timeline state

- {errorMsgStates ?

{errorMsgStates}

: } + {errorMsgResolve ? ( +

{errorMsgResolve}

+ ) : ( + + )}

Instances

- +

Events

- +
@@ -183,20 +176,22 @@ function FilterInput({ filterChanged }: FilterInputProps) { interface TimelineStateTableProps { now: number - allStates: ResolvedStates | undefined + resolvedTimeline: ResolvedTimeline | undefined } -function TimelineStateTable({ allStates, now }: TimelineStateTableProps) { +function TimelineStateTable({ resolvedTimeline, now }: TimelineStateTableProps) { const [viewTime, setViewTime] = useState(null) const selectViewTime = useCallback((e: React.ChangeEvent) => { const val = Number(e.target.value) setViewTime(isNaN(val) ? null : val) }, []) - const state = allStates ? Resolver.getState(allStates, viewTime ?? now) : undefined - const times = _.uniq((allStates?.nextEvents ?? []).map((e) => e.time)) - const [layerFilter, setLayerFilter] = useState(undefined) + if (!resolvedTimeline) return null + + const state = getResolvedState(resolvedTimeline, viewTime ?? now, 1) + const times = _.uniq((state?.nextEvents ?? []).map((e) => e.time)) + return (
diff --git a/meteor/package.json b/meteor/package.json index 567f4b50df..41796c48da 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -99,7 +99,7 @@ "react-router-dom": "^5.3.4", "react-timer-hoc": "^2.3.0", "semver": "^7.5.3", - "superfly-timeline": "^8.3.1", + "superfly-timeline": "9.0.0-nightly-wip-big-rewrite-20230818-064242-241b192.0", "threadedclass": "^1.2.1", "timecode": "0.0.4", "type-fest": "^3.10.0", diff --git a/meteor/yarn.lock b/meteor/yarn.lock index a0c4ad71d6..97a870db1c 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1671,7 +1671,7 @@ __metadata: mongodb: ^5.5.0 p-lazy: ^3.1.0 p-timeout: ^4.1.0 - superfly-timeline: ^8.3.1 + superfly-timeline: 9.0.0-nightly-wip-big-rewrite-20230818-064242-241b192.0 threadedclass: ^1.2.1 tslib: ^2.6.0 type-fest: ^3.10.0 @@ -1685,7 +1685,7 @@ __metadata: resolution: "@sofie-automation/shared-lib@portal:../packages/shared-lib::locator=automation-core%40workspace%3A." dependencies: "@mos-connection/model": ^3.0.4 - timeline-state-resolver-types: 9.1.0-nightly-release51-20230816-095601-dcd9f0262.0 + timeline-state-resolver-types: 9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0 tslib: ^2.6.0 type-fest: ^3.10.0 languageName: node @@ -3242,7 +3242,7 @@ __metadata: semver: ^7.5.3 sinon: ^14.0.2 standard-version: ^9.5.0 - superfly-timeline: ^8.3.1 + superfly-timeline: 9.0.0-nightly-wip-big-rewrite-20230818-064242-241b192.0 threadedclass: ^1.2.1 timecode: 0.0.4 ts-jest: ^29.1.1 @@ -12272,13 +12272,12 @@ __metadata: languageName: node linkType: hard -"superfly-timeline@npm:^8.3.1": - version: 8.3.1 - resolution: "superfly-timeline@npm:8.3.1" +"superfly-timeline@npm:9.0.0-nightly-wip-big-rewrite-20230818-064242-241b192.0": + version: 9.0.0-nightly-wip-big-rewrite-20230818-064242-241b192.0 + resolution: "superfly-timeline@npm:9.0.0-nightly-wip-big-rewrite-20230818-064242-241b192.0" dependencies: - tslib: ^2.4.0 - underscore: ^1.13.6 - checksum: 3bc7dc89c6bdc4c427d6e9287c6af77948706d8e7e29524c021d4304eca6ae4bca6c4ef5952d3daa561aca39a72644f04a9c461f89fd6a93fcac7d3674b7d12c + tslib: ^2.6.0 + checksum: fdf314addf4c95579ad9d534e24b48154696e516e158b5bd06c894f8a74fd73ab03287e2bb948a4bb581fba49562c24bd8f3d7e180dc16589f3552461e0184cb languageName: node linkType: hard @@ -12452,12 +12451,12 @@ __metadata: languageName: node linkType: hard -"timeline-state-resolver-types@npm:9.1.0-nightly-release51-20230816-095601-dcd9f0262.0": - version: 9.1.0-nightly-release51-20230816-095601-dcd9f0262.0 - resolution: "timeline-state-resolver-types@npm:9.1.0-nightly-release51-20230816-095601-dcd9f0262.0" +"timeline-state-resolver-types@npm:9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0": + version: 9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0 + resolution: "timeline-state-resolver-types@npm:9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0" dependencies: tslib: ^2.5.1 - checksum: 5d962b6c4dcbc34f507dd8023e5775fad6f801e2ec172ca7b62376fa9e2e541b9ec6242a1a5122ae96b874755917c68ee2322df4ed5f6f97483fb48fcdf55181 + checksum: 13435383e654f3e497291db197b8448f5b94e1846f5c72546987792e287b2be107357f78adb0b4922997591fdc1aca155bc2b5970d2c95553e28f42de5fa6b2d languageName: node linkType: hard @@ -12664,7 +12663,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.4.0": +"tslib@npm:^2.1.0, tslib@npm:^2.2.0": version: 2.5.0 resolution: "tslib@npm:2.5.0" checksum: ae3ed5f9ce29932d049908ebfdf21b3a003a85653a9a140d614da6b767a93ef94f460e52c3d787f0e4f383546981713f165037dc2274df212ea9f8a4541004e1 diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index b2b6ce0061..7421658402 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -51,7 +51,7 @@ "mongodb": "^5.5.0", "p-lazy": "^3.1.0", "p-timeout": "^4.1.0", - "superfly-timeline": "^8.3.1", + "superfly-timeline": "9.0.0-nightly-wip-big-rewrite-20230818-064242-241b192.0", "threadedclass": "^1.2.1", "tslib": "^2.6.0", "type-fest": "^3.10.0", diff --git a/packages/job-worker/src/playout/timeline/generate.ts b/packages/job-worker/src/playout/timeline/generate.ts index a6841c966e..04e9c488d4 100644 --- a/packages/job-worker/src/playout/timeline/generate.ts +++ b/packages/job-worker/src/playout/timeline/generate.ts @@ -480,7 +480,7 @@ function flattenAndProcessTimelineObjects(context: JobContext, timelineObjs: Arr if (o.isGroup && o.children && o.children.length) { for (const child of o.children) { const childFixed: TimelineObjGeneric = { - ...child, + ...(child as TimelineObjGeneric), objectType: o.objectType, inGroup: o.id, priority: o.priority ?? 0, diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index b1034d3bbe..1183494cc4 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -60,7 +60,7 @@ "@sofie-automation/shared-lib": "1.51.0-in-development", "debug": "^4.3.4", "influx": "^5.9.3", - "timeline-state-resolver": "9.1.0-nightly-release51-20230816-095601-dcd9f0262.0", + "timeline-state-resolver": "9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0", "tslib": "^2.6.0", "underscore": "^1.13.6", "winston": "^3.9.0" diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index b0f4f52ac8..fbeed08609 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -39,7 +39,7 @@ ], "dependencies": { "@mos-connection/model": "^3.0.4", - "timeline-state-resolver-types": "9.1.0-nightly-release51-20230816-095601-dcd9f0262.0", + "timeline-state-resolver-types": "9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0", "tslib": "^2.6.0", "type-fest": "^3.10.0" }, diff --git a/packages/yarn.lock b/packages/yarn.lock index 1f5212453d..0f85706c40 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -6136,7 +6136,7 @@ __metadata: mongodb: ^5.5.0 p-lazy: ^3.1.0 p-timeout: ^4.1.0 - superfly-timeline: ^8.3.1 + superfly-timeline: 9.0.0-nightly-wip-big-rewrite-20230818-064242-241b192.0 threadedclass: ^1.2.1 tslib: ^2.6.0 type-fest: ^3.10.0 @@ -6176,7 +6176,7 @@ __metadata: resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: "@mos-connection/model": ^3.0.4 - timeline-state-resolver-types: 9.1.0-nightly-release51-20230816-095601-dcd9f0262.0 + timeline-state-resolver-types: 9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0 tslib: ^2.6.0 type-fest: ^3.10.0 languageName: unknown @@ -19613,7 +19613,7 @@ asn1@evs-broadcast/node-asn1: "@sofie-automation/shared-lib": 1.51.0-in-development debug: ^4.3.4 influx: ^5.9.3 - timeline-state-resolver: 9.1.0-nightly-release51-20230816-095601-dcd9f0262.0 + timeline-state-resolver: 9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0 tslib: ^2.6.0 underscore: ^1.13.6 winston: ^3.9.0 @@ -22962,13 +22962,21 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"superfly-timeline@npm:^8.3.1": - version: 8.3.1 - resolution: "superfly-timeline@npm:8.3.1" +"superfly-timeline@npm:9.0.0-nightly-wip-big-rewrite-20230814-192948-870dbc7.0": + version: 9.0.0-nightly-wip-big-rewrite-20230814-192948-870dbc7.0 + resolution: "superfly-timeline@npm:9.0.0-nightly-wip-big-rewrite-20230814-192948-870dbc7.0" dependencies: - tslib: ^2.4.0 - underscore: ^1.13.6 - checksum: 3bc7dc89c6bdc4c427d6e9287c6af77948706d8e7e29524c021d4304eca6ae4bca6c4ef5952d3daa561aca39a72644f04a9c461f89fd6a93fcac7d3674b7d12c + tslib: ^2.6.0 + checksum: 25888ade1fcb7a8a471a184de11c36754e4a582265b436bcf463e5899b918a929689fe6df8b941d6574e09834380dbef7c1c2d9b709240cbb6ff12d1be33f161 + languageName: node + linkType: hard + +"superfly-timeline@npm:9.0.0-nightly-wip-big-rewrite-20230818-064242-241b192.0": + version: 9.0.0-nightly-wip-big-rewrite-20230818-064242-241b192.0 + resolution: "superfly-timeline@npm:9.0.0-nightly-wip-big-rewrite-20230818-064242-241b192.0" + dependencies: + tslib: ^2.6.0 + checksum: fdf314addf4c95579ad9d534e24b48154696e516e158b5bd06c894f8a74fd73ab03287e2bb948a4bb581fba49562c24bd8f3d7e180dc16589f3552461e0184cb languageName: node linkType: hard @@ -23353,18 +23361,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"timeline-state-resolver-types@npm:9.1.0-nightly-release51-20230816-095601-dcd9f0262.0": - version: 9.1.0-nightly-release51-20230816-095601-dcd9f0262.0 - resolution: "timeline-state-resolver-types@npm:9.1.0-nightly-release51-20230816-095601-dcd9f0262.0" +"timeline-state-resolver-types@npm:9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0": + version: 9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0 + resolution: "timeline-state-resolver-types@npm:9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0" dependencies: tslib: ^2.5.1 - checksum: 5d962b6c4dcbc34f507dd8023e5775fad6f801e2ec172ca7b62376fa9e2e541b9ec6242a1a5122ae96b874755917c68ee2322df4ed5f6f97483fb48fcdf55181 + checksum: 13435383e654f3e497291db197b8448f5b94e1846f5c72546987792e287b2be107357f78adb0b4922997591fdc1aca155bc2b5970d2c95553e28f42de5fa6b2d languageName: node linkType: hard -"timeline-state-resolver@npm:9.1.0-nightly-release51-20230816-095601-dcd9f0262.0": - version: 9.1.0-nightly-release51-20230816-095601-dcd9f0262.0 - resolution: "timeline-state-resolver@npm:9.1.0-nightly-release51-20230816-095601-dcd9f0262.0" +"timeline-state-resolver@npm:9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0": + version: 9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0 + resolution: "timeline-state-resolver@npm:9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0" dependencies: "@tv2media/v-connection": ^7.3.0 atem-connection: 2.4.0 @@ -23385,9 +23393,9 @@ asn1@evs-broadcast/node-asn1: p-queue: ^6.6.2 p-timeout: ^3.2.0 sprintf-js: ^1.1.2 - superfly-timeline: ^8.3.1 + superfly-timeline: 9.0.0-nightly-wip-big-rewrite-20230814-192948-870dbc7.0 threadedclass: ^1.2.1 - timeline-state-resolver-types: 9.1.0-nightly-release51-20230816-095601-dcd9f0262.0 + timeline-state-resolver-types: 9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0 tslib: ^2.5.1 tv-automation-quantel-gateway-client: ^3.1.11-agent-debug-20230117-160455-c2c4ee4.0 underscore: ^1.13.6 @@ -23395,7 +23403,7 @@ asn1@evs-broadcast/node-asn1: utf-8-validate: ^5.0.10 ws: ^7.5.9 xml-js: ^1.6.11 - checksum: a439131d4df7a64c7b19480df7dfff439dbba876a1d5b204dc55fb70169aa22415c786c52ab4d45801135718c71df0844d107565c538feb09e7fd779e5445669 + checksum: 3a88ab5119ec79c6d34bd92780c378564256642bf896f424585bc61cfe87b157acb37195a56905d4e5f56c777f8064462635220acec45a1f05b7240ae68b20fe languageName: node linkType: hard From f448092459ce5d5ae26e952a8aa0df966a230c1a Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Fri, 18 Aug 2023 09:28:33 +0200 Subject: [PATCH 061/479] fix: type fix --- packages/shared-lib/src/core/model/Timeline.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared-lib/src/core/model/Timeline.ts b/packages/shared-lib/src/core/model/Timeline.ts index c1ade04c89..82382715b8 100644 --- a/packages/shared-lib/src/core/model/Timeline.ts +++ b/packages/shared-lib/src/core/model/Timeline.ts @@ -51,7 +51,7 @@ export interface TimelineObjectCoreExt< } export interface TimelineKeyframeCoreExt - extends TSR.Timeline.TimelineKeyframe { + extends TSR.Timeline.TimelineKeyframe> { metaData?: TKeyframeMetadata /** Whether to keep this keyframe when the object is copied for lookahead. By default all keyframes are removed */ preserveForLookahead?: boolean From 34de75da48323fddc37e80fb8237e682a96b1b9d Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Fri, 18 Aug 2023 09:37:05 +0200 Subject: [PATCH 062/479] chore: fix tests & update TSR dep --- meteor/lib/__tests__/timeline.test.ts | 6 ++-- meteor/yarn.lock | 10 +++--- .../blueprints/__tests__/postProcess.test.ts | 4 +-- packages/playout-gateway/package.json | 2 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 33 +++++++------------ 6 files changed, 24 insertions(+), 33 deletions(-) diff --git a/meteor/lib/__tests__/timeline.test.ts b/meteor/lib/__tests__/timeline.test.ts index c065d86935..10dba8ddac 100644 --- a/meteor/lib/__tests__/timeline.test.ts +++ b/meteor/lib/__tests__/timeline.test.ts @@ -68,7 +68,7 @@ describe('lib/timeline', () => { start: 0, }, content: { - // @ts-expect-error temporary ignoring typing issue + deviceType: TSR.DeviceType.ABSTRACT, callBack: 'partPlaybackStarted', callBackData: { rundownId: 'myRundown0', @@ -77,7 +77,7 @@ describe('lib/timeline', () => { callBackStopped: 'partPlaybackStopped', }, layer: 'L1', - partId: 'myPart0', + // partId: 'myPart0', priority: 0, }, { @@ -87,7 +87,7 @@ describe('lib/timeline', () => { start: 0, }, content: { - // @ts-expect-error temporary ignoring typing issue + deviceType: TSR.DeviceType.ABSTRACT, callBack: 'piecePlaybackStarted', callBackData: { rundownId: 'myRundown0', diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 97a870db1c..71d3d0402e 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1685,7 +1685,7 @@ __metadata: resolution: "@sofie-automation/shared-lib@portal:../packages/shared-lib::locator=automation-core%40workspace%3A." dependencies: "@mos-connection/model": ^3.0.4 - timeline-state-resolver-types: 9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0 + timeline-state-resolver-types: 9.1.0-nightly-feat-timeline-v9-20230818-074540-a4d1c490a.0 tslib: ^2.6.0 type-fest: ^3.10.0 languageName: node @@ -12451,12 +12451,12 @@ __metadata: languageName: node linkType: hard -"timeline-state-resolver-types@npm:9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0": - version: 9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0 - resolution: "timeline-state-resolver-types@npm:9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0" +"timeline-state-resolver-types@npm:9.1.0-nightly-feat-timeline-v9-20230818-074540-a4d1c490a.0": + version: 9.1.0-nightly-feat-timeline-v9-20230818-074540-a4d1c490a.0 + resolution: "timeline-state-resolver-types@npm:9.1.0-nightly-feat-timeline-v9-20230818-074540-a4d1c490a.0" dependencies: tslib: ^2.5.1 - checksum: 13435383e654f3e497291db197b8448f5b94e1846f5c72546987792e287b2be107357f78adb0b4922997591fdc1aca155bc2b5970d2c95553e28f42de5fa6b2d + checksum: 8fb4a7f6cc6662f479a48d18b8c6a7ccde96f3ab418325186810bd50eab373136c675e2c167af415f6381fad1ce4c1a045cd942c50efb38764de3233d85c24b0 languageName: node linkType: hard diff --git a/packages/job-worker/src/blueprints/__tests__/postProcess.test.ts b/packages/job-worker/src/blueprints/__tests__/postProcess.test.ts index 7864c2eec2..82ce7e1108 100644 --- a/packages/job-worker/src/blueprints/__tests__/postProcess.test.ts +++ b/packages/job-worker/src/blueprints/__tests__/postProcess.test.ts @@ -617,8 +617,8 @@ describe('Test blueprint post-process', () => { ) // Error in blueprint "blueprint9": Validation of timelineObjs failed: // Error: Object "IJ0Ud5lJhbIllA0_kWFIVz51eL4_": "classes[0]": - // Error: The string "i-am-an-invalid-class" contains a character ("-") which isn't allowed in Timeline (is an operator) - }).toThrow(/error in blueprint.*contains a character/i) + // Error: Error in blueprint "blueprint9": Validation of timelineObjs failed: Error: "classes[0]": Error: The string "i-am-an-invalid-class" contains characters which aren't allowed in Timeline: "-", "-", "-", "-" (is an operator) + }).toThrow(/error in blueprint.*contains characters/i) }) }) }) diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index 1183494cc4..6251b40dc0 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -60,7 +60,7 @@ "@sofie-automation/shared-lib": "1.51.0-in-development", "debug": "^4.3.4", "influx": "^5.9.3", - "timeline-state-resolver": "9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0", + "timeline-state-resolver": "9.1.0-nightly-feat-timeline-v9-20230818-074540-a4d1c490a.0", "tslib": "^2.6.0", "underscore": "^1.13.6", "winston": "^3.9.0" diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index fbeed08609..481488ea6b 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -39,7 +39,7 @@ ], "dependencies": { "@mos-connection/model": "^3.0.4", - "timeline-state-resolver-types": "9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0", + "timeline-state-resolver-types": "9.1.0-nightly-feat-timeline-v9-20230818-074540-a4d1c490a.0", "tslib": "^2.6.0", "type-fest": "^3.10.0" }, diff --git a/packages/yarn.lock b/packages/yarn.lock index 0f85706c40..545846dec2 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -6176,7 +6176,7 @@ __metadata: resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: "@mos-connection/model": ^3.0.4 - timeline-state-resolver-types: 9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0 + timeline-state-resolver-types: 9.1.0-nightly-feat-timeline-v9-20230818-074540-a4d1c490a.0 tslib: ^2.6.0 type-fest: ^3.10.0 languageName: unknown @@ -19613,7 +19613,7 @@ asn1@evs-broadcast/node-asn1: "@sofie-automation/shared-lib": 1.51.0-in-development debug: ^4.3.4 influx: ^5.9.3 - timeline-state-resolver: 9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0 + timeline-state-resolver: 9.1.0-nightly-feat-timeline-v9-20230818-074540-a4d1c490a.0 tslib: ^2.6.0 underscore: ^1.13.6 winston: ^3.9.0 @@ -22962,15 +22962,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"superfly-timeline@npm:9.0.0-nightly-wip-big-rewrite-20230814-192948-870dbc7.0": - version: 9.0.0-nightly-wip-big-rewrite-20230814-192948-870dbc7.0 - resolution: "superfly-timeline@npm:9.0.0-nightly-wip-big-rewrite-20230814-192948-870dbc7.0" - dependencies: - tslib: ^2.6.0 - checksum: 25888ade1fcb7a8a471a184de11c36754e4a582265b436bcf463e5899b918a929689fe6df8b941d6574e09834380dbef7c1c2d9b709240cbb6ff12d1be33f161 - languageName: node - linkType: hard - "superfly-timeline@npm:9.0.0-nightly-wip-big-rewrite-20230818-064242-241b192.0": version: 9.0.0-nightly-wip-big-rewrite-20230818-064242-241b192.0 resolution: "superfly-timeline@npm:9.0.0-nightly-wip-big-rewrite-20230818-064242-241b192.0" @@ -23361,18 +23352,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"timeline-state-resolver-types@npm:9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0": - version: 9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0 - resolution: "timeline-state-resolver-types@npm:9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0" +"timeline-state-resolver-types@npm:9.1.0-nightly-feat-timeline-v9-20230818-074540-a4d1c490a.0": + version: 9.1.0-nightly-feat-timeline-v9-20230818-074540-a4d1c490a.0 + resolution: "timeline-state-resolver-types@npm:9.1.0-nightly-feat-timeline-v9-20230818-074540-a4d1c490a.0" dependencies: tslib: ^2.5.1 - checksum: 13435383e654f3e497291db197b8448f5b94e1846f5c72546987792e287b2be107357f78adb0b4922997591fdc1aca155bc2b5970d2c95553e28f42de5fa6b2d + checksum: 8fb4a7f6cc6662f479a48d18b8c6a7ccde96f3ab418325186810bd50eab373136c675e2c167af415f6381fad1ce4c1a045cd942c50efb38764de3233d85c24b0 languageName: node linkType: hard -"timeline-state-resolver@npm:9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0": - version: 9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0 - resolution: "timeline-state-resolver@npm:9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0" +"timeline-state-resolver@npm:9.1.0-nightly-feat-timeline-v9-20230818-074540-a4d1c490a.0": + version: 9.1.0-nightly-feat-timeline-v9-20230818-074540-a4d1c490a.0 + resolution: "timeline-state-resolver@npm:9.1.0-nightly-feat-timeline-v9-20230818-074540-a4d1c490a.0" dependencies: "@tv2media/v-connection": ^7.3.0 atem-connection: 2.4.0 @@ -23393,9 +23384,9 @@ asn1@evs-broadcast/node-asn1: p-queue: ^6.6.2 p-timeout: ^3.2.0 sprintf-js: ^1.1.2 - superfly-timeline: 9.0.0-nightly-wip-big-rewrite-20230814-192948-870dbc7.0 + superfly-timeline: 9.0.0-nightly-wip-big-rewrite-20230818-064242-241b192.0 threadedclass: ^1.2.1 - timeline-state-resolver-types: 9.1.0-nightly-feat-timeline-v9-20230818-050500-38a0a228a.0 + timeline-state-resolver-types: 9.1.0-nightly-feat-timeline-v9-20230818-074540-a4d1c490a.0 tslib: ^2.5.1 tv-automation-quantel-gateway-client: ^3.1.11-agent-debug-20230117-160455-c2c4ee4.0 underscore: ^1.13.6 @@ -23403,7 +23394,7 @@ asn1@evs-broadcast/node-asn1: utf-8-validate: ^5.0.10 ws: ^7.5.9 xml-js: ^1.6.11 - checksum: 3a88ab5119ec79c6d34bd92780c378564256642bf896f424585bc61cfe87b157acb37195a56905d4e5f56c777f8064462635220acec45a1f05b7240ae68b20fe + checksum: 1cb73d2c720cc445a70281665b3eec848efe76b859426c1d1418f80d0bacfc9f652da0d2aef4d6303bd01474339fce5a0aad80440a98969c3b98a0a056fb3b65 languageName: node linkType: hard From b181c76c0e70e9afe3f7074661a2aacb6cab1ae1 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Fri, 18 Aug 2023 16:37:20 +0200 Subject: [PATCH 063/479] chore: package.json formating --- meteor/package.json | 2 +- packages/blueprints-integration/package.json | 2 +- packages/corelib/package.json | 2 +- packages/documentation/package.json | 2 +- packages/job-worker/package.json | 2 +- packages/live-status-gateway/package.json | 2 +- packages/mos-gateway/package.json | 2 +- packages/openapi/package.json | 2 +- packages/playout-gateway/package.json | 2 +- packages/server-core-integration/package.json | 2 +- packages/shared-lib/package.json | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/meteor/package.json b/meteor/package.json index 677ab48e01..567f4b50df 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -206,4 +206,4 @@ "@sofie-automation/shared-lib": "portal:../packages/shared-lib" }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} diff --git a/packages/blueprints-integration/package.json b/packages/blueprints-integration/package.json index d0d3a98393..3c2ab8b965 100644 --- a/packages/blueprints-integration/package.json +++ b/packages/blueprints-integration/package.json @@ -51,4 +51,4 @@ ] }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} diff --git a/packages/corelib/package.json b/packages/corelib/package.json index 89dff54aaf..c08081b630 100644 --- a/packages/corelib/package.json +++ b/packages/corelib/package.json @@ -65,4 +65,4 @@ ] }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} diff --git a/packages/documentation/package.json b/packages/documentation/package.json index 3153d34a2c..b0510c87a8 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -38,4 +38,4 @@ "last 1 safari version" ] } -} \ No newline at end of file +} diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index 4590b55b7d..b2b6ce0061 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -68,4 +68,4 @@ ] }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} diff --git a/packages/live-status-gateway/package.json b/packages/live-status-gateway/package.json index 002020d6cb..9bb26856a6 100644 --- a/packages/live-status-gateway/package.json +++ b/packages/live-status-gateway/package.json @@ -76,4 +76,4 @@ "yarn lint:raw" ] } -} \ No newline at end of file +} diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index 27711cdeda..ec84b1ce7f 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -83,4 +83,4 @@ ] }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} diff --git a/packages/openapi/package.json b/packages/openapi/package.json index a2576877b0..e2072b6d70 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -52,4 +52,4 @@ "yarn lint:raw" ] } -} \ No newline at end of file +} diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index 5cf0bd0648..b1034d3bbe 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -74,4 +74,4 @@ ] }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} diff --git a/packages/server-core-integration/package.json b/packages/server-core-integration/package.json index 4ba40a8e6e..7e2c135dc0 100644 --- a/packages/server-core-integration/package.json +++ b/packages/server-core-integration/package.json @@ -87,4 +87,4 @@ ] }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index af0bc8c00f..b0f4f52ac8 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -53,4 +53,4 @@ ] }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} From d123037ec455743d3e5fbe54a886a5cc7d287dd1 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Sun, 20 Aug 2023 13:30:04 +0200 Subject: [PATCH 064/479] feat: add "processing" warning notification --- meteor/client/lib/ui/icons/notifications.tsx | 19 +++++++++++++++++- .../ui/MediaStatus/MediaStatusIndicator.tsx | 11 ++++++++-- .../MediaStatusPopUp/MediaStatusPopUpItem.tsx | 4 +++- .../ui/RundownView/MediaStatusPopUp/index.tsx | 3 +++ .../SegmentTimeline/withMediaObjectStatus.tsx | 1 + .../media-status/MediaStatusListItem.tsx | 4 +++- .../client/ui/Status/media-status/index.tsx | 3 +++ meteor/lib/mediaObjects.ts | 2 ++ .../public/images/warning-transferring.webp | Bin 0 -> 7468 bytes .../checkPieceContentStatus.ts | 12 +++++++++++ 10 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 meteor/public/images/warning-transferring.webp diff --git a/meteor/client/lib/ui/icons/notifications.tsx b/meteor/client/lib/ui/icons/notifications.tsx index 0ade64292e..e8d25b20c8 100644 --- a/meteor/client/lib/ui/icons/notifications.tsx +++ b/meteor/client/lib/ui/icons/notifications.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import React from 'react' export function CriticalIcon(): JSX.Element { return ( @@ -153,6 +153,23 @@ export function WarningIconSmall(): JSX.Element { ) } +// const WARNING_WORKING_ON_IT_CONFIG = { +// loop: true, +// autoplay: true, +// animationData: WarningIconSmallWorkingOnItAnimation, +// rendererSettings: { +// preserveAspectRatio: 'xMidYMid meet', +// }, +// } + +// export function WarningIconSmallWorkingOnIt(): JSX.Element { +// return +// } + +export function WarningIconSmallWorkingOnIt(): JSX.Element { + return Warning +} + export function InformationIconSmall(): JSX.Element { return ( + icon = isWorking ? : break default: assertNever(status) diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.tsx b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.tsx index 005bd307e1..ea742ce45f 100644 --- a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.tsx +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.tsx @@ -15,6 +15,7 @@ export const MediaStatusItem = withTiming< segmentId: SegmentId | undefined partInstanceId: PartInstanceId | undefined status: PieceStatusCode + isWorkingOn: boolean statusOverlay?: string | undefined sourceLayerType?: SourceLayerType | undefined sourceLayerName?: string | undefined @@ -35,6 +36,7 @@ export const MediaStatusItem = withTiming< partInstanceId, segmentId, status, + isWorkingOn, statusOverlay, sourceLayerType, sourceLayerName, @@ -83,7 +85,7 @@ export const MediaStatusItem = withTiming< ) : null}
- - - - - - - ) -} diff --git a/meteor/client/ui/Settings/Upgrades/Components.tsx b/meteor/client/ui/Settings/Upgrades/Components.tsx new file mode 100644 index 0000000000..4829c1b265 --- /dev/null +++ b/meteor/client/ui/Settings/Upgrades/Components.tsx @@ -0,0 +1,172 @@ +import React, { useCallback } from 'react' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faDatabase, faEye } from '@fortawesome/free-solid-svg-icons' +import { MeteorCall } from '../../../../lib/api/methods' +import { TFunction, useTranslation } from 'react-i18next' +import { i18nTranslator } from '../../i18n' +import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' +import { doModalDialog } from '../../../lib/ModalDialog' +import { NoteSeverity } from '@sofie-automation/blueprints-integration' +import { NotificationCenter, NoticeLevel, Notification } from '../../../../lib/notifications/notifications' +import { + UIBlueprintUpgradeStatusBase, + UIBlueprintUpgradeStatusShowStyle, + UIBlueprintUpgradeStatusStudio, +} from '../../../../lib/api/upgradeStatus' +import { assertNever } from '@sofie-automation/corelib/dist/lib' +import { catchError } from '../../../lib/lib' + +export function getUpgradeStatusMessage(t: TFunction, upgradeResult: UIBlueprintUpgradeStatusBase): string | null { + if (upgradeResult.invalidReason) + return `${t('Unable to upgrade')}: ${translateMessage(upgradeResult.invalidReason, i18nTranslator)}` + + if (upgradeResult.changes.length > 0) return t('Upgrade required') + + return null +} + +interface UpgradeStatusButtonsProps { + upgradeResult: UIBlueprintUpgradeStatusStudio | UIBlueprintUpgradeStatusShowStyle +} +export function UpgradeStatusButtons({ upgradeResult }: UpgradeStatusButtonsProps): JSX.Element { + const { t } = useTranslation() + + const validateConfig = useCallback(async () => { + switch (upgradeResult.documentType) { + case 'studio': + return MeteorCall.migration.validateConfigForStudio(upgradeResult.documentId) + case 'showStyle': + return MeteorCall.migration.validateConfigForShowStyleBase(upgradeResult.documentId) + default: + assertNever(upgradeResult) + throw new Error(`Unknown UIBlueprintUpgradeStatusBase documentType`) + } + }, [upgradeResult.documentId, upgradeResult.documentType]) + const applyConfig = useCallback(async () => { + switch (upgradeResult.documentType) { + case 'studio': + return MeteorCall.migration.runUpgradeForStudio(upgradeResult.documentId) + case 'showStyle': + return MeteorCall.migration.runUpgradeForShowStyleBase(upgradeResult.documentId) + default: + assertNever(upgradeResult) + throw new Error(`Unknown UIBlueprintUpgradeStatusBase documentType`) + } + }, [upgradeResult.documentId, upgradeResult.documentType]) + + const clickValidate = useCallback(() => { + validateConfig() + .then((res) => { + const nonInfoMessagesCount = res.messages.filter((msg) => msg.level !== NoteSeverity.INFO).length + + doModalDialog({ + title: t('Upgrade config for {{name}}', { name: upgradeResult.name }), + message: ( +
+ {res.messages.length === 0 &&

{t('Config looks good')}

} + {res.messages.map((msg, i) => ( +

+ {NoteSeverity[msg.level]}: {translateMessage(msg.message, i18nTranslator)} +

+ ))} +
+ ), + yes: nonInfoMessagesCount === 0 ? t('Apply') : t('Ignore and apply'), + no: t('Cancel'), + onAccept: () => { + applyConfig() + .then(() => { + NotificationCenter.push( + new Notification( + undefined, + NoticeLevel.NOTIFICATION, + t('Config for {{name}} upgraded successfully', { name: upgradeResult.name }), + 'UpgradesView' + ) + ) + }) + .catch((e) => { + catchError('Upgrade applyConfig')(e) + NotificationCenter.push( + new Notification( + undefined, + NoticeLevel.WARNING, + t('Config for {{name}} upgraded failed', { name: upgradeResult.name }), + 'UpgradesView' + ) + ) + }) + }, + }) + }) + .catch(() => { + doModalDialog({ + title: t('Upgrade config for {{name}}', { name: upgradeResult.name }), + message: t('Failed to validate config'), + yes: t('Ignore and apply'), + no: t('Cancel'), + onAccept: () => { + applyConfig() + .then(() => { + NotificationCenter.push( + new Notification( + undefined, + NoticeLevel.NOTIFICATION, + t('Config for {{name}} upgraded successfully', { name: upgradeResult.name }), + 'UpgradesView' + ) + ) + }) + .catch((e) => { + catchError('Upgrade applyConfig')(e) + NotificationCenter.push( + new Notification( + undefined, + NoticeLevel.WARNING, + t('Config for {{name}} upgraded failed', { name: upgradeResult.name }), + 'UpgradesView' + ) + ) + }) + }, + }) + }) + }, [upgradeResult, validateConfig, applyConfig]) + + const clickShowChanges = useCallback(() => { + doModalDialog({ + title: t('Upgrade config for {{name}}', { name: upgradeResult.name }), + message: ( +
+ {upgradeResult.changes.length === 0 &&

{t('No changes')}

} + {upgradeResult.changes.map((msg, i) => ( +

{translateMessage(msg, i18nTranslator)}

+ ))} +
+ ), + acceptOnly: true, + yes: t('Dismiss'), + onAccept: () => { + // Do nothing + }, + }) + }, [upgradeResult]) + + return ( +
+ + + +
+ ) +} diff --git a/meteor/client/ui/Settings/Upgrades/View.tsx b/meteor/client/ui/Settings/Upgrades/View.tsx new file mode 100644 index 0000000000..059755e852 --- /dev/null +++ b/meteor/client/ui/Settings/Upgrades/View.tsx @@ -0,0 +1,86 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { Spinner } from '../../../lib/Spinner' +import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' +import { useSubscription, useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData' +import { PubSub } from '../../../../lib/api/pubsub' +import { UIBlueprintUpgradeStatuses } from '../../Collections' +import { UIBlueprintUpgradeStatusShowStyle, UIBlueprintUpgradeStatusStudio } from '../../../../lib/api/upgradeStatus' +import { getUpgradeStatusMessage, UpgradeStatusButtons } from './Components' + +export function UpgradesView(): JSX.Element { + const { t } = useTranslation() + + const isReady = useSubscription(PubSub.uiBlueprintUpgradeStatuses) + + const statuses = useTracker(() => UIBlueprintUpgradeStatuses.find().fetch(), []) + + return ( +
+

{t('Apply blueprint upgrades')}

+ +
+ {(!isReady || !statuses) && } + +
+ +
+ {!emptyFilter ? t('No Media matches this filter') : t('No Media required for this Rundown')} +
{label} +
{label}
+
{duration ? formatTime(duration) : null}
+
+ {emptyFilter ? t('No Media required by this system') : t('No Media matches this filter')} +
- {segmentIdentifier ? ( -
{segmentIdentifier}
- ) : null} - {partIdentifier ?
{partIdentifier}
: null} -
{label}
- changeSortOrder('sourceLayer', order)} /> .media-status-item__label { - font-weight: 400; + font-weight: 500; } > .media-status-item__label { @@ -45,6 +45,10 @@ } } + > .media-status-item__identifiers { + text-align: center; + } + .media-status-item__source-layer-indicator { position: relative; color: #fff; @@ -106,5 +110,6 @@ color: #fff; font-weight: 400; font-size: 0.875em; + min-width: 2.5em; } } diff --git a/meteor/client/ui/Status/media-status/MediaStatusListItem.tsx b/meteor/client/ui/Status/media-status/MediaStatusListItem.tsx index 1449e12b8b..51aba09c34 100644 --- a/meteor/client/ui/Status/media-status/MediaStatusListItem.tsx +++ b/meteor/client/ui/Status/media-status/MediaStatusListItem.tsx @@ -40,6 +40,10 @@ export function MediaStatusListItem({ {rundownTo ? {rundownName} : rundownName} + {segmentIdentifier ?
{segmentIdentifier}
: null} + {partIdentifier ?
{partIdentifier}
: null} +
- {segmentIdentifier ?
{segmentIdentifier}
: null} - {partIdentifier ?
{partIdentifier}
: null} -
{label}
- +
translateMessage(message, t)) .join(', ')} status={item.status} + isWorkingOn={ + item.pieceContentStatus?.progress !== undefined && item.pieceContentStatus?.progress > 0 + } isAdLib={item.isAdLib} isLive={isLive} isNext={isNext} diff --git a/meteor/client/ui/SegmentTimeline/withMediaObjectStatus.tsx b/meteor/client/ui/SegmentTimeline/withMediaObjectStatus.tsx index 40be71117b..bc4b86b720 100644 --- a/meteor/client/ui/SegmentTimeline/withMediaObjectStatus.tsx +++ b/meteor/client/ui/SegmentTimeline/withMediaObjectStatus.tsx @@ -30,6 +30,7 @@ type IWrappedComponent = new (props: IProps, st const DEFAULT_STATUS = deepFreeze({ status: PieceStatusCode.UNKNOWN, messages: [], + progress: undefined, blacks: [], freezes: [], diff --git a/meteor/client/ui/Status/media-status/MediaStatusListItem.tsx b/meteor/client/ui/Status/media-status/MediaStatusListItem.tsx index 51aba09c34..fd1369b301 100644 --- a/meteor/client/ui/Status/media-status/MediaStatusListItem.tsx +++ b/meteor/client/ui/Status/media-status/MediaStatusListItem.tsx @@ -11,6 +11,7 @@ export function MediaStatusListItem({ rundownName, rundownTo, status, + isWorkingOn, statusOverlay, sourceLayerType, sourceLayerName, @@ -23,6 +24,7 @@ export function MediaStatusListItem({ rundownName: string rundownTo?: string status: PieceStatusCode + isWorkingOn: boolean statusOverlay?: string | undefined sourceLayerType?: SourceLayerType | undefined sourceLayerName?: string | undefined @@ -45,7 +47,7 @@ export function MediaStatusListItem({ {partIdentifier ?
{partIdentifier}
: null}
- +
0 + } statusOverlay={item.pieceContentStatus?.messages .map((message) => translateMessage(message, t)) .join(', ')} diff --git a/meteor/lib/mediaObjects.ts b/meteor/lib/mediaObjects.ts index d59a024f61..28f4e3768d 100644 --- a/meteor/lib/mediaObjects.ts +++ b/meteor/lib/mediaObjects.ts @@ -27,4 +27,6 @@ export interface PieceContentStatusObj { packageName: string | null contentDuration: number | undefined + + progress: number | undefined } diff --git a/meteor/public/images/warning-transferring.webp b/meteor/public/images/warning-transferring.webp new file mode 100644 index 0000000000000000000000000000000000000000..e250c495ea2448506b3254e630a61c605d8765d5 GIT binary patch literal 7468 zcma)>byU;wzs5(G)aa0Mv@}dgI!8?eMM^q10j0Y`0b!B?f=HJrFi<)a6a+-c0V74a z8@4<3=RNn2>$!Jl`{T2-owM`$K6~EJ`}rE{YH5M40swb4)u4BvvS#D}004-4(fxj^ z8tNJl<4#!I0{~DpG|;lf!ySJ={(OQ10Kmh2PhSi7z8GBxAq~l^0yvvoM#-_1ED;>l z_eQ!kzcZ#%2SG3Vl%-vz`}FQZ;}cuJ2|ti*6e7@&(K@5o>TI~i9;A0+G^5A+efFqi z_SJW}@44pHj|61+8yd%Q#}LpR&TB5bWB6f|X^?v04$Gq3v4i12c{?&;jV+wWM5yqn|ug$ zSJy~XXdA1_xo^$OIChlu=BmrIPW8J`WPS5pg~FGwLrOQ!Ka)!GN1$)+?lG@iAE>_% z@LVh3>&xk)>xk=(nEtXwvQe^TBbg^`VxA|_5Z(O6Pg2YM8zY)cRtZg*uYF?ii?Yo> zC~N64oCkOMpNLa+{m%_7f;Mb9dbqw7I>vF-2Ue zL(bT{mDX+S0UU*dOJSieq~d>K3BzmB`ZdcJGbjwx8`X8XD}VR}%o>3i25Gae==7Jz zW6-Z{6>~-EfxT(ov=sR@>Z$^}Q`dNE41c8Lieniyz6RY+A?vx%mbQ_fh$k3|iPxR4 z`KWh-*yjH1)JU8ke~z zUz10I#*IU)3D3*#`%tvDk6!MIvKLyl@j=4&t2E<;mEE?FU$H2^HEj9$QAlj3q8`Yk zG5RbF&Hu7Sukn10lFA(gewEqc55o~geGB9FBB8y^2Mni6<_P*EFL%BCJj@f1R83mB zIJFKDx^87B&3neES&#L4tRYIRT-_tj%@$Aj5N6szI2)wVGb7y5H~A4;C2+|$<AnC!mM6bS8kGK-T@qVe=;UBd1|I!}nDu9Eu z127;?)$Je!DQV{Vq$Po&{H1F@Lp+@Mn8%Tuc40(i>exq{!CKjzJ%Anlf(QTuuqDMN z@~R~Jj!NuRYi8SUQfiR@d?bO8gU-&RE<#5ow?uah0crpy81SrS{LJvbk6dc(W^Oau z<5xB2^+LnYrI134MhOrM5#aj*1hintak!^b0Rkd47!o$$>ufcX^ynZBEY7Q(QOJN@ z*g4y2+{51v5`IM{rkfAmb~#uo2)5CWclEMDw{c=w)!6A)V2s9&zSGHlipo!%WK_2r z$#!5YCaFj?7~qK!DoV$n78Z8Bti~tTT{no{z?9~u?LX4)TTvbh&2C!$Zqjj5N}uW9 zBx7Pd#=-O0hP_>KQAv`cCDcZ>ci-OsdYLTT(^K6?9#(z?zTy zftK$5U#z|V!P??4Yo_k9B#^exMZ~Of>8cxRA`1MlXuW;-d__7V$_tJXMT(DxvmPja zi(yrIHIPXLz`lj1sNwT#_;agKQ*6|B5hz^WQ6mu{^yliLV5;)$qM~gBBhyCs%IOJ3 zEJV!eT^i^Ww{sfJrA+WKplk{CRV?zI{DQ-kw`w{rK0M)@f))te60v=lU@x8;3zz zqZcSt0RD45w%pR!mf`((Q*hEN|vrXG4Ib!lQ#---xNvJ8E_R5@p&Q2l#w?{pB0S=M%tDZq9v zF{A5N_;w&vN%bYpV@}~U&ZuqdMO4!uS0R>81>uWCCD&^5G|+haLj~PQxappbx@411 zD6X5k-V5xmRMYMs$ar%-HuSw`S}$m+L`BE?Q+Jt6LS zrDM210S2A8ID8fcDIfFE4bH!_F54njjJdzi#tTen0DW&G_~SQihrbRvxMm_|FNoNm^=X8_Y-XUpFc|YdXaK_%dFUkmd`T;bu z!0n3nTmAoX9?QS#=Uz3N*JYQ4HPj*;qnGw_F=MGQ;TJ-wPmVl0hZ7r~{+j*NQP4~y` z6)c|$qP#}KxlqW}pM3l{*Fokn=;FE_U3tm zjeN8Y%j}50pqW|vxaDMB4$n$6#OHnLJqPDFPg-i=3)CPn+A$150%txVI(^TSDa$!JW7EM2E8yJ)$_D#1$X zdyNpJ+)BzRm~+&?NADsO2>GJ3v6fNxRxc#qY6e&VDg2DxR@^k6sd-W%xp+(vp9;%C z9$5_i;(rRihGqYc_=_NQ_XG#Z8Rw|~sdcNgWn{(hX!f1nr%>2uPzX*8-AC;e& zeI-dbV>1sZyZk^1uvAiW6J>L=DG9p^rh?Z3ojO+)${p+C^lXeSttamtJeF)a#`@gs zW*FLwzifFI-xp1FQsZb(m)e-Jz;Jq&h@kR&;_LBNG1qYX#_?0J`)^YX>H1{eN390e zDOjOrm0MesmSh~f-xJ9vfkc0+|NqQ5?Og)61EFrcy-d{3S?5k|>a7!(VDVmRshhE>8DdH!616q|7J2TgO)gi;64PQsf5o*jVI zfQ;?^kh$&9r;G1Agl-i>7T$xtzlFN7S64phwSDf*rkxN{TJlL2Vxn8-YOGoQz;y80 z6c|!2G{fa?T=5v9WYx?&*MWrB1-x@L{xxiU6|ir5{y0ua?#!UzK7d1$(Ra^A1cEmc3@?Baex-P+dCM7OXk>xD-%bTv=%VE#TnKr6l0`g4pz^zOh2uWh2W9S`&J zGc5_f3&5^_$qJ*yx^D_cv4>N{b{K+o$RtF=p_}bt@4%QAy&}KE2l5Z}*-#GAA5n&t zlQ8gFgK@^aoGYo_ZeWgsX80T9MSHHw@z=xdygtA46~RYEk+PIA&ACK3(Rsc&@)4c-sK5+`ylzM4oiVoseCg$q z!d{^s%`mL^Ue8+x>^gW|1!3{9z1y`KZuF?bYuJpPpx}AUqgrBnmA5Let6UjGk_7Ua zjS6@N69*#ji9Dap zTXY_vlgjfEHO9Ng5)9DC0 z{kitUC0OXg5(W#8?+D(r3rzTsDiD**st%CcS#@hVzh>Kb&jpL23nWfnU-`%f2mnFp z0m&MKZZ!~b`UyDDOm5eRaBTi1M)Xb+;ezyou)Fwz79XBJc}~u{zg8z?M;^SV_|Vd_ z23D%C(5~4760chzlJM6Tsm~B}kUP1=1ix-=E{#*cqnfK=IV;E{`mO#u|F|Mbf3HY> zS1rzDrou+LDTilIb;oQbkZ{nZ^6EUX%k~ARQx*Z^y(IE81d^YwsPM3h04|#6;+P<$ zP}OKg*0O>)Ah14VKKv?JM_h$~CD6qtoFR6GspbgwCEWS=MYe?qDv_lcJO2DlrDGkh zId+KeR0*xU7Xp&w(#SCA{ zn!4^rAAkt)q;`DT!Or%>zxwvTz&@ik&1{f$vT38?ld0{*bTV(glol}pjU^qW5y?%D zQC0dEgWMtQe0>NE4B6bh8yujNKh%F(TyCbnyZiW&r$}RZq zxId~|8@87cHxXR{2kN3g;Dl;zRN#TY?d-sV6KWI&EBp9;3#Uq=1FF6O9r%mvJM_wJ z%o|H*ny*+gE(1!@Ox0}89pSg@7MiVAx=qk($1d_|Wb>MzZm0cKLL}{OI`9RMOheHbKpX&$@#1QJcGgtsIT(r{1!d0-c8aP2ZrY39*O3)!m}*Cc`cK zUx_60A4G9s@lRg}Y#K$17$I0BRlzz!oi}vA1&>f%SJRKSpkG}zHCGH*x3(^B9HEH4 z&SyOfoN)de#gY|H$@LBnU>lwW7o^rVs~5WRe4S3%+H3Cji}_}bdO_^TF3s2rC@WcK zuj*t5-Qz-1(e&eIbH88p<>#^!jvlUvmYcxS&5Fdw+@|c+-nr{E_g+E@<=b?;$Lor( zWk7R8@V1>-PMxL`UdDCp;qk(nD%g&&B!^#>jy83Z_w)upVtV~%32SJ1Yf`X2^^Xs2 z_%f%D;A#l@6t^za>$KDLL)0Ek%eAo15!%JuG&9*5y?&|6TTk~leiJw^n&TF6yMR(bzZDOYUJH?4k~Bo*+? z5{;K$I%}Eas{&mxo1}1KS-tB4*|`jnG)cfRz#(O$%9>lMI8AbxL*^a{wGI!oi$j!e z|4n?uZ*Bjp>oWLzCB_$iWkU$YeTq%=73yhE)kq@ZKJjhI>a6V5Cd=*K~;hjkZG&6_`~t ztf!T$%{wtPGp8HN4cctf0F9cqINgW)+hqlJy0y`=yC2Au@jmOnHsZ+hpDpOuuK&OFu<1M+3ghu7iBp5~q<# zMcA;0(m^FdJCa|yJf3gTF(m<#lI6s3SWKJ8M#1khAnk%e9C7tFI!r@AlU2W`b)aMN(+eF&$;LI4Of zgF&C4g(a{^!4N{GM;T>%Ax@T=E8M9j_bAvuBH{AK`M*Zurze|5So1c8Uo$bKLOxEN ze~7~0@t#cSm z7n$=f$r|I!ckF5OGwk`ehvqYB)kNlLYrU~o7EG&SR>CeMfu94EX(xN9$&E>B>59?$ zX+>JsG=7jBw_1scs+F z(^auT4ep&>6=kKuHC><(?1vw+3drBOX00wJ$`899t%g4_L=@g+ST)DgdCJFNx z;8eRi3{px4q_~nuTON&iq(&G{48H6S-R@v5kMV{j_ZvN?T#k?WFr4DiVFhJn-db@+ zXbWxe@$9RBNzmj28d$OITh`_EPY-TRq7LiP*O|V3szp_0$`89gYCJz|LGQ(rG&npA zAbhX?pFQVaTmFCdi~P8T1*9#9ky#Wwvr{cRQnVV<45_b-p#7fSeGzKU)~ z`MVP0GDN1>8RtS?OqeyzxqVDlve)NIo`tW@Mf1t6L-Fy&5z_{a6QoZFb5AJg2dB}r zx=~i;&-BC29Q$g2j6a)LzmPG`!^9uO|DWuc7JuCxs@UR?-SODJJ{?6C z3&&N83!qkKtc*v@-;U)+KBENbHfQxqDnO_%0w_vWuKjf1_vY3(N5Z{Pr*|7)7hA4o zu5)j4n?5SPV{j&BEK_dYRUD^R*vla*D*7pBY0pSa4e`h)^`eGL7hz%mDTEardjy4M z*~SrZOxVUU5&wJ{MFPfk@kE9bwo%lEC-c9Kwn1%IXkaM$*%lo4<_qH+uCQ4*Ox?-N z-{r1gpw;WwBTkbG`Xe=^Qq$W-i8Ej$&+E3PchZS}^rn~f$Bp9kbmuck^cPi-w@gkN z_ne}a@WaiQwy)Qdx;|$ooKTzduRM-A7TtZclyjcs7pzk!H`MkjNp+`nr*%cYC4-5( zK9-JWs(w^19&V>Dy(iCoHt(tt7crHo@3@y9_zKl-yYcR6_o@slt%MCM9!&=?y$>w4 IKrjLR3q1)@lK=n! literal 0 HcmV?d00001 diff --git a/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts index 038e6e3548..3023786520 100644 --- a/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts +++ b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts @@ -243,6 +243,7 @@ export async function checkPieceContentStatusAndDependencies( { status: PieceStatusCode.UNKNOWN, messages: [], + progress: undefined, freezes: [], blacks: [], @@ -425,6 +426,7 @@ async function checkPieceContentMediaObjectStatus( return { status: pieceStatus, messages: messages.map((msg) => msg.message), + progress: 0, freezes, blacks, @@ -487,6 +489,7 @@ async function checkPieceContentExpectedPackageStatus( let thumbnailUrl: string | undefined let previewUrl: string | undefined + let progress: number | undefined if (piece.expectedPackages && piece.expectedPackages.length) { const routes = getActiveRoutes(studio.routeSets) @@ -562,6 +565,8 @@ async function checkPieceContentExpectedPackageStatus( } } + progress = getPackageProgress(packageOnPackageContainer) ?? undefined + const warningMessage = getPackageWarningMessage(packageOnPackageContainer, sourceLayer) if (warningMessage) { messages.push(warningMessage) @@ -670,6 +675,7 @@ async function checkPieceContentExpectedPackageStatus( return { status: pieceStatus, messages: messages.map((msg) => msg.message), + progress, freezes, blacks, @@ -715,6 +721,12 @@ function getAssetUrlFromExpectedPackages( } } +function getPackageProgress( + packageOnPackageContainer: Pick | undefined +): number | null { + return packageOnPackageContainer?.status.progress ?? null +} + function getPackageWarningMessage( packageOnPackageContainer: Pick | undefined, sourceLayer: ISourceLayer From 5682379d85df92b1b1a6d2f08e1a485f701387e1 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Sun, 20 Aug 2023 13:31:16 +0200 Subject: [PATCH 065/479] fix(HelpPanel): invalid child React error --- meteor/client/ui/Shelf/HotkeyHelpPanel.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meteor/client/ui/Shelf/HotkeyHelpPanel.tsx b/meteor/client/ui/Shelf/HotkeyHelpPanel.tsx index d38876f072..419d1ffca9 100644 --- a/meteor/client/ui/Shelf/HotkeyHelpPanel.tsx +++ b/meteor/client/ui/Shelf/HotkeyHelpPanel.tsx @@ -89,8 +89,8 @@ export const HotkeyHelpPanel: React.FC = function HotkeyHelpPanel({ visi .concat(genericMountedTriggers) .concat(adLibMountedTriggers) .concat(showStyleBase.hotkeyLegend || []) - .map((hotkey) => ( -
+ .map((hotkey, index) => ( +
{hotkeyHelper.shortcutLabel(hotkey.key, _isMacLike)}
From 230cb9afcd64aa76d7b1283abbaf80e6f3214640 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Sun, 20 Aug 2023 13:32:24 +0200 Subject: [PATCH 066/479] fix(SupportPopUp): invalid child React error --- meteor/client/ui/SupportPopUp.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/meteor/client/ui/SupportPopUp.tsx b/meteor/client/ui/SupportPopUp.tsx index d028216112..9bb2b2de21 100644 --- a/meteor/client/ui/SupportPopUp.tsx +++ b/meteor/client/ui/SupportPopUp.tsx @@ -66,7 +66,7 @@ export function DocumentationLink(): JSX.Element { const { t } = useTranslation() return ( -

+

{getHelpMode() ? (
{t('Disable hints by adding this to the URL:')}  @@ -78,8 +78,10 @@ export function DocumentationLink(): JSX.Element { ?help=1
)} - {t('More documentation available at:')}  - https://nrkno.github.io/sofie-core/ -

+

+ {t('More documentation available at:')}  + https://nrkno.github.io/sofie-core/ +

+
) } From 6163c7bfd9cbece9a710c75a89d2a62fe639a036 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Sun, 20 Aug 2023 13:56:44 +0200 Subject: [PATCH 067/479] chore(MediaStatusListHeader): remove duplicate CSS declaration --- .../client/ui/Status/media-status/MediaStatusListHeader.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/meteor/client/ui/Status/media-status/MediaStatusListHeader.scss b/meteor/client/ui/Status/media-status/MediaStatusListHeader.scss index 052a7d01cb..6270db7b8c 100644 --- a/meteor/client/ui/Status/media-status/MediaStatusListHeader.scss +++ b/meteor/client/ui/Status/media-status/MediaStatusListHeader.scss @@ -21,12 +21,11 @@ .media-status-list-header__sort-button { background: none; - padding: 0; margin: 0; + padding: 5px 5px; border: none; text-align: center; min-width: 1.5em; - padding: 5px 5px; > svg { vertical-align: top; From 37b618d4843ea4e2c375b88f6fb648b72a82fff3 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Sun, 20 Aug 2023 14:23:50 +0200 Subject: [PATCH 068/479] chore: deduplicate code --- meteor/client/ui/MediaStatus/MediaStatus.tsx | 88 ++++++++++++++++++- .../ui/RundownView/MediaStatusPopUp/index.tsx | 72 ++------------- .../client/ui/Status/media-status/index.tsx | 68 +------------- 3 files changed, 95 insertions(+), 133 deletions(-) diff --git a/meteor/client/ui/MediaStatus/MediaStatus.tsx b/meteor/client/ui/MediaStatus/MediaStatus.tsx index cf4014c689..a81499411d 100644 --- a/meteor/client/ui/MediaStatus/MediaStatus.tsx +++ b/meteor/client/ui/MediaStatus/MediaStatus.tsx @@ -31,7 +31,7 @@ import { PartInvalidReason } from '@sofie-automation/corelib/dist/dataModel/Part import { IBlueprintActionManifestDisplayContent, SourceLayerType } from '@sofie-automation/blueprints-integration' import { PieceContentStatusObj } from '../../../lib/mediaObjects' import { Piece, PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' -import { literal } from '@sofie-automation/corelib/dist/lib' +import { assertNever, literal } from '@sofie-automation/corelib/dist/lib' import { UIPieceContentStatuses, UIShowStyleBases } from '../Collections' import { isTranslatableMessage, translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import { i18nTranslator } from '../i18n' @@ -159,12 +159,14 @@ function useRundownPlaylists(playlistIds: RundownPlaylistId[]) { segments.forEach(({ segment, partInstances }, segmentRank) => { partInstances.forEach((partInstance, partInstanceRank) => { const rundown = rundowns.find((rundown) => rundown._id === segment.rundownId) + const rundownIndex = playlist.rundownIdsInOrder.indexOf(segment.rundownId) if (partInstance.isTemporary) { partIds.push(partInstance.part._id) partMeta.set(partInstance.part._id, { playlistId: playlist._id, playlistName: playlist.name, rundownName: rundown?.name, + rundownRank: rundownIndex, showStyleBaseId: rundown?.showStyleBaseId, playlistRank, segmentRank, @@ -183,6 +185,7 @@ function useRundownPlaylists(playlistIds: RundownPlaylistId[]) { playlistId: playlist._id, playlistName: playlist.name, rundownName: rundown?.name, + rundownRank: rundownIndex, showStyleBaseId: rundown?.showStyleBaseId, playlistRank, segmentRank, @@ -213,7 +216,7 @@ function useRundownPlaylists(playlistIds: RundownPlaylistId[]) { const rundownMeta = useMemo(() => { const rundownMeta = new Map() - rundowns.forEach((rundown) => { + rundowns.forEach((rundown, rundownIndex) => { const playlistIndex = playlistsWithContent.findIndex( (playlistWithContent) => playlistWithContent.playlist._id === rundown.playlistId ) @@ -225,6 +228,7 @@ function useRundownPlaylists(playlistIds: RundownPlaylistId[]) { playlistId: rundown.playlistId, playlistName: playlist.name, playlistRank: playlistIndex, + rundownRank: rundownIndex, rundownName: rundown.name, rank, showStyleBaseId: rundown.showStyleBaseId, @@ -562,6 +566,7 @@ interface PartMeta { playlistName: string playlistRank: number rundownName: string | undefined + rundownRank: number showStyleBaseId: ShowStyleBaseId | undefined segmentRank: number segmentIdentifier: string | undefined @@ -577,6 +582,7 @@ interface RundownMeta { playlistName: string playlistRank: number rundownName: string | undefined + rundownRank: number showStyleBaseId: ShowStyleBaseId | undefined rank: number } @@ -596,6 +602,7 @@ export interface MediaStatusListItem { playlistName: string playlistRank: number rundownName: string | undefined + rundownRank: number segmentRank: number | undefined partRank: number | undefined invalid: boolean @@ -627,6 +634,7 @@ function getListItemFromRundownPieceAndRundownMeta( const playlistName = meta.playlistName const playlistRank = meta.playlistRank const rundownName = meta.rundownName + const rundownRank = meta.rundownRank const playlistId = meta.playlistId const uiPieceContentStatus = UIPieceContentStatuses.findOne({ @@ -653,9 +661,10 @@ function getListItemFromRundownPieceAndRundownMeta( partRank, invalid, playlistName, + playlistRank, rundownName, + rundownRank, duration, - playlistRank, rank, status, pieceContentStatus: uiPieceContentStatus?.status, @@ -685,6 +694,7 @@ function getListItemFromPieceAndPartMeta( const segmentRank = meta.segmentRank const playlistName = meta.playlistName const playlistRank = meta.playlistRank + const rundownRank = meta.rundownRank const rundownName = meta.rundownName const playlistId = meta.playlistId @@ -716,6 +726,7 @@ function getListItemFromPieceAndPartMeta( partRank, playlistName, rundownName, + rundownRank, duration, playlistRank, rank, @@ -726,3 +737,74 @@ function getListItemFromPieceAndPartMeta( } type SomePieceId = PieceId | AdLibActionId | RundownBaselineAdLibActionId | PieceInstanceId + +export function sortItems( + a: MediaStatusListItem, + b: MediaStatusListItem, + sortBy: SortBy, + sortOrder: SortOrder +): number { + let result = 0 + switch (sortBy) { + case 'name': + result = sortByName(a, b) + break + case 'status': + result = sortByStatus(a, b) + break + case 'sourceLayer': + result = sortBySourceLayer(a, b) + break + case 'rundown': + result = sortByPlaylist(a, b) + break + default: + assertNever(sortBy) + break + } + + if (sortOrder === 'desc') return result * -1 + return result +} + +function sortByName(a: MediaStatusListItem, b: MediaStatusListItem) { + return a.name.localeCompare(b.name) || sortByRundown(a, b) +} + +function sortByStatus(a: MediaStatusListItem, b: MediaStatusListItem) { + return a.status - b.status || sortByRundown(a, b) +} + +function sortBySourceLayer(a: MediaStatusListItem, b: MediaStatusListItem) { + if (a.sourceLayerName === b.sourceLayerName) return sortByRundown(a, b) + if (a.sourceLayerName === undefined) return 1 + if (b.sourceLayerName === undefined) return -1 + return a.sourceLayerName.localeCompare(b.sourceLayerName) +} + +function sortByPlaylist(a: MediaStatusListItem, b: MediaStatusListItem) { + if (a.playlistRank === b.playlistRank) return sortByRundown(a, b) + return a.playlistRank - b.playlistRank +} + +function sortByRundown(a: MediaStatusListItem, b: MediaStatusListItem) { + if (a.rundownRank === b.rundownRank) return sortBySegmentRank(a, b) + return a.rundownRank - b.rundownRank +} + +function sortBySegmentRank(a: MediaStatusListItem, b: MediaStatusListItem) { + if (a.segmentRank === b.segmentRank) return sortByPartRank(a, b) + return (a.segmentRank ?? 0) - (b.segmentRank ?? 0) +} + +function sortByPartRank(a: MediaStatusListItem, b: MediaStatusListItem) { + if (a.partRank === b.partRank) return sortByRank(a, b) + return (a.partRank ?? 0) - (b.partRank ?? 0) +} + +function sortByRank(a: MediaStatusListItem, b: MediaStatusListItem) { + return a.rank - b.rank +} + +export type SortBy = 'rundown' | 'status' | 'sourceLayer' | 'name' +export type SortOrder = 'asc' | 'desc' | 'inactive' diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx b/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx index f636658410..42fb805346 100644 --- a/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx @@ -6,10 +6,15 @@ import { useTranslation } from 'react-i18next' import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' -import { MediaStatus, MediaStatusListItem as IMediaStatusListItem } from '../../MediaStatus/MediaStatus' +import { + MediaStatus, + MediaStatusListItem as IMediaStatusListItem, + sortItems, + SortBy, + SortOrder, +} from '../../MediaStatus/MediaStatus' import { MediaStatusItem } from './MediaStatusPopUpItem' import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' -import { assertNever } from '@sofie-automation/corelib/dist/lib' import { MediaStatusPopUpHeader } from './MediaStatusPopUpHeader' import { RundownPlaylists } from '../../../collections' import { MediaStatusPopUpSegmentRule } from './MediaStatusPopUpSegmentRule' @@ -164,66 +169,3 @@ export function MediaStatusPopUp({ playlistId }: IProps): JSX.Element {
) } - -function sortItems(a: IMediaStatusListItem, b: IMediaStatusListItem, sortBy: SortBy, sortOrder: SortOrder) { - let result = 0 - switch (sortBy) { - case 'name': - result = sortByName(a, b) - break - case 'status': - result = sortByStatus(a, b) - break - case 'sourceLayer': - result = sortBySourceLayer(a, b) - break - case 'rundown': - result = sortByRundown(a, b) - break - default: - assertNever(sortBy) - break - } - - if (sortOrder === 'desc') return result * -1 - return result -} - -function sortByName(a: IMediaStatusListItem, b: IMediaStatusListItem) { - return a.name.localeCompare(b.name) || sortByRundown(a, b) -} - -function sortByStatus(a: IMediaStatusListItem, b: IMediaStatusListItem) { - return a.status - b.status || sortByRundown(a, b) -} - -function sortBySourceLayer(a: IMediaStatusListItem, b: IMediaStatusListItem) { - if (a.sourceLayerName === b.sourceLayerName) return sortByRundown(a, b) - if (a.sourceLayerName === undefined) return 1 - if (b.sourceLayerName === undefined) return -1 - return a.sourceLayerName.localeCompare(b.sourceLayerName) -} - -function sortByRundown(a: IMediaStatusListItem, b: IMediaStatusListItem) { - if (a.rundownName === b.rundownName) return sortBySegmentRank(a, b) - if (a.rundownName === undefined) return 1 - if (b.rundownName === undefined) return -1 - return a.rundownName?.localeCompare(b.rundownName) -} - -function sortBySegmentRank(a: IMediaStatusListItem, b: IMediaStatusListItem) { - if (a.segmentRank === b.segmentRank) return sortByPartRank(a, b) - return (a.segmentRank ?? 0) - (b.segmentRank ?? 0) -} - -function sortByPartRank(a: IMediaStatusListItem, b: IMediaStatusListItem) { - if (a.partRank === b.partRank) return sortByRank(a, b) - return (a.partRank ?? 0) - (b.partRank ?? 0) -} - -function sortByRank(a: IMediaStatusListItem, b: IMediaStatusListItem) { - return a.rank - b.rank -} - -type SortBy = 'rundown' | 'status' | 'sourceLayer' | 'name' -type SortOrder = 'asc' | 'desc' | 'inactive' diff --git a/meteor/client/ui/Status/media-status/index.tsx b/meteor/client/ui/Status/media-status/index.tsx index fa671acbb9..fb3a93504e 100644 --- a/meteor/client/ui/Status/media-status/index.tsx +++ b/meteor/client/ui/Status/media-status/index.tsx @@ -2,6 +2,9 @@ import React, { CSSProperties, useCallback, useEffect, useMemo, useRef, useState import { MediaStatus as MediaStatusComponent, MediaStatusListItem as IMediaStatusListItem, + sortItems, + SortBy, + SortOrder, } from '../../MediaStatus/MediaStatus' import { useSubscription, useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData' import { RundownPlaylists } from '../../../collections' @@ -12,7 +15,6 @@ import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { useTranslation } from 'react-i18next' import { MediaStatusListHeader } from './MediaStatusListHeader' import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' -import { assertNever } from '@sofie-automation/corelib/dist/lib' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faTimes } from '@fortawesome/free-solid-svg-icons' import { mapOrFallback } from '../../../lib/lib' @@ -137,67 +139,3 @@ export function MediaStatus(): JSX.Element | null {
) } - -function sortItems(a: IMediaStatusListItem, b: IMediaStatusListItem, sortBy: SortBy, sortOrder: SortOrder) { - let result = 0 - switch (sortBy) { - case 'name': - result = sortByName(a, b) - break - case 'status': - result = sortByStatus(a, b) - break - case 'sourceLayer': - result = sortBySourceLayer(a, b) - break - case 'rundown': - result = sortByRundown(a, b) - break - default: - assertNever(sortBy) - break - } - - if (sortOrder === 'desc') return result * -1 - return result -} - -function sortByName(a: IMediaStatusListItem, b: IMediaStatusListItem) { - return a.name.localeCompare(b.name) || sortByRundown(a, b) -} - -function sortByStatus(a: IMediaStatusListItem, b: IMediaStatusListItem) { - return a.status - b.status || sortByRundown(a, b) -} - -function sortBySourceLayer(a: IMediaStatusListItem, b: IMediaStatusListItem) { - if (a.sourceLayerName === b.sourceLayerName) return sortByRundown(a, b) - if (a.sourceLayerName === undefined) return 1 - if (b.sourceLayerName === undefined) return -1 - return a.sourceLayerName.localeCompare(b.sourceLayerName) -} - -function sortByRundown(a: IMediaStatusListItem, b: IMediaStatusListItem) { - if (a.playlistRank !== b.playlistRank) return a.playlistRank - b.playlistRank - if (a.rundownName === b.rundownName) return sortBySegmentRank(a, b) - if (a.rundownName === undefined) return 1 - if (b.rundownName === undefined) return -1 - return a.rundownName?.localeCompare(b.rundownName) -} - -function sortBySegmentRank(a: IMediaStatusListItem, b: IMediaStatusListItem) { - if (a.segmentRank === b.segmentRank) return sortByPartRank(a, b) - return (a.segmentRank ?? 0) - (b.segmentRank ?? 0) -} - -function sortByPartRank(a: IMediaStatusListItem, b: IMediaStatusListItem) { - if (a.partRank === b.partRank) return sortByRank(a, b) - return (a.partRank ?? 0) - (b.partRank ?? 0) -} - -function sortByRank(a: IMediaStatusListItem, b: IMediaStatusListItem) { - return a.rank - b.rank -} - -type SortBy = 'rundown' | 'status' | 'sourceLayer' | 'name' -type SortOrder = 'asc' | 'desc' | 'inactive' From c97159c15b27f85cabddb7d1d1b35ec1fa794a99 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Sun, 20 Aug 2023 15:29:44 +0200 Subject: [PATCH 069/479] chore: remove code smells --- meteor/client/lib/ui/icons/mediaStatus.tsx | 2 +- meteor/client/lib/ui/icons/notifications.tsx | 2 +- meteor/client/lib/ui/icons/sorting.tsx | 2 +- meteor/client/ui/MediaStatus/MediaStatus.tsx | 6 +++--- meteor/client/ui/MediaStatus/MediaStatusIndicator.tsx | 2 +- meteor/client/ui/MediaStatus/SortOrderButton.tsx | 2 +- .../MediaStatusPopUp/MediaStatusPopUpHeader.tsx | 2 +- .../MediaStatusPopUp/MediaStatusPopUpItem.scss | 10 ++++------ .../MediaStatusPopUp/MediaStatusPopUpItem.tsx | 2 +- .../MediaStatusPopUp/MediaStatusPopUpSegmentRule.tsx | 2 +- .../ui/Status/media-status/MediaStatusListHeader.tsx | 2 +- .../ui/Status/media-status/MediaStatusListItem.tsx | 2 +- meteor/client/ui/Status/media-status/index.tsx | 2 +- 13 files changed, 18 insertions(+), 20 deletions(-) diff --git a/meteor/client/lib/ui/icons/mediaStatus.tsx b/meteor/client/lib/ui/icons/mediaStatus.tsx index 8457f37c20..46c28f3787 100644 --- a/meteor/client/lib/ui/icons/mediaStatus.tsx +++ b/meteor/client/lib/ui/icons/mediaStatus.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { JSX } from 'react' export function MediaStatusIcon(): JSX.Element { return ( diff --git a/meteor/client/lib/ui/icons/notifications.tsx b/meteor/client/lib/ui/icons/notifications.tsx index e8d25b20c8..e955973775 100644 --- a/meteor/client/lib/ui/icons/notifications.tsx +++ b/meteor/client/lib/ui/icons/notifications.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { JSX } from 'react' export function CriticalIcon(): JSX.Element { return ( diff --git a/meteor/client/lib/ui/icons/sorting.tsx b/meteor/client/lib/ui/icons/sorting.tsx index b7a45a7747..a28ae3dc69 100644 --- a/meteor/client/lib/ui/icons/sorting.tsx +++ b/meteor/client/lib/ui/icons/sorting.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { JSX } from 'react' export function SortDescending(): JSX.Element { return ( diff --git a/meteor/client/ui/MediaStatus/MediaStatus.tsx b/meteor/client/ui/MediaStatus/MediaStatus.tsx index a81499411d..3c44fca9cf 100644 --- a/meteor/client/ui/MediaStatus/MediaStatus.tsx +++ b/meteor/client/ui/MediaStatus/MediaStatus.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react' +import { useMemo, JSX } from 'react' import { useSubscription, useSubscriptions, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { PubSub } from '../../../lib/api/pubsub' import { getSegmentsWithPartInstances } from '../../../lib/Rundown' @@ -625,7 +625,7 @@ function getListItemFromRundownPieceAndRundownMeta( const showStyleBase = meta.showStyleBaseId && UIShowStyleBases.findOne(meta.showStyleBaseId) const sourceLayer = piece.sourceLayerId !== undefined ? showStyleBase?.sourceLayers?.[piece.sourceLayerId] : undefined - if (sourceLayer && sourceLayer.isHidden) return + if (sourceLayer?.isHidden) return const partIdentifier = undefined const segmentIdentifier = undefined @@ -686,7 +686,7 @@ function getListItemFromPieceAndPartMeta( const showStyleBase = meta.showStyleBaseId && UIShowStyleBases.findOne(meta.showStyleBaseId) const sourceLayer = piece.sourceLayerId !== undefined ? showStyleBase?.sourceLayers?.[piece.sourceLayerId] : undefined - if (sourceLayer && sourceLayer.isHidden) return + if (sourceLayer?.isHidden) return const partIdentifier = meta.identifier const segmentIdentifier = meta.segmentIdentifier diff --git a/meteor/client/ui/MediaStatus/MediaStatusIndicator.tsx b/meteor/client/ui/MediaStatus/MediaStatusIndicator.tsx index 67e3741a07..669d6d5aff 100644 --- a/meteor/client/ui/MediaStatus/MediaStatusIndicator.tsx +++ b/meteor/client/ui/MediaStatus/MediaStatusIndicator.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { JSX } from 'react' import Tooltip from 'rc-tooltip' import { PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' import { assertNever } from '@sofie-automation/corelib/dist/lib' diff --git a/meteor/client/ui/MediaStatus/SortOrderButton.tsx b/meteor/client/ui/MediaStatus/SortOrderButton.tsx index 44a089d213..97dcde7656 100644 --- a/meteor/client/ui/MediaStatus/SortOrderButton.tsx +++ b/meteor/client/ui/MediaStatus/SortOrderButton.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { JSX } from 'react' import { assertNever } from '@sofie-automation/corelib/dist/lib' import { SortAscending, SortDescending, SortDisabled } from '../../lib/ui/icons/sorting' diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader.tsx b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader.tsx index 39a26c4cc9..685b6bd05c 100644 --- a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader.tsx +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader.tsx @@ -1,4 +1,4 @@ -import React, { ChangeEvent, useCallback } from 'react' +import React, { ChangeEvent, useCallback, JSX } from 'react' import { useTranslation } from 'react-i18next' import { SortOrderButton } from '../../MediaStatus/SortOrderButton' import classNames from 'classnames' diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.scss b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.scss index 1a00bd8e50..36b0925a2b 100644 --- a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.scss +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.scss @@ -2,12 +2,6 @@ @import '../../../styles/itemTypeColors'; .media-status-popup-item { - &:first-child { - > td { - padding-top: 3px; - } - } - .media-status-popup-item__playout-indicator { width: 1.5em; max-width: 1.5em; @@ -36,6 +30,10 @@ } &:first-child { + > td { + padding-top: 3px; + } + > .media-status-popup-item__countdown { padding-top: calc(3px + 0.1em); } diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.tsx b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.tsx index ea742ce45f..c7474ea6f8 100644 --- a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.tsx +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react' +import React, { useCallback, JSX } from 'react' import { PartId, PartInstanceId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { SourceLayerType } from '@sofie-automation/blueprints-integration' import { PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpSegmentRule.tsx b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpSegmentRule.tsx index b939178056..8f8a481e73 100644 --- a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpSegmentRule.tsx +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpSegmentRule.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { JSX } from 'react' export function MediaStatusPopUpSegmentRule(): JSX.Element { return ( diff --git a/meteor/client/ui/Status/media-status/MediaStatusListHeader.tsx b/meteor/client/ui/Status/media-status/MediaStatusListHeader.tsx index 05a9ff4a02..b03b635b48 100644 --- a/meteor/client/ui/Status/media-status/MediaStatusListHeader.tsx +++ b/meteor/client/ui/Status/media-status/MediaStatusListHeader.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { JSX } from 'react' import classNames from 'classnames' import { useTranslation } from 'react-i18next' import { SortOrderButton } from '../../MediaStatus/SortOrderButton' diff --git a/meteor/client/ui/Status/media-status/MediaStatusListItem.tsx b/meteor/client/ui/Status/media-status/MediaStatusListItem.tsx index fd1369b301..f6c31bf493 100644 --- a/meteor/client/ui/Status/media-status/MediaStatusListItem.tsx +++ b/meteor/client/ui/Status/media-status/MediaStatusListItem.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { JSX } from 'react' import { SourceLayerType } from '@sofie-automation/blueprints-integration' import { PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' import { NavLink } from 'react-router-dom' diff --git a/meteor/client/ui/Status/media-status/index.tsx b/meteor/client/ui/Status/media-status/index.tsx index fb3a93504e..ec6df33b82 100644 --- a/meteor/client/ui/Status/media-status/index.tsx +++ b/meteor/client/ui/Status/media-status/index.tsx @@ -1,4 +1,4 @@ -import React, { CSSProperties, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useRef, useState, JSX, CSSProperties } from 'react' import { MediaStatus as MediaStatusComponent, MediaStatusListItem as IMediaStatusListItem, From c312b43a88b9b451402919fef0ff0d488f9d252b Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Sun, 20 Aug 2023 15:50:13 +0200 Subject: [PATCH 070/479] feat(MediaStatusPopUp/MediaStatus): add filter debounce for a better typing experience --- .../ui/RundownView/MediaStatusPopUp/index.tsx | 18 ++++++++++-------- meteor/client/ui/Status/media-status/index.tsx | 15 +++++++++------ 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx b/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx index 42fb805346..393248449e 100644 --- a/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx @@ -18,7 +18,7 @@ import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMes import { MediaStatusPopUpHeader } from './MediaStatusPopUpHeader' import { RundownPlaylists } from '../../../collections' import { MediaStatusPopUpSegmentRule } from './MediaStatusPopUpSegmentRule' -import { mapOrFallback } from '../../../lib/lib' +import { mapOrFallback, useDebounce } from '../../../lib/lib' import { Spinner } from '../../../lib/Spinner' import { NavLink } from 'react-router-dom' import { MediaStatusPopOutIcon } from '../../../lib/ui/icons/mediaStatus' @@ -36,24 +36,26 @@ export function MediaStatusPopUp({ playlistId }: IProps): JSX.Element { const [sortBy, setSortBy] = useState<'rundown' | 'status' | 'sourceLayer' | 'name'>('rundown') const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc') const [filter, setFilter] = useState('') - // const [filter, setFilter] = useState('') + const debouncedFilter = useDebounce(filter, 100) function onChangeSort(sortBy: SortBy, sortOrder: SortOrder) { setSortOrder(sortOrder === 'inactive' ? 'asc' : sortOrder) setSortBy(sortBy) } - const emptyFilter = !filter || filter.trim().length === 0 + const emptyFilter = !debouncedFilter || debouncedFilter.trim().length === 0 const filterItems = useCallback( (item: IMediaStatusListItem) => { - if (emptyFilter) return true - if (item.name.toLowerCase().indexOf(filter.toLowerCase().trim()) >= 0) return true - if ((item.partIdentifier?.toLocaleLowerCase() ?? '').indexOf(filter.toLowerCase().trim()) === 0) return true - if ((item.segmentIdentifier?.toLocaleLowerCase() ?? '').indexOf(filter.toLowerCase().trim()) === 0) return true + if (debouncedFilter) return true + if (item.name.toLowerCase().indexOf(debouncedFilter.toLowerCase().trim()) >= 0) return true + if ((item.partIdentifier?.toLocaleLowerCase() ?? '').indexOf(debouncedFilter.toLowerCase().trim()) === 0) + return true + if ((item.segmentIdentifier?.toLocaleLowerCase() ?? '').indexOf(debouncedFilter.toLowerCase().trim()) === 0) + return true return false }, - [filter, emptyFilter] + [debouncedFilter, emptyFilter] ) const playlistIds = useMemo(() => [playlistId], [playlistId]) diff --git a/meteor/client/ui/Status/media-status/index.tsx b/meteor/client/ui/Status/media-status/index.tsx index ec6df33b82..b82cbc83f7 100644 --- a/meteor/client/ui/Status/media-status/index.tsx +++ b/meteor/client/ui/Status/media-status/index.tsx @@ -17,7 +17,7 @@ import { MediaStatusListHeader } from './MediaStatusListHeader' import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faTimes } from '@fortawesome/free-solid-svg-icons' -import { mapOrFallback } from '../../../lib/lib' +import { mapOrFallback, useDebounce } from '../../../lib/lib' export function MediaStatus(): JSX.Element | null { const scrollBox = useRef(null) @@ -26,6 +26,7 @@ export function MediaStatus(): JSX.Element | null { const [sortBy, setSortBy] = useState<'rundown' | 'status' | 'sourceLayer' | 'name'>('rundown') const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc') const [filter, setFilter] = useState('') + const debouncedFilter = useDebounce(filter, 100) const playlistIds = useTracker(() => RundownPlaylists.find({}).map((playlist) => playlist._id), [], []) @@ -44,17 +45,19 @@ export function MediaStatus(): JSX.Element | null { setOffsetTop(scrollBox.current.offsetTop) }, []) - const emptyFilter = !filter || filter.trim().length === 0 + const emptyFilter = !debouncedFilter || debouncedFilter.trim().length === 0 const filterItems = useCallback( (item: IMediaStatusListItem) => { if (emptyFilter) return true - if (item.name.toLowerCase().indexOf(filter.toLowerCase().trim()) >= 0) return true - if ((item.partIdentifier?.toLocaleLowerCase() ?? '').indexOf(filter.toLowerCase().trim()) === 0) return true - if ((item.segmentIdentifier?.toLocaleLowerCase() ?? '').indexOf(filter.toLowerCase().trim()) === 0) return true + if (item.name.toLowerCase().indexOf(debouncedFilter.toLowerCase().trim()) >= 0) return true + if ((item.partIdentifier?.toLocaleLowerCase() ?? '').indexOf(debouncedFilter.toLowerCase().trim()) === 0) + return true + if ((item.segmentIdentifier?.toLocaleLowerCase() ?? '').indexOf(debouncedFilter.toLowerCase().trim()) === 0) + return true return false }, - [filter, emptyFilter] + [debouncedFilter, emptyFilter] ) const scrollBoxStyle = useMemo( From 5329a771c8711029fc1cd838a64d52d4e0422645 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Mon, 21 Aug 2023 10:50:57 +0200 Subject: [PATCH 071/479] Update meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx Co-authored-by: Johan Nyman --- meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx b/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx index 55ff4385d3..abe32a6f08 100644 --- a/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx +++ b/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx @@ -356,7 +356,9 @@ export function useSubscription(sub: K, ...args: Pa return ready } - +/** + * Sets up multiple subscriptions of the same type, but with different arguments + */ export function useSubscriptions( sub: K, argsArray: Parameters[] From a1a36828e50bfdd12466cada411951746f31b4cd Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Mon, 21 Aug 2023 10:52:01 +0200 Subject: [PATCH 072/479] chore: rename MediaStatusItem to MediaStatusPopUpItem --- .../ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.tsx | 4 ++-- meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.tsx b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.tsx index c7474ea6f8..3f2cbb1e8c 100644 --- a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.tsx +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.tsx @@ -9,7 +9,7 @@ import classNames from 'classnames' import { MediaStatusIndicator } from '../../MediaStatus/MediaStatusIndicator' import RundownViewEventBus, { RundownViewEvents } from '../../../../lib/api/triggers/RundownViewEventBus' -export const MediaStatusItem = withTiming< +export const MediaStatusPopUpItem = withTiming< { partId: PartId | undefined segmentId: SegmentId | undefined @@ -31,7 +31,7 @@ export const MediaStatusItem = withTiming< >({ dataResolution: TimingDataResolution.Synced, tickResolution: TimingTickResolution.Low, -})(function MediaStatusItem({ +})(function MediaStatusPopUpItem({ partId, partInstanceId, segmentId, diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx b/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx index 393248449e..fc4d573ad6 100644 --- a/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx @@ -13,7 +13,7 @@ import { SortBy, SortOrder, } from '../../MediaStatus/MediaStatus' -import { MediaStatusItem } from './MediaStatusPopUpItem' +import { MediaStatusPopUpItem } from './MediaStatusPopUpItem' import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import { MediaStatusPopUpHeader } from './MediaStatusPopUpHeader' import { RundownPlaylists } from '../../../collections' @@ -129,7 +129,7 @@ export function MediaStatusPopUp({ playlistId }: IProps): JSX.Element { return ( {line} - Date: Tue, 22 Aug 2023 15:46:46 +0200 Subject: [PATCH 073/479] feat: implement `loop` content property for Graphics --- .../Renderers => lib/ui/icons}/icon-loop.json | 0 meteor/client/lib/ui/icons/looping.tsx | 22 ++++++++++- meteor/client/styles/rundownView.scss | 16 +++++++- .../LinePartMainPiece/LinePartMainPiece.scss | 10 +++++ .../LinePartMainPiece/LinePartMainPiece.tsx | 15 +++++--- .../ui/SegmentList/LinePartTimeline.tsx | 8 ++-- .../Renderers/GraphicsRenderer.tsx | 4 ++ .../Renderers/VTRenderer.tsx | 4 ++ .../Renderers/GraphicsThumbnailRenderer.tsx | 4 ++ .../Renderers/VTThumbnailRenderer.tsx | 7 ++++ .../StoryboardPartThumbnail.scss | 10 +++++ .../Renderers/CustomLayerItemRenderer.tsx | 6 +++ .../Renderers/L3rdSourceRenderer.tsx | 4 +- .../Renderers/VTSourceRenderer.tsx | 38 +------------------ .../SegmentTimeline/withMediaObjectStatus.tsx | 16 ++++++++ .../blueprints-integration/src/content.ts | 4 +- 16 files changed, 118 insertions(+), 50 deletions(-) rename meteor/client/{ui/SegmentTimeline/Renderers => lib/ui/icons}/icon-loop.json (100%) diff --git a/meteor/client/ui/SegmentTimeline/Renderers/icon-loop.json b/meteor/client/lib/ui/icons/icon-loop.json similarity index 100% rename from meteor/client/ui/SegmentTimeline/Renderers/icon-loop.json rename to meteor/client/lib/ui/icons/icon-loop.json diff --git a/meteor/client/lib/ui/icons/looping.tsx b/meteor/client/lib/ui/icons/looping.tsx index 1315a64c98..e6050c47da 100644 --- a/meteor/client/lib/ui/icons/looping.tsx +++ b/meteor/client/lib/ui/icons/looping.tsx @@ -1,4 +1,7 @@ -import React from 'react' +import React, { JSX } from 'react' +// @ts-expect-error Not recognized by Typescript +import * as loopAnimation from './icon-loop.json' +import { Lottie } from '@crello/react-lottie' export function LoopingIcon(props?: React.SVGProps): JSX.Element { return ( @@ -11,3 +14,20 @@ export function LoopingIcon(props?: React.SVGProps): JSX.Element ) } + +export function LoopingPieceIcon({ className, playing }: { className?: string; playing: boolean }): JSX.Element { + return ( +
+ +
+ ) +} + +const LOOPING_PIECE_ICON = { + loop: true, + autoplay: false, + animationData: loopAnimation, + rendererSettings: { + preserveAspectRatio: 'xMidYMid slice', + }, +} diff --git a/meteor/client/styles/rundownView.scss b/meteor/client/styles/rundownView.scss index 1e41955124..d590da90aa 100644 --- a/meteor/client/styles/rundownView.scss +++ b/meteor/client/styles/rundownView.scss @@ -1755,6 +1755,12 @@ svg.icon { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.5); cursor: default; + .piece__status-icon { + > svg.type-warning { + margin-top: 2px; + } + } + // TODO: This is just temporary styling, to indicate that the layers are clickable &:hover { transition: none; @@ -2098,7 +2104,8 @@ svg.icon { margin: 0 -10px 0 0; } - > .label-icon.label-infinite-icon { + > .label-icon.label-infinite-icon, + > .label-icon.label-loop-icon { margin: 0 0 0 0; } } @@ -2163,6 +2170,13 @@ svg.icon { margin-left: 0.5em; } + .segment-timeline__piece__label-icon { + display: inline-block; + vertical-align: top; + margin-top: 0; + margin-bottom: -5px; + } + &.with-duration { display: inline-flex; flex-direction: row; diff --git a/meteor/client/ui/SegmentList/LinePartMainPiece/LinePartMainPiece.scss b/meteor/client/ui/SegmentList/LinePartMainPiece/LinePartMainPiece.scss index 0adc3203fe..baa5dd0821 100644 --- a/meteor/client/ui/SegmentList/LinePartMainPiece/LinePartMainPiece.scss +++ b/meteor/client/ui/SegmentList/LinePartMainPiece/LinePartMainPiece.scss @@ -34,6 +34,7 @@ z-index: 0; > .piece__status-icon { + margin-top: -1px; margin-bottom: -1em; vertical-align: top; } @@ -56,6 +57,15 @@ margin-top: 0.25em; } } + + > .segment-opl__main-piece__label-icon { + display: inline-block; + vertical-align: top; + + &.label-loop-icon { + margin: -3px 0 -2px 0; + } + } } // TODO: Loan from rundownView.scss, convert to a shared mixin diff --git a/meteor/client/ui/SegmentList/LinePartMainPiece/LinePartMainPiece.tsx b/meteor/client/ui/SegmentList/LinePartMainPiece/LinePartMainPiece.tsx index 63712e6dcd..8b95b27fb3 100644 --- a/meteor/client/ui/SegmentList/LinePartMainPiece/LinePartMainPiece.tsx +++ b/meteor/client/ui/SegmentList/LinePartMainPiece/LinePartMainPiece.tsx @@ -1,5 +1,5 @@ -import { EvsContent, SourceLayerType } from '@sofie-automation/blueprints-integration' import React, { useMemo, useState, useRef } from 'react' +import { EvsContent, SourceLayerType } from '@sofie-automation/blueprints-integration' import { PieceExtended } from '../../../../lib/Rundown' // TODO: Move to a shared lib file import { getSplitItems } from '../../SegmentContainer/getSplitItems' @@ -15,6 +15,7 @@ import { PieceStatusIcon } from '../../../lib/ui/PieceStatusIcon' import { UIStudio } from '../../../../lib/api/studios' import classNames from 'classnames' import { PieceMultistepChevron } from '../../SegmentContainer/PieceMultistepChevron' +import { LoopingPieceIcon } from '../../../lib/ui/icons/looping' interface IProps { partId: PartId @@ -39,8 +40,9 @@ function getPieceDuration( Math.min(piece.renderedDuration ?? partDuration, partDuration) : Math.max( // renderedDuration can be null. If there is a sourceDuration, use that, if not, use timelineBase - piece.renderedDuration ?? (piece.instance.piece.content.sourceDuration ? 0 : timelineBase), - piece.instance.piece.content.sourceDuration ?? 0 + piece.renderedDuration ?? + (piece.instance.piece.content?.sourceDuration && !piece.instance.piece.content?.loop ? 0 : timelineBase), + piece.instance.piece.content?.sourceDuration ?? 0 ) } @@ -193,6 +195,9 @@ export const LinePartMainPiece = withMediaObjectStatus()(function Li )} {piece.instance.piece.name} + {piece.instance.piece.content?.loop && ( + + )}
{studio && ( ()(function Li ) }) -function ColoredMark({ color }: { color: string | undefined }) { +const ColoredMark = React.memo(function ColoredMark({ color }: { color: string | undefined }) { if (!color) return null return ( @@ -218,4 +223,4 @@ function ColoredMark({ color }: { color: string | undefined }) { className="segment-opl__main-piece__label__colored-mark" > ) -} +}) diff --git a/meteor/client/ui/SegmentList/LinePartTimeline.tsx b/meteor/client/ui/SegmentList/LinePartTimeline.tsx index 21eae3edbc..3063753656 100644 --- a/meteor/client/ui/SegmentList/LinePartTimeline.tsx +++ b/meteor/client/ui/SegmentList/LinePartTimeline.tsx @@ -1,4 +1,4 @@ -import { PieceLifespan, SourceLayerType, VTContent } from '@sofie-automation/blueprints-integration' +import { PieceLifespan, SourceLayerType } from '@sofie-automation/blueprints-integration' import React, { useMemo } from 'react' import { PartExtended, PieceExtended } from '../../../lib/Rundown' import { findPieceExtendedToShowFromOrderedResolvedInstances } from '../PieceIcons/utils' @@ -87,7 +87,7 @@ export const LinePartTimeline: React.FC = function LinePartTimeline({ const timings = part.instance.partPlayoutTimings const toPartDelay = (timings?.toPartDelay ?? 0) - ((timings?.fromPartRemaining ?? 0) - (timings?.toPartDelay ?? 0)) const renderedPartDuration = part.renderedDuration + toPartDelay - const mainPieceSourceDuration = mainPiece?.instance.piece.content.sourceDuration + const mainPieceSourceDuration = mainPiece?.instance.piece.content?.sourceDuration const mainPieceInPoint = mainPiece?.renderedInPoint const maxDuration = Math.max((mainPieceInPoint ?? 0) + (mainPieceSourceDuration ?? 0), renderedPartDuration) const timelineBase = Math.max(TIMELINE_DEFAULT_BASE, maxDuration) @@ -97,8 +97,8 @@ export const LinePartTimeline: React.FC = function LinePartTimeline({ const isInvalid = !!part.instance.part.invalid - const loop = !(mainPiece?.instance.piece.content as VTContent)?.loop - const endsInFreeze = !part.instance.part.autoNext && !loop && !!mainPiece?.instance.piece.content.sourceDuration + const loop = mainPiece?.instance.piece.content?.loop + const endsInFreeze = !part.instance.part.autoNext && !loop && !!mainPiece?.instance.piece.content?.sourceDuration const mainSourceEnd = mainPiece?.instance.piece.content.sourceDuration ? (mainPieceInPoint ?? 0) + mainPiece?.instance.piece.content.sourceDuration : null diff --git a/meteor/client/ui/SegmentStoryboard/StoryboardPartSecondaryPieces/Renderers/GraphicsRenderer.tsx b/meteor/client/ui/SegmentStoryboard/StoryboardPartSecondaryPieces/Renderers/GraphicsRenderer.tsx index 104749f328..bfaaf9f800 100644 --- a/meteor/client/ui/SegmentStoryboard/StoryboardPartSecondaryPieces/Renderers/GraphicsRenderer.tsx +++ b/meteor/client/ui/SegmentStoryboard/StoryboardPartSecondaryPieces/Renderers/GraphicsRenderer.tsx @@ -3,6 +3,7 @@ import React from 'react' import { L3rdFloatingInspector } from '../../../FloatingInspectors/L3rdFloatingInspector' import { PieceMultistepChevron } from '../../../SegmentContainer/PieceMultistepChevron' import { IDefaultRendererProps } from './DefaultRenderer' +import { LoopingPieceIcon } from '../../../../lib/ui/icons/looping' export function GraphicsRenderer({ piece: pieceInstance, @@ -32,6 +33,9 @@ export function GraphicsRenderer({ /> {pieceInstance.instance.piece.name} + {pieceInstance.instance.piece.content?.loop && ( + + )} ) } diff --git a/meteor/client/ui/SegmentStoryboard/StoryboardPartSecondaryPieces/Renderers/VTRenderer.tsx b/meteor/client/ui/SegmentStoryboard/StoryboardPartSecondaryPieces/Renderers/VTRenderer.tsx index 9d01afcc73..ccdd1245ff 100644 --- a/meteor/client/ui/SegmentStoryboard/StoryboardPartSecondaryPieces/Renderers/VTRenderer.tsx +++ b/meteor/client/ui/SegmentStoryboard/StoryboardPartSecondaryPieces/Renderers/VTRenderer.tsx @@ -4,6 +4,7 @@ import { VTFloatingInspector } from '../../../FloatingInspectors/VTFloatingInspe import { IDefaultRendererProps } from './DefaultRenderer' import { getNoticeLevelForPieceStatus } from '../../../../../lib/notifications/notifications' import { PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' +import { LoopingPieceIcon } from '../../../../lib/ui/icons/looping' export function VTRenderer({ piece: pieceInstance, @@ -39,6 +40,9 @@ export function VTRenderer({ previewUrl={pieceInstance.contentStatus?.previewUrl} /> {pieceInstance.instance.piece.name} + {pieceInstance.instance.piece.content?.loop && ( + + )} ) } diff --git a/meteor/client/ui/SegmentStoryboard/StoryboardPartThumbnail/Renderers/GraphicsThumbnailRenderer.tsx b/meteor/client/ui/SegmentStoryboard/StoryboardPartThumbnail/Renderers/GraphicsThumbnailRenderer.tsx index 882d2e02fc..51455f8610 100644 --- a/meteor/client/ui/SegmentStoryboard/StoryboardPartThumbnail/Renderers/GraphicsThumbnailRenderer.tsx +++ b/meteor/client/ui/SegmentStoryboard/StoryboardPartThumbnail/Renderers/GraphicsThumbnailRenderer.tsx @@ -4,6 +4,7 @@ import { RundownUtils } from '../../../../lib/rundown' import { L3rdFloatingInspector } from '../../../FloatingInspectors/L3rdFloatingInspector' import { PieceMultistepChevron } from '../../../SegmentContainer/PieceMultistepChevron' import { IProps } from './ThumbnailRendererFactory' +import { LoopingPieceIcon } from '../../../../lib/ui/icons/looping' export function GraphicsThumbnailRenderer({ pieceInstance, @@ -35,6 +36,9 @@ export function GraphicsThumbnailRenderer({ />
+ {pieceInstance.instance.piece.content?.loop && ( + + )} {pieceInstance.instance.piece.name}
diff --git a/meteor/client/ui/SegmentStoryboard/StoryboardPartThumbnail/Renderers/VTThumbnailRenderer.tsx b/meteor/client/ui/SegmentStoryboard/StoryboardPartThumbnail/Renderers/VTThumbnailRenderer.tsx index ab4008baef..0edfe3d4c9 100644 --- a/meteor/client/ui/SegmentStoryboard/StoryboardPartThumbnail/Renderers/VTThumbnailRenderer.tsx +++ b/meteor/client/ui/SegmentStoryboard/StoryboardPartThumbnail/Renderers/VTThumbnailRenderer.tsx @@ -11,6 +11,7 @@ import { FreezeFrameIcon } from '../../../../lib/ui/icons/freezeFrame' import { PieceStatusIcon } from '../../../../lib/ui/PieceStatusIcon' import { PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' import { FREEZE_FRAME_FLASH } from '../../../SegmentContainer/withResolvedSegment' +import { LoopingPieceIcon } from '../../../../lib/ui/icons/looping' export function VTThumbnailRenderer({ partId, @@ -65,6 +66,7 @@ export function VTThumbnailRenderer({ > {(timingContext) => { if (!timingContext.partPlayed || !timingContext.partDisplayDurations) return null + if (pieceInstance.instance.piece.content?.loop) return null const partPlayed = timingContext.partPlayed[unprotectString(partId)] ?? 0 const contentEnd = @@ -104,6 +106,11 @@ export function VTThumbnailRenderer({ ) : null }} + {pieceInstance.instance.piece.content?.loop && ( +
+ +
+ )}
{noticeLevel !== null && } {pieceInstance.instance.piece.name} diff --git a/meteor/client/ui/SegmentStoryboard/StoryboardPartThumbnail/StoryboardPartThumbnail.scss b/meteor/client/ui/SegmentStoryboard/StoryboardPartThumbnail/StoryboardPartThumbnail.scss index cf4c219cb0..a604ebce52 100644 --- a/meteor/client/ui/SegmentStoryboard/StoryboardPartThumbnail/StoryboardPartThumbnail.scss +++ b/meteor/client/ui/SegmentStoryboard/StoryboardPartThumbnail/StoryboardPartThumbnail.scss @@ -56,6 +56,12 @@ vertical-align: top; } + > .segment-storyboard__thumbnail__label-icon { + float: right; + margin-top: -5px; + margin-bottom: -5px; + } + &.segment-storyboard__thumbnail__label--lg { position: absolute; text-align: center; @@ -133,6 +139,10 @@ &:last-child { margin-right: 0; } + + &.label-loop-icon { + margin-top: -3px; + } } &--playing { diff --git a/meteor/client/ui/SegmentTimeline/Renderers/CustomLayerItemRenderer.tsx b/meteor/client/ui/SegmentTimeline/Renderers/CustomLayerItemRenderer.tsx index e9f68f8659..4a19c52e56 100644 --- a/meteor/client/ui/SegmentTimeline/Renderers/CustomLayerItemRenderer.tsx +++ b/meteor/client/ui/SegmentTimeline/Renderers/CustomLayerItemRenderer.tsx @@ -9,6 +9,7 @@ import { PieceLifespan, VTContent } from '@sofie-automation/blueprints-integrati import { OffsetPosition } from '../../../utils/positions' import { CalculateTimingsPiece } from '@sofie-automation/corelib/dist/playout/timings' import { IFloatingInspectorPosition } from '../../FloatingInspectors/IFloatingInspectorPosition' +import { LoopingPieceIcon } from '../../../lib/ui/icons/looping' export type SourceDurationLabelAlignment = 'left' | 'right' @@ -123,6 +124,11 @@ export class CustomLayerItemRenderer< return false } + protected renderLoopIcon(): JSX.Element | null { + if (!this.props.piece.instance.piece.content?.loop) return null + return + } + protected renderOverflowTimeLabel(): JSX.Element | null { const overflowTime = this.doesOverflowTime() if ( diff --git a/meteor/client/ui/SegmentTimeline/Renderers/L3rdSourceRenderer.tsx b/meteor/client/ui/SegmentTimeline/Renderers/L3rdSourceRenderer.tsx index c1b84a44e8..3e8d9647d8 100644 --- a/meteor/client/ui/SegmentTimeline/Renderers/L3rdSourceRenderer.tsx +++ b/meteor/client/ui/SegmentTimeline/Renderers/L3rdSourceRenderer.tsx @@ -39,7 +39,8 @@ export class L3rdSourceRenderer extends CustomLayerItemRenderer super.componentDidUpdate(prevProps, prevState) } - const newOverflowTime = this.doesOverflowTime() > 0 ? true : false + const timeOverflow = this.doesOverflowTime() + const newOverflowTime = timeOverflow !== false && timeOverflow > 0 ? true : false if ( this.props.piece.instance.piece.name !== prevProps.piece.instance.piece.name || newOverflowTime !== this.lastOverflowTime @@ -83,6 +84,7 @@ export class L3rdSourceRenderer extends CustomLayerItemRenderer style={this.getItemLabelOffsetRight()} > {this.renderInfiniteIcon()} + {this.renderLoopIcon()} {this.renderOverflowTimeLabel()} {isMultiStep ? ( diff --git a/meteor/client/ui/SegmentTimeline/Renderers/VTSourceRenderer.tsx b/meteor/client/ui/SegmentTimeline/Renderers/VTSourceRenderer.tsx index f3575ffe36..17cf190182 100644 --- a/meteor/client/ui/SegmentTimeline/Renderers/VTSourceRenderer.tsx +++ b/meteor/client/ui/SegmentTimeline/Renderers/VTSourceRenderer.tsx @@ -6,9 +6,6 @@ import { getElementWidth } from '../../../utils/dimensions' import ClassNames from 'classnames' import { CustomLayerItemRenderer, ICustomLayerItemProps } from './CustomLayerItemRenderer' -import { Lottie } from '@crello/react-lottie' -// @ts-expect-error Not recognized by Typescript -import * as loopAnimation from './icon-loop.json' import { withTranslation, WithTranslation } from 'react-i18next' import { VTContent } from '@sofie-automation/blueprints-integration' import { PieceStatusIcon } from '../../../lib/ui/PieceStatusIcon' @@ -44,15 +41,6 @@ export class VTSourceRendererBase extends CustomLayerItemRenderer - {begin && end === '' && vtContent && vtContent.loop && ( -
- -
- )} + {begin && end === '' && this.renderLoopIcon()} {this.renderContentTrimmed()} ) : null @@ -302,8 +279,6 @@ export class VTSourceRendererBase extends CustomLayerItemRenderer - {end && vtContent && vtContent.loop && ( -
- -
- )} + {end && this.renderLoopIcon()} {end} {this.renderInfiniteIcon()} { diff --git a/meteor/client/ui/SegmentTimeline/withMediaObjectStatus.tsx b/meteor/client/ui/SegmentTimeline/withMediaObjectStatus.tsx index bc4b86b720..edf1e2a302 100644 --- a/meteor/client/ui/SegmentTimeline/withMediaObjectStatus.tsx +++ b/meteor/client/ui/SegmentTimeline/withMediaObjectStatus.tsx @@ -14,6 +14,7 @@ import { PieceContentStatusObj } from '../../../lib/mediaObjects' import { deepFreeze } from '@sofie-automation/corelib/dist/lib' import _ from 'underscore' import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' type AnyPiece = { piece?: BucketAdLibUi | IAdLibListItem | AdLibPieceUi | PieceUi | BucketAdLibActionUi | undefined @@ -172,3 +173,18 @@ export function useContentStatusForPiece( [piece?._id, piece?.startRundownId, piece?.startSegmentId] ) } + +export function useContentStatusForPieceInstance( + piece: Pick | undefined +): PieceContentStatusObj | undefined { + return useTracker( + () => + piece + ? UIPieceContentStatuses.findOne({ + pieceId: piece._id, + rundownId: piece.rundownId || { $exists: false }, + })?.status + : undefined, + [piece?._id, piece?.rundownId] + ) +} diff --git a/packages/blueprints-integration/src/content.ts b/packages/blueprints-integration/src/content.ts index 35dda52c2f..ba1eb44ff5 100644 --- a/packages/blueprints-integration/src/content.ts +++ b/packages/blueprints-integration/src/content.ts @@ -9,6 +9,8 @@ export type WithTimeline = T & { export interface BaseContent { editable?: BaseEditableParameters + loop?: boolean + sourceDuration?: number ignoreMediaObjectStatus?: boolean ignoreBlackFrames?: boolean @@ -43,7 +45,6 @@ export type UnknownContent = BaseContent export interface VTContent extends BaseContent { fileName: string path: string - loop?: boolean /** Frame that media manager should grab for thumbnail preview */ previewFrame?: number mediaFlowIds?: string[] @@ -153,7 +154,6 @@ export interface SplitsContent extends BaseContent { export interface AudioContent extends BaseContent { fileName: string path: string - loop?: boolean } // export type LowerThirdContent = GraphicsContent From 471ad593af5d2679eb1750f864c3984b40b6e82a Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 23 Aug 2023 10:23:53 +0200 Subject: [PATCH 074/479] fix: Media Status PopUp panel filtering doesn't work --- meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx b/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx index fc4d573ad6..2b3ce8ee52 100644 --- a/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/index.tsx @@ -47,7 +47,7 @@ export function MediaStatusPopUp({ playlistId }: IProps): JSX.Element { const filterItems = useCallback( (item: IMediaStatusListItem) => { - if (debouncedFilter) return true + if (emptyFilter) return true if (item.name.toLowerCase().indexOf(debouncedFilter.toLowerCase().trim()) >= 0) return true if ((item.partIdentifier?.toLocaleLowerCase() ?? '').indexOf(debouncedFilter.toLowerCase().trim()) === 0) return true From f04f18f8e94b706dd1a3da1525feb9787f6eb85f Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 23 Aug 2023 13:23:49 +0100 Subject: [PATCH 075/479] feat: cleanup deprecations (#996) --- README.md | 26 ++++++++++++++++++- meteor/client/lib/rundown.ts | 5 ---- meteor/client/lib/ui/pieceUiClassNames.ts | 9 ------- meteor/client/ui/Prompter/prompter.ts | 4 +-- .../ConfigManifestEntryComponent.tsx | 2 +- meteor/server/collections/packages-media.ts | 1 - packages/blueprints-integration/src/action.ts | 4 +-- .../src/api/showStyle.ts | 5 ++-- .../blueprints-integration/src/api/studio.ts | 5 ++-- .../src/documents/expectedPlayoutItem.ts | 1 - .../src/documents/piece.ts | 5 ---- .../src/documents/pieceGeneric.ts | 22 +--------------- .../src/dataModel/ExpectedPlayoutItem.ts | 3 --- packages/corelib/src/dataModel/Ids.ts | 4 +-- packages/corelib/src/dataModel/Piece.ts | 7 +---- .../job-worker/src/blueprints/context/lib.ts | 4 --- .../job-worker/src/blueprints/postProcess.ts | 3 +-- .../src/ingest/expectedPlayoutItems.ts | 1 - 18 files changed, 39 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index b86061443b..56c0ba12d8 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ The "Attach" configuration in `launch.json` supports debugging blueprints. Local blueprints repo needs to be added to the Visual Studio Code workspace under the name "Blueprints". -It is required to set `devtool` to `'inline-source-map'` and `output.devtoolModuleFilenameTemplate` to `'blueprint:///[resource-path]'` in webpack config of the blueprints. +It is required to set `devtool` to `'inline-source-map'` and `output.devtoolModuleFilenameTemplate` to `'blueprint:///[resource-path]'` in webpack config of the blueprints. ### Dealing with strange errors @@ -133,6 +133,30 @@ The version numbers of the blueprints-integration and server-core-integration li The api of server-core-integration is pretty stable and rarely undergoes any breaking changes, so is ok to be mismatched. The api of blueprints-integration is rather volatile, and often has breaking changes. Because of this, we recommend matching the minor version of blueprints-integration with Sofie core. Sofie will warn if these do not match. We expect this to settle down in the future, and will review this decision when we feel it is worthwhile. +## Deprecations + +### ConfigManifests + +The ConfigManifests for Blueprints and Gateways was replaced with JSONSchema in R50. +However, one usage by AdlibActions for their userDataManifest remains as this is not something we are actively using. + +## Blueprint Migrations + +In R49, a replacement flow was added consisting of `validateConfig` and `applyConfig`. +It is no longer recommended to use the old migrations flow for showstyle and studio blueprints. + +### ExpectedMediaItems + +These are used for Media-manager which is no longer being developed. + +### Blueprints: getPieceABSessionId & getTimelineObjectAbSessionId + +With AB being a native concept supported by Sofie since R50, these are likely no longer useful to Blueprints. + +### MongoQuery `fields` specifier + +It is recommended to use `projection` instead, as it is functionally identical but follows recommended naming from mongodb. + ## Additional information Background image used for previewing graphical elements is based on "Sunset over dark forest" by Aliis Sinisalu: https://unsplash.com/photos/8NiAH5YRZPs used under the [Unsplash License](https://unsplash.com/license). diff --git a/meteor/client/lib/rundown.ts b/meteor/client/lib/rundown.ts index 21858c4b91..28c4427487 100644 --- a/meteor/client/lib/rundown.ts +++ b/meteor/client/lib/rundown.ts @@ -564,11 +564,6 @@ export namespace RundownUtils { // add the piece to the map to make future searches quicker piecesLookup.set(piece.piece._id, resPiece) - const continues = piece.piece.continuesRefId && piecesLookup.get(piece.piece.continuesRefId) - if (piece.piece.continuesRefId && continues) { - continues.continuedByRef = resPiece - resPiece.continuesRef = continues - } return resPiece }) diff --git a/meteor/client/lib/ui/pieceUiClassNames.ts b/meteor/client/lib/ui/pieceUiClassNames.ts index 1d7f6091fa..7b6f078f62 100644 --- a/meteor/client/lib/ui/pieceUiClassNames.ts +++ b/meteor/client/lib/ui/pieceUiClassNames.ts @@ -22,15 +22,6 @@ export function pieceUiClassNames( const innerPiece = pieceInstance.instance.piece return classNames(baseClassName, typeClass, { - 'with-in-transition': - innerPiece.transitions && - innerPiece.transitions.inTransition && - (innerPiece.transitions.inTransition.duration || 0) > 0, - 'with-out-transition': - innerPiece.transitions && - innerPiece.transitions.outTransition && - (innerPiece.transitions.outTransition.duration || 0) > 0, - 'hide-overflow-labels': uiState && elementWidth ? uiState.leftAnchoredWidth > 0 && uiState.leftAnchoredWidth + uiState.rightAnchoredWidth > elementWidth diff --git a/meteor/client/ui/Prompter/prompter.ts b/meteor/client/ui/Prompter/prompter.ts index 6adca038d9..8980859b73 100644 --- a/meteor/client/ui/Prompter/prompter.ts +++ b/meteor/client/ui/Prompter/prompter.ts @@ -233,10 +233,10 @@ export namespace PrompterAPI { if (piece.content && sourceLayer && sourceLayer.type === SourceLayerType.SCRIPT) { const content = piece.content as ScriptContent if (content.fullScript) { - if (piecesIncluded.indexOf(piece.continuesRefId || piece._id) >= 0) { + if (piecesIncluded.indexOf(piece._id) >= 0) { break // piece already included in prompter script } - piecesIncluded.push(piece.continuesRefId || piece._id) + piecesIncluded.push(piece._id) partData.pieces.push({ id: piece._id, text: content.fullScript, diff --git a/meteor/client/ui/Settings/components/ConfigManifestEntryComponent.tsx b/meteor/client/ui/Settings/components/ConfigManifestEntryComponent.tsx index 76ac7c2647..43f4d71c00 100644 --- a/meteor/client/ui/Settings/components/ConfigManifestEntryComponent.tsx +++ b/meteor/client/ui/Settings/components/ConfigManifestEntryComponent.tsx @@ -75,7 +75,7 @@ export interface IConfigManifestEntryComponentProps { className?: string } -/** @deprecated */ +/** @deprecated These Manifests have been replaced with JSONSchema. This has not yet been done for the userDataManifest for adlib-actions */ export function ConfigManifestEntryComponent({ configField, obj, diff --git a/meteor/server/collections/packages-media.ts b/meteor/server/collections/packages-media.ts index dc787048b2..3529e3acc2 100644 --- a/meteor/server/collections/packages-media.ts +++ b/meteor/server/collections/packages-media.ts @@ -55,7 +55,6 @@ registerIndex(ExpectedPackageWorkStatuses, { // deviceId: 1, // }) -/** @deprecated */ export const ExpectedPlayoutItems = createAsyncOnlyReadOnlyMongoCollection( CollectionName.ExpectedPlayoutItems ) diff --git a/packages/blueprints-integration/src/action.ts b/packages/blueprints-integration/src/action.ts index 3f55614166..5c39e9bd7b 100644 --- a/packages/blueprints-integration/src/action.ts +++ b/packages/blueprints-integration/src/action.ts @@ -104,9 +104,7 @@ export interface IBlueprintActionManifest { /** Optional ways of executing this action. The default option is computed from the display properties */ triggerModes?: IBlueprintActionTriggerMode[] - /** Array of items expected to be played out. This is used by playout-devices to preload stuff. - * @deprecated replaced by .expectedPackages - */ + /** Array of items expected to be played out. This is used by playout-devices to preload stuff. */ expectedPlayoutItems?: ExpectedPlayoutItemGeneric[] /** * An array of which Packages this Action uses. This is used by a Package Manager to ensure that the Package is in place for playout. diff --git a/packages/blueprints-integration/src/api/showStyle.ts b/packages/blueprints-integration/src/api/showStyle.ts index d87942e623..cfe3f0fed6 100644 --- a/packages/blueprints-integration/src/api/showStyle.ts +++ b/packages/blueprints-integration/src/api/showStyle.ts @@ -52,7 +52,9 @@ export interface ShowStyleBlueprintManifest - /** A list of Migration steps related to a ShowStyle */ + /** A list of Migration steps related to a ShowStyle + * @deprecated This has been replaced with `validateConfig` and `applyConfig` + */ showStyleMigrations: MigrationStepShowStyle[] /** The config presets exposed by this blueprint */ @@ -193,7 +195,6 @@ export interface BlueprintResultTimeline { } export interface BlueprintResultBaseline { timelineObjects: TimelineObjectCoreExt[] - /** @deprecated */ expectedPlayoutItems?: ExpectedPlayoutItemGeneric[] expectedPackages?: ExpectedPackage.Any[] } diff --git a/packages/blueprints-integration/src/api/studio.ts b/packages/blueprints-integration/src/api/studio.ts index 5593f7fffb..9b4373a535 100644 --- a/packages/blueprints-integration/src/api/studio.ts +++ b/packages/blueprints-integration/src/api/studio.ts @@ -18,7 +18,9 @@ export interface StudioBlueprintManifest - /** A list of Migration steps related to a Studio */ + /** A list of Migration steps related to a Studio + * @deprecated This has been replaced with `validateConfig` and `applyConfig` + */ studioMigrations: MigrationStepStudio[] /** The config presets exposed by this blueprint */ @@ -71,7 +73,6 @@ export interface StudioBlueprintManifest[] - /** @deprecated */ expectedPlayoutItems?: ExpectedPlayoutItemGeneric[] expectedPackages?: ExpectedPackage.Any[] } diff --git a/packages/blueprints-integration/src/documents/expectedPlayoutItem.ts b/packages/blueprints-integration/src/documents/expectedPlayoutItem.ts index bcbcf3da7b..564ed1ab6e 100644 --- a/packages/blueprints-integration/src/documents/expectedPlayoutItem.ts +++ b/packages/blueprints-integration/src/documents/expectedPlayoutItem.ts @@ -1,6 +1,5 @@ import type { TSR } from '../timeline' -/** @deprecated */ export interface ExpectedPlayoutItemGeneric { /** What type of playout device this item should be handled by */ deviceSubType: TSR.DeviceType // subset of PeripheralDeviceAPI.DeviceSubType diff --git a/packages/blueprints-integration/src/documents/piece.ts b/packages/blueprints-integration/src/documents/piece.ts index e838c201dc..a7fd5e30b5 100644 --- a/packages/blueprints-integration/src/documents/piece.ts +++ b/packages/blueprints-integration/src/documents/piece.ts @@ -19,11 +19,6 @@ export interface IBlueprintPiece extends IBlueprintPieceGen /** Whether the piece is a real piece, or exists as a marker to stop an infinite piece. If virtual, it does not add any contents to the timeline */ virtual?: boolean - /** - * @deprecated This is a remnant of an old infinite piece implementation, and has no purpose now. - * The id of the item this item is a continuation of. If it is a continuation, the inTranstion must not be set, and trigger must be 0 - */ - continuesRefId?: string /** Whether this piece is a special piece */ pieceType?: IBlueprintPieceType diff --git a/packages/blueprints-integration/src/documents/pieceGeneric.ts b/packages/blueprints-integration/src/documents/pieceGeneric.ts index 482756885e..9874aead46 100644 --- a/packages/blueprints-integration/src/documents/pieceGeneric.ts +++ b/packages/blueprints-integration/src/documents/pieceGeneric.ts @@ -7,15 +7,6 @@ import type { ExpectedPlayoutItemGeneric } from './expectedPlayoutItem' export { PieceLifespan } -export declare enum PieceTransitionType { - MIX = 'MIX', - WIPE = 'WIPE', -} -export interface PieceTransition { - type: PieceTransitionType - duration: number -} - export enum IBlueprintDirectPlayType { AdLibPiece = 'adlib', AdLibAction = 'action', @@ -56,15 +47,6 @@ export interface IBlueprintPieceGeneric { /** The object describing the item in detail */ content: WithTimeline - /** The transition used by this piece to transition to and from the piece */ - /** @deprecated */ - transitions?: { - /** In transition for the piece */ - inTransition?: PieceTransition - /** The out transition for the piece */ - outTransition?: PieceTransition - } - /** * How long this piece needs to prepare its content before it will have an effect on the output. * This allows for flows such as starting a clip playing, then cutting to it after some ms once the player is outputting frames. @@ -79,9 +61,7 @@ export interface IBlueprintPieceGeneric { /** Whether the adlib should always be inserted queued */ toBeQueued?: boolean - /** Array of items expected to be played out. This is used by playout-devices to preload stuff. - * @deprecated replaced by .expectedPackages - */ + /** Array of items expected to be played out. This is used by playout-devices to preload stuff. */ expectedPlayoutItems?: ExpectedPlayoutItemGeneric[] /** User-defined tags that can be used for filtering adlibs in the shelf and identifying pieces by actions */ tags?: string[] diff --git a/packages/corelib/src/dataModel/ExpectedPlayoutItem.ts b/packages/corelib/src/dataModel/ExpectedPlayoutItem.ts index ee03d57ef2..a2a3d9ecc1 100644 --- a/packages/corelib/src/dataModel/ExpectedPlayoutItem.ts +++ b/packages/corelib/src/dataModel/ExpectedPlayoutItem.ts @@ -8,7 +8,6 @@ export interface ExpectedPlayoutItemBase extends ExpectedPlayoutItemGeneric { /** The studio installation this ExpectedPlayoutItem was generated in */ studioId: StudioId } -/** @deprecated */ export interface ExpectedPlayoutItemRundown extends ExpectedPlayoutItemBase { /** The rundown id that is the source of this PlayoutItem */ rundownId: RundownId @@ -19,9 +18,7 @@ export interface ExpectedPlayoutItemRundown extends ExpectedPlayoutItemBase { /** Is created for studio/rundown baseline */ baseline?: 'rundown' } -/** @deprecated */ export interface ExpectedPlayoutItemStudio extends ExpectedPlayoutItemBase { baseline: 'studio' } -/** @deprecated */ export type ExpectedPlayoutItem = ExpectedPlayoutItemStudio | ExpectedPlayoutItemRundown diff --git a/packages/corelib/src/dataModel/Ids.ts b/packages/corelib/src/dataModel/Ids.ts index cff4e8f8df..840efbe33e 100644 --- a/packages/corelib/src/dataModel/Ids.ts +++ b/packages/corelib/src/dataModel/Ids.ts @@ -29,9 +29,7 @@ export type EvaluationId = ProtectedString<'EvaluationId'> */ export type ExpectedMediaItemId = ProtectedString<'ExpectedMediaItemId'> -/** A string, identifying a Rundown - * @deprecated - */ +/** A string, identifying a Rundown */ export type ExpectedPlayoutItemId = ProtectedString<'ExpectedPlayoutItemId'> /** A string, identifying a ExternalMessageQueueObj */ diff --git a/packages/corelib/src/dataModel/Piece.ts b/packages/corelib/src/dataModel/Piece.ts index bb6aeddb28..a340c45fff 100644 --- a/packages/corelib/src/dataModel/Piece.ts +++ b/packages/corelib/src/dataModel/Piece.ts @@ -45,17 +45,12 @@ export interface PieceGeneric extends Omit { /** A flag to signal that a given Piece has no content, and exists only as a marker on the timeline */ virtual?: boolean - /** - * @deprecated This is a remnant of an old infinite piece implementation, and has no purpose now. - * The id of the item this item is a continuation of. If it is a continuation, the inTranstion must not be set, and trigger must be 0 - */ - continuesRefId?: PieceId /** Stringified timelineObjects */ timelineObjectsString: PieceTimelineObjectsBlob } -export interface Piece extends PieceGeneric, Omit { +export interface Piece extends PieceGeneric, Omit { /** * This is the id of the rundown this piece starts playing in. * Currently this is the only rundown the piece could be playing in diff --git a/packages/job-worker/src/blueprints/context/lib.ts b/packages/job-worker/src/blueprints/context/lib.ts index 3625baa8fa..656f4ea906 100644 --- a/packages/job-worker/src/blueprints/context/lib.ts +++ b/packages/job-worker/src/blueprints/context/lib.ts @@ -54,7 +54,6 @@ export const IBlueprintPieceObjectsSampleKeys = allKeysOfObject externalId: true, enable: true, virtual: true, - continuesRefId: true, pieceType: true, extendOnHold: true, name: true, @@ -62,7 +61,6 @@ export const IBlueprintPieceObjectsSampleKeys = allKeysOfObject sourceLayerId: true, outputLayerId: true, content: true, - transitions: true, lifespan: true, prerollDuration: true, postrollDuration: true, @@ -179,7 +177,6 @@ function convertPieceGenericToBlueprintsInner(piece: PieceGeneric): Complete), + ...orgPiece, content: omit(orgPiece.content, 'timelineObjects'), _id: protectString(docId), - continuesRefId: protectString(orgPiece.continuesRefId), startRundownId: rundownId, startSegmentId: segmentId, startPartId: partId, diff --git a/packages/job-worker/src/ingest/expectedPlayoutItems.ts b/packages/job-worker/src/ingest/expectedPlayoutItems.ts index 11a73fe621..3aba8d3747 100644 --- a/packages/job-worker/src/ingest/expectedPlayoutItems.ts +++ b/packages/job-worker/src/ingest/expectedPlayoutItems.ts @@ -43,7 +43,6 @@ function extractExpectedPlayoutItems( return expectedPlayoutItemsGeneric } -/** @deprecated */ export async function updateExpectedPlayoutItemsOnRundown(context: JobContext, cache: CacheForIngest): Promise { const expectedPlayoutItems: ExpectedPlayoutItem[] = [] From ddfb66a1ef8afbd507535037d12752742c7dfeda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Mareti=C4=87?= Date: Mon, 19 Jun 2023 16:28:59 +0200 Subject: [PATCH 076/479] feat: Editable fields as JSON --- packages/blueprints-integration/src/action.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/blueprints-integration/src/action.ts b/packages/blueprints-integration/src/action.ts index 5c39e9bd7b..7bd0121117 100644 --- a/packages/blueprints-integration/src/action.ts +++ b/packages/blueprints-integration/src/action.ts @@ -1,8 +1,9 @@ import { ExpectedPackage } from './package' -import { ConfigManifestEntry } from './config' import { SomeContent } from './content' import { ITranslatableMessage } from './translations' import { ExpectedPlayoutItemGeneric } from './documents' +import { JSONBlob } from '@sofie-automation/shared-lib/dist/lib/JSONBlob' +import { JSONSchema } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes' export interface ActionUserData { [key: string]: any @@ -92,7 +93,7 @@ export interface IBlueprintActionManifest { userDataManifest: { /** List of editable fields in userData, to allow for customising */ - editableFields?: ConfigManifestEntry[] + editableFields?: JSONBlob /** Execute the action after userData is changed. If not present ActionExecuteAfterChanged.none is assumed. */ executeOnUserDataChanged?: ActionExecuteAfterChanged // Potential future properties: From 66561b81accf03e5df8267311c171966e95b3a65 Mon Sep 17 00:00:00 2001 From: Baud Date: Fri, 25 Aug 2023 11:39:03 +0100 Subject: [PATCH 077/479] fix: Remove inspector for editable fields --- .../Inspector/ItemRenderers/ActionItemRenderer.tsx | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/meteor/client/ui/Shelf/Inspector/ItemRenderers/ActionItemRenderer.tsx b/meteor/client/ui/Shelf/Inspector/ItemRenderers/ActionItemRenderer.tsx index ad9f6b30c3..6ff89512bd 100644 --- a/meteor/client/ui/Shelf/Inspector/ItemRenderers/ActionItemRenderer.tsx +++ b/meteor/client/ui/Shelf/Inspector/ItemRenderers/ActionItemRenderer.tsx @@ -254,19 +254,6 @@ export default translateWithTracker((props: IProps) = studio={this.props.studio} />
-
- {action.userDataManifest && action.userDataManifest.editableFields && !targetAction ? ( - - ) : action.userDataManifest && action.userDataManifest.editableFields && targetAction ? ( - <> - {this.renderConfigFields( - action.userDataManifest.editableFields, - targetAction, - 'transformedUserData.' - )} - - ) : null} -
{modes?.length ? (
- {resourceName}:{upgradeResult.name} - - {upgradeResult.invalidReason && ( - <> - {t('Unable to upgrade')}: {translateMessage(upgradeResult.invalidReason, i18nTranslator)} - - )} - {upgradeResult.changes.length > 0 && t('Upgrade required')} - -
- - - -
-
+ + + + + + + {isReady && statuses && statuses.length === 0 && ( + + + + )} + + {statuses?.map( + (document) => + document.documentType === 'studio' && ( + + ) + )} + + {statuses?.map( + (document) => + document.documentType === 'showStyle' && ( + + ) + )} + +
Name  
No Studios or ShowStyles were found
+
+
+ ) +} + +interface ShowUpgradesRowProps { + resourceName: string + upgradeResult: UIBlueprintUpgradeStatusStudio | UIBlueprintUpgradeStatusShowStyle +} +function ShowUpgradesRow({ resourceName, upgradeResult }: ShowUpgradesRowProps) { + const { t } = useTranslation() + + return ( + + + {resourceName}:{upgradeResult.name} + + + {getUpgradeStatusMessage(t, upgradeResult)} + + + + + + ) +} diff --git a/meteor/lib/api/migration.ts b/meteor/lib/api/migration.ts index ce2b8c0ae9..d2c4d145b6 100644 --- a/meteor/lib/api/migration.ts +++ b/meteor/lib/api/migration.ts @@ -1,6 +1,5 @@ import { MigrationStepInput, MigrationStepInputResult } from '@sofie-automation/blueprints-integration' import { BlueprintId, ShowStyleBaseId, SnapshotId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { ITranslatableMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import { BlueprintValidateConfigForStudioResult } from '@sofie-automation/corelib/dist/worker/studio' export interface NewMigrationAPI { @@ -14,11 +13,6 @@ export interface NewMigrationAPI { forceMigration(chunks: Array): Promise resetDatabaseVersions(): Promise - /** - * Get the status information for each Studio and ShowStyle on their blueprintConfig upgrade status - */ - getUpgradeStatus(): Promise - /** * Run `validateConfig` on the blueprint for a Studio * @param studioId Id of the Studio @@ -59,28 +53,6 @@ export enum MigrationAPIMethods { 'runUpgradeForShowStyleBase' = 'migration.runUpgradeForShowStyleBase', } -export interface GetUpgradeStatusResultStudio { - studioId: StudioId - name: string - - invalidReason?: ITranslatableMessage - - changes: ITranslatableMessage[] -} -export interface GetUpgradeStatusResultShowStyleBase { - showStyleBaseId: ShowStyleBaseId - name: string - - invalidReason?: ITranslatableMessage - - changes: ITranslatableMessage[] -} - -export interface GetUpgradeStatusResult { - studios: GetUpgradeStatusResultStudio[] - showStyleBases: GetUpgradeStatusResultShowStyleBase[] -} - export interface GetMigrationStatusResult { migrationNeeded: boolean diff --git a/meteor/lib/api/pubsub.ts b/meteor/lib/api/pubsub.ts index 6b08d13566..d0ebc7ba31 100644 --- a/meteor/lib/api/pubsub.ts +++ b/meteor/lib/api/pubsub.ts @@ -63,6 +63,7 @@ import { import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { RoutedMappings } from '@sofie-automation/shared-lib/dist/core/model/Timeline' import { logger } from '../logging' +import { UIBlueprintUpgradeStatus } from './upgradeStatus' /** * Ids of possible DDP subscriptions @@ -139,6 +140,7 @@ export enum PubSub { uiSegmentPartNotes = 'uiSegmentPartNotes', uiPieceContentStatuses = 'uiPieceContentStatuses', uiBucketContentStatuses = 'uiBucketContentStatuses', + uiBlueprintUpgradeStatuses = 'uiBlueprintUpgradeStatuses', packageManagerPlayoutContext = 'packageManagerPlayoutContext', packageManagerPackageContainers = 'packageManagerPackageContainers', @@ -250,6 +252,7 @@ export interface PubSubTypes { [PubSub.uiSegmentPartNotes]: (playlistId: RundownPlaylistId | null) => UISegmentPartNote [PubSub.uiPieceContentStatuses]: (rundownPlaylistId: RundownPlaylistId | null) => UIPieceContentStatus [PubSub.uiBucketContentStatuses]: (studioId: StudioId, bucketId: BucketId) => UIBucketContentStatus + [PubSub.uiBlueprintUpgradeStatuses]: () => UIBlueprintUpgradeStatus /** Custom publications for package-manager */ [PubSub.packageManagerPlayoutContext]: ( @@ -283,6 +286,7 @@ export enum CustomCollectionName { UISegmentPartNotes = 'uiSegmentPartNotes', UIPieceContentStatuses = 'uiPieceContentStatuses', UIBucketContentStatuses = 'uiBucketContentStatuses', + UIBlueprintUpgradeStatuses = 'uiBlueprintUpgradeStatuses', PackageManagerPlayoutContext = 'packageManagerPlayoutContext', PackageManagerPackageContainers = 'packageManagerPackageContainers', @@ -306,6 +310,7 @@ export type CustomCollectionType = { [CustomCollectionName.UISegmentPartNotes]: UISegmentPartNote [CustomCollectionName.UIPieceContentStatuses]: UIPieceContentStatus [CustomCollectionName.UIBucketContentStatuses]: UIBucketContentStatus + [CustomCollectionName.UIBlueprintUpgradeStatuses]: UIBlueprintUpgradeStatus [CustomCollectionName.PackageManagerPlayoutContext]: PackageManagerPlayoutContext [CustomCollectionName.PackageManagerPackageContainers]: PackageManagerPackageContainers [CustomCollectionName.PackageManagerExpectedPackages]: PackageManagerExpectedPackage diff --git a/meteor/lib/api/upgradeStatus.ts b/meteor/lib/api/upgradeStatus.ts new file mode 100644 index 0000000000..f1eda15071 --- /dev/null +++ b/meteor/lib/api/upgradeStatus.ts @@ -0,0 +1,29 @@ +import { ITranslatableMessage } from '@sofie-automation/blueprints-integration' +import { StudioId, ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { ProtectedString } from '../lib' + +export type UIBlueprintUpgradeStatusId = ProtectedString<'UIBlueprintUpgradeStatus'> + +export type UIBlueprintUpgradeStatus = UIBlueprintUpgradeStatusStudio | UIBlueprintUpgradeStatusShowStyle + +export interface UIBlueprintUpgradeStatusBase { + _id: UIBlueprintUpgradeStatusId + + documentType: 'studio' | 'showStyle' + documentId: StudioId | ShowStyleBaseId + + name: string + + invalidReason?: ITranslatableMessage + + changes: ITranslatableMessage[] +} + +export interface UIBlueprintUpgradeStatusStudio extends UIBlueprintUpgradeStatusBase { + documentType: 'studio' + documentId: StudioId +} +export interface UIBlueprintUpgradeStatusShowStyle extends UIBlueprintUpgradeStatusBase { + documentType: 'showStyle' + documentId: ShowStyleBaseId +} diff --git a/meteor/server/lib/customPublication/__tests__/optimizedObserver.test.ts b/meteor/server/lib/customPublication/__tests__/optimizedObserver.test.ts index 5038d3b5d3..d4bec4a0a8 100644 --- a/meteor/server/lib/customPublication/__tests__/optimizedObserver.test.ts +++ b/meteor/server/lib/customPublication/__tests__/optimizedObserver.test.ts @@ -9,7 +9,7 @@ interface CustomPublishMockExt { } class CustomPublishMock }> - implements Omit, '#onStop' | '#isReady'>, CustomPublishMockExt + implements CustomPublish, CustomPublishMockExt { static create }>(): CustomPublish & CustomPublishMockExt { const mock = new CustomPublishMock() diff --git a/meteor/server/lib/customPublication/publish.ts b/meteor/server/lib/customPublication/publish.ts index 59677c857b..740dc3cdbf 100644 --- a/meteor/server/lib/customPublication/publish.ts +++ b/meteor/server/lib/customPublication/publish.ts @@ -10,7 +10,26 @@ export interface CustomPublishChanges }> { removed: T['_id'][] } -export class CustomPublish }> { +export interface CustomPublish }> { + get isReady(): boolean + + /** + * Register a function to be called when the subscriber unsubscribes + */ + onStop(callback: () => void): void + + /** + * Send the intial documents to the subscriber + */ + init(docs: DBObj[]): void + + /** + * Send a batch of changes to the subscriber + */ + changed(changes: CustomPublishChanges): void +} + +export class CustomPublishMeteor }> { #onStop: (() => void) | undefined #isReady = false @@ -80,6 +99,6 @@ export function meteorCustomPublish( ) => Promise ): void { meteorPublishUnsafe(publicationName, async function (this: SubscriptionContext, ...args: any[]) { - return cb.call(this, new CustomPublish(this, customCollectionName), ...(args as any)) + return cb.call(this, new CustomPublishMeteor(this, customCollectionName), ...(args as any)) }) } diff --git a/meteor/server/migration/api.ts b/meteor/server/migration/api.ts index 408ef1978b..c56690f8b7 100644 --- a/meteor/server/migration/api.ts +++ b/meteor/server/migration/api.ts @@ -1,12 +1,11 @@ import { check, Match } from '../../lib/check' import { registerClassToMeteorMethods } from '../methods' -import { MigrationChunk, NewMigrationAPI, MigrationAPIMethods, GetUpgradeStatusResult } from '../../lib/api/migration' +import { MigrationChunk, NewMigrationAPI, MigrationAPIMethods } from '../../lib/api/migration' import * as Migrations from './databaseMigration' import { MigrationStepInputResult } from '@sofie-automation/blueprints-integration' import { MethodContextAPI } from '../../lib/api/methods' import { SystemWriteAccess } from '../security/system' import { - getUpgradeStatus, runUpgradeForShowStyleBase, runUpgradeForStudio, validateConfigForShowStyleBase, @@ -50,12 +49,6 @@ class ServerMigrationAPI extends MethodContextAPI implements NewMigrationAPI { return Migrations.resetDatabaseVersions() } - async getUpgradeStatus(): Promise { - await SystemWriteAccess.migrations(this) - - return getUpgradeStatus() - } - async validateConfigForStudio(studioId: StudioId): Promise { check(studioId, String) diff --git a/meteor/server/migration/upgrades/__tests__/checkStatus.test.ts b/meteor/server/migration/upgrades/__tests__/checkStatus.test.ts deleted file mode 100644 index 83041c037e..0000000000 --- a/meteor/server/migration/upgrades/__tests__/checkStatus.test.ts +++ /dev/null @@ -1,341 +0,0 @@ -import '../../../../__mocks__/_extendJest' -import { protectString } from '@sofie-automation/corelib/dist/protectedString' -import { testInFiber } from '../../../../__mocks__/helpers/jest' -import { getUpgradeStatus } from '../checkStatus' -import { literal } from '@sofie-automation/corelib/dist/lib' -import { GetUpgradeStatusResult } from '../../../../lib/api/migration' -import { - setupMockShowStyleBase, - setupMockShowStyleBlueprint, - setupMockStudio, - setupMockStudioBlueprint, -} from '../../../../__mocks__/helpers/database' -import { generateTranslation } from '../../../../lib/lib' -import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { ShowStyleBases, Studios } from '../../../collections' - -describe('getUpgradeStatus', () => { - afterEach(async () => { - await Studios.removeAsync({}) - await ShowStyleBases.removeAsync({}) - }) - - testInFiber('no studios or showstyles', async () => { - const result = await getUpgradeStatus() - expect(result).toEqual( - literal({ - showStyleBases: [], - studios: [], - }) - ) - }) - - testInFiber('Studios and showStyles missing blueprints', async () => { - const studio0 = await setupMockStudio() - const studio1 = await setupMockStudio() - const showStyle0 = await setupMockShowStyleBase(protectString('')) - const showStyle1 = await setupMockShowStyleBase(protectString('')) - - const result = await getUpgradeStatus() - expect(result).toEqual( - literal({ - showStyleBases: [ - { - showStyleBaseId: showStyle0._id, - name: showStyle0.name, - changes: [], - invalidReason: generateTranslation('Invalid blueprint: "{{blueprintId}}"', { - blueprintId: showStyle0.blueprintId, - }), - }, - { - showStyleBaseId: showStyle1._id, - name: showStyle1.name, - changes: [], - invalidReason: generateTranslation('Invalid blueprint: "{{blueprintId}}"', { - blueprintId: showStyle1.blueprintId, - }), - }, - ], - studios: [ - { - studioId: studio0._id, - name: studio0.name, - changes: [], - invalidReason: generateTranslation('Invalid blueprint: "{{blueprintId}}"', { - blueprintId: studio0.blueprintId, - }), - }, - { - studioId: studio1._id, - name: studio1.name, - changes: [], - invalidReason: generateTranslation('Invalid blueprint: "{{blueprintId}}"', { - blueprintId: studio1.blueprintId, - }), - }, - ], - }) - ) - }) - - testInFiber('Invalid config preset', async () => { - const showStyleBlueprint = await setupMockShowStyleBlueprint(protectString('')) - const showStyle0 = await setupMockShowStyleBase(showStyleBlueprint._id) - - const result = await getUpgradeStatus() - expect(result).toEqual( - literal({ - showStyleBases: [ - { - showStyleBaseId: showStyle0._id, - name: showStyle0.name, - changes: [], - invalidReason: generateTranslation( - 'Invalid config preset for blueprint: "{{configPresetId}}" ({{blueprintId}})', - { - configPresetId: showStyle0.blueprintConfigPresetId ?? '', - blueprintId: showStyle0.blueprintId, - } - ), - }, - ], - studios: [], - }) - ) - }) - - testInFiber('Not run before', async () => { - const showStyleBlueprint = await setupMockShowStyleBlueprint(protectString('')) - const showStyle0 = await setupMockShowStyleBase(showStyleBlueprint._id, { blueprintConfigPresetId: 'main' }) - - const result = await getUpgradeStatus() - expect(result).toEqual( - literal({ - showStyleBases: [ - { - showStyleBaseId: showStyle0._id, - name: showStyle0.name, - changes: [generateTranslation('Config has not been applied before')], - }, - ], - studios: [], - }) - ) - }) - - testInFiber('Blueprint id has changed', async () => { - const showStyleBlueprint = await setupMockShowStyleBlueprint(protectString('')) - const showStyle0 = await setupMockShowStyleBase(showStyleBlueprint._id, { - blueprintConfigPresetId: 'main', - lastBlueprintConfig: { - blueprintHash: showStyleBlueprint.blueprintHash, - blueprintId: protectString('old-blueprint'), - blueprintConfigPresetId: 'main', - config: {}, - }, - }) - - const result = await getUpgradeStatus() - expect(result).toEqual( - literal({ - showStyleBases: [ - { - showStyleBaseId: showStyle0._id, - name: showStyle0.name, - changes: [ - generateTranslation( - 'Blueprint has been changed. From "{{ oldValue }}", to "{{ newValue }}"', - { - oldValue: protectString('old-blueprint'), - newValue: showStyle0.blueprintId || '', - } - ), - ], - }, - ], - studios: [], - }) - ) - }) - - testInFiber('Config preset has changed', async () => { - const showStyleBlueprint = await setupMockShowStyleBlueprint(protectString('')) - const showStyle0 = await setupMockShowStyleBase(showStyleBlueprint._id, { - blueprintConfigPresetId: 'main', - lastBlueprintConfig: { - blueprintHash: showStyleBlueprint.blueprintHash, - blueprintId: showStyleBlueprint._id, - blueprintConfigPresetId: 'old', - config: {}, - }, - }) - - const result = await getUpgradeStatus() - expect(result).toEqual( - literal({ - showStyleBases: [ - { - showStyleBaseId: showStyle0._id, - name: showStyle0.name, - changes: [ - generateTranslation( - 'Blueprint config preset has been changed. From "{{ oldValue }}", to "{{ newValue }}"', - { - oldValue: 'old', - newValue: showStyle0.blueprintConfigPresetId || '', - } - ), - ], - }, - ], - studios: [], - }) - ) - }) - - testInFiber('Blueprint hash has changed', async () => { - const showStyleBlueprint = await setupMockShowStyleBlueprint(protectString('')) - const showStyle0 = await setupMockShowStyleBase(showStyleBlueprint._id, { - blueprintConfigPresetId: 'main', - lastBlueprintConfig: { - blueprintHash: protectString('old-hash'), - blueprintId: showStyleBlueprint._id, - blueprintConfigPresetId: 'main', - config: {}, - }, - }) - - const result = await getUpgradeStatus() - expect(result).toEqual( - literal({ - showStyleBases: [ - { - showStyleBaseId: showStyle0._id, - name: showStyle0.name, - changes: [generateTranslation('Blueprint has a new version')], - }, - ], - studios: [], - }) - ) - }) - - testInFiber('Conifg has changed', async () => { - const showStyleBlueprint = await setupMockShowStyleBlueprint(protectString('')) - const showStyle0 = await setupMockShowStyleBase(showStyleBlueprint._id, { - blueprintConfigPresetId: 'main', - blueprintConfigWithOverrides: wrapDefaultObject({ - prop1: 'new-value', - some: { - deep: { - property: 2, - }, - }, - prop3: false, - }), - lastBlueprintConfig: { - blueprintHash: showStyleBlueprint.blueprintHash, - blueprintId: showStyleBlueprint._id, - blueprintConfigPresetId: 'main', - config: { - prop1: 'old-value', - some: { - deep: { - property: 1, - }, - }, - prop2: true, - }, - }, - }) - - const result = await getUpgradeStatus() - expect(result).toEqual( - literal({ - showStyleBases: [ - { - showStyleBaseId: showStyle0._id, - name: showStyle0.name, - changes: [generateTranslation('Blueprint config has changed')], - }, - ], - studios: [], - }) - ) - }) - - testInFiber('Good studios and showstyles', async () => { - const showStyleBlueprint = await setupMockShowStyleBlueprint(protectString('')) - const studioBlueprint = await setupMockStudioBlueprint(protectString('')) - - const studio0 = await setupMockStudio({ - blueprintId: studioBlueprint._id, - blueprintConfigPresetId: 'main', - lastBlueprintConfig: { - blueprintHash: studioBlueprint.blueprintHash, - blueprintId: studioBlueprint._id, - blueprintConfigPresetId: 'main', - config: {}, - }, - }) - const studio1 = await setupMockStudio({ - blueprintId: studioBlueprint._id, - blueprintConfigPresetId: 'main', - lastBlueprintConfig: { - blueprintHash: studioBlueprint.blueprintHash, - blueprintId: studioBlueprint._id, - blueprintConfigPresetId: 'main', - config: {}, - }, - }) - const showStyle0 = await setupMockShowStyleBase(showStyleBlueprint._id, { - blueprintConfigPresetId: 'main', - lastBlueprintConfig: { - blueprintHash: showStyleBlueprint.blueprintHash, - blueprintId: showStyleBlueprint._id, - blueprintConfigPresetId: 'main', - config: {}, - }, - }) - const showStyle1 = await setupMockShowStyleBase(showStyleBlueprint._id, { - blueprintConfigPresetId: 'main', - lastBlueprintConfig: { - blueprintHash: showStyleBlueprint.blueprintHash, - blueprintId: showStyleBlueprint._id, - blueprintConfigPresetId: 'main', - config: {}, - }, - }) - - const result = await getUpgradeStatus() - expect(result).toEqual( - literal({ - showStyleBases: [ - { - showStyleBaseId: showStyle0._id, - name: showStyle0.name, - changes: [], - }, - { - showStyleBaseId: showStyle1._id, - name: showStyle1.name, - changes: [], - }, - ], - studios: [ - { - studioId: studio0._id, - name: studio0.name, - changes: [], - }, - { - studioId: studio1._id, - name: studio1.name, - changes: [], - }, - ], - }) - ) - }) -}) diff --git a/meteor/server/migration/upgrades/index.ts b/meteor/server/migration/upgrades/index.ts index 59706f9fdb..301efe5fd0 100644 --- a/meteor/server/migration/upgrades/index.ts +++ b/meteor/server/migration/upgrades/index.ts @@ -1,4 +1,2 @@ -export * from './checkStatus' export * from './showStyleBase' export * from './studio' -export * from './systemStatus' diff --git a/meteor/server/migration/upgrades/systemStatus.ts b/meteor/server/migration/upgrades/systemStatus.ts deleted file mode 100644 index 18a4d5a936..0000000000 --- a/meteor/server/migration/upgrades/systemStatus.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { StatusCode } from '@sofie-automation/blueprints-integration' -import { literal } from '@sofie-automation/corelib/dist/lib' -import { Component } from '../../../lib/api/systemStatus' -import { status2ExternalStatus } from '../../systemStatus/systemStatus' -import { getUpgradeStatus } from './checkStatus' - -export async function getUpgradeSystemStatusMessages(): Promise { - const result: Component[] = [] - - const upgradeStatus = await getUpgradeStatus() - for (const studioStatus of upgradeStatus.studios) { - let status = StatusCode.GOOD - const messages: string[] = [] - if (studioStatus.invalidReason) { - status = StatusCode.WARNING_MAJOR - messages.push('Invalid configuration') - } else if (studioStatus.changes.length) { - status = StatusCode.WARNING_MINOR - messages.push('Configuration changed') - } - - result.push( - literal({ - name: `studio-blueprints-upgrade-${studioStatus.studioId}`, - status: status2ExternalStatus(status), - updated: new Date().toISOString(), - _status: status, - _internal: { - statusCodeString: StatusCode[status], - messages: messages, - versions: {}, - }, - }) - ) - } - for (const showStyleStatus of upgradeStatus.showStyleBases) { - let status = StatusCode.GOOD - const messages: string[] = [] - if (showStyleStatus.invalidReason) { - status = StatusCode.WARNING_MAJOR - messages.push('Invalid configuration') - } else if (showStyleStatus.changes.length) { - status = StatusCode.WARNING_MINOR - messages.push('Configuration changed') - } - - result.push( - literal({ - name: `showStyleBase-blueprints-upgrade-${showStyleStatus.showStyleBaseId}`, - status: status2ExternalStatus(status), - updated: new Date().toISOString(), - _status: status, - _internal: { - statusCodeString: StatusCode[status], - messages: messages, - versions: {}, - }, - }) - ) - } - - return result -} diff --git a/meteor/server/publications/_publications.ts b/meteor/server/publications/_publications.ts index 18057c5009..e05588e8df 100644 --- a/meteor/server/publications/_publications.ts +++ b/meteor/server/publications/_publications.ts @@ -1,6 +1,7 @@ import './lib' import './buckets' +import './blueprintUpgradeStatus/publication' import './packageManager/expectedPackages/publication' import './packageManager/packageContainers' import './packageManager/playoutContext' diff --git a/meteor/server/migration/upgrades/checkStatus.ts b/meteor/server/publications/blueprintUpgradeStatus/checkStatus.ts similarity index 53% rename from meteor/server/migration/upgrades/checkStatus.ts rename to meteor/server/publications/blueprintUpgradeStatus/checkStatus.ts index 3d7c8d422b..df317bb66d 100644 --- a/meteor/server/migration/upgrades/checkStatus.ts +++ b/meteor/server/publications/blueprintUpgradeStatus/checkStatus.ts @@ -1,181 +1,37 @@ import { - BlueprintManifestType, - IStudioConfigPreset, + getSchemaUIField, IShowStyleConfigPreset, + IStudioConfigPreset, ITranslatableMessage, + JSONBlob, + JSONBlobParse, + JSONSchema, + SchemaFormUIField, } from '@sofie-automation/blueprints-integration' -import { Blueprint, BlueprintHash } from '@sofie-automation/corelib/dist/dataModel/Blueprint' +import { BlueprintHash } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { BlueprintId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { - normalizeArrayToMap, - literal, - objectPathGet, - joinObjectPathFragments, -} from '@sofie-automation/corelib/dist/lib' -import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' -import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import _ from 'underscore' -import { - GetUpgradeStatusResult, - GetUpgradeStatusResultStudio, - GetUpgradeStatusResultShowStyleBase, -} from '../../../lib/api/migration' -import { Blueprints, ShowStyleBases, Studios } from '../../collections' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { joinObjectPathFragments, objectPathGet } from '@sofie-automation/corelib/dist/lib' +import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { generateTranslation } from '../../../lib/lib' -import { JSONBlob, JSONBlobParse } from '@sofie-automation/shared-lib/dist/lib/JSONBlob' -import { JSONSchema } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes' import { logger } from '../../logging' +import { ShowStyleBaseFields, StudioFields } from './reactiveContentCache' +import _ from 'underscore' +import { UIBlueprintUpgradeStatusBase } from '../../../lib/api/upgradeStatus' +import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' -export async function getUpgradeStatus(): Promise { - const studioUpgrades = await checkStudiosUpgradeStatus() - const showStyleUpgrades = await checkShowStyleBaseUpgradeStatus() - - return { - studios: studioUpgrades, - showStyleBases: showStyleUpgrades, - } -} - -async function checkStudiosUpgradeStatus(): Promise { - const result: GetUpgradeStatusResultStudio[] = [] - - const studios = (await Studios.findFetchAsync( - {}, - { - fields: { - _id: 1, - blueprintId: 1, - blueprintConfigPresetId: 1, - lastBlueprintConfig: 1, - blueprintConfigWithOverrides: 1, - name: 1, - }, - } - )) as Array - - const studioBlueprints = (await Blueprints.findFetchAsync( - { - blueprintType: BlueprintManifestType.STUDIO, - _id: { $in: _.compact(studios.map((st) => st.blueprintId)) }, - }, - { - fields: { - _id: 1, - studioConfigPresets: 1, - studioConfigSchema: 1, - blueprintHash: 1, - }, - } - )) as Array - - // Check each studio - const blueprintsMap = normalizeArrayToMap( - studioBlueprints.map((doc) => - literal({ - _id: doc._id, - configPresets: doc.studioConfigPresets, - configSchema: doc.studioConfigSchema, - blueprintHash: doc.blueprintHash, - }) - ), - '_id' - ) - for (const studio of studios) { - result.push({ - ...checkDocUpgradeStatus(blueprintsMap, studio), - studioId: studio._id, - name: studio.name, - }) - } - - return result -} - -async function checkShowStyleBaseUpgradeStatus(): Promise { - const result: GetUpgradeStatusResultShowStyleBase[] = [] - - const showStyles = (await ShowStyleBases.findFetchAsync( - {}, - { - fields: { - _id: 1, - blueprintId: 1, - blueprintConfigPresetId: 1, - lastBlueprintConfig: 1, - blueprintConfigWithOverrides: 1, - name: 1, - }, - } - )) as Array - - const showStyleBlueprints = (await Blueprints.findFetchAsync( - { - blueprintType: BlueprintManifestType.SHOWSTYLE, - _id: { $in: _.compact(showStyles.map((st) => st.blueprintId)) }, - }, - { - fields: { - _id: 1, - showStyleConfigPresets: 1, - showStyleConfigSchema: 1, - blueprintHash: 1, - }, - } - )) as Array - - // Check each studio - const blueprintsMap = normalizeArrayToMap( - showStyleBlueprints.map((doc) => - literal({ - _id: doc._id, - configPresets: doc.showStyleConfigPresets, - configSchema: doc.showStyleConfigSchema, - blueprintHash: doc.blueprintHash, - }) - ), - '_id' - ) - for (const showStyle of showStyles) { - result.push({ - ...checkDocUpgradeStatus(blueprintsMap, showStyle), - showStyleBaseId: showStyle._id, - name: showStyle.name, - }) - } - - return result -} - -type StudioForUpgradeCheck = Pick< - DBStudio, - '_id' | 'blueprintId' | 'blueprintConfigPresetId' | 'lastBlueprintConfig' | 'blueprintConfigWithOverrides' | 'name' -> -type ShowStyleBaseForUpgradeCheck = Pick< - DBShowStyleBase, - '_id' | 'blueprintId' | 'blueprintConfigPresetId' | 'lastBlueprintConfig' | 'blueprintConfigWithOverrides' | 'name' -> -type StudioBlueprintForUpgradeCheck = Pick< - Blueprint, - '_id' | 'studioConfigPresets' | 'studioConfigSchema' | 'blueprintHash' -> -type ShowStyleBlueprintForUpgradeCheck = Pick< - Blueprint, - '_id' | 'showStyleConfigPresets' | 'showStyleConfigSchema' | 'blueprintHash' -> - -interface BlueprintMapEntry { +export interface BlueprintMapEntry { _id: BlueprintId configPresets: Record | Record | undefined configSchema: JSONBlob | undefined blueprintHash: BlueprintHash | undefined } -function checkDocUpgradeStatus( +export function checkDocUpgradeStatus( blueprintMap: Map, - doc: StudioForUpgradeCheck | ShowStyleBaseForUpgradeCheck -): Pick { + doc: Pick | Pick +): Pick { // Check the blueprintId is valid const blueprint = doc.blueprintId ? blueprintMap.get(doc.blueprintId) : null if (!blueprint || !blueprint.configPresets) { @@ -288,7 +144,7 @@ function diffJsonSchemaObjects( generateTranslation( 'Config value "{{ name }}" has changed. From "{{ oldValue }}", to "{{ newValue }}"', { - name: (propSchema as any)['ui:title'] || propPath, + name: getSchemaUIField(propSchema, SchemaFormUIField.Title) || propPath, // Future: this is not pretty when it is an object oldValue: JSON.stringify(valueA) ?? '', newValue: JSON.stringify(valueB) ?? '', diff --git a/meteor/server/publications/blueprintUpgradeStatus/publication.ts b/meteor/server/publications/blueprintUpgradeStatus/publication.ts new file mode 100644 index 0000000000..553b1960c6 --- /dev/null +++ b/meteor/server/publications/blueprintUpgradeStatus/publication.ts @@ -0,0 +1,260 @@ +import { BlueprintId, ShowStyleBaseId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mongo' +import { ReadonlyDeep } from 'type-fest' +import { CustomCollectionName, PubSub } from '../../../lib/api/pubsub' +import { literal, ProtectedString, protectString } from '../../../lib/lib' +import { + CustomPublish, + CustomPublishCollection, + meteorCustomPublish, + setUpCollectionOptimizedObserver, + TriggerUpdate, +} from '../../lib/customPublication' +import { logger } from '../../logging' +import { resolveCredentials } from '../../security/lib/credentials' +import { NoSecurityReadAccess } from '../../security/noSecurity' +import { LiveQueryHandle } from '../../lib/lib' +import { ContentCache, createReactiveContentCache, ShowStyleBaseFields, StudioFields } from './reactiveContentCache' +import { UpgradesContentObserver } from './upgradesContentObserver' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { RundownPlaylists } from '../../collections' +import { BlueprintMapEntry, checkDocUpgradeStatus } from './checkStatus' +import { BlueprintManifestType } from '@sofie-automation/blueprints-integration' +import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { UIBlueprintUpgradeStatus, UIBlueprintUpgradeStatusId } from '../../../lib/api/upgradeStatus' + +type BlueprintUpgradeStatusArgs = Record + +export interface BlueprintUpgradeStatusState { + contentCache: ReadonlyDeep +} + +interface BlueprintUpgradeStatusUpdateProps { + newCache: ContentCache + + invalidateStudioIds: StudioId[] + invalidateShowStyleBaseIds: ShowStyleBaseId[] + invalidateBlueprintIds: BlueprintId[] +} + +type RundownPlaylistFields = '_id' | 'studioId' +const rundownPlaylistFieldSpecifier = literal< + MongoFieldSpecifierOnesStrict> +>({ + _id: 1, + studioId: 1, +}) + +async function setupBlueprintUpgradeStatusPublicationObservers( + args: ReadonlyDeep, + triggerUpdate: TriggerUpdate +): Promise { + const playlist = (await RundownPlaylists.findOneAsync(args.playlistId, { + projection: rundownPlaylistFieldSpecifier, + })) as Pick | undefined + if (!playlist) throw new Error(`RundownPlaylist "${args.playlistId}" not found!`) + + // TODO - can this be done cheaper? + const cache = createReactiveContentCache() + + // Push update + triggerUpdate({ newCache: cache }) + + const mongoObserver = new UpgradesContentObserver(cache) + + // Set up observers: + return [ + mongoObserver, + + cache.Studios.find({}).observeChanges({ + added: (id) => triggerUpdate({ invalidateStudioIds: [protectString(id)] }), + changed: (id) => triggerUpdate({ invalidateStudioIds: [protectString(id)] }), + removed: (id) => triggerUpdate({ invalidateStudioIds: [protectString(id)] }), + }), + cache.ShowStyleBases.find({}).observeChanges({ + added: (id) => triggerUpdate({ invalidateShowStyleBaseIds: [protectString(id)] }), + changed: (id) => triggerUpdate({ invalidateShowStyleBaseIds: [protectString(id)] }), + removed: (id) => triggerUpdate({ invalidateShowStyleBaseIds: [protectString(id)] }), + }), + cache.Blueprints.find({}).observeChanges({ + added: (id) => triggerUpdate({ invalidateBlueprintIds: [protectString(id)] }), + changed: (id) => triggerUpdate({ invalidateBlueprintIds: [protectString(id)] }), + removed: (id) => triggerUpdate({ invalidateBlueprintIds: [protectString(id)] }), + }), + ] +} + +function getDocumentId(type: 'studio' | 'showStyle', id: ProtectedString): UIBlueprintUpgradeStatusId { + return protectString(`${type}:${id}`) +} + +export async function manipulateBlueprintUpgradeStatusPublicationData( + _args: BlueprintUpgradeStatusArgs, + state: Partial, + collection: CustomPublishCollection, + updateProps: Partial> | undefined +): Promise { + // Prepare data for publication: + + // We know that `collection` does diffing when 'commiting' all of the changes we have made + // meaning that for anything we will call `replace()` on, we can `remove()` it first for no extra cost + + if (updateProps?.newCache !== undefined) { + state.contentCache = updateProps.newCache ?? undefined + } + + if (!state.contentCache) { + // Remove all the notes + collection.remove(null) + + return + } + + const studioBlueprintsMap = new Map() + const showStyleBlueprintsMap = new Map() + state.contentCache.Blueprints.find({}).forEach((blueprint) => { + switch (blueprint.blueprintType) { + case BlueprintManifestType.SHOWSTYLE: + showStyleBlueprintsMap.set(blueprint._id, { + _id: blueprint._id, + configPresets: blueprint.showStyleConfigPresets, + configSchema: blueprint.showStyleConfigSchema, + blueprintHash: blueprint.blueprintHash, + }) + break + case BlueprintManifestType.STUDIO: + studioBlueprintsMap.set(blueprint._id, { + _id: blueprint._id, + configPresets: blueprint.studioConfigPresets, + configSchema: blueprint.studioConfigSchema, + blueprintHash: blueprint.blueprintHash, + }) + break + // TODO - default? + } + }) + + const updateAll = !updateProps || !!updateProps?.newCache + if (updateAll) { + // Remove all the notes + collection.remove(null) + + state.contentCache.Studios.find({}).forEach((studio) => { + updateStudioUpgradeStatus(collection, studioBlueprintsMap, studio) + }) + + state.contentCache.ShowStyleBases.find({}).forEach((showStyleBase) => { + updateShowStyleUpgradeStatus(collection, showStyleBlueprintsMap, showStyleBase) + }) + } else { + const regenerateForStudioIds = new Set(updateProps.invalidateStudioIds) + const regenerateForShowStyleBaseIds = new Set(updateProps.invalidateShowStyleBaseIds) + + if (updateProps.invalidateBlueprintIds) { + // Find Studios whose blueprint triggered an invalidation + const invalidatedStudios = state.contentCache.Studios.find({ + blueprintId: { $in: updateProps.invalidateBlueprintIds }, + }) + for (const studio of invalidatedStudios) { + regenerateForStudioIds.add(studio._id) + } + + // Find ShowStyleBases whose blueprint triggered an invalidation + const invalidatedShowStyles = state.contentCache.ShowStyleBases.find({ + blueprintId: { $in: updateProps.invalidateBlueprintIds }, + }) + for (const showStyle of invalidatedShowStyles) { + regenerateForShowStyleBaseIds.add(showStyle._id) + } + } + + // Regenerate Studios + for (const studioId of regenerateForStudioIds) { + const studio = state.contentCache.Studios.findOne(studioId) + + if (studio) { + updateStudioUpgradeStatus(collection, studioBlueprintsMap, studio) + } else { + // Has already been removed + collection.remove(getDocumentId('studio', studioId)) + } + } + + // Regenerate ShowStyles + for (const showStyleBaseId of regenerateForShowStyleBaseIds) { + const showStyleBase = state.contentCache.ShowStyleBases.findOne(showStyleBaseId) + + if (showStyleBase) { + updateShowStyleUpgradeStatus(collection, showStyleBlueprintsMap, showStyleBase) + } else { + // Has already been removed + collection.remove(getDocumentId('showStyle', showStyleBaseId)) + } + } + } +} + +function updateStudioUpgradeStatus( + collection: CustomPublishCollection, + blueprintsMap: Map, + studio: Pick +) { + const status = checkDocUpgradeStatus(blueprintsMap, studio) + + collection.replace({ + ...status, + _id: getDocumentId('studio', studio._id), + documentType: 'studio', + documentId: studio._id, + name: studio.name, + }) +} + +function updateShowStyleUpgradeStatus( + collection: CustomPublishCollection, + blueprintsMap: Map, + showStyleBase: Pick +) { + const status = checkDocUpgradeStatus(blueprintsMap, showStyleBase) + + collection.replace({ + ...status, + _id: getDocumentId('showStyle', showStyleBase._id), + documentType: 'showStyle', + documentId: showStyleBase._id, + name: showStyleBase.name, + }) +} + +export async function createBlueprintUpgradeStatusSubscriptionHandle( + pub: CustomPublish +): Promise { + await setUpCollectionOptimizedObserver< + UIBlueprintUpgradeStatus, + BlueprintUpgradeStatusArgs, + BlueprintUpgradeStatusState, + BlueprintUpgradeStatusUpdateProps + >( + `pub_${PubSub.uiBlueprintUpgradeStatuses}`, + {}, + setupBlueprintUpgradeStatusPublicationObservers, + manipulateBlueprintUpgradeStatusPublicationData, + pub, + 100 + ) +} + +meteorCustomPublish( + PubSub.uiBlueprintUpgradeStatuses, + CustomCollectionName.UIBlueprintUpgradeStatuses, + async function (pub) { + const cred = await resolveCredentials({ userId: this.userId, token: undefined }) + + if (!cred || NoSecurityReadAccess.any()) { + await createBlueprintUpgradeStatusSubscriptionHandle(pub) + } else { + logger.warn(`Pub.${CustomCollectionName.UIBlueprintUpgradeStatuses}: Not allowed`) + } + } +) diff --git a/meteor/server/publications/blueprintUpgradeStatus/reactiveContentCache.ts b/meteor/server/publications/blueprintUpgradeStatus/reactiveContentCache.ts new file mode 100644 index 0000000000..cda605ba80 --- /dev/null +++ b/meteor/server/publications/blueprintUpgradeStatus/reactiveContentCache.ts @@ -0,0 +1,74 @@ +import { ReactiveCacheCollection } from '../lib/ReactiveCacheCollection' +import { literal } from '@sofie-automation/corelib/dist/lib' +import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mongo' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' +import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' + +export type StudioFields = + | '_id' + | 'blueprintId' + | 'blueprintConfigPresetId' + | 'lastBlueprintConfig' + | 'blueprintConfigWithOverrides' + | 'name' +export const studioFieldSpecifier = literal>>({ + _id: 1, + blueprintId: 1, + blueprintConfigPresetId: 1, + lastBlueprintConfig: 1, + blueprintConfigWithOverrides: 1, + name: 1, +}) + +export type ShowStyleBaseFields = + | '_id' + | 'blueprintId' + | 'blueprintConfigPresetId' + | 'lastBlueprintConfig' + | 'blueprintConfigWithOverrides' + | 'name' +export const showStyleFieldSpecifier = literal< + MongoFieldSpecifierOnesStrict> +>({ + _id: 1, + blueprintId: 1, + blueprintConfigPresetId: 1, + lastBlueprintConfig: 1, + blueprintConfigWithOverrides: 1, + name: 1, +}) + +export type BlueprintFields = + | '_id' + | 'studioConfigPresets' + | 'studioConfigSchema' + | 'showStyleConfigPresets' + | 'showStyleConfigSchema' + | 'blueprintHash' + | 'blueprintType' +export const blueprintFieldSpecifier = literal>>({ + _id: 1, + studioConfigPresets: 1, + studioConfigSchema: 1, + showStyleConfigPresets: 1, + showStyleConfigSchema: 1, + blueprintHash: 1, + blueprintType: 1, +}) + +export interface ContentCache { + Studios: ReactiveCacheCollection> + ShowStyleBases: ReactiveCacheCollection> + Blueprints: ReactiveCacheCollection> +} + +export function createReactiveContentCache(): ContentCache { + const cache: ContentCache = { + Studios: new ReactiveCacheCollection>('studios'), + ShowStyleBases: new ReactiveCacheCollection>('showStyleBases'), + Blueprints: new ReactiveCacheCollection>('blueprints'), + } + + return cache +} diff --git a/meteor/server/publications/blueprintUpgradeStatus/systemStatus.ts b/meteor/server/publications/blueprintUpgradeStatus/systemStatus.ts new file mode 100644 index 0000000000..f1df572202 --- /dev/null +++ b/meteor/server/publications/blueprintUpgradeStatus/systemStatus.ts @@ -0,0 +1,86 @@ +import { createManualPromise } from '@sofie-automation/corelib/dist/lib' +import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' +import { Meteor } from 'meteor/meteor' +import { UIBlueprintUpgradeStatus } from '../../../lib/api/upgradeStatus' +import { CustomPublish, CustomPublishChanges } from '../../lib/customPublication' +import { createBlueprintUpgradeStatusSubscriptionHandle } from './publication' + +class CustomPublishToMap }> implements CustomPublish { + #isReady = false + #documents = new Map() + #readyPromise = createManualPromise() + + get isReady(): boolean { + return this.#isReady + } + + get documents(): DBObj[] { + return Array.from(this.#documents.values()) + } + + async waitForReady(): Promise { + return this.#readyPromise + } + + /** + * Register a function to be called when the subscriber unsubscribes + */ + onStop(_callback: () => void): void { + // Ignore, this publication never stops + } + + /** + * Send the intial documents to the subscriber + */ + init(docs: DBObj[]): void { + if (this.#isReady) throw new Meteor.Error(500, 'CustomPublishToMap has already been initialised') + + for (const doc of docs) { + this.#documents.set(doc._id, doc) + } + + this.#isReady = true + + Meteor.defer(() => this.#readyPromise.manualResolve()) + } + + /** + * Send a batch of changes to the subscriber + */ + changed(changes: CustomPublishChanges): void { + if (!this.#isReady) throw new Meteor.Error(500, 'CustomPublish has not been initialised') + + for (const doc of changes.added.values()) { + this.#documents.set(doc._id, doc) + } + + for (const doc of changes.changed.values()) { + const existingDoc = this.#documents.get(doc._id) + if (!existingDoc) continue // TODO - throw? + this.#documents.set(doc._id, { + ...existingDoc, + ...doc, + }) + } + + for (const id of changes.removed.values()) { + this.#documents.delete(id) + } + } +} + +const cachedPublisher = new CustomPublishToMap() +let existingPublicationSubscription: Promise | undefined + +export async function getServerBlueprintUpgradeStatuses(): Promise { + if (Meteor.isTest) throw new Meteor.Error(500, 'getServerBlueprintUpgradeStatuses is not allowed during tests') + + if (!existingPublicationSubscription) { + existingPublicationSubscription = createBlueprintUpgradeStatusSubscriptionHandle(cachedPublisher) + } + await existingPublicationSubscription + + await cachedPublisher.waitForReady() + + return cachedPublisher.documents +} diff --git a/meteor/server/publications/blueprintUpgradeStatus/upgradesContentObserver.ts b/meteor/server/publications/blueprintUpgradeStatus/upgradesContentObserver.ts new file mode 100644 index 0000000000..c26ea657e5 --- /dev/null +++ b/meteor/server/publications/blueprintUpgradeStatus/upgradesContentObserver.ts @@ -0,0 +1,39 @@ +import { Meteor } from 'meteor/meteor' +import { logger } from '../../logging' +import { + blueprintFieldSpecifier, + ContentCache, + showStyleFieldSpecifier, + studioFieldSpecifier, +} from './reactiveContentCache' +import { Blueprints, ShowStyleBases, Studios } from '../../collections' + +export class UpgradesContentObserver { + #observers: Meteor.LiveQueryHandle[] = [] + #cache: ContentCache + + constructor(cache: ContentCache) { + logger.silly(`Creating UpgradesContentObserver`) + this.#cache = cache + + this.#observers = [ + Studios.observeChanges({}, cache.Studios.link(), { + projection: studioFieldSpecifier, + }), + ShowStyleBases.observeChanges({}, cache.ShowStyleBases.link(), { + projection: showStyleFieldSpecifier, + }), + Blueprints.observeChanges({}, cache.Blueprints.link(), { + projection: blueprintFieldSpecifier, + }), + ] + } + + public get cache(): ContentCache { + return this.#cache + } + + public stop = (): void => { + this.#observers.forEach((observer) => observer.stop()) + } +} diff --git a/meteor/server/systemStatus/__tests__/api.test.ts b/meteor/server/systemStatus/__tests__/api.test.ts index 5eee2af3d7..cc73898b28 100644 --- a/meteor/server/systemStatus/__tests__/api.test.ts +++ b/meteor/server/systemStatus/__tests__/api.test.ts @@ -6,9 +6,9 @@ import { status2ExternalStatus, setSystemStatus } from '../systemStatus' import { StatusResponse } from '../../../lib/api/systemStatus' import { StatusCode } from '@sofie-automation/blueprints-integration' import { MeteorCall } from '../../../lib/api/methods' -import { GetUpgradeStatusResult } from '../../../lib/api/migration' import { callKoaRoute } from '../../../__mocks__/koa-util' import { healthRouter } from '../api' +import { UIBlueprintUpgradeStatus } from '../../../lib/api/upgradeStatus' // we don't want the deviceTriggers observer to start up at this time jest.mock('../../api/deviceTriggers/observer') @@ -17,14 +17,9 @@ require('../api') require('../../coreSystem/index') const PackageInfo = require('../../../package.json') -import * as checkUpgradeStatus from '../../migration/upgrades/checkStatus' -jest.spyOn(checkUpgradeStatus, 'getUpgradeStatus').mockReturnValue( - Promise.resolve( - literal({ - studios: [], - showStyleBases: [], - }) - ) +import * as getServerBlueprintUpgradeStatuses from '../../publications/blueprintUpgradeStatus/systemStatus' +jest.spyOn(getServerBlueprintUpgradeStatuses, 'getServerBlueprintUpgradeStatuses').mockReturnValue( + Promise.resolve(literal([])) ) describe('systemStatus API', () => { diff --git a/meteor/server/systemStatus/__tests__/systemStatus.test.ts b/meteor/server/systemStatus/__tests__/systemStatus.test.ts index c572ba7e06..102237759c 100644 --- a/meteor/server/systemStatus/__tests__/systemStatus.test.ts +++ b/meteor/server/systemStatus/__tests__/systemStatus.test.ts @@ -10,6 +10,8 @@ import semver from 'semver' import { StatusCode } from '@sofie-automation/blueprints-integration' import { MeteorCall } from '../../../lib/api/methods' import { PeripheralDeviceStatusObject } from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI' +import { PeripheralDevices } from '../../collections' +import { UIBlueprintUpgradeStatus } from '../../../lib/api/upgradeStatus' // we don't want the deviceTriggers observer to start up at this time jest.mock('../../api/deviceTriggers/observer') @@ -17,24 +19,18 @@ jest.mock('../../api/deviceTriggers/observer') require('../api') const PackageInfo = require('../../../package.json') -import * as checkUpgradeStatus from '../../migration/upgrades/checkStatus' -import { GetUpgradeStatusResult } from '../../../lib/api/migration' -import { PeripheralDevices } from '../../collections' -const getUpgradeStatusMock = jest.spyOn(checkUpgradeStatus, 'getUpgradeStatus') +import * as getServerBlueprintUpgradeStatuses from '../../publications/blueprintUpgradeStatus/systemStatus' +const getServerBlueprintUpgradeStatusesMock = jest.spyOn( + getServerBlueprintUpgradeStatuses, + 'getServerBlueprintUpgradeStatuses' +) describe('systemStatus', () => { beforeEach(() => { - getUpgradeStatusMock.mockReturnValue( - Promise.resolve( - literal({ - studios: [], - showStyleBases: [], - }) - ) - ) + getServerBlueprintUpgradeStatusesMock.mockReturnValue(Promise.resolve(literal([]))) }) afterEach(() => { - getUpgradeStatusMock.mockReset() + getServerBlueprintUpgradeStatusesMock.mockReset() }) let env: DefaultEnvironment @@ -251,28 +247,29 @@ describe('systemStatus', () => { status: status2ExternalStatus(expectedStatus), _status: expectedStatus, }) - expect(getUpgradeStatusMock).toHaveBeenCalledTimes(1) + expect(getServerBlueprintUpgradeStatusesMock).toHaveBeenCalledTimes(1) } // Fake some studio upgrade errors - getUpgradeStatusMock.mockReturnValue( + getServerBlueprintUpgradeStatusesMock.mockReturnValue( Promise.resolve( - literal({ - studios: [ - { - studioId: protectString('studio0'), - name: 'Test Studio #0', - changes: [generateTranslation('something changed')], - }, - { - studioId: protectString('studio1'), - name: 'Test Studio #1', - invalidReason: generateTranslation('some invalid reason'), - changes: [], - }, - ], - showStyleBases: [], - }) + literal([ + { + _id: protectString('studio-studio0'), + documentType: 'studio', + documentId: protectString('studio0'), + name: 'Test Studio #0', + changes: [generateTranslation('something changed')], + }, + { + _id: protectString('studio-studio1'), + documentType: 'studio', + documentId: protectString('studio1'), + name: 'Test Studio #1', + invalidReason: generateTranslation('some invalid reason'), + changes: [], + }, + ]) ) ) @@ -284,27 +281,28 @@ describe('systemStatus', () => { status: status2ExternalStatus(expectedStatus), _status: expectedStatus, }) - expect(getUpgradeStatusMock).toHaveBeenCalledTimes(2) + expect(getServerBlueprintUpgradeStatusesMock).toHaveBeenCalledTimes(2) } // Just a minor studio warning - getUpgradeStatusMock.mockReturnValue( + getServerBlueprintUpgradeStatusesMock.mockReturnValue( Promise.resolve( - literal({ - studios: [ - { - studioId: protectString('studio0'), - name: 'Test Studio #0', - changes: [generateTranslation('something changed')], - }, - { - studioId: protectString('studio1'), - name: 'Test Studio #1', - changes: [], - }, - ], - showStyleBases: [], - }) + literal([ + { + _id: protectString('studio-studio0'), + documentType: 'studio', + documentId: protectString('studio0'), + name: 'Test Studio #0', + changes: [generateTranslation('something changed')], + }, + { + _id: protectString('studio-studio1'), + documentType: 'studio', + documentId: protectString('studio1'), + name: 'Test Studio #1', + changes: [], + }, + ]) ) ) @@ -316,27 +314,28 @@ describe('systemStatus', () => { status: status2ExternalStatus(expectedStatus), _status: expectedStatus, }) - expect(getUpgradeStatusMock).toHaveBeenCalledTimes(3) + expect(getServerBlueprintUpgradeStatusesMock).toHaveBeenCalledTimes(3) } // Nothing wrong with a studio - getUpgradeStatusMock.mockReturnValue( + getServerBlueprintUpgradeStatusesMock.mockReturnValue( Promise.resolve( - literal({ - studios: [ - { - studioId: protectString('studio0'), - name: 'Test Studio #0', - changes: [], - }, - { - studioId: protectString('studio1'), - name: 'Test Studio #1', - changes: [], - }, - ], - showStyleBases: [], - }) + literal([ + { + _id: protectString('studio-studio0'), + documentType: 'studio', + documentId: protectString('studio0'), + name: 'Test Studio #0', + changes: [], + }, + { + _id: protectString('studio-studio1'), + documentType: 'studio', + documentId: protectString('studio1'), + name: 'Test Studio #1', + changes: [], + }, + ]) ) ) @@ -348,28 +347,29 @@ describe('systemStatus', () => { status: status2ExternalStatus(expectedStatus), _status: expectedStatus, }) - expect(getUpgradeStatusMock).toHaveBeenCalledTimes(4) + expect(getServerBlueprintUpgradeStatusesMock).toHaveBeenCalledTimes(4) } // Fake some showStyleBase upgrade errors - getUpgradeStatusMock.mockReturnValue( + getServerBlueprintUpgradeStatusesMock.mockReturnValue( Promise.resolve( - literal({ - studios: [], - showStyleBases: [ - { - showStyleBaseId: protectString('showStyleBase0'), - name: 'Test Show Style Base #0', - changes: [generateTranslation('something changed')], - }, - { - showStyleBaseId: protectString('showStyleBase1'), - name: 'Test Show Style Base #1', - invalidReason: generateTranslation('some invalid reason'), - changes: [], - }, - ], - }) + literal([ + { + _id: protectString('showStyle-showStyleBase0'), + documentType: 'showStyle', + documentId: protectString('showStyleBase0'), + name: 'Test Show Style Base #0', + changes: [generateTranslation('something changed')], + }, + { + _id: protectString('showStyle-showStyleBase1'), + documentType: 'showStyle', + documentId: protectString('showStyleBase1'), + name: 'Test Show Style Base #1', + invalidReason: generateTranslation('some invalid reason'), + changes: [], + }, + ]) ) ) @@ -381,27 +381,28 @@ describe('systemStatus', () => { status: status2ExternalStatus(expectedStatus), _status: expectedStatus, }) - expect(getUpgradeStatusMock).toHaveBeenCalledTimes(5) + expect(getServerBlueprintUpgradeStatusesMock).toHaveBeenCalledTimes(5) } // Just a minor showStyleBase warning - getUpgradeStatusMock.mockReturnValue( + getServerBlueprintUpgradeStatusesMock.mockReturnValue( Promise.resolve( - literal({ - studios: [], - showStyleBases: [ - { - showStyleBaseId: protectString('showStyleBase0'), - name: 'Test Show Style Base #0', - changes: [generateTranslation('something changed')], - }, - { - showStyleBaseId: protectString('showStyleBase1'), - name: 'Test Show Style Base #1', - changes: [], - }, - ], - }) + literal([ + { + _id: protectString('showStyle-showStyleBase0'), + documentType: 'showStyle', + documentId: protectString('showStyleBase0'), + name: 'Test Show Style Base #0', + changes: [generateTranslation('something changed')], + }, + { + _id: protectString('showStyle-showStyleBase1'), + documentType: 'showStyle', + documentId: protectString('showStyleBase1'), + name: 'Test Show Style Base #1', + changes: [], + }, + ]) ) ) @@ -413,27 +414,28 @@ describe('systemStatus', () => { status: status2ExternalStatus(expectedStatus), _status: expectedStatus, }) - expect(getUpgradeStatusMock).toHaveBeenCalledTimes(6) + expect(getServerBlueprintUpgradeStatusesMock).toHaveBeenCalledTimes(6) } // Nothing wrong with a showStyleBase - getUpgradeStatusMock.mockReturnValue( + getServerBlueprintUpgradeStatusesMock.mockReturnValue( Promise.resolve( - literal({ - studios: [], - showStyleBases: [ - { - showStyleBaseId: protectString('showStyleBase0'), - name: 'Test Show Style Base #0', - changes: [], - }, - { - showStyleBaseId: protectString('showStyleBase1'), - name: 'Test Show Style Base #1', - changes: [], - }, - ], - }) + literal([ + { + _id: protectString('showStyle-showStyleBase0'), + documentType: 'showStyle', + documentId: protectString('showStyleBase0'), + name: 'Test Show Style Base #0', + changes: [], + }, + { + _id: protectString('showStyle-showStyleBase1'), + documentType: 'showStyle', + documentId: protectString('showStyleBase1'), + name: 'Test Show Style Base #1', + changes: [], + }, + ]) ) ) @@ -445,7 +447,7 @@ describe('systemStatus', () => { status: status2ExternalStatus(expectedStatus), _status: expectedStatus, }) - expect(getUpgradeStatusMock).toHaveBeenCalledTimes(7) + expect(getServerBlueprintUpgradeStatusesMock).toHaveBeenCalledTimes(7) } }) }) diff --git a/meteor/server/systemStatus/blueprintUpgradeStatus.ts b/meteor/server/systemStatus/blueprintUpgradeStatus.ts new file mode 100644 index 0000000000..0506da5215 --- /dev/null +++ b/meteor/server/systemStatus/blueprintUpgradeStatus.ts @@ -0,0 +1,38 @@ +import { StatusCode } from '@sofie-automation/blueprints-integration' +import { literal } from '@sofie-automation/corelib/dist/lib' +import { Component } from '../../lib/api/systemStatus' +import { status2ExternalStatus } from './systemStatus' +import { getServerBlueprintUpgradeStatuses } from '../publications/blueprintUpgradeStatus/systemStatus' + +export async function getUpgradeSystemStatusMessages(): Promise { + const result: Component[] = [] + + const upgradeStatus = await getServerBlueprintUpgradeStatuses() + for (const statusDocument of upgradeStatus) { + let status = StatusCode.GOOD + const messages: string[] = [] + if (statusDocument.invalidReason) { + status = StatusCode.WARNING_MAJOR + messages.push('Invalid configuration') + } else if (statusDocument.changes.length) { + status = StatusCode.WARNING_MINOR + messages.push('Configuration changed') + } + + result.push( + literal({ + name: `blueprints-upgrade-${statusDocument._id}`, + status: status2ExternalStatus(status), + updated: new Date().toISOString(), + _status: status, + _internal: { + statusCodeString: StatusCode[status], + messages: messages, + versions: {}, + }, + }) + ) + } + + return result +} diff --git a/meteor/server/systemStatus/systemStatus.ts b/meteor/server/systemStatus/systemStatus.ts index c329dfa622..50c7002aa7 100644 --- a/meteor/server/systemStatus/systemStatus.ts +++ b/meteor/server/systemStatus/systemStatus.ts @@ -24,12 +24,12 @@ import { resolveCredentials, Credentials } from '../security/lib/credentials' import { SystemReadAccess } from '../security/system' import { StatusCode } from '@sofie-automation/blueprints-integration' import { PeripheralDevices, Workers, WorkerThreadStatuses } from '../collections' -import { getUpgradeSystemStatusMessages } from '../migration/upgrades' import { PeripheralDeviceId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ServerPeripheralDeviceAPI } from '../api/peripheralDevice' import { PeripheralDeviceContentWriteAccess } from '../security/peripheralDevice' import { MethodContext } from '../../lib/api/methods' import { getBlueprintVersions } from './blueprintVersions' +import { getUpgradeSystemStatusMessages } from './blueprintUpgradeStatus' const PackageInfo = require('../../package.json') const integrationVersionRange = parseCoreIntegrationCompatabilityRange(PackageInfo.version) From 90f4e99b0c2eb803dada47e4cb6a1fb9ab3931cb Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 13 Oct 2023 14:28:58 +0100 Subject: [PATCH 116/479] chore: move `PieceContentStatusObj` into more sensibly named file --- meteor/client/lib/shelf.ts | 2 +- meteor/client/ui/MediaStatus/MediaStatus.tsx | 2 +- .../ui/SegmentContainer/withResolvedSegment.ts | 2 +- .../ui/SegmentTimeline/withMediaObjectStatus.tsx | 2 +- meteor/lib/Rundown.ts | 2 +- .../{mediaObjects.ts => api/pieceContentStatus.ts} | 11 ----------- meteor/lib/api/rundownNotifications.ts | 2 +- .../pieceContentStatusUI/checkPieceContentStatus.ts | 13 ++++++++++++- 8 files changed, 18 insertions(+), 18 deletions(-) rename meteor/lib/{mediaObjects.ts => api/pieceContentStatus.ts} (65%) diff --git a/meteor/client/lib/shelf.ts b/meteor/client/lib/shelf.ts index 20db188267..b6318705ce 100644 --- a/meteor/client/lib/shelf.ts +++ b/meteor/client/lib/shelf.ts @@ -13,7 +13,7 @@ import { UIShowStyleBase } from '../../lib/api/showStyles' import { PieceId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PieceInstances } from '../collections' import { ReadonlyDeep } from 'type-fest' -import { PieceContentStatusObj } from '../../lib/mediaObjects' +import { PieceContentStatusObj } from '../../lib/api/pieceContentStatus' export interface ShelfDisplayOptions { enableBuckets: boolean diff --git a/meteor/client/ui/MediaStatus/MediaStatus.tsx b/meteor/client/ui/MediaStatus/MediaStatus.tsx index a8eb61656e..42f3b250a7 100644 --- a/meteor/client/ui/MediaStatus/MediaStatus.tsx +++ b/meteor/client/ui/MediaStatus/MediaStatus.tsx @@ -29,7 +29,7 @@ import { ProtectedString, unprotectString } from '@sofie-automation/corelib/dist import { ExpectedPackage } from '@sofie-automation/shared-lib/dist/package-manager/package' import { PartInvalidReason } from '@sofie-automation/corelib/dist/dataModel/Part' import { IBlueprintActionManifestDisplayContent, SourceLayerType } from '@sofie-automation/blueprints-integration' -import { PieceContentStatusObj } from '../../../lib/mediaObjects' +import { PieceContentStatusObj } from '../../../lib/api/pieceContentStatus' import { Piece, PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' import { assertNever, literal } from '@sofie-automation/corelib/dist/lib' import { UIPieceContentStatuses, UIShowStyleBases } from '../Collections' diff --git a/meteor/client/ui/SegmentContainer/withResolvedSegment.ts b/meteor/client/ui/SegmentContainer/withResolvedSegment.ts index 4fd57ba66c..3fd0fa997c 100644 --- a/meteor/client/ui/SegmentContainer/withResolvedSegment.ts +++ b/meteor/client/ui/SegmentContainer/withResolvedSegment.ts @@ -37,7 +37,7 @@ import { PieceInstances, Segments } from '../../collections' import { RundownPlaylistCollectionUtil } from '../../../lib/collections/rundownPlaylistUtil' import { CalculateTimingsPiece } from '@sofie-automation/corelib/dist/playout/timings' import { ReadonlyDeep } from 'type-fest' -import { PieceContentStatusObj } from '../../../lib/mediaObjects' +import { PieceContentStatusObj } from '../../../lib/api/pieceContentStatus' import { SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' export interface SegmentUi extends SegmentExtended { diff --git a/meteor/client/ui/SegmentTimeline/withMediaObjectStatus.tsx b/meteor/client/ui/SegmentTimeline/withMediaObjectStatus.tsx index edf1e2a302..63725a20f6 100644 --- a/meteor/client/ui/SegmentTimeline/withMediaObjectStatus.tsx +++ b/meteor/client/ui/SegmentTimeline/withMediaObjectStatus.tsx @@ -10,7 +10,7 @@ import { AdLibPieceUi } from '../../lib/shelf' import { UIStudio } from '../../../lib/api/studios' import { UIBucketContentStatuses, UIPieceContentStatuses } from '../Collections' import { Piece, PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' -import { PieceContentStatusObj } from '../../../lib/mediaObjects' +import { PieceContentStatusObj } from '../../../lib/api/pieceContentStatus' import { deepFreeze } from '@sofie-automation/corelib/dist/lib' import _ from 'underscore' import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' diff --git a/meteor/lib/Rundown.ts b/meteor/lib/Rundown.ts index 759adc0682..c39a3c4610 100644 --- a/meteor/lib/Rundown.ts +++ b/meteor/lib/Rundown.ts @@ -25,7 +25,7 @@ import { } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PieceInstances, Pieces } from './collections/libCollections' import { RundownPlaylistCollectionUtil } from './collections/rundownPlaylistUtil' -import { PieceContentStatusObj } from './mediaObjects' +import { PieceContentStatusObj } from './api/pieceContentStatus' import { ReadonlyDeep } from 'type-fest' import { PieceInstanceWithTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' diff --git a/meteor/lib/mediaObjects.ts b/meteor/lib/api/pieceContentStatus.ts similarity index 65% rename from meteor/lib/mediaObjects.ts rename to meteor/lib/api/pieceContentStatus.ts index 28f4e3768d..d6612512c2 100644 --- a/meteor/lib/mediaObjects.ts +++ b/meteor/lib/api/pieceContentStatus.ts @@ -2,17 +2,6 @@ import { PackageInfo } from '@sofie-automation/blueprints-integration' import { PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' import { ITranslatableMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' -export interface ScanInfoForPackages { - [packageId: string]: ScanInfoForPackage -} -export interface ScanInfoForPackage { - /** Display name of the package */ - packageName: string - scan?: PackageInfo.FFProbeScan['payload'] - deepScan?: PackageInfo.FFProbeDeepScan['payload'] - timebase?: number // derived from scan -} - export interface PieceContentStatusObj { status: PieceStatusCode messages: ITranslatableMessage[] diff --git a/meteor/lib/api/rundownNotifications.ts b/meteor/lib/api/rundownNotifications.ts index 46554b9b8f..c03042594b 100644 --- a/meteor/lib/api/rundownNotifications.ts +++ b/meteor/lib/api/rundownNotifications.ts @@ -13,7 +13,7 @@ import { SegmentId, } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ProtectedString } from '../lib' -import { PieceContentStatusObj } from '../mediaObjects' +import { PieceContentStatusObj } from './pieceContentStatus' import { ITranslatableMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' export type UISegmentPartNoteId = ProtectedString<'UISegmentPartNote'> diff --git a/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts index 364019f5d8..5c82683df9 100644 --- a/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts +++ b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts @@ -31,7 +31,7 @@ import _ from 'underscore' import { getSideEffect } from '../../../lib/collections/ExpectedPackages' import { getActiveRoutes, getRoutedMappings } from '../../../lib/collections/Studios' import { ensureHasTrailingSlash, generateTranslation, unprotectString } from '../../../lib/lib' -import { PieceContentStatusObj, ScanInfoForPackage, ScanInfoForPackages } from '../../../lib/mediaObjects' +import { PieceContentStatusObj } from '../../../lib/api/pieceContentStatus' import { MediaObjects, PackageContainerPackageStatuses, PackageInfos } from '../../collections' import { mediaObjectFieldSpecifier, @@ -43,6 +43,17 @@ import { PieceDependencies, } from './common' +interface ScanInfoForPackages { + [packageId: string]: ScanInfoForPackage +} +interface ScanInfoForPackage { + /** Display name of the package */ + packageName: string + scan?: PackageInfo.FFProbeScan['payload'] + deepScan?: PackageInfo.FFProbeDeepScan['payload'] + timebase?: number // derived from scan +} + /** * Take properties from the mediainfo / medistream and transform into a * formatted string From ea6c3435079172c682dd1e23d7813ae33fcdee78 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 18 Oct 2023 12:49:18 +0100 Subject: [PATCH 117/479] feat: blueprint config fixup step SOFIE-2258 (#1050) --- meteor/__mocks__/defaultCollectionObjects.ts | 1 + meteor/__mocks__/helpers/database.ts | 1 + .../BlueprintConfiguration/index.tsx | 73 +-- .../Studio/BlueprintConfiguration/index.tsx | 69 +- .../ui/Settings/Upgrades/Components.tsx | 146 ++++- .../ui/Settings/util/OverrideOpHelper.tsx | 70 +- meteor/lib/api/migration.ts | 34 + meteor/lib/api/upgradeStatus.ts | 11 + .../api/blueprints/__tests__/api.test.ts | 6 + meteor/server/api/blueprints/__tests__/lib.ts | 1 + meteor/server/api/blueprints/api.ts | 6 + meteor/server/api/rest/v1/typeConversion.ts | 2 + meteor/server/api/showStyles.ts | 1 + meteor/server/api/studio/api.ts | 1 + meteor/server/migration/0_1_0.ts | 3 + .../migration/__tests__/migrations.test.ts | 4 + meteor/server/migration/api.ts | 43 +- .../migration/upgrades/showStyleBase.ts | 180 ++++-- meteor/server/migration/upgrades/studio.ts | 46 +- .../blueprintUpgradeStatus/checkStatus.ts | 19 +- .../blueprintUpgradeStatus/publication.ts | 2 + .../reactiveContentCache.ts | 6 + .../__tests__/systemStatus.test.ts | 12 + .../src/__tests__/context.spec.ts | 122 ++-- .../src/api/showStyle.ts | 7 + .../blueprints-integration/src/api/studio.ts | 8 +- .../src/context/fixUpConfigContext.ts | 66 ++ .../src/context/index.ts | 1 + packages/corelib/src/dataModel/Blueprint.ts | 3 + .../corelib/src/dataModel/ShowStyleBase.ts | 4 +- packages/corelib/src/dataModel/Studio.ts | 4 +- .../__tests__/context.spec.ts | 609 ++++++++++++++++++ .../src/fixUpBlueprintConfig/context.ts | 304 +++++++++ .../src/settings/objectWithOverrides.ts | 89 +++ packages/corelib/src/worker/studio.ts | 21 + .../src/__mocks__/defaultCollectionObjects.ts | 1 + .../src/__mocks__/presetCollections.ts | 1 + packages/job-worker/src/playout/upgrade.ts | 88 ++- .../job-worker/src/workers/studio/jobs.ts | 9 +- 39 files changed, 1787 insertions(+), 287 deletions(-) create mode 100644 packages/blueprints-integration/src/context/fixUpConfigContext.ts create mode 100644 packages/corelib/src/fixUpBlueprintConfig/__tests__/context.spec.ts create mode 100644 packages/corelib/src/fixUpBlueprintConfig/context.ts diff --git a/meteor/__mocks__/defaultCollectionObjects.ts b/meteor/__mocks__/defaultCollectionObjects.ts index 857182a9dc..8c27d3d611 100644 --- a/meteor/__mocks__/defaultCollectionObjects.ts +++ b/meteor/__mocks__/defaultCollectionObjects.ts @@ -116,6 +116,7 @@ export function defaultStudio(_id: StudioId): DBStudio { inputDevices: wrapDefaultObject({}), }, lastBlueprintConfig: undefined, + lastBlueprintFixUpHash: undefined, } } diff --git a/meteor/__mocks__/helpers/database.ts b/meteor/__mocks__/helpers/database.ts index eec258e839..e07522b9fb 100644 --- a/meteor/__mocks__/helpers/database.ts +++ b/meteor/__mocks__/helpers/database.ts @@ -293,6 +293,7 @@ export async function setupMockShowStyleBase( // hotkeyLegend?: Array _rundownVersionHash: '', lastBlueprintConfig: undefined, + lastBlueprintFixUpHash: undefined, } const showStyleBase = _.extend(defaultShowStyleBase, doc) await ShowStyleBases.insertAsync(showStyleBase) diff --git a/meteor/client/ui/Settings/ShowStyle/BlueprintConfiguration/index.tsx b/meteor/client/ui/Settings/ShowStyle/BlueprintConfiguration/index.tsx index 7c6e51695e..a09b55b851 100644 --- a/meteor/client/ui/Settings/ShowStyle/BlueprintConfiguration/index.tsx +++ b/meteor/client/ui/Settings/ShowStyle/BlueprintConfiguration/index.tsx @@ -8,7 +8,6 @@ import { MappingsExt } from '@sofie-automation/corelib/dist/dataModel/Studio' import { DBShowStyleBase, SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { SelectConfigPreset } from './SelectConfigPreset' import { SelectBlueprint } from './SelectBlueprint' -import { ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PubSub } from '../../../../../lib/api/pubsub' import { useSubscription, useTracker } from '../../../../lib/ReactMeteorData/ReactMeteorData' import { UIBlueprintUpgradeStatuses } from '../../../Collections' @@ -28,6 +27,17 @@ export function ShowStyleBaseBlueprintConfigurationSettings( ): JSX.Element { const { t } = useTranslation() + const isStatusReady = useSubscription(PubSub.uiBlueprintUpgradeStatuses) + const status = useTracker( + () => + UIBlueprintUpgradeStatuses.findOne({ + documentId: props.showStyleBase._id, + documentType: 'showStyle', + }), + [props.showStyleBase._id] + ) + const statusMessage = isStatusReady && status ? getUpgradeStatusMessage(t, status) ?? t('OK') : t('Loading...') + const translationNamespaces = useMemo( () => ['blueprint_' + props.showStyleBase.blueprintId], [props.showStyleBase.blueprintId] @@ -51,45 +61,28 @@ export function ShowStyleBaseBlueprintConfigurationSettings( - - - +

+ {t('Upgrade Status')}: {statusMessage} + {status && } +

+ + {!status || status.pendingRunOfFixupFunction ? ( + !status ? ( +

{t('Loading')}

+ ) : ( +

{t('Config Fix Up must be run or ignored before the configuration can be edited')}

+ ) + ) : ( + + )} ) } - -interface BlueprintUpgradeStatusProps { - showStyleBaseId: ShowStyleBaseId -} - -function BlueprintUpgradeStatus({ showStyleBaseId }: BlueprintUpgradeStatusProps): JSX.Element { - const { t } = useTranslation() - - const isReady = useSubscription(PubSub.uiBlueprintUpgradeStatuses) - - const status = useTracker( - () => - UIBlueprintUpgradeStatuses.findOne({ - documentId: showStyleBaseId, - documentType: 'showStyle', - }), - [showStyleBaseId] - ) - - const statusMessage = isReady && status ? getUpgradeStatusMessage(t, status) ?? t('OK') : t('Loading...') - - return ( -

- {t('Upgrade Status')}: {statusMessage} - {status && } -

- ) -} diff --git a/meteor/client/ui/Settings/Studio/BlueprintConfiguration/index.tsx b/meteor/client/ui/Settings/Studio/BlueprintConfiguration/index.tsx index 9316002382..7da0ace786 100644 --- a/meteor/client/ui/Settings/Studio/BlueprintConfiguration/index.tsx +++ b/meteor/client/ui/Settings/Studio/BlueprintConfiguration/index.tsx @@ -12,7 +12,6 @@ import { useTranslation } from 'react-i18next' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { SelectConfigPreset } from './SelectConfigPreset' import { SelectBlueprint } from './SelectBlueprint' -import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PubSub } from '../../../../../lib/api/pubsub' import { UIBlueprintUpgradeStatuses } from '../../../Collections' import { getUpgradeStatusMessage, UpgradeStatusButtons } from '../../Upgrades/Components' @@ -24,6 +23,17 @@ interface StudioBlueprintConfigurationSettingsProps { export function StudioBlueprintConfigurationSettings(props: StudioBlueprintConfigurationSettingsProps): JSX.Element { const { t } = useTranslation() + const isStatusReady = useSubscription(PubSub.uiBlueprintUpgradeStatuses) + const status = useTracker( + () => + UIBlueprintUpgradeStatuses.findOne({ + documentId: props.studio._id, + documentType: 'studio', + }), + [props.studio._id] + ) + const statusMessage = isStatusReady && status ? getUpgradeStatusMessage(t, status) ?? t('OK') : t('Loading...') + const blueprint = useTracker(() => { return props.studio.blueprintId ? Blueprints.findOne({ @@ -62,44 +72,27 @@ export function StudioBlueprintConfigurationSettings(props: StudioBlueprintConfi - +

+ {t('Upgrade Status')}: {statusMessage} + {status && } +

- + {!status || status.pendingRunOfFixupFunction ? ( + !status ? ( +

{t('Loading')}

+ ) : ( +

{t('Config Fix Up must be run or ignored before the configuration can be edited')}

+ ) + ) : ( + + )} ) } - -interface BlueprintUpgradeStatusProps { - studioId: StudioId -} - -function BlueprintUpgradeStatus({ studioId }: BlueprintUpgradeStatusProps): JSX.Element { - const { t } = useTranslation() - - const isReady = useSubscription(PubSub.uiBlueprintUpgradeStatuses) - - const status = useTracker( - () => - UIBlueprintUpgradeStatuses.findOne({ - documentId: studioId, - documentType: 'studio', - }), - [studioId] - ) - - const statusMessage = isReady && status ? getUpgradeStatusMessage(t, status) ?? t('OK') : t('Loading...') - - return ( -

- {t('Upgrade Status')}: {statusMessage} - {status && } -

- ) -} diff --git a/meteor/client/ui/Settings/Upgrades/Components.tsx b/meteor/client/ui/Settings/Upgrades/Components.tsx index 4829c1b265..951efe2a3c 100644 --- a/meteor/client/ui/Settings/Upgrades/Components.tsx +++ b/meteor/client/ui/Settings/Upgrades/Components.tsx @@ -1,6 +1,6 @@ import React, { useCallback } from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faDatabase, faEye } from '@fortawesome/free-solid-svg-icons' +import { faDatabase, faEye, faWarning } from '@fortawesome/free-solid-svg-icons' import { MeteorCall } from '../../../../lib/api/methods' import { TFunction, useTranslation } from 'react-i18next' import { i18nTranslator } from '../../i18n' @@ -53,6 +53,28 @@ export function UpgradeStatusButtons({ upgradeResult }: UpgradeStatusButtonsProp throw new Error(`Unknown UIBlueprintUpgradeStatusBase documentType`) } }, [upgradeResult.documentId, upgradeResult.documentType]) + const fixupConfig = useCallback(async () => { + switch (upgradeResult.documentType) { + case 'studio': + return MeteorCall.migration.fixupConfigForStudio(upgradeResult.documentId) + case 'showStyle': + return MeteorCall.migration.fixupConfigForShowStyleBase(upgradeResult.documentId) + default: + assertNever(upgradeResult) + throw new Error(`Unknown UIBlueprintUpgradeStatusBase documentType`) + } + }, [upgradeResult.documentId, upgradeResult.documentType]) + const ignoreFixupConfig = useCallback(async () => { + switch (upgradeResult.documentType) { + case 'studio': + return MeteorCall.migration.ignoreFixupConfigForStudio(upgradeResult.documentId) + case 'showStyle': + return MeteorCall.migration.ignoreFixupConfigForShowStyleBase(upgradeResult.documentId) + default: + assertNever(upgradeResult) + throw new Error(`Unknown UIBlueprintUpgradeStatusBase documentType`) + } + }, [upgradeResult.documentId, upgradeResult.documentType]) const clickValidate = useCallback(() => { validateConfig() @@ -152,21 +174,117 @@ export function UpgradeStatusButtons({ upgradeResult }: UpgradeStatusButtonsProp }) }, [upgradeResult]) + const clickIgnoreFixup = useCallback(() => { + doModalDialog({ + title: t('Are you sure you want to skip the fix up config step for {{name}}', { name: upgradeResult.name }), + message: ( +
+

{t('This could leave the configuration in a broken state')}

+
+ ), + acceptOnly: true, + yes: t('Confirm'), + onAccept: () => { + ignoreFixupConfig() + .then(() => { + NotificationCenter.push( + new Notification( + undefined, + NoticeLevel.NOTIFICATION, + t('for {{name}} fix skipped successfully', { name: upgradeResult.name }), + 'UpgradesView' + ) + ) + }) + .catch((e) => { + catchError('Upgrade applyConfig')(e) + NotificationCenter.push( + new Notification( + undefined, + NoticeLevel.WARNING, + t('Config for {{name}} fix failed', { name: upgradeResult.name }), + 'UpgradesView' + ) + ) + }) + }, + }) + }, [upgradeResult, ignoreFixupConfig]) + + const clickFixup = useCallback(() => { + fixupConfig() + .then((messages) => { + if (messages.length) { + doModalDialog({ + title: t('Completed with warnings', {}), + message: ( +
+ {' '} + {messages.map((msg, i) => ( + // TODO - use path? +

{translateMessage(msg.message, t)}

+ ))} +
+ ), + acceptOnly: true, + yes: t('Dismiss'), + onAccept: () => { + // Dismiss + }, + }) + } else { + NotificationCenter.push( + new Notification( + undefined, + NoticeLevel.NOTIFICATION, + t('Config for {{name}} fixed successfully', { name: upgradeResult.name }), + 'UpgradesView' + ) + ) + } + }) + .catch((e) => { + catchError('Upgrade fixupConfig')(e) + NotificationCenter.push( + new Notification( + undefined, + NoticeLevel.WARNING, + t('Config for {{name}} fix failed', { name: upgradeResult.name }), + 'UpgradesView' + ) + ) + }) + }, [upgradeResult, fixupConfig]) + return (
- - - + {upgradeResult.pendingRunOfFixupFunction ? ( + <> + + + + ) : ( + <> + + + + )}
) } diff --git a/meteor/client/ui/Settings/util/OverrideOpHelper.tsx b/meteor/client/ui/Settings/util/OverrideOpHelper.tsx index 20aca135ed..b3589435b7 100644 --- a/meteor/client/ui/Settings/util/OverrideOpHelper.tsx +++ b/meteor/client/ui/Settings/util/OverrideOpHelper.tsx @@ -5,35 +5,12 @@ import { ObjectOverrideDeleteOp, ObjectOverrideSetOp, applyAndValidateOverrides, + filterOverrideOpsForPrefix, + findParentOpToUpdate, } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { useRef, useMemo, useEffect, MutableRefObject } from 'react' import { ReadonlyDeep } from 'type-fest' -/** - * Split a list of SomeObjectOverrideOp based on whether they match a specified prefix - * @param allOps The array of SomeObjectOverrideOp - * @param prefix The prefix to match, without a trailing `.` - */ -export function filterOverrideOpsForPrefix( - allOps: ReadonlyDeep, - prefix: string -): { opsForPrefix: ReadonlyDeep[]; otherOps: ReadonlyDeep[] } { - const res: { opsForPrefix: ReadonlyDeep[]; otherOps: ReadonlyDeep[] } = { - opsForPrefix: [], - otherOps: [], - } - - for (const op of allOps) { - if (op.path === prefix || op.path.startsWith(`${prefix}.`)) { - res.opsForPrefix.push(op) - } else { - res.otherOps.push(op) - } - } - - return res -} - export interface WrappedOverridableItemDeleted { type: 'deleted' id: string @@ -255,8 +232,6 @@ class OverrideOpHelperImpl implements OverrideOpHelper { newOps.push(newOp) } else { - let newOp: ObjectOverrideSetOp | undefined - // Look for a op which encompasses this new value const parentOp = findParentOpToUpdate(opsForId, subPath) if (parentOp) { @@ -264,14 +239,12 @@ class OverrideOpHelperImpl implements OverrideOpHelper { objectPathSet(parentOp.op.value, parentOp.newSubPath, value) } else { // Insert new op - newOp = literal({ + const newOp = literal({ op: 'set', path: `${itemId}.${subPath}`, value: value, }) - } - if (newOp) { const newOpAsPrefix = `${newOp.path}.` // Preserve any other overrides @@ -333,40 +306,3 @@ export function useOverrideOpHelper( return helper } - -function findParentOpToUpdate( - opsForId: SomeObjectOverrideOp[], - subPath: string -): - | { - op: ObjectOverrideSetOp - newSubPath: string - } - | undefined { - const revOps = [...opsForId].reverse() - - for (const op of revOps) { - if (subPath === op.path) { - // There is an op at the same path, this should be replaced by the current one - return undefined - } - - if (subPath.startsWith(`${op.path}.`)) { - // The new value is inside of this op - if (op.op === 'delete') { - // Can't mutate a delete op like this - return undefined - } - - // It's a set op, so we would be better to modify in place rather than add another mutate op - return { - op, - newSubPath: subPath.slice(op.path.length + 1), - } - } - } - // - - // Nothing matched - return undefined -} diff --git a/meteor/lib/api/migration.ts b/meteor/lib/api/migration.ts index d2c4d145b6..40df1b77a0 100644 --- a/meteor/lib/api/migration.ts +++ b/meteor/lib/api/migration.ts @@ -1,7 +1,13 @@ import { MigrationStepInput, MigrationStepInputResult } from '@sofie-automation/blueprints-integration' import { BlueprintId, ShowStyleBaseId, SnapshotId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { ITranslatableMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import { BlueprintValidateConfigForStudioResult } from '@sofie-automation/corelib/dist/worker/studio' +export interface BlueprintFixUpConfigMessage { + message: ITranslatableMessage + path: string +} + export interface NewMigrationAPI { getMigrationStatus(): Promise runMigration( @@ -13,6 +19,18 @@ export interface NewMigrationAPI { forceMigration(chunks: Array): Promise resetDatabaseVersions(): Promise + /** + * Run `fixupConfig` on the blueprint for a Studio + * @param studioId Id of the Studio + */ + fixupConfigForStudio(studioId: StudioId): Promise + + /** + * Ignore that `fixupConfig` needs to be run for a Studio + * @param studioId Id of the Studio + */ + ignoreFixupConfigForStudio(studioId: StudioId): Promise + /** * Run `validateConfig` on the blueprint for a Studio * @param studioId Id of the Studio @@ -26,6 +44,18 @@ export interface NewMigrationAPI { */ runUpgradeForStudio(studioId: StudioId): Promise + /** + * Run `fixupConfig` on the blueprint for a ShowStyleBase + * @param showStyleBaseId Id of the ShowStyleBase + */ + fixupConfigForShowStyleBase(showStyleBaseId: ShowStyleBaseId): Promise + + /** + * Ignore that `fixupConfig` needs to be run for a ShowStyleBase + * @param showStyleBaseId Id of the ShowStyleBase + */ + ignoreFixupConfigForShowStyleBase(showStyleBaseId: ShowStyleBaseId): Promise + /** * Run `validateConfig` on the blueprint for a ShowStyleBase * @param showStyleBaseId Id of the ShowStyleBase @@ -47,8 +77,12 @@ export enum MigrationAPIMethods { 'resetDatabaseVersions' = 'migration.resetDatabaseVersions', 'getUpgradeStatus' = 'migration.getUpgradeStatus', + 'fixupConfigForStudio' = 'migration.fixupConfigForStudio', + 'ignoreFixupConfigForStudio' = 'migration.ignoreFixupConfigForStudio', 'validateConfigForStudio' = 'migration.validateConfigForStudio', 'runUpgradeForStudio' = 'migration.runUpgradeForStudio', + 'fixupConfigForShowStyleBase' = 'migration.fixupConfigForShowStyleBase', + 'ignoreFixupConfigForShowStyleBase' = 'migration.ignoreFixupConfigForShowStyleBase', 'validateConfigForShowStyleBase' = 'migration.validateConfigForShowStyleBase', 'runUpgradeForShowStyleBase' = 'migration.runUpgradeForShowStyleBase', } diff --git a/meteor/lib/api/upgradeStatus.ts b/meteor/lib/api/upgradeStatus.ts index f1eda15071..d3a5891d4e 100644 --- a/meteor/lib/api/upgradeStatus.ts +++ b/meteor/lib/api/upgradeStatus.ts @@ -14,8 +14,19 @@ export interface UIBlueprintUpgradeStatusBase { name: string + /** + * If set, there is something wrong that must be resolved before the config can be validated or applied + */ invalidReason?: ITranslatableMessage + /** + * Whether the 'fixup' must be run before the config can be validated or applied + */ + pendingRunOfFixupFunction: boolean + + /** + * User facing list of changes to be reviewed + */ changes: ITranslatableMessage[] } diff --git a/meteor/server/api/blueprints/__tests__/api.test.ts b/meteor/server/api/blueprints/__tests__/api.test.ts index cf949eeb0f..6f2a3318e2 100644 --- a/meteor/server/api/blueprints/__tests__/api.test.ts +++ b/meteor/server/api/blueprints/__tests__/api.test.ts @@ -69,6 +69,7 @@ describe('Test blueprint management api', () => { blueprintVersion: '', integrationVersion: '', TSRVersion: '', + hasFixUpFunction: false, } await Blueprints.insertAsync(blueprint) return blueprint @@ -319,6 +320,7 @@ describe('Test blueprint management api', () => { showStyleConfigSchema: JSON.stringify({ show1: true }) as any, hasCode: !!blueprintStr, code: blueprintStr, + hasFixUpFunction: false, }) ) expect(blueprint.studioConfigSchema).toBeUndefined() @@ -361,6 +363,7 @@ describe('Test blueprint management api', () => { studioConfigSchema: JSONBlobStringify({ studio1: true } as any), hasCode: !!blueprintStr, code: blueprintStr, + hasFixUpFunction: false, }) ) expect(blueprint.showStyleConfigSchema).toBeUndefined() @@ -403,6 +406,7 @@ describe('Test blueprint management api', () => { TSRVersion: '0.3.0', hasCode: !!blueprintStr, code: blueprintStr, + hasFixUpFunction: false, }) ) expect(blueprint.showStyleConfigSchema).toBeUndefined() @@ -447,6 +451,7 @@ describe('Test blueprint management api', () => { studioConfigSchema: JSONBlobStringify({ studio1: true } as any), hasCode: !!blueprintStr, code: blueprintStr, + hasFixUpFunction: false, }) ) expect(blueprint.showStyleConfigSchema).toBeUndefined() @@ -492,6 +497,7 @@ describe('Test blueprint management api', () => { showStyleConfigSchema: JSONBlobStringify({ show1: true } as any), hasCode: !!blueprintStr, code: blueprintStr, + hasFixUpFunction: false, }) ) expect(blueprint.studioConfigSchema).toBeUndefined() diff --git a/meteor/server/api/blueprints/__tests__/lib.ts b/meteor/server/api/blueprints/__tests__/lib.ts index f8ebb094be..012231094a 100644 --- a/meteor/server/api/blueprints/__tests__/lib.ts +++ b/meteor/server/api/blueprints/__tests__/lib.ts @@ -51,5 +51,6 @@ export function generateFakeBlueprint( blueprintVersion: '', integrationVersion: '', TSRVersion: '', + hasFixUpFunction: false, }) } diff --git a/meteor/server/api/blueprints/api.ts b/meteor/server/api/blueprints/api.ts index e8023264cc..3f7e508ad7 100644 --- a/meteor/server/api/blueprints/api.ts +++ b/meteor/server/api/blueprints/api.ts @@ -65,6 +65,7 @@ export async function insertBlueprint( TSRVersion: '', blueprintHash: getRandomId(), + hasFixUpFunction: false, }) } export async function removeBlueprint(methodContext: MethodContext, blueprintId: BlueprintId): Promise { @@ -163,6 +164,7 @@ async function innerUploadBlueprint( disableVersionChecks: false, blueprintType: undefined, blueprintHash: getRandomId(), + hasFixUpFunction: false, } let blueprintManifest: SomeBlueprintManifest | undefined @@ -217,9 +219,13 @@ async function innerUploadBlueprint( if (blueprintManifest.blueprintType === BlueprintManifestType.SHOWSTYLE) { newBlueprint.showStyleConfigSchema = blueprintManifest.showStyleConfigSchema newBlueprint.showStyleConfigPresets = blueprintManifest.configPresets + newBlueprint.hasFixUpFunction = !!blueprintManifest.fixUpConfig } else if (blueprintManifest.blueprintType === BlueprintManifestType.STUDIO) { newBlueprint.studioConfigSchema = blueprintManifest.studioConfigSchema newBlueprint.studioConfigPresets = blueprintManifest.configPresets + newBlueprint.hasFixUpFunction = !!blueprintManifest.fixUpConfig + } else { + newBlueprint.hasFixUpFunction = false } // Parse the versions, just to verify that the format is correct: diff --git a/meteor/server/api/rest/v1/typeConversion.ts b/meteor/server/api/rest/v1/typeConversion.ts index cb9e3e7dc5..e18434a0a4 100644 --- a/meteor/server/api/rest/v1/typeConversion.ts +++ b/meteor/server/api/rest/v1/typeConversion.ts @@ -80,6 +80,7 @@ export async function showStyleBaseFrom( blueprintConfigWithOverrides: blueprintConfig, _rundownVersionHash: '', lastBlueprintConfig: undefined, + lastBlueprintFixUpHash: undefined, } } @@ -274,6 +275,7 @@ export async function studioFrom(apiStudio: APIStudio, existingId?: StudioId): P inputDevices: wrapDefaultObject({}), }, lastBlueprintConfig: undefined, + lastBlueprintFixUpHash: undefined, } } diff --git a/meteor/server/api/showStyles.ts b/meteor/server/api/showStyles.ts index d38c516116..da9095ade1 100644 --- a/meteor/server/api/showStyles.ts +++ b/meteor/server/api/showStyles.ts @@ -85,6 +85,7 @@ export async function insertShowStyleBaseInner(organizationId: OrganizationId | blueprintConfigWithOverrides: wrapDefaultObject({}), _rundownVersionHash: '', lastBlueprintConfig: undefined, + lastBlueprintFixUpHash: undefined, } await ShowStyleBases.insertAsync(showStyleBase) diff --git a/meteor/server/api/studio/api.ts b/meteor/server/api/studio/api.ts index cb14111dd5..cd212f28ee 100644 --- a/meteor/server/api/studio/api.ts +++ b/meteor/server/api/studio/api.ts @@ -60,6 +60,7 @@ export async function insertStudioInner(organizationId: OrganizationId | null, n inputDevices: wrapDefaultObject({}), }, lastBlueprintConfig: undefined, + lastBlueprintFixUpHash: undefined, }) ) } diff --git a/meteor/server/migration/0_1_0.ts b/meteor/server/migration/0_1_0.ts index fcd7800ba8..a38c18b55c 100644 --- a/meteor/server/migration/0_1_0.ts +++ b/meteor/server/migration/0_1_0.ts @@ -456,6 +456,7 @@ export const addSteps = addMigrationSteps('0.1.0', [ inputDevices: wrapDefaultObject({}), }, lastBlueprintConfig: undefined, + lastBlueprintFixUpHash: undefined, }) }, }, @@ -488,6 +489,7 @@ export const addSteps = addMigrationSteps('0.1.0', [ blueprintConfigWithOverrides: wrapDefaultObject({}), _rundownVersionHash: '', lastBlueprintConfig: undefined, + lastBlueprintFixUpHash: undefined, }) const variantId: ShowStyleVariantId = getRandomId() @@ -522,6 +524,7 @@ export const addSteps = addMigrationSteps('0.1.0', [ blueprintConfigWithOverrides: wrapDefaultObject({}), _rundownVersionHash: '', lastBlueprintConfig: undefined, + lastBlueprintFixUpHash: undefined, }) await ShowStyleVariants.insertAsync({ diff --git a/meteor/server/migration/__tests__/migrations.test.ts b/meteor/server/migration/__tests__/migrations.test.ts index 633a9ae090..bb0362cc4d 100644 --- a/meteor/server/migration/__tests__/migrations.test.ts +++ b/meteor/server/migration/__tests__/migrations.test.ts @@ -141,6 +141,7 @@ describe('Migrations', () => { inputDevices: wrapDefaultObject({}), }, lastBlueprintConfig: undefined, + lastBlueprintFixUpHash: undefined, }) }, }, @@ -178,6 +179,7 @@ describe('Migrations', () => { inputDevices: wrapDefaultObject({}), }, lastBlueprintConfig: undefined, + lastBlueprintFixUpHash: undefined, }) }, }, @@ -215,6 +217,7 @@ describe('Migrations', () => { inputDevices: wrapDefaultObject({}), }, lastBlueprintConfig: undefined, + lastBlueprintFixUpHash: undefined, }) }, }, @@ -401,6 +404,7 @@ describe('Migrations', () => { blueprintConfigWithOverrides: wrapDefaultObject({}), _rundownVersionHash: '', lastBlueprintConfig: undefined, + lastBlueprintFixUpHash: undefined, }) await ShowStyleVariants.insertAsync({ diff --git a/meteor/server/migration/api.ts b/meteor/server/migration/api.ts index c56690f8b7..d01a756fa5 100644 --- a/meteor/server/migration/api.ts +++ b/meteor/server/migration/api.ts @@ -1,11 +1,20 @@ import { check, Match } from '../../lib/check' import { registerClassToMeteorMethods } from '../methods' -import { MigrationChunk, NewMigrationAPI, MigrationAPIMethods } from '../../lib/api/migration' +import { + MigrationChunk, + NewMigrationAPI, + MigrationAPIMethods, + BlueprintFixUpConfigMessage, +} from '../../lib/api/migration' import * as Migrations from './databaseMigration' import { MigrationStepInputResult } from '@sofie-automation/blueprints-integration' import { MethodContextAPI } from '../../lib/api/methods' import { SystemWriteAccess } from '../security/system' import { + fixupConfigForShowStyleBase, + fixupConfigForStudio, + ignoreFixupConfigForShowStyleBase, + ignoreFixupConfigForStudio, runUpgradeForShowStyleBase, runUpgradeForStudio, validateConfigForShowStyleBase, @@ -49,6 +58,22 @@ class ServerMigrationAPI extends MethodContextAPI implements NewMigrationAPI { return Migrations.resetDatabaseVersions() } + async fixupConfigForStudio(studioId: StudioId): Promise { + check(studioId, String) + + await SystemWriteAccess.migrations(this) + + return fixupConfigForStudio(studioId) + } + + async ignoreFixupConfigForStudio(studioId: StudioId): Promise { + check(studioId, String) + + await SystemWriteAccess.migrations(this) + + return ignoreFixupConfigForStudio(studioId) + } + async validateConfigForStudio(studioId: StudioId): Promise { check(studioId, String) @@ -65,6 +90,22 @@ class ServerMigrationAPI extends MethodContextAPI implements NewMigrationAPI { return runUpgradeForStudio(studioId) } + async fixupConfigForShowStyleBase(showStyleBaseId: ShowStyleBaseId): Promise { + check(showStyleBaseId, String) + + await SystemWriteAccess.migrations(this) + + return fixupConfigForShowStyleBase(showStyleBaseId) + } + + async ignoreFixupConfigForShowStyleBase(showStyleBaseId: ShowStyleBaseId): Promise { + check(showStyleBaseId, String) + + await SystemWriteAccess.migrations(this) + + return ignoreFixupConfigForShowStyleBase(showStyleBaseId) + } + async validateConfigForShowStyleBase( showStyleBaseId: ShowStyleBaseId ): Promise { diff --git a/meteor/server/migration/upgrades/showStyleBase.ts b/meteor/server/migration/upgrades/showStyleBase.ts index f762d098bc..9d96c8d115 100644 --- a/meteor/server/migration/upgrades/showStyleBase.ts +++ b/meteor/server/migration/upgrades/showStyleBase.ts @@ -1,4 +1,8 @@ -import { BlueprintManifestType, ShowStyleBlueprintManifest } from '@sofie-automation/blueprints-integration' +import { + BlueprintManifestType, + JSONBlobParse, + ShowStyleBlueprintManifest, +} from '@sofie-automation/blueprints-integration' import { ShowStyleBaseId, TriggeredActionId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { normalizeArray, normalizeArrayToMap, getRandomId, literal, Complete } from '@sofie-automation/corelib/dist/lib' import { @@ -15,39 +19,86 @@ import { evalBlueprint } from '../../api/blueprints/cache' import { logger } from '../../logging' import { CommonContext } from './context' import type { AnyBulkWriteOperation } from 'mongodb' +import { FixUpBlueprintConfigContext } from '@sofie-automation/corelib/dist/fixUpBlueprintConfig/context' +import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' +import { BlueprintFixUpConfigMessage } from '../../../lib/api/migration' -export async function validateConfigForShowStyleBase( +export async function fixupConfigForShowStyleBase( showStyleBaseId: ShowStyleBaseId -): Promise { - const showStyleBase = (await ShowStyleBases.findOneAsync(showStyleBaseId, { - fields: { - _id: 1, - blueprintId: 1, - blueprintConfigPresetId: 1, - blueprintConfigWithOverrides: 1, +): Promise { + const { showStyleBase, blueprint, blueprintManifest } = await loadShowStyleAndBlueprint(showStyleBaseId) + + if (typeof blueprintManifest.fixUpConfig !== 'function') { + if (showStyleBase.lastBlueprintFixUpHash) { + // Cleanup property to avoid getting stuck + await ShowStyleBases.updateAsync(showStyleBaseId, { + $unset: { + lastBlueprintFixUpHash: 1, + }, + }) + } + throw new Meteor.Error(500, 'Blueprint does not support this config flow') + } + + const commonContext = new CommonContext( + 'fixupConfig', + `showStyleBase:${showStyleBaseId},blueprint:${blueprint._id}` + ) + const blueprintContext = new FixUpBlueprintConfigContext( + commonContext, + JSONBlobParse(blueprintManifest.showStyleConfigSchema), + showStyleBase.blueprintConfigWithOverrides + ) + + blueprintManifest.fixUpConfig(blueprintContext) + + // Save the 'fixed' config + await ShowStyleBases.updateAsync(showStyleBaseId, { + $set: { + lastBlueprintFixUpHash: blueprint.blueprintHash, + blueprintConfigWithOverrides: blueprintContext.configObject, }, - })) as - | Pick - | undefined - if (!showStyleBase) throw new Meteor.Error(404, `ShowStyleBase "${showStyleBaseId}" not found!`) + }) - if (!showStyleBase.blueprintConfigPresetId) throw new Meteor.Error(500, 'ShowStyleBase is missing config preset') + return blueprintContext.messages.map((msg) => ({ + message: wrapTranslatableMessageFromBlueprints(msg.message, [blueprint._id]), + path: msg.path, + })) +} - const blueprint = showStyleBase.blueprintId - ? await Blueprints.findOneAsync({ - _id: showStyleBase.blueprintId, - blueprintType: BlueprintManifestType.SHOWSTYLE, - }) - : undefined - if (!blueprint) throw new Meteor.Error(404, `Blueprint "${showStyleBase.blueprintId}" not found!`) +export async function ignoreFixupConfigForShowStyleBase(showStyleBaseId: ShowStyleBaseId): Promise { + const { showStyleBase, blueprint, blueprintManifest } = await loadShowStyleAndBlueprint(showStyleBaseId) - if (!blueprint.blueprintHash) throw new Meteor.Error(500, 'Blueprint is not valid') + if (typeof blueprintManifest.fixUpConfig !== 'function') { + if (showStyleBase.lastBlueprintFixUpHash) { + // Cleanup property to avoid getting stuck + await ShowStyleBases.updateAsync(showStyleBaseId, { + $unset: { + lastBlueprintFixUpHash: 1, + }, + }) + } + throw new Meteor.Error(500, 'Blueprint does not support this config flow') + } - const blueprintManifest = evalBlueprint(blueprint) as ShowStyleBlueprintManifest + // Save the 'fixed' config + await ShowStyleBases.updateAsync(showStyleBaseId, { + $set: { + lastBlueprintFixUpHash: blueprint.blueprintHash, + }, + }) +} + +export async function validateConfigForShowStyleBase( + showStyleBaseId: ShowStyleBaseId +): Promise { + const { showStyleBase, blueprint, blueprintManifest } = await loadShowStyleAndBlueprint(showStyleBaseId) if (typeof blueprintManifest.validateConfig !== 'function') throw new Meteor.Error(500, 'Blueprint does not support this config flow') + throwIfNeedsFixupConfigRunning(showStyleBase, blueprint, blueprintManifest) + const blueprintContext = new CommonContext( 'applyConfig', `showStyleBase:${showStyleBaseId},blueprint:${blueprint._id}` @@ -67,35 +118,13 @@ export async function validateConfigForShowStyleBase( export async function runUpgradeForShowStyleBase(showStyleBaseId: ShowStyleBaseId): Promise { logger.info(`Running upgrade for ShowStyleBase "${showStyleBaseId}"`) - const showStyleBase = (await ShowStyleBases.findOneAsync(showStyleBaseId, { - fields: { - _id: 1, - blueprintId: 1, - blueprintConfigPresetId: 1, - blueprintConfigWithOverrides: 1, - }, - })) as - | Pick - | undefined - if (!showStyleBase) throw new Meteor.Error(404, `ShowStyleBase "${showStyleBaseId}" not found!`) - - if (!showStyleBase.blueprintConfigPresetId) throw new Meteor.Error(500, 'ShowStyleBase is missing config preset') - - const blueprint = showStyleBase.blueprintId - ? await Blueprints.findOneAsync({ - _id: showStyleBase.blueprintId, - blueprintType: BlueprintManifestType.SHOWSTYLE, - }) - : undefined - if (!blueprint) throw new Meteor.Error(404, `Blueprint "${showStyleBase.blueprintId}" not found!`) - - if (!blueprint.blueprintHash) throw new Meteor.Error(500, 'Blueprint is not valid') - - const blueprintManifest = evalBlueprint(blueprint) as ShowStyleBlueprintManifest + const { showStyleBase, blueprint, blueprintManifest } = await loadShowStyleAndBlueprint(showStyleBaseId) if (typeof blueprintManifest.applyConfig !== 'function') throw new Meteor.Error(500, 'Blueprint does not support this config flow') + throwIfNeedsFixupConfigRunning(showStyleBase, blueprint, blueprintManifest) + const blueprintContext = new CommonContext( 'applyConfig', `showStyleBase:${showStyleBaseId},blueprint:${blueprint.blueprintId}` @@ -111,7 +140,7 @@ export async function runUpgradeForShowStyleBase(showStyleBaseId: ShowStyleBaseI lastBlueprintConfig: { blueprintHash: blueprint.blueprintHash, blueprintId: blueprint._id, - blueprintConfigPresetId: showStyleBase.blueprintConfigPresetId, + blueprintConfigPresetId: showStyleBase.blueprintConfigPresetId ?? '', config: rawBlueprintConfig, }, }, @@ -180,3 +209,56 @@ export async function runUpgradeForShowStyleBase(showStyleBaseId: ShowStyleBaseI await TriggeredActions.bulkWriteAsync(bulkOps) } + +async function loadShowStyleAndBlueprint(showStyleBaseId: ShowStyleBaseId) { + const showStyleBase = (await ShowStyleBases.findOneAsync(showStyleBaseId, { + fields: { + _id: 1, + blueprintId: 1, + blueprintConfigPresetId: 1, + blueprintConfigWithOverrides: 1, + lastBlueprintFixUpHash: 1, + }, + })) as + | Pick< + DBShowStyleBase, + | '_id' + | 'blueprintId' + | 'blueprintConfigPresetId' + | 'blueprintConfigWithOverrides' + | 'lastBlueprintFixUpHash' + > + | undefined + if (!showStyleBase) throw new Meteor.Error(404, `ShowStyleBase "${showStyleBaseId}" not found!`) + + if (!showStyleBase.blueprintConfigPresetId) throw new Meteor.Error(500, 'ShowStyleBase is missing config preset') + + const blueprint = showStyleBase.blueprintId + ? await Blueprints.findOneAsync({ + _id: showStyleBase.blueprintId, + blueprintType: BlueprintManifestType.SHOWSTYLE, + }) + : undefined + if (!blueprint) throw new Meteor.Error(404, `Blueprint "${showStyleBase.blueprintId}" not found!`) + + if (!blueprint.blueprintHash) throw new Meteor.Error(500, 'Blueprint is not valid') + + const blueprintManifest = evalBlueprint(blueprint) as ShowStyleBlueprintManifest + + return { + showStyleBase, + blueprint, + blueprintManifest, + } +} + +function throwIfNeedsFixupConfigRunning( + showStyleBase: Pick, + blueprint: Blueprint, + blueprintManifest: ShowStyleBlueprintManifest +): void { + if (typeof blueprintManifest.fixUpConfig !== 'function') return + + if (blueprint.blueprintHash !== showStyleBase.lastBlueprintFixUpHash) + throw new Meteor.Error(500, `fixupConfigForShowStyleBase must be called first`) +} diff --git a/meteor/server/migration/upgrades/studio.ts b/meteor/server/migration/upgrades/studio.ts index 46fba1d821..5d31602f2f 100644 --- a/meteor/server/migration/upgrades/studio.ts +++ b/meteor/server/migration/upgrades/studio.ts @@ -6,8 +6,9 @@ import { profiler } from '../../api/profiler' import { Studios } from '../../collections' import { logger } from '../../logging' import { QueueStudioJob } from '../../worker/worker' +import { BlueprintFixUpConfigMessage } from '../../../lib/api/migration' -export async function validateConfigForStudio(studioId: StudioId): Promise { +async function getStudio(studioId: StudioId): Promise> { const studio = (await Studios.findOneAsync(studioId, { fields: { _id: 1, @@ -15,6 +16,42 @@ export async function validateConfigForStudio(studioId: StudioId): Promise | undefined if (!studio) throw new Meteor.Error(404, `Studio "${studioId}" not found!`) + return studio +} + +export async function fixupConfigForStudio(studioId: StudioId): Promise { + await getStudio(studioId) + + const queuedJob = await QueueStudioJob(StudioJobs.BlueprintFixUpConfigForStudio, studioId, undefined) + + const span = profiler.startSpan('queued-job') + try { + const res = await queuedJob.complete + // explicitly await before returning + return res.messages + } finally { + span?.end() + } +} + +export async function ignoreFixupConfigForStudio(studioId: StudioId): Promise { + await getStudio(studioId) + + const queuedJob = await QueueStudioJob(StudioJobs.BlueprintIgnoreFixUpConfigForStudio, studioId, undefined) + + const span = profiler.startSpan('queued-job') + try { + const res = await queuedJob.complete + // explicitly await before returning + return res + } finally { + span?.end() + } +} + +export async function validateConfigForStudio(studioId: StudioId): Promise { + await getStudio(studioId) + const queuedJob = await QueueStudioJob(StudioJobs.BlueprintValidateConfigForStudio, studioId, undefined) const span = profiler.startSpan('queued-job') @@ -29,12 +66,7 @@ export async function validateConfigForStudio(studioId: StudioId): Promise { logger.info(`Running upgrade for Studio "${studioId}"`) - const studio = (await Studios.findOneAsync(studioId, { - fields: { - _id: 1, - }, - })) as Pick | undefined - if (!studio) throw new Meteor.Error(404, `Studio "${studioId}" not found!`) + await getStudio(studioId) const queuedJob = await QueueStudioJob(StudioJobs.BlueprintUpgradeForStudio, studioId, undefined) diff --git a/meteor/server/publications/blueprintUpgradeStatus/checkStatus.ts b/meteor/server/publications/blueprintUpgradeStatus/checkStatus.ts index df317bb66d..752fc62764 100644 --- a/meteor/server/publications/blueprintUpgradeStatus/checkStatus.ts +++ b/meteor/server/publications/blueprintUpgradeStatus/checkStatus.ts @@ -26,12 +26,13 @@ export interface BlueprintMapEntry { configPresets: Record | Record | undefined configSchema: JSONBlob | undefined blueprintHash: BlueprintHash | undefined + hasFixUpFunction: boolean } export function checkDocUpgradeStatus( blueprintMap: Map, doc: Pick | Pick -): Pick { +): Pick { // Check the blueprintId is valid const blueprint = doc.blueprintId ? blueprintMap.get(doc.blueprintId) : null if (!blueprint || !blueprint.configPresets) { @@ -40,6 +41,7 @@ export function checkDocUpgradeStatus( invalidReason: generateTranslation('Invalid blueprint: "{{blueprintId}}"', { blueprintId: doc.blueprintId, }), + pendingRunOfFixupFunction: false, changes: [], } } @@ -55,10 +57,24 @@ export function checkDocUpgradeStatus( blueprintId: doc.blueprintId, } ), + pendingRunOfFixupFunction: false, // TODO - verify changes: [], } } + if (blueprint.hasFixUpFunction) { + // If the blueprint has a 'fixUpConfig' function, that must be run before we can do any real diffing of the config + const pendingRunOfFixupFunction = + !doc.lastBlueprintFixUpHash || doc.lastBlueprintFixUpHash !== blueprint.blueprintHash + + if (pendingRunOfFixupFunction) { + return { + pendingRunOfFixupFunction: true, + changes: [generateTranslation('Config requires fixing up before it can be validated')], + } + } + } + const changes: ITranslatableMessage[] = [] // Some basic property checks @@ -115,6 +131,7 @@ export function checkDocUpgradeStatus( return { changes, + pendingRunOfFixupFunction: false, } } diff --git a/meteor/server/publications/blueprintUpgradeStatus/publication.ts b/meteor/server/publications/blueprintUpgradeStatus/publication.ts index 553b1960c6..c95113db75 100644 --- a/meteor/server/publications/blueprintUpgradeStatus/publication.ts +++ b/meteor/server/publications/blueprintUpgradeStatus/publication.ts @@ -121,6 +121,7 @@ export async function manipulateBlueprintUpgradeStatusPublicationData( configPresets: blueprint.showStyleConfigPresets, configSchema: blueprint.showStyleConfigSchema, blueprintHash: blueprint.blueprintHash, + hasFixUpFunction: blueprint.hasFixUpFunction, }) break case BlueprintManifestType.STUDIO: @@ -129,6 +130,7 @@ export async function manipulateBlueprintUpgradeStatusPublicationData( configPresets: blueprint.studioConfigPresets, configSchema: blueprint.studioConfigSchema, blueprintHash: blueprint.blueprintHash, + hasFixUpFunction: blueprint.hasFixUpFunction, }) break // TODO - default? diff --git a/meteor/server/publications/blueprintUpgradeStatus/reactiveContentCache.ts b/meteor/server/publications/blueprintUpgradeStatus/reactiveContentCache.ts index cda605ba80..501e678062 100644 --- a/meteor/server/publications/blueprintUpgradeStatus/reactiveContentCache.ts +++ b/meteor/server/publications/blueprintUpgradeStatus/reactiveContentCache.ts @@ -10,6 +10,7 @@ export type StudioFields = | 'blueprintId' | 'blueprintConfigPresetId' | 'lastBlueprintConfig' + | 'lastBlueprintFixUpHash' | 'blueprintConfigWithOverrides' | 'name' export const studioFieldSpecifier = literal>>({ @@ -17,6 +18,7 @@ export const studioFieldSpecifier = literal>>({ _id: 1, studioConfigPresets: 1, @@ -55,6 +60,7 @@ export const blueprintFieldSpecifier = literal { documentType: 'studio', documentId: protectString('studio0'), name: 'Test Studio #0', + pendingRunOfFixupFunction: false, changes: [generateTranslation('something changed')], }, { @@ -267,6 +268,7 @@ describe('systemStatus', () => { documentId: protectString('studio1'), name: 'Test Studio #1', invalidReason: generateTranslation('some invalid reason'), + pendingRunOfFixupFunction: false, changes: [], }, ]) @@ -293,6 +295,7 @@ describe('systemStatus', () => { documentType: 'studio', documentId: protectString('studio0'), name: 'Test Studio #0', + pendingRunOfFixupFunction: false, changes: [generateTranslation('something changed')], }, { @@ -300,6 +303,7 @@ describe('systemStatus', () => { documentType: 'studio', documentId: protectString('studio1'), name: 'Test Studio #1', + pendingRunOfFixupFunction: false, changes: [], }, ]) @@ -326,6 +330,7 @@ describe('systemStatus', () => { documentType: 'studio', documentId: protectString('studio0'), name: 'Test Studio #0', + pendingRunOfFixupFunction: false, changes: [], }, { @@ -333,6 +338,7 @@ describe('systemStatus', () => { documentType: 'studio', documentId: protectString('studio1'), name: 'Test Studio #1', + pendingRunOfFixupFunction: false, changes: [], }, ]) @@ -359,6 +365,7 @@ describe('systemStatus', () => { documentType: 'showStyle', documentId: protectString('showStyleBase0'), name: 'Test Show Style Base #0', + pendingRunOfFixupFunction: false, changes: [generateTranslation('something changed')], }, { @@ -367,6 +374,7 @@ describe('systemStatus', () => { documentId: protectString('showStyleBase1'), name: 'Test Show Style Base #1', invalidReason: generateTranslation('some invalid reason'), + pendingRunOfFixupFunction: false, changes: [], }, ]) @@ -393,6 +401,7 @@ describe('systemStatus', () => { documentType: 'showStyle', documentId: protectString('showStyleBase0'), name: 'Test Show Style Base #0', + pendingRunOfFixupFunction: false, changes: [generateTranslation('something changed')], }, { @@ -400,6 +409,7 @@ describe('systemStatus', () => { documentType: 'showStyle', documentId: protectString('showStyleBase1'), name: 'Test Show Style Base #1', + pendingRunOfFixupFunction: false, changes: [], }, ]) @@ -426,6 +436,7 @@ describe('systemStatus', () => { documentType: 'showStyle', documentId: protectString('showStyleBase0'), name: 'Test Show Style Base #0', + pendingRunOfFixupFunction: false, changes: [], }, { @@ -433,6 +444,7 @@ describe('systemStatus', () => { documentType: 'showStyle', documentId: protectString('showStyleBase1'), name: 'Test Show Style Base #1', + pendingRunOfFixupFunction: false, changes: [], }, ]) diff --git a/packages/blueprints-integration/src/__tests__/context.spec.ts b/packages/blueprints-integration/src/__tests__/context.spec.ts index f2e654ccd6..ecce66cee9 100644 --- a/packages/blueprints-integration/src/__tests__/context.spec.ts +++ b/packages/blueprints-integration/src/__tests__/context.spec.ts @@ -10,95 +10,93 @@ describe('Context', () => { logError: () => undefined, } describe('ICommonContext predicate function', () => { - { - it('should return false for undefined', () => { - expect(isCommonContext(undefined)).toBe(false) - }) + it('should return false for undefined', () => { + expect(isCommonContext(undefined)).toBe(false) + }) - it('should return false for null', () => { - expect(isCommonContext(null)).toBe(false) - }) + it('should return false for null', () => { + expect(isCommonContext(null)).toBe(false) + }) - it('should return false for literal value', () => { - expect(isCommonContext('hehe')).toBe(false) - }) + it('should return false for literal value', () => { + expect(isCommonContext('hehe')).toBe(false) + }) - it('should return false for an object where getHashId is missing', () => { - const invalid = Object.assign({}, validCommonContext, { getHashId: undefined }) + it('should return false for an object where getHashId is missing', () => { + const invalid = Object.assign({}, validCommonContext, { getHashId: undefined }) - expect(isCommonContext(invalid)).toBe(false) - }) + expect(isCommonContext(invalid)).toBe(false) + }) - it('should return false for an object where getHashId is not a function', () => { - const invalid = Object.assign({}, validCommonContext, { getHashId: { hehe: 'lol' } }) + it('should return false for an object where getHashId is not a function', () => { + const invalid = Object.assign({}, validCommonContext, { getHashId: { hehe: 'lol' } }) - expect(isCommonContext(invalid)).toBe(false) - }) + expect(isCommonContext(invalid)).toBe(false) + }) - it('should return false for an object where unhashId is missing', () => { - const invalid = Object.assign({}, validCommonContext, { unhashId: undefined }) + it('should return false for an object where unhashId is missing', () => { + const invalid = Object.assign({}, validCommonContext, { unhashId: undefined }) - expect(isCommonContext(invalid)).toBe(false) - }) + expect(isCommonContext(invalid)).toBe(false) + }) - it('should return false for an object where unhashId is not a function', () => { - const invalid = Object.assign({}, validCommonContext, { unhashId: { hehe: 'lol' } }) + it('should return false for an object where unhashId is not a function', () => { + const invalid = Object.assign({}, validCommonContext, { unhashId: { hehe: 'lol' } }) - expect(isCommonContext(invalid)).toBe(false) - }) + expect(isCommonContext(invalid)).toBe(false) + }) - it('should return false for an object where logDebug is missing', () => { - const invalid = Object.assign({}, validCommonContext, { logDebug: undefined }) + it('should return false for an object where logDebug is missing', () => { + const invalid = Object.assign({}, validCommonContext, { logDebug: undefined }) - expect(isCommonContext(invalid)).toBe(false) - }) + expect(isCommonContext(invalid)).toBe(false) + }) - it('should return false for an object where logDebug is not a function', () => { - const invalid = Object.assign({}, validCommonContext, { logDebug: { hehe: 'lol' } }) + it('should return false for an object where logDebug is not a function', () => { + const invalid = Object.assign({}, validCommonContext, { logDebug: { hehe: 'lol' } }) - expect(isCommonContext(invalid)).toBe(false) - }) + expect(isCommonContext(invalid)).toBe(false) + }) - it('should return false for an object where logInfo is missing', () => { - const invalid = Object.assign({}, validCommonContext, { logInfo: undefined }) + it('should return false for an object where logInfo is missing', () => { + const invalid = Object.assign({}, validCommonContext, { logInfo: undefined }) - expect(isCommonContext(invalid)).toBe(false) - }) + expect(isCommonContext(invalid)).toBe(false) + }) - it('should return false for an object where logInfo is not a function', () => { - const invalid = Object.assign({}, validCommonContext, { logInfo: { hehe: 'lol' } }) + it('should return false for an object where logInfo is not a function', () => { + const invalid = Object.assign({}, validCommonContext, { logInfo: { hehe: 'lol' } }) - expect(isCommonContext(invalid)).toBe(false) - }) + expect(isCommonContext(invalid)).toBe(false) + }) - it('should return false for an object where logWarning is missing', () => { - const invalid = Object.assign({}, validCommonContext, { logWarning: undefined }) + it('should return false for an object where logWarning is missing', () => { + const invalid = Object.assign({}, validCommonContext, { logWarning: undefined }) - expect(isCommonContext(invalid)).toBe(false) - }) + expect(isCommonContext(invalid)).toBe(false) + }) - it('should return false for an object where logWarning is not a function', () => { - const invalid = Object.assign({}, validCommonContext, { logWarning: { hehe: 'lol' } }) + it('should return false for an object where logWarning is not a function', () => { + const invalid = Object.assign({}, validCommonContext, { logWarning: { hehe: 'lol' } }) - expect(isCommonContext(invalid)).toBe(false) - }) + expect(isCommonContext(invalid)).toBe(false) + }) - it('should return false for an object where logError is missing', () => { - const invalid = Object.assign({}, validCommonContext, { logError: undefined }) + it('should return false for an object where logError is missing', () => { + const invalid = Object.assign({}, validCommonContext, { logError: undefined }) - expect(isCommonContext(invalid)).toBe(false) - }) + expect(isCommonContext(invalid)).toBe(false) + }) - it('should return false for an object where logError is not a function', () => { - const invalid = Object.assign({}, validCommonContext, { logError: { hehe: 'lol' } }) + it('should return false for an object where logError is not a function', () => { + const invalid = Object.assign({}, validCommonContext, { logError: { hehe: 'lol' } }) - expect(isCommonContext(invalid)).toBe(false) - }) + expect(isCommonContext(invalid)).toBe(false) + }) - it('should return true for a valid context', () => { - expect(isCommonContext(validCommonContext)).toBe(true) - }) - } + it('should return true for a valid context', () => { + expect(isCommonContext(validCommonContext)).toBe(true) + }) }) describe('IUserNotesContext predicate function', () => { diff --git a/packages/blueprints-integration/src/api/showStyle.ts b/packages/blueprints-integration/src/api/showStyle.ts index cfe3f0fed6..0416d6e356 100644 --- a/packages/blueprints-integration/src/api/showStyle.ts +++ b/packages/blueprints-integration/src/api/showStyle.ts @@ -15,6 +15,7 @@ import type { IDataStoreActionExecutionContext, IRundownActivationContext, IShowStyleContext, + IFixUpConfigContext, } from '../context' import type { IngestAdlib, ExtendedIngestRundown, IngestSegment } from '../ingest' import type { IBlueprintExternalMessageQueueObj } from '../message' @@ -126,6 +127,12 @@ export interface ShowStyleBlueprintManifest IBlueprintAdLibPiece | IBlueprintActionManifest | null + /** + * Apply automatic upgrades to the structure of user specified config overrides + * This lets you apply various changes to the user's values in an abstract way + */ + fixUpConfig?: (context: IFixUpConfigContext) => void + /** * 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. diff --git a/packages/blueprints-integration/src/api/studio.ts b/packages/blueprints-integration/src/api/studio.ts index 9b4373a535..a4be296f26 100644 --- a/packages/blueprints-integration/src/api/studio.ts +++ b/packages/blueprints-integration/src/api/studio.ts @@ -4,7 +4,7 @@ import type { BlueprintConfigCoreConfig, BlueprintManifestBase, BlueprintManifes import type { JSONSchema } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes' import type { JSONBlob } from '@sofie-automation/shared-lib/dist/lib/JSONBlob' import type { MigrationStepStudio } from '../migrations' -import type { ICommonContext, IStudioBaselineContext, IStudioUserContext } from '../context' +import type { ICommonContext, IFixUpConfigContext, IStudioBaselineContext, IStudioUserContext } from '../context' import type { IBlueprintShowStyleBase } from '../showStyle' import type { ExtendedIngestRundown } from '../ingest' import type { ExpectedPlayoutItemGeneric, IBlueprintResultRundownPlaylist, IBlueprintRundownDB } from '../documents' @@ -46,6 +46,12 @@ export interface StudioBlueprintManifest BlueprintResultRundownPlaylist | null + /** + * Apply automatic upgrades to the structure of user specified config overrides + * This lets you apply various changes to the user's values in an abstract way + */ + fixUpConfig?: (context: IFixUpConfigContext) => void + /** * 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. diff --git a/packages/blueprints-integration/src/context/fixUpConfigContext.ts b/packages/blueprints-integration/src/context/fixUpConfigContext.ts new file mode 100644 index 0000000000..b8a8cbce1f --- /dev/null +++ b/packages/blueprints-integration/src/context/fixUpConfigContext.ts @@ -0,0 +1,66 @@ +import type { IBlueprintConfig } from '../common' +import type { ITranslatableMessage } from '../translations' +import type { ICommonContext } from './baseContext' + +export interface IFixUpConfigContext extends ICommonContext { + /** + * Get the current config, with any unsaved changes applied + */ + getConfig(): TConfig + + /** + * List all paths with values + */ + listPaths(): string[] + + /** + * List all paths with values that don't align with the current config structure + */ + listInvalidPaths(): string[] + + /** + * Check if there are any values for the specified path + * @param path Object path prefix to check + */ + hasOperations(path: string): boolean + + /** + * Add a new user defined set operation for the specified path + * @param path Exact object path to be set + * @param value Value to be stored + */ + addSetOperation(path: string, value: any): void + /** + * Add a new user defined delete operation for the specified path + * @param path Exact object path to be deleted + */ + addDeleteOperation(path: string): void + + /** + * Remove operations for a path + * All nested operations within the path will removed + * @param path Object path prefix to be removed (`a` will match `a` and `a.b`) + */ + removeOperations(path: string): void + + /** + * Rename operations for a path + * All nested operations within the path will renamed + * eg `a` will match `a` and `a.b` + * @param fromPath Object path prefix to be renamed (`a` will match `a` and `a.b`) + * @param toPath Object path prefix to be substituted + */ + renameOperations(fromPath: string, toPath: string): void + + /* + * Future: a way of transforming values would be useful, but more direction is needed on + * what kind of transformations are needed and possible to do in this flow + */ + /** + * Show a warning to the user about a change that they will need to convert/migrate manually + * This will be persisted until the next call to `applyConfig` + * @param path Object path prefix the message is related to + * @param message Message to show to the user + */ + warnUnfixable(path: string, message: ITranslatableMessage): void +} diff --git a/packages/blueprints-integration/src/context/index.ts b/packages/blueprints-integration/src/context/index.ts index 6d411bb400..121a8e3f0b 100644 --- a/packages/blueprints-integration/src/context/index.ts +++ b/packages/blueprints-integration/src/context/index.ts @@ -1,6 +1,7 @@ export * from './adlibActionContext' export * from './baseContext' export * from './eventContext' +export * from './fixUpConfigContext' export * from './packageInfoContext' export * from './rundownContext' export * from './showStyleContext' diff --git a/packages/corelib/src/dataModel/Blueprint.ts b/packages/corelib/src/dataModel/Blueprint.ts index 0b679c6683..99e025bfdf 100644 --- a/packages/corelib/src/dataModel/Blueprint.ts +++ b/packages/corelib/src/dataModel/Blueprint.ts @@ -55,6 +55,9 @@ export interface Blueprint { /** Hash for the blueprint, changed each time it is changed */ blueprintHash: BlueprintHash + + /** Whether the blueprint this wraps has a `fixUpConfig` function defined */ + hasFixUpFunction: boolean } /** Describes the last state a Blueprint document was in when applying config changes */ diff --git a/packages/corelib/src/dataModel/ShowStyleBase.ts b/packages/corelib/src/dataModel/ShowStyleBase.ts index b979595eb8..03a19d7d46 100644 --- a/packages/corelib/src/dataModel/ShowStyleBase.ts +++ b/packages/corelib/src/dataModel/ShowStyleBase.ts @@ -1,6 +1,6 @@ import { IBlueprintConfig, IOutputLayer, ISourceLayer, SourceLayerType } from '@sofie-automation/blueprints-integration' import { ObjectWithOverrides } from '../settings/objectWithOverrides' -import { LastBlueprintConfig } from './Blueprint' +import { BlueprintHash, LastBlueprintConfig } from './Blueprint' import { BlueprintId, OrganizationId, ShowStyleBaseId } from './Ids' export interface HotkeyDefinition { @@ -55,4 +55,6 @@ export interface DBShowStyleBase { /** Details on the last blueprint used to generate the defaults values for this */ lastBlueprintConfig: LastBlueprintConfig | undefined + /** Last BlueprintHash where the fixupConfig method was run */ + lastBlueprintFixUpHash: BlueprintHash | undefined } diff --git a/packages/corelib/src/dataModel/Studio.ts b/packages/corelib/src/dataModel/Studio.ts index 444da29d63..19653e22db 100644 --- a/packages/corelib/src/dataModel/Studio.ts +++ b/packages/corelib/src/dataModel/Studio.ts @@ -1,7 +1,7 @@ import { BlueprintMapping, IBlueprintConfig, PackageContainer, TSR } from '@sofie-automation/blueprints-integration' import { ObjectWithOverrides } from '../settings/objectWithOverrides' import { StudioId, OrganizationId, BlueprintId, ShowStyleBaseId, MappingsHash, PeripheralDeviceId } from './Ids' -import { LastBlueprintConfig } from './Blueprint' +import { BlueprintHash, LastBlueprintConfig } from './Blueprint' import { MappingsExt, MappingExt } from '@sofie-automation/shared-lib/dist/core/model/Timeline' export { MappingsExt, MappingExt, MappingsHash } @@ -106,6 +106,8 @@ export interface DBStudio { /** Details on the last blueprint used to generate the defaults values for this */ lastBlueprintConfig: LastBlueprintConfig | undefined + /** Last BlueprintHash where the fixupConfig method was run */ + lastBlueprintFixUpHash: BlueprintHash | undefined } export interface StudioPeripheralDeviceSettings { diff --git a/packages/corelib/src/fixUpBlueprintConfig/__tests__/context.spec.ts b/packages/corelib/src/fixUpBlueprintConfig/__tests__/context.spec.ts new file mode 100644 index 0000000000..ef98ba05dc --- /dev/null +++ b/packages/corelib/src/fixUpBlueprintConfig/__tests__/context.spec.ts @@ -0,0 +1,609 @@ +import { + ObjectOverrideDeleteOp, + ObjectOverrideSetOp, + ObjectWithOverrides, + wrapDefaultObject, +} from '../../settings/objectWithOverrides' +import { literal } from '../../lib' +import { FixUpBlueprintConfigContext } from '../context' +import { ICommonContext, JSONSchema } from '@sofie-automation/blueprints-integration' + +function createSimpleConfigBlob() { + return literal>({ + defaults: { + a: 1, + b: 2, + c: 3, + }, + overrides: [ + { op: 'delete', path: 'b' }, + { op: 'set', path: 'c', value: 5 }, + ], + }) +} + +function createComplexConfigBlob() { + return literal>({ + defaults: { + a: 1, + b: 2, + c: 3, + obj: { + a: 1, + b: 2, + }, + arr: [3, 4, 5], + table: { + abc: { + number: 1, + }, + def: { + number: 5, + }, + }, + }, + overrides: [ + { op: 'delete', path: 'b' }, + { op: 'set', path: 'c', value: 5 }, + { op: 'set', path: 'obj.b', value: 5 }, + { op: 'set', path: 'arr', value: [6, 7] }, + { op: 'set', path: 'table.c', value: { number: 99 } }, + ], + }) +} + +function createComplexBlobSchema(): JSONSchema { + return { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + properties: { + a: { + type: 'string', + }, + b: { + type: 'integer', + }, + c: { + type: 'integer', + }, + obj: { + type: 'object', + properties: { + a: { + type: 'integer', + }, + b: { + type: 'integer', + }, + c: { + type: 'integer', + }, + }, + }, + arr: { + type: 'array', + items: { + type: 'integer', + }, + }, + table: { + type: 'object', + patternProperties: { + '': { + type: 'object', + properties: { + number: { + type: 'integer', + }, + }, + }, + }, + }, + }, + } +} + +describe('getConfig', () => { + const fakeCommonContext: ICommonContext = null as any + + test('Initial', () => { + const context = new FixUpBlueprintConfigContext(fakeCommonContext, {}, createSimpleConfigBlob()) + + expect(context.getConfig()).toEqual({ + a: 1, + c: 5, + }) + }) + + test('Mutate (externally) is reactive', () => { + const context = new FixUpBlueprintConfigContext(fakeCommonContext, {}, createSimpleConfigBlob()) + context.configObject.overrides.push({ op: 'set', path: 'c', value: 10 }) + + expect(context.getConfig()).toEqual({ + a: 1, + c: 10, + }) + }) +}) + +describe('listPaths', () => { + const fakeCommonContext: ICommonContext = null as any + + test('Initial', () => { + const context = new FixUpBlueprintConfigContext(fakeCommonContext, {}, createSimpleConfigBlob()) + + expect(context.listPaths()).toEqual(['b', 'c']) + }) + + test('Mutate (externally) is reactive', () => { + const context = new FixUpBlueprintConfigContext(fakeCommonContext, {}, createSimpleConfigBlob()) + context.configObject.overrides.push({ op: 'set', path: 'c', value: 10 }) + + expect(context.listPaths()).toEqual(['b', 'c', 'c']) + }) +}) + +describe('listInvalidPaths', () => { + const fakeCommonContext: ICommonContext = null as any + + test('Initial', () => { + const context = new FixUpBlueprintConfigContext(fakeCommonContext, {}, createSimpleConfigBlob()) + + expect(context.listInvalidPaths()).toEqual([]) + }) + + test('Mutate (externally) is reactive', () => { + const context = new FixUpBlueprintConfigContext(fakeCommonContext, {}, createSimpleConfigBlob()) + context.configObject.overrides.push({ op: 'set', path: 'c', value: 10 }) + context.configObject.overrides.push({ op: 'set', path: 'b.1', value: 10 }) + + expect(context.listInvalidPaths()).toEqual(['b.1']) + }) +}) + +describe('hasOperations', () => { + const fakeCommonContext: ICommonContext = null as any + + test('hasOperations', () => { + const context = new FixUpBlueprintConfigContext(fakeCommonContext, {}, createSimpleConfigBlob()) + + expect(context.hasOperations('a')).toBeFalsy() + expect(context.hasOperations('d')).toBeFalsy() + expect(context.hasOperations('c.1')).toBeFalsy() + expect(context.hasOperations('')).toBeFalsy() + + expect(context.hasOperations('b')).toBeTruthy() + expect(context.hasOperations('c')).toBeTruthy() + + context.configObject.overrides.push({ op: 'set', path: 'a.1', value: 10 }) + expect(context.hasOperations('a')).toBeTruthy() + }) +}) + +describe('addSetOperation', () => { + const fakeCommonContext: ICommonContext = null as any + const configSchema = createComplexBlobSchema() + + test('set invalid path', () => { + const initialConfig = createComplexConfigBlob() + const context = new FixUpBlueprintConfigContext(fakeCommonContext, configSchema, initialConfig) + + expect(() => context.addSetOperation('zz', true)).toThrow(/does not exist/) + }) + + test('set new path', () => { + const initialConfig = createComplexConfigBlob() + const context = new FixUpBlueprintConfigContext(fakeCommonContext, configSchema, initialConfig) + + context.addSetOperation('a', true) + expect(context.configObject.overrides).toEqual([ + ...initialConfig.overrides, + { op: 'set', path: 'a', value: true } satisfies ObjectOverrideSetOp, + ]) + }) + + test('override existing path', () => { + const initialConfig = createComplexConfigBlob() + const context = new FixUpBlueprintConfigContext(fakeCommonContext, configSchema, initialConfig) + + context.addSetOperation('b', true) + expect(context.configObject.overrides).toEqual([ + ...initialConfig.overrides.filter((op) => op.path !== 'b'), + { op: 'set', path: 'b', value: true } satisfies ObjectOverrideSetOp, + ]) + }) + + test('set child property path', () => { + const initialConfig = createComplexConfigBlob() + const context = new FixUpBlueprintConfigContext(fakeCommonContext, configSchema, initialConfig) + + context.addSetOperation('obj.c', true) + expect(context.configObject.overrides).toEqual([ + ...initialConfig.overrides, + { op: 'set', path: 'obj.c', value: true } satisfies ObjectOverrideSetOp, + ]) + }) + + test('override child property path', () => { + const initialConfig = createComplexConfigBlob() + const context = new FixUpBlueprintConfigContext(fakeCommonContext, configSchema, initialConfig) + + context.addSetOperation('obj.b', true) + expect(context.configObject.overrides).toEqual([ + ...initialConfig.overrides.filter((op) => op.path !== 'obj.b'), + { op: 'set', path: 'obj.b', value: true } satisfies ObjectOverrideSetOp, + ]) + }) + + test('set non-table object with child values', () => { + const initialConfig = createComplexConfigBlob() + const context = new FixUpBlueprintConfigContext(fakeCommonContext, configSchema, initialConfig) + + // Not allowed, as 'obj.a' is where the value should be set + expect(() => context.addSetOperation('obj', true)).toThrow('does not exist') + }) + + test('set table array', () => { + const initialConfig = createComplexConfigBlob() + const context = new FixUpBlueprintConfigContext(fakeCommonContext, configSchema, initialConfig) + + const newValue = [9, 9, 9] + context.addSetOperation('arr', newValue) + expect(context.configObject.overrides).toEqual([ + ...initialConfig.overrides.filter((op) => op.path !== 'arr'), + { op: 'set', path: 'arr', value: newValue } satisfies ObjectOverrideSetOp, + ]) + }) + + test('set table array row', () => { + const initialConfig = createComplexConfigBlob() + const context = new FixUpBlueprintConfigContext(fakeCommonContext, configSchema, initialConfig) + + context.addSetOperation('arr.1', 5) + expect(context.configObject.overrides).toEqual([ + ...initialConfig.overrides.filter((op) => op.path !== 'arr'), + { op: 'set', path: 'arr', value: [6, 5] } satisfies ObjectOverrideSetOp, + ]) + }) + + test('set whole table object', () => { + const initialConfig = createComplexConfigBlob() + const context = new FixUpBlueprintConfigContext(fakeCommonContext, configSchema, initialConfig) + + const newValue = { a: { number: 56 } } + expect(() => context.addSetOperation('table', newValue)).toThrow('Cannot set') + }) + + test('set table object row', () => { + const initialConfig = createComplexConfigBlob() + const context = new FixUpBlueprintConfigContext(fakeCommonContext, configSchema, initialConfig) + + context.addSetOperation('table.abc', { number: 8 }) + expect(context.configObject.overrides).toEqual([ + ...initialConfig.overrides, + { op: 'set', path: 'table.abc', value: { number: 8 } } satisfies ObjectOverrideSetOp, + ]) + }) + + test('override table object row value', () => { + const initialConfig = createComplexConfigBlob() + const context = new FixUpBlueprintConfigContext(fakeCommonContext, configSchema, initialConfig) + + context.addSetOperation('table.abc.number', 8) + expect(context.configObject.overrides).toEqual([ + ...initialConfig.overrides, + { op: 'set', path: 'table.abc.number', value: 8 } satisfies ObjectOverrideSetOp, + ]) + }) + + test('override table object row', () => { + const initialConfig = createComplexConfigBlob() + const context = new FixUpBlueprintConfigContext(fakeCommonContext, configSchema, initialConfig) + + context.addSetOperation('table.c', { number: 8 }) + expect(context.configObject.overrides).toEqual([ + ...initialConfig.overrides.filter((op) => op.path !== 'table.c'), + { op: 'set', path: 'table.c', value: { number: 8 } } satisfies ObjectOverrideSetOp, + ]) + }) +}) + +describe('addDeleteOperation', () => { + const fakeCommonContext: ICommonContext = null as any + const configSchema = createComplexBlobSchema() + + test('Invalid path', () => { + const initialConfig = createComplexConfigBlob() + const context = new FixUpBlueprintConfigContext(fakeCommonContext, configSchema, initialConfig) + + expect(() => context.addDeleteOperation('zz')).toThrow(/does not exist/) + }) + + test('Valid path', () => { + const initialConfig = createComplexConfigBlob() + const context = new FixUpBlueprintConfigContext(fakeCommonContext, configSchema, initialConfig) + + context.addDeleteOperation('a') + expect(context.configObject.overrides).toEqual([ + ...initialConfig.overrides, + { op: 'delete', path: 'a' } satisfies ObjectOverrideDeleteOp, + ]) + }) + + test('Path with existing set operation', () => { + const initialConfig = createComplexConfigBlob() + const context = new FixUpBlueprintConfigContext(fakeCommonContext, configSchema, initialConfig) + + context.addDeleteOperation('b') + expect(context.configObject.overrides).toEqual([ + ...initialConfig.overrides.filter((op) => op.path !== 'b'), + { op: 'delete', path: 'b' } satisfies ObjectOverrideDeleteOp, + ]) + }) + + test('Child property path', () => { + const initialConfig = createComplexConfigBlob() + const context = new FixUpBlueprintConfigContext(fakeCommonContext, configSchema, initialConfig) + + context.addDeleteOperation('obj.c') + expect(context.configObject.overrides).toEqual([ + ...initialConfig.overrides, + { op: 'delete', path: 'obj.c' } satisfies ObjectOverrideDeleteOp, + ]) + }) + + test('Child property path with existing set operation', () => { + const initialConfig = createComplexConfigBlob() + const context = new FixUpBlueprintConfigContext(fakeCommonContext, configSchema, initialConfig) + + context.addDeleteOperation('obj.b') + expect(context.configObject.overrides).toEqual([ + ...initialConfig.overrides.filter((op) => op.path !== 'obj.b'), + { op: 'delete', path: 'obj.b' } satisfies ObjectOverrideDeleteOp, + ]) + }) + + test('Fail to delete root of table', () => { + const initialConfig = createComplexConfigBlob() + const context = new FixUpBlueprintConfigContext(fakeCommonContext, configSchema, initialConfig) + + // Not allowed, as 'obj.a' is where the value should be set + expect(() => context.addDeleteOperation('obj')).toThrow('does not exist') + }) + + test('Root of table array', () => { + const initialConfig = createComplexConfigBlob() + const context = new FixUpBlueprintConfigContext(fakeCommonContext, configSchema, initialConfig) + + context.addDeleteOperation('arr') + expect(context.configObject.overrides).toEqual([ + ...initialConfig.overrides.filter((op) => op.path !== 'arr'), + { op: 'delete', path: 'arr' } satisfies ObjectOverrideDeleteOp, + ]) + }) + + test('Row inside of table array', () => { + const initialConfig = createComplexConfigBlob() + const context = new FixUpBlueprintConfigContext(fakeCommonContext, configSchema, initialConfig) + + context.addDeleteOperation('arr.1') + expect(context.configObject.overrides).toEqual([ + ...initialConfig.overrides.filter((op) => op.path !== 'arr'), + { op: 'set', path: 'arr', value: [6] } satisfies ObjectOverrideSetOp, + ]) + }) + + test('Fail on root of table object', () => { + const initialConfig = createComplexConfigBlob() + const context = new FixUpBlueprintConfigContext(fakeCommonContext, configSchema, initialConfig) + + expect(() => context.addDeleteOperation('table')).toThrow('Cannot set') + }) + + test('Row of table object', () => { + const initialConfig = createComplexConfigBlob() + const context = new FixUpBlueprintConfigContext(fakeCommonContext, configSchema, initialConfig) + + context.addDeleteOperation('table.abc') + expect(context.configObject.overrides).toEqual([ + ...initialConfig.overrides, + { op: 'delete', path: 'table.abc' } satisfies ObjectOverrideDeleteOp, + ]) + }) + + test('Value inside row of table object', () => { + const initialConfig = createComplexConfigBlob() + const context = new FixUpBlueprintConfigContext(fakeCommonContext, configSchema, initialConfig) + + context.addDeleteOperation('table.abc.number') + expect(context.configObject.overrides).toEqual([ + ...initialConfig.overrides, + { op: 'delete', path: 'table.abc.number' } satisfies ObjectOverrideDeleteOp, + ]) + }) + + test('Replace row of table object', () => { + const initialConfig = createComplexConfigBlob() + const context = new FixUpBlueprintConfigContext(fakeCommonContext, configSchema, initialConfig) + + context.addDeleteOperation('table.c') + expect(context.configObject.overrides).toEqual([ + ...initialConfig.overrides.filter((op) => op.path !== 'table.c'), + { op: 'delete', path: 'table.c' } satisfies ObjectOverrideDeleteOp, + ]) + }) +}) + +describe('removeOperations', () => { + const fakeCommonContext: ICommonContext = null as any + + function getOverridePaths(config: ObjectWithOverrides): string[] { + return config.overrides.map((op) => op.path) + } + + test('empty path', () => { + const context = new FixUpBlueprintConfigContext(fakeCommonContext, {}, createComplexConfigBlob()) + + const currentPaths = getOverridePaths(context.configObject) + context.removeOperations('') + expect(getOverridePaths(context.configObject)).toEqual(currentPaths) + }) + + test('path with no ops', () => { + const context = new FixUpBlueprintConfigContext(fakeCommonContext, {}, createComplexConfigBlob()) + + { + const currentPaths = getOverridePaths(context.configObject) + context.removeOperations('a') + expect(getOverridePaths(context.configObject)).toEqual(currentPaths) + } + + { + const currentPaths = getOverridePaths(context.configObject) + context.removeOperations('obj.a') + expect(getOverridePaths(context.configObject)).toEqual(currentPaths) + } + }) + + test('unmatched path', () => { + const context = new FixUpBlueprintConfigContext(fakeCommonContext, {}, createComplexConfigBlob()) + + const currentPaths = getOverridePaths(context.configObject) + context.removeOperations('notreal') + expect(getOverridePaths(context.configObject)).toEqual(currentPaths) + }) + + test('root level path', () => { + const context = new FixUpBlueprintConfigContext(fakeCommonContext, {}, createComplexConfigBlob()) + + const pathsToRemove = ['b'] + + const currentPaths = getOverridePaths(context.configObject).filter((p) => !pathsToRemove.includes(p)) + context.removeOperations('b') + expect(getOverridePaths(context.configObject)).toEqual(currentPaths) + }) + + test('root level path affects children', () => { + const context = new FixUpBlueprintConfigContext(fakeCommonContext, {}, createComplexConfigBlob()) + + const pathsToRemove = ['obj.b'] + + const currentPaths = getOverridePaths(context.configObject).filter((p) => !pathsToRemove.includes(p)) + context.removeOperations('obj') + expect(getOverridePaths(context.configObject)).toEqual(currentPaths) + }) + + test('nested path', () => { + const context = new FixUpBlueprintConfigContext(fakeCommonContext, {}, createComplexConfigBlob()) + + const pathsToRemove = ['obj.b'] + + const currentPaths = getOverridePaths(context.configObject).filter((p) => !pathsToRemove.includes(p)) + context.removeOperations('obj.b') + expect(getOverridePaths(context.configObject)).toEqual(currentPaths) + }) +}) + +describe('renameOperations', () => { + const fakeCommonContext: ICommonContext = null as any + + function getOverridePaths(config: ObjectWithOverrides): string[] { + return config.overrides.map((op) => op.path) + } + + test('empty path', () => { + const context = new FixUpBlueprintConfigContext(fakeCommonContext, {}, createComplexConfigBlob()) + + const currentPaths = getOverridePaths(context.configObject) + context.renameOperations('', 'ab') + expect(getOverridePaths(context.configObject)).toEqual(currentPaths) + }) + + test('path with no ops', () => { + const context = new FixUpBlueprintConfigContext(fakeCommonContext, {}, createComplexConfigBlob()) + + { + const currentPaths = getOverridePaths(context.configObject) + context.renameOperations('a', 'z') + expect(getOverridePaths(context.configObject)).toEqual(currentPaths) + } + + { + const currentPaths = getOverridePaths(context.configObject) + context.renameOperations('obj.a', 'z.z') + expect(getOverridePaths(context.configObject)).toEqual(currentPaths) + } + }) + + test('unmatched path', () => { + const context = new FixUpBlueprintConfigContext(fakeCommonContext, {}, createComplexConfigBlob()) + + const currentPaths = getOverridePaths(context.configObject) + context.renameOperations('notreal', 'z') + expect(getOverridePaths(context.configObject)).toEqual(currentPaths) + }) + + test('root level path', () => { + const context = new FixUpBlueprintConfigContext(fakeCommonContext, {}, createComplexConfigBlob()) + + const pathsToRemove = ['b'] + + const currentPaths = getOverridePaths(context.configObject) + .filter((p) => !pathsToRemove.includes(p)) + .concat('z') + context.renameOperations('b', 'z') + expect(getOverridePaths(context.configObject)).toEqual(currentPaths) + }) + + test('root level path affects children', () => { + const context = new FixUpBlueprintConfigContext(fakeCommonContext, {}, createComplexConfigBlob()) + + const pathsToRemove = ['obj.b'] + + const currentPaths = getOverridePaths(context.configObject) + .filter((p) => !pathsToRemove.includes(p)) + .concat('z.b') + context.renameOperations('obj', 'z') + expect(getOverridePaths(context.configObject)).toEqual(currentPaths) + }) + + test('nested path', () => { + const context = new FixUpBlueprintConfigContext(fakeCommonContext, {}, createComplexConfigBlob()) + + const pathsToRemove = ['obj.b'] + + const currentPaths = getOverridePaths(context.configObject) + .filter((p) => !pathsToRemove.includes(p)) + .concat('z') + context.renameOperations('obj.b', 'z') + expect(getOverridePaths(context.configObject)).toEqual(currentPaths) + }) +}) + +describe('warnUnfixable', () => { + const fakeCommonContext: ICommonContext = null as any + test('warnUnfixable', () => { + const context = new FixUpBlueprintConfigContext(fakeCommonContext, {}, wrapDefaultObject({})) + + expect(context.messages).toHaveLength(0) + + context.warnUnfixable('MyPath', { key: 'Test message' }) + context.warnUnfixable('MyPath2', { key: 'Another message', args: { a: 1 } }) + + expect(context.messages).toEqual([ + { + message: { + key: 'Test message', + }, + path: 'MyPath', + }, + { + message: { + key: 'Another message', + args: { a: 1 }, + }, + path: 'MyPath2', + }, + ]) + }) +}) diff --git a/packages/corelib/src/fixUpBlueprintConfig/context.ts b/packages/corelib/src/fixUpBlueprintConfig/context.ts new file mode 100644 index 0000000000..cfa60872f1 --- /dev/null +++ b/packages/corelib/src/fixUpBlueprintConfig/context.ts @@ -0,0 +1,304 @@ +import { + IBlueprintConfig, + ICommonContext, + IFixUpConfigContext, + ITranslatableMessage, + JSONSchema, +} from '@sofie-automation/blueprints-integration' +import objectPath = require('object-path') +import { ReadonlyDeep } from 'type-fest' +import { literal, objectPathGet, objectPathSet, clone } from '../lib' +import { + applyAndValidateOverrides, + filterOverrideOpsForPrefix, + findParentOpToUpdate, + ObjectOverrideDeleteOp, + ObjectOverrideSetOp, + ObjectWithOverrides, + SomeObjectOverrideOp, +} from '../settings/objectWithOverrides' + +interface MatchedSchemaEntry { + path: string + subpath: string + type: 'value' | 'array' | 'object' +} + +export interface FixUpBlueprintConfigMessage { + path: string + message: ITranslatableMessage +} + +export class FixUpBlueprintConfigContext implements IFixUpConfigContext { + readonly #commonContext: ICommonContext + readonly #configSchema: ReadonlyDeep + + readonly configObject: ObjectWithOverrides + readonly messages: FixUpBlueprintConfigMessage[] = [] + + constructor( + commonContext: ICommonContext, + configSchema: ReadonlyDeep, + configObject: ReadonlyDeep> + ) { + this.#commonContext = commonContext + this.#configSchema = configSchema + this.configObject = clone>(configObject) + } + + #findParentSchemaEntry(path: string): MatchedSchemaEntry | null { + if (!path) return null + + const fragments = path.split('.') + + let subSchema: ReadonlyDeep | undefined = this.#configSchema + const subSchemaPath: string[] = [] + for (const fragment of fragments) { + const newSubSchema: ReadonlyDeep | undefined = subSchema?.properties?.[fragment] + if (!newSubSchema) break + subSchemaPath.push(fragment) + subSchema = newSubSchema + + const flatPath = subSchemaPath.join('.') + const subpath = path.slice(flatPath.length + 1) + + switch (subSchema.type) { + case 'array': { + // This looks like a table object + return { + path: flatPath, + subpath: subpath, + type: 'array', + } + } + case 'object': + if (subSchema.patternProperties) { + // This looks like a table object + return { + path: flatPath, + subpath: subpath, + type: 'object', + } + } else { + // Run the loop again + } + break + default: { + if (subpath) throw new Error('Cannot set a path inside of a value object') + return { + path: flatPath, + subpath: subpath, + type: 'value', + } + } + } + } + + return null + } + + getConfig(): IBlueprintConfig { + return applyAndValidateOverrides(this.configObject).obj + } + + listPaths(): string[] { + return this.configObject.overrides.map((op) => op.path) + } + + listInvalidPaths(): string[] { + const appliedConfig = applyAndValidateOverrides(this.configObject) + return appliedConfig.invalid.map((op) => op.path) + } + + hasOperations(path: string): boolean { + const { opsForPrefix } = filterOverrideOpsForPrefix(this.configObject.overrides, path) + return opsForPrefix.length > 0 + } + + addSetOperation(path: string, value: unknown): void { + const parentSchemaEntry = this.#findParentSchemaEntry(path) + if (!parentSchemaEntry) throw new Error(`Path "${path}" does not exist in the current config schema`) + + if (parentSchemaEntry.type === 'array') { + this.#addSetOperationForObjectArray(parentSchemaEntry, value) + } else if (parentSchemaEntry.type === 'object') { + this.#addSetOperationForObjectTable(parentSchemaEntry, path, value) + } else { + this.#addSetOperationForValue(parentSchemaEntry, path, value) + } + } + + #addSetOperationForObjectTable(tableSchemaEntry: MatchedSchemaEntry, path: string, value: any) { + if (tableSchemaEntry.subpath === '') throw new Error('Cannot set an object to a value') + + const { opsForPrefix: opsForRoot } = filterOverrideOpsForPrefix( + this.configObject.overrides, + tableSchemaEntry.path + ) + + const existingParentOp = findParentOpToUpdate(opsForRoot, tableSchemaEntry.subpath) + if (existingParentOp) { + // Found an op at a higher level that can be modified instead + objectPathSet(existingParentOp.op.value, existingParentOp.newSubPath, value) + + // Mutation was performed in place + } else { + // Insert new op + const setOp = literal({ + op: 'set', + path: path, + value: value, + }) + + const { otherOps } = filterOverrideOpsForPrefix(this.configObject.overrides, path) + + this.configObject.overrides = [...otherOps, setOp] + } + } + #addSetOperationForObjectArray(arraySchemaEntry: MatchedSchemaEntry, value: any) { + // Arrays can only be overwritten as a single object + + let newOpForArray: ObjectOverrideSetOp + if (arraySchemaEntry.subpath === '') { + newOpForArray = { op: 'set', path: arraySchemaEntry.path, value: value } + } else { + const currentObj = applyAndValidateOverrides(this.configObject).obj // Note: this will not be very performant, but it is safer + const currentValue = objectPathGet(currentObj, arraySchemaEntry.path) + + // Mutate in place, as we have a clone + objectPathSet(currentValue, arraySchemaEntry.subpath, value) + + newOpForArray = { op: 'set', path: arraySchemaEntry.path, value: currentValue } + } + + const { otherOps } = filterOverrideOpsForPrefix(this.configObject.overrides, arraySchemaEntry.path) + + this.configObject.overrides = [...otherOps, newOpForArray] + } + #addSetOperationForValue(valueSchemaEntry: MatchedSchemaEntry, path: string, value: any) { + // Insert new op + const setOp = literal({ + op: 'set', + path: path, + value: value, + }) + + const { otherOps } = filterOverrideOpsForPrefix(this.configObject.overrides, valueSchemaEntry.path) + + this.configObject.overrides = [...otherOps, setOp] + } + + addDeleteOperation(path: string): void { + const parentSchemaEntry = this.#findParentSchemaEntry(path) + if (!parentSchemaEntry) throw new Error(`Path "${path}" does not exist in the current config schema`) + + if (parentSchemaEntry.type === 'array') { + this.#addDeleteOperationForObjectArray(parentSchemaEntry) + } else if (parentSchemaEntry.type === 'object') { + this.#addDeleteOperationForObjectTable(parentSchemaEntry, path) + } else { + this.#addDeleteOperationForValue(parentSchemaEntry, path) + } + } + + #addDeleteOperationForObjectTable(tableSchemaEntry: MatchedSchemaEntry, path: string) { + if (tableSchemaEntry.subpath === '') throw new Error('Cannot set an object to a value') + + const { opsForPrefix: opsForRoot } = filterOverrideOpsForPrefix( + this.configObject.overrides, + tableSchemaEntry.path + ) + + const existingParentOp = findParentOpToUpdate(opsForRoot, tableSchemaEntry.subpath) + if (existingParentOp) { + // Found an op at a higher level that can be modified instead + objectPath.del(existingParentOp.op.value, existingParentOp.newSubPath) + + // Mutation was performed in place + } else { + // Insert new op + const setOp = literal({ + op: 'delete', + path: path, + }) + + const { otherOps } = filterOverrideOpsForPrefix(this.configObject.overrides, path) + + this.configObject.overrides = [...otherOps, setOp] + } + } + #addDeleteOperationForObjectArray(arraySchemaEntry: MatchedSchemaEntry) { + // Arrays can only be overwritten as a single object + + let newOpForArray: SomeObjectOverrideOp + if (arraySchemaEntry.subpath === '') { + newOpForArray = { op: 'delete', path: arraySchemaEntry.path } + } else { + const currentObj = applyAndValidateOverrides(this.configObject).obj // Note: this will not be very performant, but it is safer + const currentValue = objectPathGet(currentObj, arraySchemaEntry.path) + + // Mutate in place, as we have a clone + objectPath.del(currentValue, arraySchemaEntry.subpath) + + newOpForArray = { op: 'set', path: arraySchemaEntry.path, value: currentValue } + } + + const { otherOps } = filterOverrideOpsForPrefix(this.configObject.overrides, arraySchemaEntry.path) + + this.configObject.overrides = [...otherOps, newOpForArray] + } + #addDeleteOperationForValue(valueSchemaEntry: MatchedSchemaEntry, path: string) { + // Insert new op + const setOp = literal({ + op: 'delete', + path: path, + }) + + const { otherOps } = filterOverrideOpsForPrefix(this.configObject.overrides, valueSchemaEntry.path) + + this.configObject.overrides = [...otherOps, setOp] + } + + removeOperations(path: string): void { + const { opsForPrefix, otherOps } = filterOverrideOpsForPrefix(this.configObject.overrides, path) + if (opsForPrefix.length === 0) return + + this.configObject.overrides = otherOps + } + + renameOperations(fromPath: string, toPath: string): void { + const { opsForPrefix, otherOps } = filterOverrideOpsForPrefix(this.configObject.overrides, fromPath) + if (opsForPrefix.length === 0) return + + const opsWithUpdatedPrefix = opsForPrefix.map((op) => ({ + ...op, + path: `${toPath}${op.path.slice(fromPath.length)}`, + })) + + this.configObject.overrides = [...otherOps, ...opsWithUpdatedPrefix] + } + + warnUnfixable(path: string, message: ITranslatableMessage): void { + this.messages.push({ message, path }) + } + + // Forward to wrapped ICommonContext + getHashId(...args: Parameters): string { + return this.#commonContext.getHashId(...args) + } + unhashId(...args: Parameters): string { + return this.#commonContext.unhashId(...args) + } + logDebug(...args: Parameters): void { + return this.#commonContext.logDebug(...args) + } + logInfo(...args: Parameters): void { + return this.#commonContext.logInfo(...args) + } + logWarning(...args: Parameters): void { + return this.#commonContext.logWarning(...args) + } + logError(...args: Parameters): void { + return this.#commonContext.logError(...args) + } +} diff --git a/packages/corelib/src/settings/objectWithOverrides.ts b/packages/corelib/src/settings/objectWithOverrides.ts index d88b4d16ec..228234eae7 100644 --- a/packages/corelib/src/settings/objectWithOverrides.ts +++ b/packages/corelib/src/settings/objectWithOverrides.ts @@ -177,6 +177,12 @@ function applySetOp(result: ApplyOverridesResult, operation } else { result.preserve.push(operation) + if (!canApplyToPath(result.obj, operation.path)) { + // Can't set at this path + result.invalid.push(operation) + return + } + const existingValue = objectPath.get(result.obj, operation.path) if (_.isEqual(existingValue, operation.value)) { // Same value @@ -193,6 +199,12 @@ function applySetOp(result: ApplyOverridesResult, operation } function applyDeleteOp(result: ApplyOverridesResult, operation: ObjectOverrideDeleteOp): void { + if (!canApplyToPath(result.obj, operation.path)) { + // Can't set at this path + result.invalid.push(operation) + return + } + if (objectPath.has(result.obj, operation.path)) { // It exists in the path, so do the delete objectPath.del(result.obj, operation.path) @@ -203,3 +215,80 @@ function applyDeleteOp(result: ApplyOverridesResult, operat // Always keep the delete op result.preserve.push(operation) } + +function canApplyToPath(resultObj: T, path: string): boolean { + let parentPath: string | undefined = path + while ((parentPath = getParentObjectPath(parentPath)) !== undefined) { + const parentValue = objectPath.get(resultObj, parentPath) + if (parentValue) { + return typeof parentValue === 'object' || Array.isArray(parentValue) + } + } + + return true +} + +/** + * Split a list of SomeObjectOverrideOp based on whether they match a specified prefix + * @param allOps The array of SomeObjectOverrideOp + * @param prefix The prefix to match, without a trailing `.` + */ +export function filterOverrideOpsForPrefix( + allOps: ReadonlyDeep, + prefix: string +): { opsForPrefix: ReadonlyDeep[]; otherOps: ReadonlyDeep[] } { + const res: { opsForPrefix: ReadonlyDeep[]; otherOps: ReadonlyDeep[] } = + { + opsForPrefix: [], + otherOps: [], + } + + const pathAsPrefix = prefix.endsWith('.') ? prefix : `${prefix}.` + + for (const op of allOps) { + if (op.path === prefix || op.path.startsWith(pathAsPrefix)) { + res.opsForPrefix.push(op) + } else { + res.otherOps.push(op) + } + } + + return res +} + +export function findParentOpToUpdate( + opsForId: SomeObjectOverrideOp[], + subPath: string +): + | { + op: ObjectOverrideSetOp + newSubPath: string + } + | undefined { + const revOps = [...opsForId].reverse() + + for (const op of revOps) { + if (subPath === op.path) { + // There is an op at the same path, this should be replaced by the current one + return undefined + } + + if (subPath.startsWith(`${op.path}.`)) { + // The new value is inside of this op + if (op.op === 'delete') { + // Can't mutate a delete op like this + return undefined + } + + // It's a set op, so we would be better to modify in place rather than add another mutate op + return { + op, + newSubPath: subPath.slice(op.path.length + 1), + } + } + } + // + + // Nothing matched + return undefined +} diff --git a/packages/corelib/src/worker/studio.ts b/packages/corelib/src/worker/studio.ts index 1048ae14a9..4d298aa7f6 100644 --- a/packages/corelib/src/worker/studio.ts +++ b/packages/corelib/src/worker/studio.ts @@ -167,6 +167,18 @@ export enum StudioJobs { */ BlueprintValidateConfigForStudio = 'blueprintValidateConfigForStudio', + /** + * Run the 'fixUpConfig' method for the Studio blueprint config + */ + BlueprintFixUpConfigForStudio = 'blueprintFixUpConfigForStudio', + /** + * Ignore the 'fixUpConfig' method for the Studio blueprint config + */ + BlueprintIgnoreFixUpConfigForStudio = 'blueprintIgnoreFixUpConfigForStudio', + + /** + * Activate scratchpad mode for the Rundown containing the nexted Part. + */ ActivateScratchpad = 'activateScratchpad', } @@ -287,6 +299,13 @@ export interface BlueprintValidateConfigForStudioResult { }> } +export interface BlueprintFixUpConfigForStudioResult { + messages: Array<{ + path: string + message: ITranslatableMessage + }> +} + export interface ActivateScratchpadProps extends RundownPlayoutPropsBase { rundownId: RundownId } @@ -337,6 +356,8 @@ export type StudioJobFunc = { [StudioJobs.BlueprintUpgradeForStudio]: () => void [StudioJobs.BlueprintValidateConfigForStudio]: () => BlueprintValidateConfigForStudioResult + [StudioJobs.BlueprintFixUpConfigForStudio]: () => BlueprintFixUpConfigForStudioResult + [StudioJobs.BlueprintIgnoreFixUpConfigForStudio]: () => void [StudioJobs.ActivateScratchpad]: (data: ActivateScratchpadProps) => void } diff --git a/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts b/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts index 6931adad17..aadeec1fd1 100644 --- a/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts +++ b/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts @@ -113,6 +113,7 @@ export function defaultStudio(_id: StudioId): DBStudio { }, _rundownVersionHash: '', lastBlueprintConfig: undefined, + lastBlueprintFixUpHash: undefined, } } diff --git a/packages/job-worker/src/__mocks__/presetCollections.ts b/packages/job-worker/src/__mocks__/presetCollections.ts index b8e1aeb881..d7bdecb7f2 100644 --- a/packages/job-worker/src/__mocks__/presetCollections.ts +++ b/packages/job-worker/src/__mocks__/presetCollections.ts @@ -124,6 +124,7 @@ export async function setupMockShowStyleBase( // hotkeyLegend?: Array _rundownVersionHash: '', lastBlueprintConfig: undefined, + lastBlueprintFixUpHash: undefined, } const showStyleBase = _.extend(defaultShowStyleBase, doc) await context.mockCollections.ShowStyleBases.insertOne(showStyleBase) diff --git a/packages/job-worker/src/playout/upgrade.ts b/packages/job-worker/src/playout/upgrade.ts index 2d73192d26..df57d5a6b3 100644 --- a/packages/job-worker/src/playout/upgrade.ts +++ b/packages/job-worker/src/playout/upgrade.ts @@ -1,4 +1,4 @@ -import { BlueprintMapping, BlueprintMappings, TSR } from '@sofie-automation/blueprints-integration' +import { BlueprintMapping, BlueprintMappings, JSONBlobParse, TSR } from '@sofie-automation/blueprints-integration' import { MappingsExt, StudioIngestDevice, @@ -9,10 +9,14 @@ import { Complete, clone, literal } from '@sofie-automation/corelib/dist/lib' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { wrapTranslatableMessageFromBlueprints } from '@sofie-automation/corelib/dist/TranslatableMessage' -import { BlueprintValidateConfigForStudioResult } from '@sofie-automation/corelib/dist/worker/studio' +import { + BlueprintFixUpConfigForStudioResult, + BlueprintValidateConfigForStudioResult, +} from '@sofie-automation/corelib/dist/worker/studio' import { compileCoreConfigValues } from '../blueprints/config' import { CommonContext } from '../blueprints/context' import { JobContext } from '../jobs' +import { FixUpBlueprintConfigContext } from '@sofie-automation/corelib/dist/fixUpBlueprintConfig/context' /** * Run the Blueprint applyConfig for the studio @@ -112,8 +116,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 = blueprint.blueprint.validateConfig(blueprintContext, rawBlueprintConfig) return { messages: messages.map((msg) => ({ @@ -122,3 +125,80 @@ export async function handleBlueprintValidateConfigForStudio( })), } } + +export async function handleBlueprintFixUpConfigForStudio( + context: JobContext, + _data: unknown +): Promise { + const blueprint = context.studioBlueprint + if (typeof blueprint.blueprint.validateConfig !== 'function') + throw new Error('Blueprint does not support this config flow') + if (!blueprint.blueprintDoc?.blueprintHash) throw new Error('Blueprint is not valid') + if (!context.studio.blueprintConfigPresetId) throw new Error('Studio is missing config preset') + + if (typeof blueprint.blueprint.fixUpConfig !== 'function') { + if (context.studio.lastBlueprintFixUpHash) { + // Cleanup property to avoid getting stuck + await context.directCollections.Studios.update(context.studioId, { + $unset: { + lastBlueprintFixUpHash: 1, + }, + }) + } + throw new Error('Blueprint does not support this config flow') + } + + const commonContext = new CommonContext({ + name: 'fixupConfig', + identifier: `studio:${context.studioId},blueprint:${blueprint.blueprintId}`, + }) + const blueprintContext = new FixUpBlueprintConfigContext( + commonContext, + JSONBlobParse(blueprint.blueprint.studioConfigSchema), + context.studio.blueprintConfigWithOverrides + ) + + blueprint.blueprint.fixUpConfig(blueprintContext) + + // Save the 'fixed' config + await context.directCollections.Studios.update(context.studioId, { + $set: { + lastBlueprintFixUpHash: blueprint.blueprintDoc.blueprintHash, + blueprintConfigWithOverrides: blueprintContext.configObject, + }, + }) + + return { + messages: blueprintContext.messages.map((msg) => ({ + message: wrapTranslatableMessageFromBlueprints(msg.message, [blueprint.blueprintId]), + path: msg.path, + })), + } +} + +export async function handleBlueprintIgnoreFixUpConfigForStudio(context: JobContext, _data: unknown): Promise { + const blueprint = context.studioBlueprint + if (typeof blueprint.blueprint.validateConfig !== 'function') + throw new Error('Blueprint does not support this config flow') + if (!blueprint.blueprintDoc?.blueprintHash) throw new Error('Blueprint is not valid') + if (!context.studio.blueprintConfigPresetId) throw new Error('Studio is missing config preset') + + if (typeof blueprint.blueprint.fixUpConfig !== 'function') { + if (context.studio.lastBlueprintFixUpHash) { + // Cleanup property to avoid getting stuck + await context.directCollections.Studios.update(context.studioId, { + $unset: { + lastBlueprintFixUpHash: 1, + }, + }) + } + throw new Error('Blueprint does not support this config flow') + } + + // Save the 'fixed' config + await context.directCollections.Studios.update(context.studioId, { + $set: { + lastBlueprintFixUpHash: blueprint.blueprintDoc.blueprintHash, + }, + }) +} diff --git a/packages/job-worker/src/workers/studio/jobs.ts b/packages/job-worker/src/workers/studio/jobs.ts index 853320af9f..f97b9a9dea 100644 --- a/packages/job-worker/src/workers/studio/jobs.ts +++ b/packages/job-worker/src/workers/studio/jobs.ts @@ -35,7 +35,12 @@ import { handleRestoreRundownsInPlaylistToDefaultOrder, } from '../../rundownPlaylists' import { handleGeneratePlaylistSnapshot, handleRestorePlaylistSnapshot } from '../../playout/snapshot' -import { handleBlueprintUpgradeForStudio, handleBlueprintValidateConfigForStudio } from '../../playout/upgrade' +import { + handleBlueprintFixUpConfigForStudio, + handleBlueprintIgnoreFixUpConfigForStudio, + handleBlueprintUpgradeForStudio, + handleBlueprintValidateConfigForStudio, +} from '../../playout/upgrade' import { handleTimelineTriggerTime, handleOnPlayoutPlaybackChanged } from '../../playout/timings' import { handleExecuteAdlibAction } from '../../playout/adlibAction' import { handleTakeNextPart } from '../../playout/take' @@ -92,6 +97,8 @@ export const studioJobHandlers: StudioJobHandlers = { [StudioJobs.BlueprintUpgradeForStudio]: handleBlueprintUpgradeForStudio, [StudioJobs.BlueprintValidateConfigForStudio]: handleBlueprintValidateConfigForStudio, + [StudioJobs.BlueprintFixUpConfigForStudio]: handleBlueprintFixUpConfigForStudio, + [StudioJobs.BlueprintIgnoreFixUpConfigForStudio]: handleBlueprintIgnoreFixUpConfigForStudio, [StudioJobs.ActivateScratchpad]: handleActivateScratchpad, } From df7ed0c653b897774f01a33c9e60a5e22fac99e4 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 18 Oct 2023 13:09:41 +0100 Subject: [PATCH 118/479] feat: support packageinfo update flow for buckets and studio baseline SOFIE-2655 (#1051) --- meteor/server/api/__tests__/cleanup.test.ts | 2 + meteor/server/api/buckets.ts | 5 +- meteor/server/api/ingest/packageInfo.ts | 114 ++++++++++++---- meteor/server/publications/buckets.ts | 8 +- .../src/api/showStyle.ts | 6 +- .../src/dataModel/BucketAdLibAction.ts | 3 + .../corelib/src/dataModel/BucketAdLibPiece.ts | 19 ++- .../corelib/src/dataModel/ExpectedPackages.ts | 4 + packages/corelib/src/worker/ingest.ts | 12 +- .../job-worker/src/blueprints/postProcess.ts | 20 ++- .../job-worker/src/ingest/bucket/import.ts | 126 ++++++++++++++---- .../job-worker/src/ingest/expectedPackages.ts | 2 + packages/job-worker/src/ingest/packageInfo.ts | 7 +- .../job-worker/src/playout/adlibAction.ts | 6 +- .../job-worker/src/workers/ingest/jobs.ts | 5 +- 15 files changed, 262 insertions(+), 77 deletions(-) diff --git a/meteor/server/api/__tests__/cleanup.test.ts b/meteor/server/api/__tests__/cleanup.test.ts index 391babd8f9..b9f2f34ec4 100644 --- a/meteor/server/api/__tests__/cleanup.test.ts +++ b/meteor/server/api/__tests__/cleanup.test.ts @@ -221,6 +221,7 @@ async function setDefaultDatatoDB(env: DefaultEnvironment, now: number) { actionId: '', display: {} as any, importVersions: {} as any, + ingestInfo: undefined, showStyleVariantId, userData: {} as any, userDataManifest: {} as any, @@ -232,6 +233,7 @@ async function setDefaultDatatoDB(env: DefaultEnvironment, now: number) { studioId, showStyleBaseId: env.showStyleBaseId, importVersions: {} as any, + ingestInfo: undefined, showStyleVariantId, _rank: 0, content: {} as any, diff --git a/meteor/server/api/buckets.ts b/meteor/server/api/buckets.ts index 3c327aeed0..7d2c75a9c2 100644 --- a/meteor/server/api/buckets.ts +++ b/meteor/server/api/buckets.ts @@ -1,7 +1,7 @@ import * as _ from 'underscore' import { Meteor } from 'meteor/meteor' import { Bucket } from '../../lib/collections/Buckets' -import { getRandomId, literal } from '../../lib/lib' +import { getRandomId, getRandomString, literal } from '../../lib/lib' import { BucketSecurity } from '../security/buckets' import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' import { AdLibAction, AdLibActionCommon } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' @@ -165,12 +165,13 @@ export namespace BucketsAPI { adLibAction = { ...(_.omit(action, ['partId', 'rundownId']) as Omit), _id: getRandomId(), - externalId: '', // TODO - is this ok? + externalId: getRandomString(), // This needs to be something unique, so that the regenerate logic doesn't get it mixed up with something else bucketId: access.bucket._id, studioId: access.studioId, showStyleBaseId: rundown.showStyleBaseId, showStyleVariantId: action.allVariants ? null : rundown.showStyleVariantId, importVersions: rundown.importVersions, + ingestInfo: undefined, } } diff --git a/meteor/server/api/ingest/packageInfo.ts b/meteor/server/api/ingest/packageInfo.ts index 50559cd61a..31d7de51d7 100644 --- a/meteor/server/api/ingest/packageInfo.ts +++ b/meteor/server/api/ingest/packageInfo.ts @@ -1,4 +1,11 @@ -import { ExpectedPackageDBType } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' +import { + ExpectedPackageDBFromBucketAdLib, + ExpectedPackageDBFromBucketAdLibAction, + ExpectedPackageDBFromRundownBaselineObjects, + ExpectedPackageDBFromStudioBaselineObjects, + ExpectedPackageDBType, + ExpectedPackageFromRundown, +} from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' import { PackageInfoDB } from '@sofie-automation/corelib/dist/dataModel/PackageInfos' import { ExpectedPackages, Rundowns } from '../../collections' import { assertNever, lazyIgnore } from '../../../lib/lib' @@ -6,6 +13,9 @@ import { logger } from '../../logging' import { runIngestOperation } from './lib' import { IngestJobs } from '@sofie-automation/corelib/dist/worker/ingest' import { ExpectedPackageId, RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { QueueStudioJob } from '../../worker/worker' +import { StudioJobs } from '@sofie-automation/corelib/dist/worker/studio' export async function onUpdatedPackageInfo(packageId: ExpectedPackageId, _doc: PackageInfoDB | null): Promise { logger.info(`PackageInfo updated "${packageId}"`) @@ -23,35 +33,15 @@ export async function onUpdatedPackageInfo(packageId: ExpectedPackageId, _doc: P case ExpectedPackageDBType.ADLIB_ACTION: case ExpectedPackageDBType.BASELINE_ADLIB_PIECE: case ExpectedPackageDBType.BASELINE_ADLIB_ACTION: - case ExpectedPackageDBType.RUNDOWN_BASELINE_OBJECTS: { - const existingEntry = pendingPackageUpdates.get(pkg.rundownId) - if (existingEntry) { - // already queued, add to the batch - existingEntry.push(pkg._id) - } else { - pendingPackageUpdates.set(pkg.rundownId, [pkg._id]) - } - - // TODO: Scaling - this won't batch correctly if package manager directs calls to multiple instances - lazyIgnore( - `onUpdatedPackageInfoForRundown_${pkg.rundownId}`, - () => { - const packageIds = pendingPackageUpdates.get(pkg.rundownId) - if (packageIds) { - pendingPackageUpdates.delete(pkg.rundownId) - onUpdatedPackageInfoForRundown(pkg.rundownId, packageIds).catch((e) => { - logger.error(`Updating ExpectedPackages for Rundown "${pkg.rundownId}" failed: ${e}`) - }) - } - }, - 1000 - ) + case ExpectedPackageDBType.RUNDOWN_BASELINE_OBJECTS: + onUpdatedPackageInfoForRundownDebounce(pkg) break - } case ExpectedPackageDBType.BUCKET_ADLIB: case ExpectedPackageDBType.BUCKET_ADLIB_ACTION: + onUpdatedPackageInfoForBucketItemDebounce(pkg) + break case ExpectedPackageDBType.STUDIO_BASELINE_OBJECTS: - // Ignore, as we can't handle that for now + onUpdatedPackageInfoForStudioBaselineDebounce(pkg) break default: assertNever(pkg) @@ -60,7 +50,33 @@ export async function onUpdatedPackageInfo(packageId: ExpectedPackageId, _doc: P } } -const pendingPackageUpdates = new Map>() +const pendingRundownPackageUpdates = new Map>() +function onUpdatedPackageInfoForRundownDebounce( + pkg: ExpectedPackageFromRundown | ExpectedPackageDBFromRundownBaselineObjects +) { + const existingEntry = pendingRundownPackageUpdates.get(pkg.rundownId) + if (existingEntry) { + // already queued, add to the batch + existingEntry.push(pkg._id) + } else { + pendingRundownPackageUpdates.set(pkg.rundownId, [pkg._id]) + } + + // TODO: Scaling - this won't batch correctly if package manager directs calls to multiple instances + lazyIgnore( + `onUpdatedPackageInfoForRundown_${pkg.rundownId}`, + () => { + const packageIds = pendingRundownPackageUpdates.get(pkg.rundownId) + if (packageIds) { + pendingRundownPackageUpdates.delete(pkg.rundownId) + onUpdatedPackageInfoForRundown(pkg.rundownId, packageIds).catch((e) => { + logger.error(`Updating ExpectedPackages for Rundown "${pkg.rundownId}" failed: ${e}`) + }) + } + }, + 1000 + ) +} async function onUpdatedPackageInfoForRundown( rundownId: RundownId, @@ -70,7 +86,12 @@ async function onUpdatedPackageInfoForRundown( return } - const tmpRundown = await Rundowns.findOneAsync(rundownId) + const tmpRundown = (await Rundowns.findOneAsync(rundownId, { + projection: { + studioId: 1, + externalId: 1, + }, + })) as Pick | undefined if (!tmpRundown) { logger.error( `onUpdatedPackageInfoForRundown: Missing rundown "${rundownId}" for packages "${packageIds.join(', ')}"` @@ -78,9 +99,44 @@ async function onUpdatedPackageInfoForRundown( return } - await runIngestOperation(tmpRundown.studioId, IngestJobs.PackageInfosUpdated, { + await runIngestOperation(tmpRundown.studioId, IngestJobs.PackageInfosUpdatedRundown, { rundownExternalId: tmpRundown.externalId, peripheralDeviceId: null, packageIds, }) } + +function onUpdatedPackageInfoForBucketItemDebounce( + pkg: ExpectedPackageDBFromBucketAdLib | ExpectedPackageDBFromBucketAdLibAction +) { + lazyIgnore( + `onUpdatedPackageInfoForBucket_${pkg.studioId}_${pkg.bucketId}_${pkg.pieceExternalId}`, + () => { + runIngestOperation(pkg.studioId, IngestJobs.BucketItemRegenerate, { + bucketId: pkg.bucketId, + externalId: pkg.pieceExternalId, + }).catch((err) => { + logger.error( + `Updating ExpectedPackages for Bucket "${pkg.bucketId}" Item "${pkg.pieceExternalId}" failed: ${err}` + ) + }) + }, + 1000 + ) +} + +function onUpdatedPackageInfoForStudioBaselineDebounce(pkg: ExpectedPackageDBFromStudioBaselineObjects) { + lazyIgnore( + `onUpdatedPackageInfoForStudioBaseline_${pkg.studioId}`, + () => { + QueueStudioJob(StudioJobs.UpdateStudioBaseline, pkg.studioId, undefined) + .then(async (job) => { + await job.complete + }) + .catch((err) => { + logger.error(`Updating ExpectedPackages for StudioBaseline "${pkg.studioId}" failed: ${err}`) + }) + }, + 1000 + ) +} diff --git a/meteor/server/publications/buckets.ts b/meteor/server/publications/buckets.ts index 3e5cac7384..a3361916a6 100644 --- a/meteor/server/publications/buckets.ts +++ b/meteor/server/publications/buckets.ts @@ -45,7 +45,9 @@ meteorPublish( meteorPublish(PubSub.bucketAdLibPieces, async function (selector: MongoQuery, _token: string | undefined) { if (!selector) throw new Meteor.Error(400, 'selector argument missing') const modifier: FindOptions = { - fields: {}, + fields: { + ingestInfo: 0, // This is a large blob, and is not of interest to the UI + }, } if (isProtectedString(selector.bucketId) && (await BucketSecurity.allowReadAccess(this, selector.bucketId))) { return BucketAdLibs.findWithCursor(selector, modifier) @@ -58,7 +60,9 @@ meteorPublish( async function (selector: MongoQuery, _token: string | undefined) { if (!selector) throw new Meteor.Error(400, 'selector argument missing') const modifier: FindOptions = { - fields: {}, + fields: { + ingestInfo: 0, // This is a large blob, and is not of interest to the UI + }, } if (isProtectedString(selector.bucketId) && (await BucketSecurity.allowReadAccess(this, selector.bucketId))) { return BucketAdLibActions.findWithCursor(selector, modifier) diff --git a/packages/blueprints-integration/src/api/showStyle.ts b/packages/blueprints-integration/src/api/showStyle.ts index 0416d6e356..b6d53e80ef 100644 --- a/packages/blueprints-integration/src/api/showStyle.ts +++ b/packages/blueprints-integration/src/api/showStyle.ts @@ -125,7 +125,11 @@ export interface ShowStyleBlueprintManifest IBlueprintAdLibPiece | IBlueprintActionManifest | null + ) => + | Promise + | IBlueprintAdLibPiece + | IBlueprintActionManifest + | null /** * Apply automatic upgrades to the structure of user specified config overrides diff --git a/packages/corelib/src/dataModel/BucketAdLibAction.ts b/packages/corelib/src/dataModel/BucketAdLibAction.ts index 3190a83467..7fc0313871 100644 --- a/packages/corelib/src/dataModel/BucketAdLibAction.ts +++ b/packages/corelib/src/dataModel/BucketAdLibAction.ts @@ -1,6 +1,7 @@ import { BucketAdLibActionId, BucketId, StudioId, ShowStyleVariantId, ShowStyleBaseId } from './Ids' import { RundownImportVersions } from './Rundown' import { AdLibActionCommon } from './AdlibAction' +import { BucketAdLibIngestInfo } from './BucketAdLibPiece' export interface BucketAdLibAction extends Omit { _id: BucketAdLibActionId @@ -18,6 +19,8 @@ export interface BucketAdLibAction extends Omit /** if showStyleVariantId is null, the adlibAction can be used with any variant */ showStyleVariantId: ShowStyleVariantId | null importVersions: RundownImportVersions // TODO - is this good? + /** Information used to generate the adlib. If set, this adlib can be regenerated */ + ingestInfo: BucketAdLibIngestInfo | undefined /** The following extended interface allows assigning namespace information to the actions as they are stored in the * database after being emitted from the blueprints diff --git a/packages/corelib/src/dataModel/BucketAdLibPiece.ts b/packages/corelib/src/dataModel/BucketAdLibPiece.ts index 146e630121..8af1a4fbeb 100644 --- a/packages/corelib/src/dataModel/BucketAdLibPiece.ts +++ b/packages/corelib/src/dataModel/BucketAdLibPiece.ts @@ -1,8 +1,23 @@ -import { IBlueprintAdLibPiece, SomeContent } from '@sofie-automation/blueprints-integration' +import { IBlueprintAdLibPiece, IngestAdlib, SomeContent } from '@sofie-automation/blueprints-integration' import { BucketAdLibId, BucketId, StudioId, ShowStyleVariantId, ShowStyleBaseId } from './Ids' import { PieceTimelineObjectsBlob } from './Piece' import { RundownImportVersions } from './Rundown' +/** + * Information used to 'ingest' a Bucket Adlib item + */ +export interface BucketAdLibIngestInfo { + /** + * If set, the adlib should be limited to the specified ShowStyleVariants. + * If undefined, the adlib will be generated for all Variants of the ShowStyleBase. + */ + limitToShowStyleVariantIds: ShowStyleVariantId[] | undefined + /** + * The ingest payload the Adlib was generated from + */ + payload: IngestAdlib +} + export interface BucketAdLib extends Omit { _id: BucketAdLibId bucketId: BucketId @@ -20,6 +35,8 @@ export interface BucketAdLib extends Omit { showStyleVariantId: ShowStyleVariantId | null importVersions: RundownImportVersions // TODO - is this good? + /** Information used to generate the adlib. If set, this adlib can be regenerated */ + ingestInfo: BucketAdLibIngestInfo | undefined /** Stringified timelineObjects */ timelineObjectsString: PieceTimelineObjectsBlob diff --git a/packages/corelib/src/dataModel/ExpectedPackages.ts b/packages/corelib/src/dataModel/ExpectedPackages.ts index 5ff9e590c2..2940e10409 100644 --- a/packages/corelib/src/dataModel/ExpectedPackages.ts +++ b/packages/corelib/src/dataModel/ExpectedPackages.ts @@ -114,12 +114,16 @@ export interface ExpectedPackageDBFromBucketAdLib extends ExpectedPackageDBBase bucketId: BucketId /** The Bucket adlib this package belongs to */ pieceId: BucketAdLibId + /** The `externalId` of the Bucket adlib this package belongs to */ + pieceExternalId: string } export interface ExpectedPackageDBFromBucketAdLibAction extends ExpectedPackageDBBase { fromPieceType: ExpectedPackageDBType.BUCKET_ADLIB_ACTION bucketId: BucketId /** The Bucket adlib-action this package belongs to */ pieceId: BucketAdLibActionId + /** The `externalId` of the Bucket adlib-action this package belongs to */ + pieceExternalId: string } export function getContentVersionHash(expectedPackage: Omit): string { diff --git a/packages/corelib/src/worker/ingest.ts b/packages/corelib/src/worker/ingest.ts index 52aef0ce2d..8f42ce066d 100644 --- a/packages/corelib/src/worker/ingest.ts +++ b/packages/corelib/src/worker/ingest.ts @@ -106,7 +106,7 @@ export enum IngestJobs { /** * Some PackageInfos have been updated, regenerate any Parts which depend on these PackageInfos */ - PackageInfosUpdated = 'packageInfosUpdated', + PackageInfosUpdatedRundown = 'packageInfosUpdatedRundown', /** * User requested removing a rundown @@ -119,6 +119,7 @@ export enum IngestJobs { // For now these are in this queue, but if this gets split up to be per rundown, then a single bucket queue will be needed BucketItemImport = 'bucketItemImport', + BucketItemRegenerate = 'bucketItemRegenerate', BucketActionRegenerateExpectedPackages = 'bucketActionRegenerateExpectedPackages', BucketActionModify = 'bucketActionModify', BucketPieceModify = 'bucketPieceModify', @@ -210,7 +211,7 @@ export interface MosSwapStoryProps extends IngestPropsBase { export interface ExpectedPackagesRegenerateProps { rundownId: RundownId } -export interface PackageInfosUpdatedProps extends IngestPropsBase { +export interface PackageInfosUpdatedRundownProps extends IngestPropsBase { packageIds: ExpectedPackageId[] } @@ -228,6 +229,10 @@ export interface BucketItemImportProps { showStyleVariantIds?: ShowStyleVariantId[] payload: IngestAdlib } +export interface BucketItemRegenerateProps { + bucketId: BucketId + externalId: string +} export interface BucketActionRegenerateExpectedPackagesProps { actionId: BucketAdLibActionId } @@ -278,12 +283,13 @@ export type IngestJobFunc = { [IngestJobs.MosSwapStory]: (data: MosSwapStoryProps) => void [IngestJobs.ExpectedPackagesRegenerate]: (data: ExpectedPackagesRegenerateProps) => void - [IngestJobs.PackageInfosUpdated]: (data: PackageInfosUpdatedProps) => void + [IngestJobs.PackageInfosUpdatedRundown]: (data: PackageInfosUpdatedRundownProps) => void [IngestJobs.UserRemoveRundown]: (data: UserRemoveRundownProps) => void [IngestJobs.UserUnsyncRundown]: (data: UserUnsyncRundownProps) => void [IngestJobs.BucketItemImport]: (data: BucketItemImportProps) => void + [IngestJobs.BucketItemRegenerate]: (data: BucketItemRegenerateProps) => void [IngestJobs.BucketActionModify]: (data: BucketActionModifyProps) => void [IngestJobs.BucketPieceModify]: (data: BucketPieceModifyProps) => void [IngestJobs.BucketActionRegenerateExpectedPackages]: (data: BucketActionRegenerateExpectedPackagesProps) => void diff --git a/packages/job-worker/src/blueprints/postProcess.ts b/packages/job-worker/src/blueprints/postProcess.ts index 9ee2556a36..6e9d2f8fca 100644 --- a/packages/job-worker/src/blueprints/postProcess.ts +++ b/packages/job-worker/src/blueprints/postProcess.ts @@ -35,7 +35,7 @@ import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataM import { ArrayElement, getHash, literal, omit } from '@sofie-automation/corelib/dist/lib' import { BucketAdLibAction } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibAction' import { RundownImportVersions } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' +import { BucketAdLib, BucketAdLibIngestInfo } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' import { interpollateTranslation, wrapTranslatableMessageFromBlueprints, @@ -393,25 +393,28 @@ export function postProcessBucketAdLib( context: JobContext, showStyleCompound: ReadonlyDeep, itemOrig: IBlueprintAdLibPiece, - externalId: string, + ingestInfo: BucketAdLibIngestInfo, blueprintId: BlueprintId, bucketId: BucketId, rank: number | undefined, importVersions: RundownImportVersions ): BucketAdLib { const id: PieceId = protectString( - getHash(`${showStyleCompound.showStyleVariantId}_${context.studioId}_${bucketId}_bucket_adlib_${externalId}`) + getHash( + `${showStyleCompound.showStyleVariantId}_${context.studioId}_${bucketId}_bucket_adlib_${ingestInfo.payload.externalId}` + ) ) const piece: BucketAdLib = { ...itemOrig, content: omit(itemOrig.content, 'timelineObjects'), _id: id, - externalId, + externalId: ingestInfo.payload.externalId, studioId: context.studioId, showStyleBaseId: showStyleCompound._id, showStyleVariantId: showStyleCompound.showStyleVariantId, bucketId, importVersions, + ingestInfo, _rank: rank || itemOrig._rank, timelineObjectsString: EmptyPieceTimelineObjectsBlob, } @@ -439,24 +442,27 @@ export function postProcessBucketAction( context: JobContext, showStyleCompound: ReadonlyDeep, itemOrig: IBlueprintActionManifest, - externalId: string, + ingestInfo: BucketAdLibIngestInfo, blueprintId: BlueprintId, bucketId: BucketId, rank: number | undefined, importVersions: RundownImportVersions ): BucketAdLibAction { const id: AdLibActionId = protectString( - getHash(`${showStyleCompound.showStyleVariantId}_${context.studioId}_${bucketId}_bucket_adlib_${externalId}`) + getHash( + `${showStyleCompound.showStyleVariantId}_${context.studioId}_${bucketId}_bucket_adlib_${ingestInfo.payload.externalId}` + ) ) const action: BucketAdLibAction = { ...omit(itemOrig, 'partId'), _id: id, - externalId, + externalId: ingestInfo.payload.externalId, studioId: context.studioId, showStyleBaseId: showStyleCompound._id, showStyleVariantId: itemOrig.allVariants ? null : showStyleCompound.showStyleVariantId, bucketId, importVersions, + ingestInfo, ...processAdLibActionITranslatableMessages(itemOrig, blueprintId, rank), } diff --git a/packages/job-worker/src/ingest/bucket/import.ts b/packages/job-worker/src/ingest/bucket/import.ts index e7fc0e1284..af4edd32b5 100644 --- a/packages/job-worker/src/ingest/bucket/import.ts +++ b/packages/job-worker/src/ingest/bucket/import.ts @@ -4,7 +4,7 @@ import { IBlueprintActionManifest, IBlueprintAdLibPiece, IngestAdlib } from '@so import { WatchedPackagesHelper } from '../../blueprints/context/watchedPackages' import { JobContext, ProcessedShowStyleCompound } from '../../jobs' import { getSystemVersion } from '../../lib' -import { BucketItemImportProps } from '@sofie-automation/corelib/dist/worker/ingest' +import { BucketItemImportProps, BucketItemRegenerateProps } from '@sofie-automation/corelib/dist/worker/ingest' import { cleanUpExpectedPackagesForBucketAdLibs, cleanUpExpectedPackagesForBucketAdLibsActions, @@ -19,47 +19,112 @@ import { } from '../expectedMediaItems' import { postProcessBucketAction, postProcessBucketAdLib } from '../../blueprints/postProcess' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' -import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' +import { BucketAdLib, BucketAdLibIngestInfo } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' import { BucketAdLibAction } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibAction' import { logger } from '../../logging' import { createShowStyleCompound } from '../../showStyles' import { isAdlibAction } from './util' import { WrappedShowStyleBlueprint } from '../../blueprints/cache' import { ReadonlyDeep } from 'type-fest' -import { BucketId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { BucketId, ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { ExpectedPackageDBType } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' export async function handleBucketItemImport(context: JobContext, data: BucketItemImportProps): Promise { + await regenerateBucketItemFromIngestInfo(context, data.bucketId, data.showStyleBaseId, { + limitToShowStyleVariantIds: data.showStyleVariantIds, + payload: data.payload, + }) +} + +export async function handleBucketItemRegenerate(context: JobContext, data: BucketItemRegenerateProps): Promise { + // These queries could match more than one document, but as they have the same `externalId` they all get regenerated together + const [adlibPiece, adlibAction] = await Promise.all([ + context.directCollections.BucketAdLibPieces.findOne( + { + externalId: data.externalId, + studioId: context.studio._id, + bucketId: data.bucketId, + }, + { + projection: { + showStyleBaseId: 1, + ingestInfo: 1, + }, + } + ) as Promise> | undefined, + context.directCollections.BucketAdLibActions.findOne( + { + externalId: data.externalId, + studioId: context.studio._id, + bucketId: data.bucketId, + }, + { + projection: { + showStyleBaseId: 1, + ingestInfo: 1, + }, + } + ) as Promise> | undefined, + ]) + + // TODO - UserErrors? + if (adlibAction) { + if (!adlibAction.ingestInfo) throw new Error(`Bucket AdLibAction cannot be resynced, it has no ingest data`) + await regenerateBucketItemFromIngestInfo( + context, + data.bucketId, + adlibAction.showStyleBaseId, + adlibAction.ingestInfo + ) + } else if (adlibPiece) { + if (!adlibPiece.ingestInfo) throw new Error(`Bucket AdLibPiece cannot be resynced, it has no ingest data`) + await regenerateBucketItemFromIngestInfo( + context, + data.bucketId, + adlibPiece.showStyleBaseId, + adlibPiece.ingestInfo + ) + } else { + throw new Error(`No Bucket Items with externalId "${data.externalId}" were found`) + } +} + +async function regenerateBucketItemFromIngestInfo( + context: JobContext, + bucketId: BucketId, + showStyleBaseId: ShowStyleBaseId, + ingestInfo: BucketAdLibIngestInfo +): Promise { const [showStyleBase, allShowStyleVariants, allOldAdLibPieces, allOldAdLibActions, blueprint] = await Promise.all([ - context.getShowStyleBase(data.showStyleBaseId), - context.getShowStyleVariants(data.showStyleBaseId), + context.getShowStyleBase(showStyleBaseId), + context.getShowStyleVariants(showStyleBaseId), context.directCollections.BucketAdLibPieces.findFetch( { - externalId: data.payload.externalId, - showStyleBaseId: data.showStyleBaseId, + externalId: ingestInfo.payload.externalId, + showStyleBaseId: showStyleBaseId, studioId: context.studio._id, - bucketId: data.bucketId, + bucketId: bucketId, }, { projection: { _id: 1 } } ) as Promise[]>, context.directCollections.BucketAdLibActions.findFetch( { - externalId: data.payload.externalId, - showStyleBaseId: data.showStyleBaseId, + externalId: ingestInfo.payload.externalId, + showStyleBaseId: showStyleBaseId, studioId: context.studio._id, - bucketId: data.bucketId, + bucketId: bucketId, }, { projection: { _id: 1 } } ) as Promise[]>, - context.getShowStyleBlueprint(data.showStyleBaseId), + context.getShowStyleBlueprint(showStyleBaseId), ]) - if (!showStyleBase) throw new Error(`ShowStyleBase "${data.showStyleBaseId}" not found`) + if (!showStyleBase) throw new Error(`ShowStyleBase "${showStyleBaseId}" not found`) - const showStyleVariants = allShowStyleVariants.filter((v) => { - if (data.showStyleVariantIds) return data.showStyleVariantIds.includes(v._id) - else return true - }) - if (showStyleVariants.length === 0) throw new Error(`No ShowStyleVariants found for ${data.showStyleBaseId}`) + const showStyleVariants = allShowStyleVariants.filter( + (v) => !ingestInfo.limitToShowStyleVariantIds || ingestInfo.limitToShowStyleVariantIds.includes(v._id) + ) + if (showStyleVariants.length === 0) throw new Error(`No ShowStyleVariants found for ${showStyleBaseId}`) const adlibIdsToRemove = new Set(allOldAdLibPieces.map((p) => p._id)) const actionIdsToRemove = new Set(allOldAdLibActions.map((p) => p._id)) @@ -74,7 +139,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, ingestInfo.payload) if (rawAdlib) { const importVersions: RundownImportVersions = { @@ -87,7 +152,7 @@ export async function handleBucketItemImport(context: JobContext, data: BucketIt // Cache the newRank, so we only have to calculate it once: if (newRank === undefined) { - newRank = (await calculateHighestRankInBucket(context, data.bucketId)) + 1 + newRank = (await calculateHighestRankInBucket(context, bucketId)) + 1 } else { newRank++ } @@ -105,9 +170,9 @@ export async function handleBucketItemImport(context: JobContext, data: BucketIt context, showStyleCompound, rawAdlib, - data.payload.externalId, + ingestInfo, blueprint.blueprintId, - data.bucketId, + bucketId, newRank, importVersions ) @@ -125,9 +190,9 @@ export async function handleBucketItemImport(context: JobContext, data: BucketIt context, showStyleCompound, rawAdlib, - data.payload.externalId, + ingestInfo, blueprint.blueprintId, - data.bucketId, + bucketId, newRank, importVersions ) @@ -172,13 +237,20 @@ export async function handleBucketItemImport(context: JobContext, data: BucketIt await Promise.all(ps) } -function generateBucketAdlibForVariant( +async function generateBucketAdlibForVariant( context: JobContext, blueprint: ReadonlyDeep, showStyleCompound: ReadonlyDeep, + // pieceId: BucketAdLibId | BucketAdLibActionId, payload: IngestAdlib -): IBlueprintAdLibPiece | IBlueprintActionManifest | null { - const watchedPackages = WatchedPackagesHelper.empty(context) +): Promise { + const watchedPackages = await WatchedPackagesHelper.create(context, context.studio._id, { + // We don't know what the `pieceId` will be, but we do know the `externalId` + pieceExternalId: payload.externalId, + fromPieceType: { + $in: [ExpectedPackageDBType.BUCKET_ADLIB, ExpectedPackageDBType.BUCKET_ADLIB_ACTION], + }, + }) const contextForVariant = new ShowStyleUserContext( { diff --git a/packages/job-worker/src/ingest/expectedPackages.ts b/packages/job-worker/src/ingest/expectedPackages.ts index bf25a33114..f7d9ed09fe 100644 --- a/packages/job-worker/src/ingest/expectedPackages.ts +++ b/packages/job-worker/src/ingest/expectedPackages.ts @@ -267,6 +267,7 @@ function generateExpectedPackagesForBucketAdlib(studio: ReadonlyDeep, ...base, bucketId: adlib.bucketId, pieceId: adlib._id, + pieceExternalId: adlib.externalId, fromPieceType: ExpectedPackageDBType.BUCKET_ADLIB, }) } @@ -287,6 +288,7 @@ function generateExpectedPackagesForBucketAdlibAction( ...base, bucketId: action.bucketId, pieceId: action._id, + pieceExternalId: action.externalId, fromPieceType: ExpectedPackageDBType.BUCKET_ADLIB_ACTION, }) } diff --git a/packages/job-worker/src/ingest/packageInfo.ts b/packages/job-worker/src/ingest/packageInfo.ts index 616b5c0add..360e988730 100644 --- a/packages/job-worker/src/ingest/packageInfo.ts +++ b/packages/job-worker/src/ingest/packageInfo.ts @@ -1,6 +1,9 @@ import { ExpectedPackageDBType } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' import { SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { ExpectedPackagesRegenerateProps, PackageInfosUpdatedProps } from '@sofie-automation/corelib/dist/worker/ingest' +import { + ExpectedPackagesRegenerateProps, + PackageInfosUpdatedRundownProps, +} from '@sofie-automation/corelib/dist/worker/ingest' import { logger } from '../logging' import { JobContext } from '../jobs' import { regenerateSegmentsFromIngestData } from './generationSegment' @@ -31,7 +34,7 @@ export async function handleExpectedPackagesRegenerate( */ export async function handleUpdatedPackageInfoForRundown( context: JobContext, - data: PackageInfosUpdatedProps + data: PackageInfosUpdatedRundownProps ): Promise { if (data.packageIds.length === 0) { return diff --git a/packages/job-worker/src/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index f1685596e8..0c40474fde 100644 --- a/packages/job-worker/src/playout/adlibAction.ts +++ b/packages/job-worker/src/playout/adlibAction.ts @@ -51,7 +51,11 @@ export async function handleExecuteAdlibAction( const watchedPackages = await WatchedPackagesHelper.create(context, context.studio._id, { pieceId: data.actionDocId, fromPieceType: { - $in: [ExpectedPackageDBType.ADLIB_ACTION, ExpectedPackageDBType.BASELINE_ADLIB_ACTION], + $in: [ + ExpectedPackageDBType.ADLIB_ACTION, + ExpectedPackageDBType.BASELINE_ADLIB_ACTION, + ExpectedPackageDBType.BUCKET_ADLIB_ACTION, + ], }, }) diff --git a/packages/job-worker/src/workers/ingest/jobs.ts b/packages/job-worker/src/workers/ingest/jobs.ts index 4d01f28ce8..c5360ae7b1 100644 --- a/packages/job-worker/src/workers/ingest/jobs.ts +++ b/packages/job-worker/src/workers/ingest/jobs.ts @@ -38,7 +38,7 @@ import { handleBucketRemoveAdlibAction, handleBucketRemoveAdlibPiece, } from '../../ingest/bucket/bucketAdlibs' -import { handleBucketItemImport } from '../../ingest/bucket/import' +import { handleBucketItemImport, handleBucketItemRegenerate } from '../../ingest/bucket/import' type ExecutableFunction = ( context: JobContext, @@ -74,12 +74,13 @@ export const ingestJobHandlers: IngestJobHandlers = { [IngestJobs.MosSwapStory]: handleMosSwapStories, [IngestJobs.ExpectedPackagesRegenerate]: handleExpectedPackagesRegenerate, - [IngestJobs.PackageInfosUpdated]: handleUpdatedPackageInfoForRundown, + [IngestJobs.PackageInfosUpdatedRundown]: handleUpdatedPackageInfoForRundown, [IngestJobs.UserRemoveRundown]: handleUserRemoveRundown, [IngestJobs.UserUnsyncRundown]: handleUserUnsyncRundown, [IngestJobs.BucketItemImport]: handleBucketItemImport, + [IngestJobs.BucketItemRegenerate]: handleBucketItemRegenerate, [IngestJobs.BucketActionRegenerateExpectedPackages]: handleBucketActionRegenerateExpectedPackages, [IngestJobs.BucketActionModify]: handleBucketActionModify, [IngestJobs.BucketPieceModify]: handleBucketPieceModify, From dff6d2ce87da66adf43662aabdd5a0f007058181 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 10 Oct 2023 15:39:10 +0100 Subject: [PATCH 119/479] feat: rework CacheForPlayout merge conflicts SOFIE-2513 --- .../__tests__/context-adlibActions.test.ts | 36 ++++++----------- .../cache/__tests__/DatabaseCaches.test.ts | 24 +++++------ .../__snapshots__/playout.test.ts.snap | 7 ++-- .../__snapshots__/timeline.test.ts.snap | 40 +++++++++---------- .../src/playout/__tests__/timeline.test.ts | 17 ++++---- .../model/implementation/PlayoutModelImpl.ts | 9 +++-- .../PlayoutPartInstanceModelImpl.ts | 28 ++++++++++--- .../model/implementation/SavePlayoutModel.ts | 16 ++++---- packages/job-worker/src/playout/setNext.ts | 5 ++- 9 files changed, 95 insertions(+), 87 deletions(-) diff --git a/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts b/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts index ce3a3cef2e..ae952edbb4 100644 --- a/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts +++ b/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts @@ -264,7 +264,7 @@ describe('Test blueprint api context', () => { partInstanceId: info.PartInstance._id, rundownId: info.PartInstance.rundownId, manuallySelected: false, - consumesNextSegmentId: false, + consumesQueuedSegmentId: false, } } else { return { @@ -1242,7 +1242,7 @@ describe('Test blueprint api context', () => { // Ensure there are no pending updates already for (const partInstance of cache.LoadedPartInstances) { - expect((partInstance as PlayoutPartInstanceModelImpl).HasChanges).toBeFalsy() + expect((partInstance as PlayoutPartInstanceModelImpl).HasAnyChanges()).toBeFalsy() } // Update it and expect it to match @@ -1289,15 +1289,10 @@ describe('Test blueprint api context', () => { }, } expect(pieceInstance1).toEqual(pieceInstance0After) - expect((partInstance1 as PlayoutPartInstanceModelImpl).HasChanges).toBeTruthy() - // expect( - // Array.from(cache.PieceInstances.documents.values()).filter((doc) => !doc || !!doc.updated) - // ).toMatchObject([ - // { - // updated: true, - // document: { _id: pieceInstance1._id }, - // }, - // ]) + expect((partInstance1 as PlayoutPartInstanceModelImpl).PartInstanceHasChanges).toBeFalsy() + expect((partInstance1 as PlayoutPartInstanceModelImpl).ChangedPieceInstanceIds()).toEqual([ + pieceInstance1._id, + ]) expect(context.nextPartState).toEqual(ActionPartChange.NONE) expect(context.currentPartState).toEqual(ActionPartChange.SAFE_CHANGE) @@ -1720,7 +1715,7 @@ describe('Test blueprint api context', () => { const { context } = await getActionExecutionContext(jobContext, cache) // Ensure there are no pending updates already - expect((cache.NextPartInstance! as PlayoutPartInstanceModelImpl).HasChanges).toBeFalsy() + expect((cache.NextPartInstance! as PlayoutPartInstanceModelImpl).HasAnyChanges()).toBeFalsy() // Update it and expect it to match const partInstance0Before = clone(partInstance0) @@ -1731,11 +1726,11 @@ describe('Test blueprint api context', () => { classes: ['123'], badProperty: 9, // This will be dropped } - const resultPiece = await context.updatePartInstance('next', partInstance0Delta) - const partInstance1 = cache.NextPartInstance! + const resultPart = await context.updatePartInstance('next', partInstance0Delta) + const partInstance1 = cache.NextPartInstance! as PlayoutPartInstanceModelImpl expect(partInstance1).toBeTruthy() - expect(resultPiece).toEqual(convertPartInstanceToBlueprints(partInstance1.PartInstance)) + expect(resultPart).toEqual(convertPartInstanceToBlueprints(partInstance1.PartInstance)) const pieceInstance0After = { ...partInstance0Before, @@ -1745,15 +1740,8 @@ describe('Test blueprint api context', () => { }, } expect(partInstance1.PartInstance).toEqual(pieceInstance0After) - expect((partInstance1 as PlayoutPartInstanceModelImpl).HasChanges).toBeTruthy() - // expect( - // Array.from(cache.PartInstances.documents.values()).filter((doc) => !doc || !!doc.updated) - // ).toMatchObject([ - // { - // updated: true, - // document: { _id: partInstance1._id }, - // }, - // ]) + expect(partInstance1.PartInstanceHasChanges).toBeTruthy() + expect(partInstance1.ChangedPieceInstanceIds()).toHaveLength(0) expect(context.nextPartState).toEqual(ActionPartChange.SAFE_CHANGE) expect(context.currentPartState).toEqual(ActionPartChange.NONE) diff --git a/packages/job-worker/src/cache/__tests__/DatabaseCaches.test.ts b/packages/job-worker/src/cache/__tests__/DatabaseCaches.test.ts index 9f558584bd..bcaa17afd6 100644 --- a/packages/job-worker/src/cache/__tests__/DatabaseCaches.test.ts +++ b/packages/job-worker/src/cache/__tests__/DatabaseCaches.test.ts @@ -292,18 +292,18 @@ describe('DatabaseCaches', () => { }).toThrow(/failed .+ assertion,.+ was modified/gi) } - { - const cache = await loadStudioPlayoutModel(context) - - // Insert a document: - cache.deferBeforeSave(() => { - // - }) - - expect(() => { - cache.assertNoChanges() - }).toThrow(/failed .+ assertion,.+ deferred/gi) - } + // { + // const cache = await loadStudioPlayoutModel(context) + + // // Insert a document: + // cache.deferBeforeSave(() => { + // // + // }) + + // expect(() => { + // cache.assertNoChanges() + // }).toThrow(/failed .+ assertion,.+ deferred/gi) + // } { const cache = await loadStudioPlayoutModel(context) diff --git a/packages/job-worker/src/playout/__tests__/__snapshots__/playout.test.ts.snap b/packages/job-worker/src/playout/__tests__/__snapshots__/playout.test.ts.snap index 48a2edb15c..0ca490d670 100644 --- a/packages/job-worker/src/playout/__tests__/__snapshots__/playout.test.ts.snap +++ b/packages/job-worker/src/playout/__tests__/__snapshots__/playout.test.ts.snap @@ -11,8 +11,8 @@ exports[`Playout API Basic rundown control 1`] = ` "core": "0.0.0-test", "studio": "asdf", }, - "timelineBlob": "[{"id":"playlist_randomId9000_status","objectType":"rundown","enable":{"while":1},"layer":"rundown_status","content":{"deviceType":"ABSTRACT"},"classes":["rundown_active"],"priority":0},{"id":"part_group_randomId9000_part0_0_randomId9002","objectType":"rundown","enable":{"start":"now"},"priority":5,"layer":"","content":{"deviceType":"ABSTRACT","type":"group"},"children":[],"isGroup":true,"metaData":{"isPieceTimeline":true}},{"id":"part_group_firstobject_randomId9000_part0_0_randomId9002","objectType":"rundown","enable":{"start":0},"layer":"group_first_object","content":{"deviceType":"ABSTRACT","type":"callback","callBack":"partPlaybackStarted","callBackData":{"rundownPlaylistId":"playlist_randomId9000","partInstanceId":"randomId9000_part0_0_randomId9002"},"callBackStopped":"partPlaybackStopped"},"inGroup":"part_group_randomId9000_part0_0_randomId9002","classes":[],"priority":0},{"id":"piece_group_control_randomId9000_part0_0_randomId9002_randomId9000_piece001","objectType":"rundown","enable":{"start":0},"layer":"vt0","priority":5,"content":{"deviceType":"ABSTRACT","type":"callback","callBack":"piecePlaybackStarted","callBackData":{"rundownPlaylistId":"playlist_randomId9000","partInstanceId":"randomId9000_part0_0_randomId9002","pieceInstanceId":"randomId9000_part0_0_randomId9002_randomId9000_piece001","dynamicallyInserted":false},"callBackStopped":"piecePlaybackStopped"},"classes":["current_part"],"inGroup":"part_group_randomId9000_part0_0_randomId9002","metaData":{"isPieceTimeline":true,"triggerPieceInstanceId":"randomId9000_part0_0_randomId9002_randomId9000_piece001"}},{"id":"piece_group_randomId9000_part0_0_randomId9002_randomId9000_piece001","content":{"deviceType":"ABSTRACT","type":"group"},"children":[],"inGroup":"part_group_randomId9000_part0_0_randomId9002","isGroup":true,"objectType":"rundown","enable":{"start":"#piece_group_control_randomId9000_part0_0_randomId9002_randomId9000_piece001.start - 0","end":"#piece_group_control_randomId9000_part0_0_randomId9002_randomId9000_piece001.end + 0"},"layer":"","metaData":{"isPieceTimeline":true,"pieceInstanceGroupId":"randomId9000_part0_0_randomId9002_randomId9000_piece001"},"priority":0}]", - "timelineHash": "randomId9006", + "timelineBlob": "[{"id":"playlist_randomId9000_status","objectType":"rundown","enable":{"while":1},"layer":"rundown_status","content":{"deviceType":"ABSTRACT"},"classes":["rundown_active"],"priority":0},{"id":"part_group_randomId9000_part0_0_randomId9003","objectType":"rundown","enable":{"start":"now"},"priority":5,"layer":"","content":{"deviceType":"ABSTRACT","type":"group"},"children":[],"isGroup":true,"metaData":{"isPieceTimeline":true}},{"id":"part_group_firstobject_randomId9000_part0_0_randomId9003","objectType":"rundown","enable":{"start":0},"layer":"group_first_object","content":{"deviceType":"ABSTRACT","type":"callback","callBack":"partPlaybackStarted","callBackData":{"rundownPlaylistId":"playlist_randomId9000","partInstanceId":"randomId9000_part0_0_randomId9003"},"callBackStopped":"partPlaybackStopped"},"inGroup":"part_group_randomId9000_part0_0_randomId9003","classes":[],"priority":0},{"id":"piece_group_control_randomId9000_part0_0_randomId9003_randomId9000_piece001","objectType":"rundown","enable":{"start":0},"layer":"vt0","priority":5,"content":{"deviceType":"ABSTRACT","type":"callback","callBack":"piecePlaybackStarted","callBackData":{"rundownPlaylistId":"playlist_randomId9000","partInstanceId":"randomId9000_part0_0_randomId9003","pieceInstanceId":"randomId9000_part0_0_randomId9003_randomId9000_piece001","dynamicallyInserted":false},"callBackStopped":"piecePlaybackStopped"},"classes":["current_part"],"inGroup":"part_group_randomId9000_part0_0_randomId9003","metaData":{"isPieceTimeline":true,"triggerPieceInstanceId":"randomId9000_part0_0_randomId9003_randomId9000_piece001"}},{"id":"piece_group_randomId9000_part0_0_randomId9003_randomId9000_piece001","content":{"deviceType":"ABSTRACT","type":"group"},"children":[],"inGroup":"part_group_randomId9000_part0_0_randomId9003","isGroup":true,"objectType":"rundown","enable":{"start":"#piece_group_control_randomId9000_part0_0_randomId9003_randomId9000_piece001.start - 0","end":"#piece_group_control_randomId9000_part0_0_randomId9003_randomId9000_piece001.end + 0"},"layer":"","metaData":{"isPieceTimeline":true,"pieceInstanceGroupId":"randomId9000_part0_0_randomId9003_randomId9000_piece001"},"priority":0}]", + "timelineHash": "randomId9008", }, ] `; @@ -55,7 +55,7 @@ exports[`Playout API Basic rundown control 3`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9007", + "timelineHash": "randomId9010", }, ] `; @@ -67,7 +67,6 @@ exports[`Playout API Basic rundown control 4`] = ` "currentPartInfo": null, "externalId": "MOCK_RUNDOWNPLAYLIST", "holdState": 0, - "lastTakeTime": 0, "modified": 0, "name": "Default RundownPlaylist", "nextPartInfo": null, diff --git a/packages/job-worker/src/playout/__tests__/__snapshots__/timeline.test.ts.snap b/packages/job-worker/src/playout/__tests__/__snapshots__/timeline.test.ts.snap index 633dd16968..ef48638e9a 100644 --- a/packages/job-worker/src/playout/__tests__/__snapshots__/timeline.test.ts.snap +++ b/packages/job-worker/src/playout/__tests__/__snapshots__/timeline.test.ts.snap @@ -12,7 +12,7 @@ exports[`Timeline Adlib pieces Current part with preroll 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9013", + "timelineHash": "randomId9022", }, ] `; @@ -29,7 +29,7 @@ exports[`Timeline Adlib pieces Current part with preroll and adlib preroll 1`] = "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9013", + "timelineHash": "randomId9022", }, ] `; @@ -45,8 +45,8 @@ exports[`Timeline Basic rundown 1`] = ` "core": "0.0.0-test", "studio": "asdf", }, - "timelineBlob": "[{"id":"playlist_randomId9000_status","objectType":"rundown","enable":{"while":1},"layer":"rundown_status","content":{"deviceType":"ABSTRACT"},"classes":["rundown_active"],"priority":0},{"id":"part_group_randomId9000_part0_0_randomId9004","objectType":"rundown","enable":{"start":"now"},"priority":5,"layer":"","content":{"deviceType":"ABSTRACT","type":"group"},"children":[],"isGroup":true,"metaData":{"isPieceTimeline":true}},{"id":"part_group_firstobject_randomId9000_part0_0_randomId9004","objectType":"rundown","enable":{"start":0},"layer":"group_first_object","content":{"deviceType":"ABSTRACT","type":"callback","callBack":"partPlaybackStarted","callBackData":{"rundownPlaylistId":"playlist_randomId9000","partInstanceId":"randomId9000_part0_0_randomId9004"},"callBackStopped":"partPlaybackStopped"},"inGroup":"part_group_randomId9000_part0_0_randomId9004","classes":[],"priority":0},{"id":"piece_group_control_randomId9002_randomId9000_piece001","objectType":"rundown","enable":{"start":0},"layer":"vt0","priority":5,"content":{"deviceType":"ABSTRACT","type":"callback","callBack":"piecePlaybackStarted","callBackData":{"rundownPlaylistId":"playlist_randomId9000","partInstanceId":"randomId9000_part0_0_randomId9004","pieceInstanceId":"randomId9002_randomId9000_piece001","dynamicallyInserted":false},"callBackStopped":"piecePlaybackStopped"},"classes":["current_part"],"inGroup":"part_group_randomId9000_part0_0_randomId9004","metaData":{"isPieceTimeline":true,"triggerPieceInstanceId":"randomId9002_randomId9000_piece001"}},{"id":"piece_group_randomId9002_randomId9000_piece001","content":{"deviceType":"ABSTRACT","type":"group"},"children":[],"inGroup":"part_group_randomId9000_part0_0_randomId9004","isGroup":true,"objectType":"rundown","enable":{"start":"#piece_group_control_randomId9002_randomId9000_piece001.start - 0","end":"#piece_group_control_randomId9002_randomId9000_piece001.end + 0"},"layer":"","metaData":{"isPieceTimeline":true,"pieceInstanceGroupId":"randomId9002_randomId9000_piece001"},"priority":0}]", - "timelineHash": "randomId9012", + "timelineBlob": "[{"id":"playlist_randomId9000_status","objectType":"rundown","enable":{"while":1},"layer":"rundown_status","content":{"deviceType":"ABSTRACT"},"classes":["rundown_active"],"priority":0},{"id":"part_group_randomId9000_part0_0_randomId9003","objectType":"rundown","enable":{"start":"now"},"priority":5,"layer":"","content":{"deviceType":"ABSTRACT","type":"group"},"children":[],"isGroup":true,"metaData":{"isPieceTimeline":true}},{"id":"part_group_firstobject_randomId9000_part0_0_randomId9003","objectType":"rundown","enable":{"start":0},"layer":"group_first_object","content":{"deviceType":"ABSTRACT","type":"callback","callBack":"partPlaybackStarted","callBackData":{"rundownPlaylistId":"playlist_randomId9000","partInstanceId":"randomId9000_part0_0_randomId9003"},"callBackStopped":"partPlaybackStopped"},"inGroup":"part_group_randomId9000_part0_0_randomId9003","classes":[],"priority":0},{"id":"piece_group_control_randomId9000_part0_0_randomId9003_randomId9000_piece001","objectType":"rundown","enable":{"start":0},"layer":"vt0","priority":5,"content":{"deviceType":"ABSTRACT","type":"callback","callBack":"piecePlaybackStarted","callBackData":{"rundownPlaylistId":"playlist_randomId9000","partInstanceId":"randomId9000_part0_0_randomId9003","pieceInstanceId":"randomId9000_part0_0_randomId9003_randomId9000_piece001","dynamicallyInserted":false},"callBackStopped":"piecePlaybackStopped"},"classes":["current_part"],"inGroup":"part_group_randomId9000_part0_0_randomId9003","metaData":{"isPieceTimeline":true,"triggerPieceInstanceId":"randomId9000_part0_0_randomId9003_randomId9000_piece001"}},{"id":"piece_group_randomId9000_part0_0_randomId9003_randomId9000_piece001","content":{"deviceType":"ABSTRACT","type":"group"},"children":[],"inGroup":"part_group_randomId9000_part0_0_randomId9003","isGroup":true,"objectType":"rundown","enable":{"start":"#piece_group_control_randomId9000_part0_0_randomId9003_randomId9000_piece001.start - 0","end":"#piece_group_control_randomId9000_part0_0_randomId9003_randomId9000_piece001.end + 0"},"layer":"","metaData":{"isPieceTimeline":true,"pieceInstanceGroupId":"randomId9000_part0_0_randomId9003_randomId9000_piece001"},"priority":0}]", + "timelineHash": "randomId9010", }, ] `; @@ -63,7 +63,7 @@ exports[`Timeline Basic rundown 2`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9014", + "timelineHash": "randomId9012", }, ] `; @@ -80,7 +80,7 @@ exports[`Timeline In transitions Basic inTransition 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9016", + "timelineHash": "randomId9014", }, ] `; @@ -97,7 +97,7 @@ exports[`Timeline In transitions Basic inTransition with contentDelay + preroll "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9016", + "timelineHash": "randomId9014", }, ] `; @@ -114,7 +114,7 @@ exports[`Timeline In transitions Basic inTransition with contentDelay 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9016", + "timelineHash": "randomId9014", }, ] `; @@ -131,7 +131,7 @@ exports[`Timeline In transitions Basic inTransition with planned pieces 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9016", + "timelineHash": "randomId9014", }, ] `; @@ -148,7 +148,7 @@ exports[`Timeline In transitions Preroll 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9016", + "timelineHash": "randomId9014", }, ] `; @@ -165,7 +165,7 @@ exports[`Timeline In transitions inTransition disabled 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9016", + "timelineHash": "randomId9014", }, ] `; @@ -182,7 +182,7 @@ exports[`Timeline In transitions inTransition is disabled during hold 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9018", + "timelineHash": "randomId9016", }, ] `; @@ -199,7 +199,7 @@ exports[`Timeline In transitions inTransition with existing infinites 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9017", + "timelineHash": "randomId9015", }, ] `; @@ -216,7 +216,7 @@ exports[`Timeline In transitions inTransition with new infinite 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9017", + "timelineHash": "randomId9015", }, ] `; @@ -233,7 +233,7 @@ exports[`Timeline Infinite Pieces Infinite Piece has stable timing across timeli "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9017", + "timelineHash": "randomId9016", }, ] `; @@ -250,7 +250,7 @@ exports[`Timeline Out transitions Basic outTransition 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9016", + "timelineHash": "randomId9014", }, ] `; @@ -267,7 +267,7 @@ exports[`Timeline Out transitions outTransition + inTransition 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9016", + "timelineHash": "randomId9014", }, ] `; @@ -284,7 +284,7 @@ exports[`Timeline Out transitions outTransition + preroll (2) 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9016", + "timelineHash": "randomId9014", }, ] `; @@ -301,7 +301,7 @@ exports[`Timeline Out transitions outTransition + preroll 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9016", + "timelineHash": "randomId9014", }, ] `; @@ -318,7 +318,7 @@ exports[`Timeline Out transitions outTransition is disabled during hold 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9018", + "timelineHash": "randomId9016", }, ] `; diff --git a/packages/job-worker/src/playout/__tests__/timeline.test.ts b/packages/job-worker/src/playout/__tests__/timeline.test.ts index 013c870568..6fff275e52 100644 --- a/packages/job-worker/src/playout/__tests__/timeline.test.ts +++ b/packages/job-worker/src/playout/__tests__/timeline.test.ts @@ -1129,12 +1129,11 @@ describe('Timeline', () => { }) describe('Adlib pieces', () => { - async function doStartAdlibPiece( - playlistId: RundownPlaylistId, - currentPartInstance: PlayoutPartInstanceModel, - adlibSource: AdLibPiece - ) { + async function doStartAdlibPiece(playlistId: RundownPlaylistId, adlibSource: AdLibPiece) { await runJobWithPlayoutCache(context, { playlistId }, null, async (cache) => { + const currentPartInstance = cache.CurrentPartInstance as PlayoutPartInstanceModel + expect(currentPartInstance).toBeTruthy() + const rundown = cache.getRundown(currentPartInstance.PartInstance.rundownId) as PlayoutRundownModel expect(rundown).toBeTruthy() @@ -1214,10 +1213,11 @@ describe('Timeline', () => { const { currentPartInstance } = await getPartInstances() expect(currentPartInstance).toBeTruthy() + console.log('inst', currentPartInstance?.PartInstance._id) + // Insert an adlib piece await doStartAdlibPiece( playlistId, - currentPartInstance!, literal({ _id: protectString('adlib1'), rundownId: currentPartInstance!.PartInstance.rundownId, @@ -1232,7 +1232,7 @@ describe('Timeline', () => { }) ) - const adlibbedPieceId = 'randomId9007' + const adlibbedPieceId = 'randomId9010' // The adlib should be starting at 'now' await checkTimings({ @@ -1383,7 +1383,6 @@ describe('Timeline', () => { // Insert an adlib piece await doStartAdlibPiece( playlistId, - currentPartInstance!, literal({ _id: protectString('adlib1'), rundownId: currentPartInstance!.PartInstance.rundownId, @@ -1399,7 +1398,7 @@ describe('Timeline', () => { }) ) - const adlibbedPieceId = 'randomId9007' + const adlibbedPieceId = 'randomId9010' // The adlib should be starting at 'now' await checkTimings({ diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts index 25a82c5287..1535d70caf 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts @@ -20,7 +20,7 @@ import { ReadonlyDeep } from 'type-fest' import { JobContext } from '../../../jobs' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' -import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { getPieceInstanceIdForPiece, PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { serializeTimelineBlob, TimelineComplete, @@ -263,6 +263,7 @@ export class PlayoutModelImpl implements PlayoutModel { for (const pieceInstance of pieceInstances) { // TODO - should these be PieceInstance already, or should that be handled here? + pieceInstance._id = getPieceInstanceIdForPiece(newPartInstance._id, pieceInstance.piece._id) pieceInstance.partInstanceId = newPartInstance._id } @@ -448,7 +449,7 @@ export class PlayoutModelImpl implements PlayoutModel { this.#Playlist.holdState = RundownHoldState.NONE delete this.#Playlist.lastTakeTime - delete this.#Playlist.nextSegmentId + delete this.#Playlist.queuedSegmentId this.#PlaylistHasChanged = true } @@ -492,7 +493,7 @@ export class PlayoutModelImpl implements PlayoutModel { delete this.#Playlist.rundownsStartedPlayback delete this.#Playlist.previousPersistentState delete this.#Playlist.trackedAbSessions - delete this.#Playlist.nextSegmentId + delete this.#Playlist.queuedSegmentId if (regenerateActivationId) this.#Playlist.activationId = getRandomId() @@ -571,7 +572,7 @@ export class PlayoutModelImpl implements PlayoutModel { if ( Array.from(this.#AllPartInstances.values()).find( - (part) => !part || part.PartInstanceHasChanges || part.AnyPieceInstanceHasChanges() + (part) => !part || part.PartInstanceHasChanges || part.ChangedPieceInstanceIds().length > 0 ) ) logOrThrowError(new Error(`Failed no changes in cache assertion, a PartInstance has been changed`)) diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts index 8017fadfbd..8ee08d194d 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts @@ -43,11 +43,15 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { get PartInstanceHasChanges(): boolean { return this.#PartInstanceHasChanges } - AnyPieceInstanceHasChanges(): boolean { - for (const pieceInstance of this.PieceInstancesImpl.values()) { - if (pieceInstance.changed || !pieceInstance.doc) return true + ChangedPieceInstanceIds(): PieceInstanceId[] { + const result: PieceInstanceId[] = [] + for (const [id, pieceInstance] of this.PieceInstancesImpl.entries()) { + if (pieceInstance.changed || !pieceInstance.doc) result.push(id) } - return false + return result + } + HasAnyChanges(): boolean { + return this.#PartInstanceHasChanges || this.ChangedPieceInstanceIds().length > 0 } clearChangedFlags(): void { this.#PartInstanceHasChanges = false @@ -82,7 +86,7 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { for (const pieceInstance of pieceInstances) { this.PieceInstancesImpl.set(pieceInstance._id, { doc: pieceInstance, - changed: false, + changed: hasChanges, }) } } @@ -107,6 +111,7 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { for (const pieceInstance of this.PieceInstancesImpl.values()) { if (!pieceInstance.doc) continue + pieceInstance.changed = true pieceInstance.doc.playlistActivationId = id } } @@ -180,6 +185,7 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { for (const pieceInstance of this.PieceInstancesImpl.values()) { if (!pieceInstance.doc) continue + pieceInstance.changed = true pieceInstance.doc.reset = true } @@ -265,6 +271,7 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { const pieceInstance = this.PieceInstancesImpl.get(id) if (!pieceInstance?.doc) throw new Error('Bad pieceinstance') + pieceInstance.changed = true pieceInstance.doc.piece = { ...pieceInstance.doc.piece, ...props, @@ -390,6 +397,8 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { pieceInstance.doc.plannedStartedPlayback = time delete pieceInstance.doc.plannedStoppedPlayback + pieceInstance.changed = true + return true } return false @@ -401,6 +410,8 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { if (pieceInstance.doc.plannedStoppedPlayback !== time) { pieceInstance.doc.plannedStoppedPlayback = time + pieceInstance.changed = true + return true } return false @@ -413,6 +424,8 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { pieceInstance.doc.reportedStartedPlayback = time delete pieceInstance.doc.reportedStoppedPlayback + pieceInstance.changed = true + return true } return false @@ -424,6 +437,8 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { if (pieceInstance.doc.reportedStoppedPlayback !== time) { pieceInstance.doc.reportedStoppedPlayback = time + pieceInstance.changed = true + return true } return false @@ -453,6 +468,7 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { infinitePieceId: pieceInstance.doc.piece._id, fromPreviousPart: false, } + pieceInstance.changed = true return infiniteInstanceId } @@ -502,6 +518,7 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { const pieceInstance = this.PieceInstancesImpl.get(pieceInstanceId) if (!pieceInstance?.doc) throw new Error(`PieceInstance ${pieceInstanceId} not found`) // TODO - is this ok? + pieceInstance.changed = true pieceInstance.doc.userDuration = duration } @@ -549,6 +566,7 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { const pieceInstance = this.PieceInstancesImpl.get(pieceInstanceId) if (!pieceInstance?.doc) throw new Error(`PieceInstance ${pieceInstanceId} not found`) // TODO - is this ok? + pieceInstance.changed = true pieceInstance.doc.disabled = disabled } } diff --git a/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts b/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts index 27d0b90526..434a859dd9 100644 --- a/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts +++ b/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts @@ -61,13 +61,15 @@ export function writePartInstancesAndPieceInstances( if (!partInstance) { deletedPartInstanceIds.push(partInstanceId) } else { - partInstanceOps.push({ - replaceOne: { - filter: { _id: partInstanceId }, - replacement: partInstance.PartInstanceImpl, - upsert: true, - }, - }) + if (partInstance.PartInstanceHasChanges) { + partInstanceOps.push({ + replaceOne: { + filter: { _id: partInstanceId }, + replacement: partInstance.PartInstanceImpl, + upsert: true, + }, + }) + } for (const [pieceInstanceId, pieceInstance] of partInstance.PieceInstancesImpl.entries()) { if (!pieceInstance.doc) { diff --git a/packages/job-worker/src/playout/setNext.ts b/packages/job-worker/src/playout/setNext.ts index 1842d9c1bc..cac6520617 100644 --- a/packages/job-worker/src/playout/setNext.ts +++ b/packages/job-worker/src/playout/setNext.ts @@ -1,4 +1,4 @@ -import { assertNever, getRandomId } from '@sofie-automation/corelib/dist/lib' +import { assertNever } from '@sofie-automation/corelib/dist/lib' import { SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart, isPartPlayable } from '@sofie-automation/corelib/dist/dataModel/Part' import { JobContext } from '../jobs' @@ -20,6 +20,7 @@ import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/erro import { SelectNextPartResult } from './selectNextPart' import { ReadonlyDeep } from 'type-fest' import { QueueNextSegmentResult } from '@sofie-automation/corelib/dist/worker/studio' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' /** * Set or clear the nexted part, from a given PartInstance, or SelectNextPartResult @@ -147,7 +148,7 @@ async function preparePartInstanceForPartBeingNexted( rundown, nextPart, possiblePieces, - getRandomId() // Replaced inside cache.createInstanceForPart + protectString('') // Replaced inside cache.createInstanceForPart ) return cache.createInstanceForPart(nextPart, newPieceInstances) From 9e07ed9bb8851f84364753173a7053102cc0dac5 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 10 Oct 2023 17:51:56 +0100 Subject: [PATCH 120/479] fix: extract `PlayoutPieceInstanceModel` from `PlayoutPartInstanceModel` --- .../__tests__/context-adlibActions.test.ts | 47 +-- .../SyncIngestUpdateToPartInstanceContext.ts | 33 +-- .../src/blueprints/context/adlibActions.ts | 24 +- .../src/ingest/syncChangesToPartInstance.ts | 6 +- .../src/playout/__tests__/timeline.test.ts | 31 +- packages/job-worker/src/playout/adlibJobs.ts | 51 ++-- packages/job-worker/src/playout/adlibUtils.ts | 28 +- packages/job-worker/src/playout/holdJobs.ts | 6 +- packages/job-worker/src/playout/infinites.ts | 4 +- .../src/playout/model/PlayoutModel.ts | 3 +- .../playout/model/PlayoutPartInstanceModel.ts | 38 +-- .../model/PlayoutPieceInstanceModel.ts | 21 ++ .../model/implementation/PlayoutModelImpl.ts | 3 +- .../PlayoutPartInstanceModelImpl.ts | 270 ++++++------------ .../PlayoutPieceInstanceModelImpl.ts | 113 ++++++++ .../model/implementation/SavePlayoutModel.ts | 6 +- packages/job-worker/src/playout/pieces.ts | 9 +- .../job-worker/src/playout/resolvedPieces.ts | 6 +- packages/job-worker/src/playout/take.ts | 21 +- .../src/playout/timeline/generate.ts | 6 +- .../src/playout/timeline/multi-gateway.ts | 80 +++--- .../src/playout/timings/piecePlayback.ts | 40 +-- 22 files changed, 448 insertions(+), 398 deletions(-) create mode 100644 packages/job-worker/src/playout/model/PlayoutPieceInstanceModel.ts create mode 100644 packages/job-worker/src/playout/model/implementation/PlayoutPieceInstanceModelImpl.ts diff --git a/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts b/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts index ae952edbb4..22c5614f5c 100644 --- a/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts +++ b/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts @@ -36,6 +36,7 @@ import { convertPartInstanceToBlueprints, convertPieceInstanceToBlueprints } fro import { TimelineObjRundown, TimelineObjType } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { PlayoutPartInstanceModelImpl } from '../../playout/model/implementation/PlayoutPartInstanceModelImpl' import { writePartInstancesAndPieceInstances } from '../../playout/model/implementation/SavePlayoutModel' +import { PlayoutPieceInstanceModel } from '../../playout/model/PlayoutPieceInstanceModel' import * as PlayoutAdlib from '../../playout/adlibUtils' type TinnerStopPieces = jest.MockedFunction @@ -562,12 +563,12 @@ describe('Test blueprint api context', () => { }, undefined ) - allPartInstances[0].setPieceInstancedPlannedStartedPlayback(insertedPieceInstance._id, 1000) + insertedPieceInstance.setPlannedStartedPlayback(1000) // We need to push changes back to 'mongo' for these tests await saveAllToDatabase(jobContext, cache, allPartInstances) await expect(context.findLastPieceOnLayer(sourceLayerIds[0])).resolves.toMatchObject({ - _id: insertedPieceInstance._id, + _id: insertedPieceInstance.PieceInstance._id, }) await expect( context.findLastPieceOnLayer(sourceLayerIds[0], { originalOnly: true }) @@ -590,13 +591,13 @@ describe('Test blueprint api context', () => { }, undefined ) - allPartInstances[0].setPieceInstancedPlannedStartedPlayback(insertedPieceInstance2._id, 2000) + insertedPieceInstance2.setPlannedStartedPlayback(2000) // We need to push changes back to 'mongo' for these tests await saveAllToDatabase(jobContext, cache, allPartInstances) await expect(context.findLastPieceOnLayer(sourceLayerIds[0])).resolves.toMatchObject({ - _id: insertedPieceInstance2._id, + _id: insertedPieceInstance2.PieceInstance._id, }) await expect( context.findLastPieceOnLayer(sourceLayerIds[0], { originalOnly: true }) @@ -643,7 +644,7 @@ describe('Test blueprint api context', () => { }, undefined ) - allPartInstances[0].setPieceInstancedPlannedStartedPlayback(insertedPieceInstance._id, 1000) + insertedPieceInstance.setPlannedStartedPlayback(1000) const insertedPieceInstance2 = allPartInstances[2].insertAdlibbedPiece( { _id: getRandomId(), @@ -660,18 +661,18 @@ describe('Test blueprint api context', () => { }, undefined ) - allPartInstances[2].setPieceInstancedPlannedStartedPlayback(insertedPieceInstance2._id, 2000) + insertedPieceInstance2.setPlannedStartedPlayback(2000) // We need to push changes back to 'mongo' for these tests await saveAllToDatabase(jobContext, cache, allPartInstances) // Check it await expect(context.findLastPieceOnLayer(sourceLayerIds[0])).resolves.toMatchObject({ - _id: insertedPieceInstance2._id, + _id: insertedPieceInstance2.PieceInstance._id, }) await expect( context.findLastPieceOnLayer(sourceLayerIds[0], { excludeCurrentPart: true }) ).resolves.toMatchObject({ - _id: insertedPieceInstance._id, + _id: insertedPieceInstance.PieceInstance._id, }) }) }) @@ -715,7 +716,7 @@ describe('Test blueprint api context', () => { }, undefined ) - allPartInstances[0].setPieceInstancedPlannedStartedPlayback(insertedPieceInstance._id, 1000) + insertedPieceInstance.setPlannedStartedPlayback(1000) const insertedPieceInstance2 = allPartInstances[2].insertAdlibbedPiece( { _id: getRandomId(), @@ -736,27 +737,27 @@ describe('Test blueprint api context', () => { }, undefined ) - allPartInstances[2].setPieceInstancedPlannedStartedPlayback(insertedPieceInstance2._id, 2000) + insertedPieceInstance2.setPlannedStartedPlayback(2000) // We need to push changes back to 'mongo' for these tests await saveAllToDatabase(jobContext, cache, allPartInstances) // Check it await expect(context.findLastPieceOnLayer(sourceLayerIds[0])).resolves.toMatchObject({ - _id: insertedPieceInstance2._id, + _id: insertedPieceInstance2.PieceInstance._id, }) await expect( context.findLastPieceOnLayer(sourceLayerIds[0], { pieceMetaDataFilter: {} }) ).resolves.toMatchObject({ - _id: insertedPieceInstance2._id, + _id: insertedPieceInstance2.PieceInstance._id, }) await expect( context.findLastPieceOnLayer(sourceLayerIds[0], { pieceMetaDataFilter: { prop1: 'hello' } }) - ).resolves.toMatchObject({ _id: insertedPieceInstance2._id }) + ).resolves.toMatchObject({ _id: insertedPieceInstance2.PieceInstance._id }) await expect( context.findLastPieceOnLayer(sourceLayerIds[0], { pieceMetaDataFilter: { prop1: { $ne: 'hello' } }, }) - ).resolves.toMatchObject({ _id: insertedPieceInstance._id }) + ).resolves.toMatchObject({ _id: insertedPieceInstance.PieceInstance._id }) }) }) }) @@ -1152,10 +1153,10 @@ describe('Test blueprint api context', () => { // check some properties not exposed to the blueprints const newPieceInstance = cache.findPieceInstance(protectString(newPieceInstanceId)) - ?.pieceInstance as PieceInstance + ?.pieceInstance as PlayoutPieceInstanceModel expect(newPieceInstance).toBeTruthy() - expect(newPieceInstance.dynamicallyInserted).toBeTruthy() - expect(newPieceInstance.partInstanceId).toEqual(partInstance.PartInstance._id) + expect(newPieceInstance.PieceInstance.dynamicallyInserted).toBeTruthy() + expect(newPieceInstance.PieceInstance.partInstanceId).toEqual(partInstance.PartInstance._id) }) }) }) @@ -1274,7 +1275,7 @@ describe('Test blueprint api context', () => { )! expect(pieceInstance1).toBeTruthy() - expect(resultPiece).toEqual(convertPieceInstanceToBlueprints(pieceInstance1)) + expect(resultPiece).toEqual(convertPieceInstanceToBlueprints(pieceInstance1.PieceInstance)) const pieceInstance0After = { ...pieceInstance0Before, piece: { @@ -1288,10 +1289,10 @@ describe('Test blueprint api context', () => { ), }, } - expect(pieceInstance1).toEqual(pieceInstance0After) + expect(pieceInstance1.PieceInstance).toEqual(pieceInstance0After) expect((partInstance1 as PlayoutPartInstanceModelImpl).PartInstanceHasChanges).toBeFalsy() expect((partInstance1 as PlayoutPartInstanceModelImpl).ChangedPieceInstanceIds()).toEqual([ - pieceInstance1._id, + pieceInstance1.PieceInstance._id, ]) expect(context.nextPartState).toEqual(ActionPartChange.NONE) @@ -1653,11 +1654,11 @@ describe('Test blueprint api context', () => { expect(targetPieceInstance).toBeTruthy() await expect( - context.removePieceInstances('next', [unprotectString(targetPieceInstance._id)]) - ).resolves.toEqual([unprotectString(targetPieceInstance._id)]) + context.removePieceInstances('next', [unprotectString(targetPieceInstance.PieceInstance._id)]) + ).resolves.toEqual([unprotectString(targetPieceInstance.PieceInstance._id)]) // Ensure it was all removed - expect(cache.findPieceInstance(targetPieceInstance._id)).toBeFalsy() + expect(cache.findPieceInstance(targetPieceInstance.PieceInstance._id)).toBeFalsy() expect(context.nextPartState).toEqual(ActionPartChange.SAFE_CHANGE) }) }) diff --git a/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts b/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts index 4f135c607f..371f627b3d 100644 --- a/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts +++ b/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts @@ -1,6 +1,6 @@ import { PieceInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { PieceInstance, PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { clone, normalizeArrayToMap, omit } from '@sofie-automation/corelib/dist/lib' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { normalizeArrayToMap, omit } from '@sofie-automation/corelib/dist/lib' import { protectString, protectStringArray, unprotectStringArray } from '@sofie-automation/corelib/dist/protectedString' import { PlayoutPartInstanceModel } from '../../playout/model/PlayoutPartInstanceModel' import { ReadonlyDeep } from 'type-fest' @@ -95,11 +95,9 @@ export class SyncIngestUpdateToPartInstanceContext )[0] : proposedPieceInstance.piece - const existingPieceInstance = this.partInstance.getPieceInstance(proposedPieceInstance._id) - const newPieceInstance: PieceInstance = { - ...existingPieceInstance, + const newPieceInstance: ReadonlyDeep = { ...proposedPieceInstance, - piece: clone(piece), + piece: piece, } this.partInstance.replacePieceInstance(newPieceInstance) @@ -123,7 +121,7 @@ export class SyncIngestUpdateToPartInstanceContext const newPieceInstance = this.partInstance.insertPlannedPiece(piece) - return convertPieceInstanceToBlueprints(newPieceInstance) + return convertPieceInstanceToBlueprints(newPieceInstance.PieceInstance) } updatePieceInstance(pieceInstanceId: string, updatedPiece: Partial): IBlueprintPieceInstance { // filter the submission to the allowed ones @@ -138,7 +136,7 @@ export class SyncIngestUpdateToPartInstanceContext if (!pieceInstance) { throw new Error(`PieceInstance "${pieceInstanceId}" could not be found`) } - if (pieceInstance.partInstanceId !== this.partInstance.PartInstance._id) { + if (pieceInstance.PieceInstance.partInstanceId !== this.partInstance.PartInstance._id) { throw new Error(`PieceInstance "${pieceInstanceId}" does not belong to the current PartInstance`) } @@ -146,7 +144,7 @@ export class SyncIngestUpdateToPartInstanceContext if (trimmedPiece.content?.timelineObjects) { timelineObjectsString = serializePieceTimelineObjectsBlob( postProcessTimelineObjects( - pieceInstance.piece._id, + pieceInstance.PieceInstance.piece._id, this.showStyleCompound.blueprintId, trimmedPiece.content.timelineObjects ) @@ -155,19 +153,14 @@ export class SyncIngestUpdateToPartInstanceContext trimmedPiece.content = omit(trimmedPiece.content, 'timelineObjects') as WithTimeline } - this.partInstance.updatePieceProps(pieceInstance._id, trimmedPiece as any) // TODO: this needs to be more type safe + pieceInstance.updatePieceProps(trimmedPiece as any) // TODO: this needs to be more type safe if (timelineObjectsString !== undefined) { - this.partInstance.updatePieceProps(pieceInstance._id, { + pieceInstance.updatePieceProps({ timelineObjectsString, }) } - const updatedPieceInstance = this.partInstance.getPieceInstance(pieceInstance._id) - if (!updatedPieceInstance) { - throw new Error(`PieceInstance "${pieceInstanceId}" could not be found, after applying changes`) - } - - return convertPieceInstanceToBlueprints(updatedPieceInstance) + return convertPieceInstanceToBlueprints(pieceInstance.PieceInstance) } updatePartInstance(updatePart: Partial): IBlueprintPartInstance { // filter the submission to the allowed ones @@ -204,9 +197,11 @@ export class SyncIngestUpdateToPartInstanceContext if (!this.partInstance) throw new Error(`PartInstance has been removed`) const rawPieceInstanceIdSet = new Set(protectStringArray(pieceInstanceIds)) - const pieceInstances = this.partInstance.PieceInstances.filter((p) => rawPieceInstanceIdSet.has(p._id)) + const pieceInstances = this.partInstance.PieceInstances.filter((p) => + rawPieceInstanceIdSet.has(p.PieceInstance._id) + ) - const pieceInstanceIdsToRemove = pieceInstances.map((p) => p._id) + const pieceInstanceIdsToRemove = pieceInstances.map((p) => p.PieceInstance._id) for (const id of pieceInstanceIdsToRemove) { this.partInstance.removePieceInstance(id) diff --git a/packages/job-worker/src/blueprints/context/adlibActions.ts b/packages/job-worker/src/blueprints/context/adlibActions.ts index 0cb61337cd..503a63308c 100644 --- a/packages/job-worker/src/blueprints/context/adlibActions.ts +++ b/packages/job-worker/src/blueprints/context/adlibActions.ts @@ -168,7 +168,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct } async getPieceInstances(part: 'current' | 'next'): Promise { const partInstance = this._getPartInstance(part) - return partInstance?.PieceInstances?.map(convertPieceInstanceToBlueprints) ?? [] + return partInstance?.PieceInstances?.map((p) => convertPieceInstanceToBlueprints(p.PieceInstance)) ?? [] } async getResolvedPieceInstances(part: 'current' | 'next'): Promise { const partInstance = this._getPartInstance(part) @@ -320,7 +320,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct this.nextPartState = Math.max(this.nextPartState, ActionPartChange.SAFE_CHANGE) } - return convertPieceInstanceToBlueprints(newPieceInstance) + return convertPieceInstanceToBlueprints(newPieceInstance.PieceInstance) } async updatePieceInstance( pieceInstanceId: string, @@ -337,18 +337,18 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct throw new Error('PieceInstance could not be found') } - const { partInstance, pieceInstance } = foundPieceInstance + const { pieceInstance } = foundPieceInstance - if (pieceInstance.infinite?.fromPreviousPart) { + if (pieceInstance.PieceInstance.infinite?.fromPreviousPart) { throw new Error('Cannot update an infinite piece that is continued from a previous part') } const updatesCurrentPart: ActionPartChange = - pieceInstance.partInstanceId === this._cache.Playlist.currentPartInfo?.partInstanceId + pieceInstance.PieceInstance.partInstanceId === this._cache.Playlist.currentPartInfo?.partInstanceId ? ActionPartChange.SAFE_CHANGE : ActionPartChange.NONE const updatesNextPart: ActionPartChange = - pieceInstance.partInstanceId === this._cache.Playlist.nextPartInfo?.partInstanceId + pieceInstance.PieceInstance.partInstanceId === this._cache.Playlist.nextPartInfo?.partInstanceId ? ActionPartChange.SAFE_CHANGE : ActionPartChange.NONE if (!updatesCurrentPart && !updatesNextPart) { @@ -359,7 +359,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct if (trimmedPiece.content?.timelineObjects) { timelineObjectsString = serializePieceTimelineObjectsBlob( postProcessTimelineObjects( - pieceInstance.piece._id, + pieceInstance.PieceInstance.piece._id, this.showStyleCompound.blueprintId, trimmedPiece.content.timelineObjects ) @@ -368,19 +368,15 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct trimmedPiece.content = omit(trimmedPiece.content, 'timelineObjects') as WithTimeline } - partInstance.updatePieceProps(pieceInstance._id, trimmedPiece as any) // TODO: this needs to be more type safe - if (timelineObjectsString !== undefined) - partInstance.updatePieceProps(pieceInstance._id, { timelineObjectsString }) + pieceInstance.updatePieceProps(trimmedPiece as any) // TODO: this needs to be more type safe + if (timelineObjectsString !== undefined) pieceInstance.updatePieceProps({ timelineObjectsString }) // setupPieceInstanceInfiniteProperties(pieceInstance) this.nextPartState = Math.max(this.nextPartState, updatesNextPart) this.currentPartState = Math.max(this.currentPartState, updatesCurrentPart) - const updatedPieceInstance = partInstance.getPieceInstance(pieceInstance._id) - if (!updatedPieceInstance) throw new Error('PieceInstance disappeared!') - - return convertPieceInstanceToBlueprints(updatedPieceInstance) + return convertPieceInstanceToBlueprints(pieceInstance.PieceInstance) } async queuePart(rawPart: IBlueprintPart, rawPieces: IBlueprintPiece[]): Promise { const currentPartInstance = this._cache.CurrentPartInstance diff --git a/packages/job-worker/src/ingest/syncChangesToPartInstance.ts b/packages/job-worker/src/ingest/syncChangesToPartInstance.ts index 464386b7a7..9ada58f6ae 100644 --- a/packages/job-worker/src/ingest/syncChangesToPartInstance.ts +++ b/packages/job-worker/src/ingest/syncChangesToPartInstance.ts @@ -126,7 +126,7 @@ export async function syncChangesToPartInstances( const partId = existingPartInstance.PartInstance.part._id const existingResultPartInstance: BlueprintSyncIngestPartInstance = { partInstance: convertPartInstanceToBlueprints(existingPartInstance.PartInstance), - pieceInstances: pieceInstancesInPart.map(convertPieceInstanceToBlueprints), + pieceInstances: pieceInstancesInPart.map((p) => convertPieceInstanceToBlueprints(p.PieceInstance)), } const proposedPieceInstances = getPieceInstancesForPart( @@ -141,7 +141,9 @@ export async function syncChangesToPartInstances( logger.info(`Syncing ingest changes for part: ${partId} (orphaned: ${!!newPart})`) - const referencedAdlibIds = new Set(_.compact(pieceInstancesInPart.map((p) => p.adLibSourceId))) + const referencedAdlibIds = new Set( + _.compact(pieceInstancesInPart.map((p) => p.PieceInstance.adLibSourceId)) + ) const newResultData: BlueprintSyncIngestNewData = { part: newPart ? convertPartToBlueprints(newPart) : undefined, pieceInstances: proposedPieceInstances.map(convertPieceInstanceToBlueprints), diff --git a/packages/job-worker/src/playout/__tests__/timeline.test.ts b/packages/job-worker/src/playout/__tests__/timeline.test.ts index 6fff275e52..3d37124748 100644 --- a/packages/job-worker/src/playout/__tests__/timeline.test.ts +++ b/packages/job-worker/src/playout/__tests__/timeline.test.ts @@ -203,13 +203,13 @@ function checkTimingsRaw( const targetCurrentPieces: PartTimelineTimings['currentPieces'] = {} const targetCurrentInfinitePieces: PartTimelineTimings['currentInfinitePieces'] = {} for (const piece of currentPieces) { - let entryId = unprotectString(piece.piece._id) + let entryId = unprotectString(piece.PieceInstance.piece._id) if (entryId.startsWith(unprotectString(rundownId))) entryId = entryId.substring(unprotectString(rundownId).length + 1) - if (piece.piece.lifespan === PieceLifespan.WithinPart) { - const pieceObj = objs.get(getPieceGroupId(piece)) - const controlObj = objs.get(getPieceControlObjectId(piece)) + if (piece.PieceInstance.piece.lifespan === PieceLifespan.WithinPart) { + const pieceObj = objs.get(getPieceGroupId(piece.PieceInstance)) + const controlObj = objs.get(getPieceControlObjectId(piece.PieceInstance)) targetCurrentPieces[entryId] = controlObj ? { @@ -218,13 +218,14 @@ function checkTimingsRaw( } : null } else { - const partGroupId = getPartGroupId(protectString(unprotectString(piece._id))) + '_infinite' + const partGroupId = + getPartGroupId(protectString(unprotectString(piece.PieceInstance._id))) + '_infinite' const partObj = objs.get(partGroupId) if (!partObj) { targetCurrentInfinitePieces[entryId] = null } else { - const pieceObj = objs.get(getPieceGroupId(piece)) - const controlObj = objs.get(getPieceControlObjectId(piece)) + const pieceObj = objs.get(getPieceGroupId(piece.PieceInstance)) + const controlObj = objs.get(getPieceControlObjectId(piece.PieceInstance)) targetCurrentInfinitePieces[entryId] = { partGroup: partObj.enable, @@ -244,11 +245,11 @@ function checkTimingsRaw( const previousPieces = previousPartInstance.PieceInstances let previousOutTransition: PartTimelineTimings['previousOutTransition'] for (const piece of previousPieces) { - if (piece.piece.pieceType === IBlueprintPieceType.OutTransition) { + if (piece.PieceInstance.piece.pieceType === IBlueprintPieceType.OutTransition) { if (previousOutTransition !== undefined) throw new Error('Too many out transition pieces were found') - const pieceObj = objs.get(getPieceGroupId(piece)) - const controlObj = objs.get(getPieceControlObjectId(piece)) + const pieceObj = objs.get(getPieceGroupId(piece.PieceInstance)) + const controlObj = objs.get(getPieceControlObjectId(piece.PieceInstance)) previousOutTransition = controlObj ? { childGroup: parsePieceGroupPrerollAndPostroll(pieceObj?.enable ?? []), @@ -1213,8 +1214,6 @@ describe('Timeline', () => { const { currentPartInstance } = await getPartInstances() expect(currentPartInstance).toBeTruthy() - console.log('inst', currentPartInstance?.PartInstance._id) - // Insert an adlib piece await doStartAdlibPiece( playlistId, @@ -1560,11 +1559,11 @@ describe('Timeline', () => { const currentPieceInstances = currentPartInstance.PieceInstances const pieceInstance0 = currentPieceInstances.find( - (instance) => instance.piece._id === protectString(`${rundownId}_piece000`) + (instance) => instance.PieceInstance.piece._id === protectString(`${rundownId}_piece000`) ) if (!pieceInstance0) throw new Error('pieceInstance0 must be defined') const pieceInstance1 = currentPieceInstances.find( - (instance) => instance.piece._id === protectString(`${rundownId}_piece001`) + (instance) => instance.PieceInstance.piece._id === protectString(`${rundownId}_piece001`) ) if (!pieceInstance1) throw new Error('pieceInstance1 must be defined') @@ -1574,8 +1573,8 @@ describe('Timeline', () => { partId: currentPartInstance.PartInstance._id, includePart: true, pieceOffsets: { - [unprotectString(pieceInstance0._id)]: 500, - [unprotectString(pieceInstance1._id)]: 500, + [unprotectString(pieceInstance0.PieceInstance._id)]: 500, + [unprotectString(pieceInstance1.PieceInstance._id)]: 500, }, }) diff --git a/packages/job-worker/src/playout/adlibJobs.ts b/packages/job-worker/src/playout/adlibJobs.ts index c3ad011b81..b2c656859c 100644 --- a/packages/job-worker/src/playout/adlibJobs.ts +++ b/packages/job-worker/src/playout/adlibJobs.ts @@ -1,6 +1,6 @@ import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' -import { PieceInstance, PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { assertNever, clone } from '@sofie-automation/corelib/dist/lib' import { logger } from '../logging' @@ -17,7 +17,7 @@ import { PlayoutPartInstanceModel } from './model/PlayoutPartInstanceModel' import { runJobWithPlayoutCache } from './lock' import { updateTimeline } from './timeline/generate' import { getCurrentTime } from '../lib' -import { convertAdLibToPieceInstance, convertPieceToAdLibPiece, sortPieceInstancesByStart } from './pieces' +import { comparePieceStart, convertAdLibToPieceInstance, convertPieceToAdLibPiece } from './pieces' import { getResolvedPiecesForCurrentPartInstance } from './resolvedPieces' import { syncPlayheadInfinitesForNextPartInstance } from './infinites' import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/error' @@ -29,6 +29,7 @@ import { WatchedPackagesHelper } from '../blueprints/context/watchedPackages' import { innerFindLastPieceOnLayer, innerStartOrQueueAdLibPiece, innerStopPieces } from './adlibUtils' import _ = require('underscore') import { executeActionInner } from './adlibAction' +import { PlayoutPieceInstanceModel } from './model/PlayoutPieceInstanceModel' /** * Play an existing Piece in the Rundown as an AdLib @@ -59,7 +60,7 @@ export async function handleTakePieceAsAdlibNow(context: JobContext, data: TakeP const pieceInstanceToCopy = cache.findPieceInstance(data.pieceInstanceIdOrPieceIdToCopy as PieceInstanceId) const pieceToCopy = pieceInstanceToCopy - ? clone(pieceInstanceToCopy.pieceInstance.piece) + ? clone(pieceInstanceToCopy.pieceInstance.PieceInstance.piece) : ((await context.directCollections.Pieces.findOne({ _id: data.pieceInstanceIdOrPieceIdToCopy as PieceId, startRundownId: { $in: rundownIds }, @@ -134,7 +135,7 @@ async function pieceTakeNowAsAdlib( currentPartInstance: PlayoutPartInstanceModel, pieceToCopy: PieceInstancePiece, pieceInstanceToCopy: - | { partInstance: PlayoutPartInstanceModel; pieceInstance: ReadonlyDeep } + | { partInstance: PlayoutPartInstanceModel; pieceInstance: PlayoutPieceInstanceModel } | undefined ): Promise { /*const newPieceInstance = */ convertAdLibToPieceInstance(context, pieceToCopy, currentPartInstance, false) @@ -142,12 +143,12 @@ async function pieceTakeNowAsAdlib( // Disable the original piece if from the same Part if ( pieceInstanceToCopy && - pieceInstanceToCopy.pieceInstance.partInstanceId === currentPartInstance.PartInstance._id + pieceInstanceToCopy.pieceInstance.PieceInstance.partInstanceId === currentPartInstance.PartInstance._id ) { // Ensure the piece being copied isnt currently live if ( - pieceInstanceToCopy.pieceInstance.plannedStartedPlayback && - pieceInstanceToCopy.pieceInstance.plannedStartedPlayback <= getCurrentTime() + pieceInstanceToCopy.pieceInstance.PieceInstance.plannedStartedPlayback && + pieceInstanceToCopy.pieceInstance.PieceInstance.plannedStartedPlayback <= getCurrentTime() ) { const resolvedPieces = getResolvedPiecesForCurrentPartInstance( context, @@ -155,7 +156,7 @@ async function pieceTakeNowAsAdlib( currentPartInstance ) const resolvedPieceBeingCopied = resolvedPieces.find( - (p) => p.instance._id === pieceInstanceToCopy.pieceInstance._id + (p) => p.instance._id === pieceInstanceToCopy.pieceInstance.PieceInstance._id ) if ( @@ -167,7 +168,7 @@ async function pieceTakeNowAsAdlib( // logger.debug(`Piece "${piece._id}" is currently live and cannot be used as an ad-lib`) throw UserError.from( new Error( - `PieceInstance "${pieceInstanceToCopy.pieceInstance._id}" is currently live and cannot be used as an ad-lib` + `PieceInstance "${pieceInstanceToCopy.pieceInstance.PieceInstance._id}" is currently live and cannot be used as an ad-lib` ), UserErrorMessage.PieceAsAdlibCurrentlyLive ) @@ -175,7 +176,7 @@ async function pieceTakeNowAsAdlib( } // TODO: is this ok? - currentPartInstance.setPieceInstanceDisabled(pieceInstanceToCopy.pieceInstance._id, true) + pieceInstanceToCopy.pieceInstance.setDisabled(true) } await syncPlayheadInfinitesForNextPartInstance(context, cache, cache.CurrentPartInstance, cache.NextPartInstance) @@ -404,23 +405,23 @@ export async function handleDisableNextPiece(context: JobContext, data: DisableN } const filteredPieces = partInstance.PieceInstances.filter((piece) => { - const sourceLayer = allowedSourceLayers[piece.piece.sourceLayerId] + const sourceLayer = allowedSourceLayers[piece.PieceInstance.piece.sourceLayerId] if ( sourceLayer?.allowDisable && - !piece.piece.virtual && - piece.piece.pieceType === IBlueprintPieceType.Normal + !piece.PieceInstance.piece.virtual && + piece.PieceInstance.piece.pieceType === IBlueprintPieceType.Normal ) return true return false }) - const sortedPieces: ReadonlyDeep[] = sortPieceInstancesByStart( - _.sortBy(filteredPieces, (piece) => { - const sourceLayer = allowedSourceLayers[piece.piece.sourceLayerId] - return sourceLayer?._rank || -9999 - }), - nowInPart - ) + const sortedByLayer = _.sortBy(filteredPieces, (piece) => { + const sourceLayer = allowedSourceLayers[piece.PieceInstance.piece.sourceLayerId] + return sourceLayer?._rank || -9999 + }) + + const sortedPieces = [...sortedByLayer] + sortedPieces.sort((a, b) => comparePieceStart(a.PieceInstance.piece, b.PieceInstance.piece, nowInPart)) const findLast = !!data.undo @@ -428,8 +429,8 @@ export async function handleDisableNextPiece(context: JobContext, data: DisableN return sortedPieces.find((piece) => { return ( - piece.piece.enable.start >= nowInPart && - ((!data.undo && !piece.disabled) || (data.undo && piece.disabled)) + piece.PieceInstance.piece.enable.start >= nowInPart && + ((!data.undo && !piece.PieceInstance.disabled) || (data.undo && piece.PieceInstance.disabled)) ) }) } @@ -447,9 +448,11 @@ export async function handleDisableNextPiece(context: JobContext, data: DisableN const candidatePieceInstance = getNextPiece(partInstance, ignoreStartedPlayback) if (candidatePieceInstance) { logger.debug( - (data.undo ? 'Disabling' : 'Enabling') + ' next PieceInstance ' + candidatePieceInstance._id + (data.undo ? 'Disabling' : 'Enabling') + + ' next PieceInstance ' + + candidatePieceInstance.PieceInstance._id ) - partInstance.setPieceInstanceDisabled(candidatePieceInstance._id, !data.undo) + candidatePieceInstance.setDisabled(!data.undo) disabledPiece = true break diff --git a/packages/job-worker/src/playout/adlibUtils.ts b/packages/job-worker/src/playout/adlibUtils.ts index 2521210953..8e9bfdf33e 100644 --- a/packages/job-worker/src/playout/adlibUtils.ts +++ b/packages/job-worker/src/playout/adlibUtils.ts @@ -267,17 +267,25 @@ export function innerStopPieces( case PieceLifespan.OutOnRundownChange: { logger.info(`Blueprint action: Cropping PieceInstance "${pieceInstance._id}" to ${stopAt}`) - const newDuration: Required['userDuration'] = cache.isMultiGatewayMode - ? { - endRelativeToNow: offsetRelativeToNow, - } - : { - endRelativeToPart: relativeStopAt, - } + const pieceInstanceModel = cache.findPieceInstance(pieceInstance._id) + if (pieceInstanceModel) { + const newDuration: Required['userDuration'] = cache.isMultiGatewayMode + ? { + endRelativeToNow: offsetRelativeToNow, + } + : { + endRelativeToPart: relativeStopAt, + } + + pieceInstanceModel.pieceInstance.setDuration(newDuration) + + stoppedInstances.push(pieceInstance._id) + } else { + logger.warn( + `Blueprint action: Failed to crop PieceInstance "${pieceInstance._id}", it was not found` + ) + } - currentPartInstance.setPieceInstanceDuration(pieceInstance._id, newDuration) - - stoppedInstances.push(pieceInstance._id) break } case PieceLifespan.OutOnSegmentEnd: diff --git a/packages/job-worker/src/playout/holdJobs.ts b/packages/job-worker/src/playout/holdJobs.ts index c39962dfd4..01cb0bf5e3 100644 --- a/packages/job-worker/src/playout/holdJobs.ts +++ b/packages/job-worker/src/playout/holdJobs.ts @@ -41,10 +41,10 @@ export async function handleActivateHold(context: JobContext, data: ActivateHold const hasDynamicallyInserted = currentPartInstance.PieceInstances.find( (p) => - !!p.dynamicallyInserted && + !!p.PieceInstance.dynamicallyInserted && // If its a continuation of an infinite adlib it is probably a graphic, so is 'fine' - !p.infinite?.fromPreviousPart && - !p.infinite?.fromPreviousPlayhead + !p.PieceInstance.infinite?.fromPreviousPart && + !p.PieceInstance.infinite?.fromPreviousPlayhead ) if (hasDynamicallyInserted) throw UserError.create(UserErrorMessage.HoldAfterAdlib) diff --git a/packages/job-worker/src/playout/infinites.ts b/packages/job-worker/src/playout/infinites.ts index 02def5b7f2..a89e3e6182 100644 --- a/packages/job-worker/src/playout/infinites.ts +++ b/packages/job-worker/src/playout/infinites.ts @@ -254,7 +254,7 @@ export async function syncPlayheadInfinitesForNextPartInstance( const nowInPart = getCurrentTime() - (fromPartInstance.PartInstance.timings?.plannedStartedPlayback ?? 0) const prunedPieceInstances = processAndPrunePieceInstanceTimings( showStyleBase.sourceLayers, - fromPartInstance.PieceInstances, + fromPartInstance.PieceInstances.map((p) => p.PieceInstance), nowInPart, undefined, true @@ -339,7 +339,7 @@ export function getPieceInstancesForPart( playlist.activationId, playingPartInstance?.PartInstance, playingSegment?.Segment, - playingPieceInstances, + playingPieceInstances.map((p) => p.PieceInstance), rundown.Rundown, segment.Segment, part, diff --git a/packages/job-worker/src/playout/model/PlayoutModel.ts b/packages/job-worker/src/playout/model/PlayoutModel.ts index 26e19c0e73..c34a8ea790 100644 --- a/packages/job-worker/src/playout/model/PlayoutModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutModel.ts @@ -24,6 +24,7 @@ import { PlayoutSegmentModel } from './PlayoutSegmentModel' import { PlayoutPartInstanceModel } from './PlayoutPartInstanceModel' import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { PlayoutPieceInstanceModel } from './PlayoutPieceInstanceModel' export type DeferredFunction = (cache: PlayoutModel) => void | Promise export type DeferredAfterSaveFunction = (cache: PlayoutModelReadonly) => void | Promise @@ -76,7 +77,7 @@ export interface PlayoutModelReadonly extends StudioPlayoutModelBaseReadonly { findPieceInstance( id: PieceInstanceId - ): { partInstance: PlayoutPartInstanceModel; pieceInstance: ReadonlyDeep } | undefined + ): { partInstance: PlayoutPartInstanceModel; pieceInstance: PlayoutPieceInstanceModel } | undefined } export interface PlayoutModel extends PlayoutModelReadonly, StudioPlayoutModelBase, ICacheBase2 { diff --git a/packages/job-worker/src/playout/model/PlayoutPartInstanceModel.ts b/packages/job-worker/src/playout/model/PlayoutPartInstanceModel.ts index 3128b72ebb..5b0b151b75 100644 --- a/packages/job-worker/src/playout/model/PlayoutPartInstanceModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutPartInstanceModel.ts @@ -10,10 +10,11 @@ import { PieceInstance, PieceInstancePiece } from '@sofie-automation/corelib/dis import { PartNote } from '@sofie-automation/corelib/dist/dataModel/Notes' import { IBlueprintMutatablePart, PieceLifespan, Time } from '@sofie-automation/blueprints-integration' import { PartCalculatedTimings } from '@sofie-automation/corelib/dist/playout/timings' +import { PlayoutPieceInstanceModel } from './PlayoutPieceInstanceModel' export interface PlayoutPartInstanceModel { readonly PartInstance: ReadonlyDeep - readonly PieceInstances: ReadonlyDeep[] + readonly PieceInstances: PlayoutPieceInstanceModel[] clone(): PlayoutPartInstanceModel @@ -22,7 +23,7 @@ export interface PlayoutPartInstanceModel { insertAdlibbedPiece( piece: Omit, fromAdlibId: PieceId | undefined - ): ReadonlyDeep + ): PlayoutPieceInstanceModel recalculateExpectedDurationWithPreroll(): void @@ -40,7 +41,6 @@ export interface PlayoutPartInstanceModel { setTaken(takeTime: number, playOffset: number): void - // TODO - better name storePlayoutTimingsAndPreviousEndState( partPlayoutTimings: PartCalculatedTimings, previousPartEndState: unknown @@ -50,15 +50,18 @@ export interface PlayoutPartInstanceModel { updatePartProps(props: Partial): void - getPieceInstance(id: PieceInstanceId): ReadonlyDeep | undefined + getPieceInstance(id: PieceInstanceId): PlayoutPieceInstanceModel | undefined - updatePieceProps(id: PieceInstanceId, props: Partial): void + /** + * Replace a PieceInstance with a new version. + * If there is an existing PieceInstance with the same id, it will be merged onto that + * Note: this will replace any playout owned properties too + * @param doc + */ + replacePieceInstance(doc: ReadonlyDeep): PlayoutPieceInstanceModel /** @deprecated HACK */ - replacePieceInstance(doc: ReadonlyDeep): void - - /** @deprecated HACK */ - insertPlannedPiece(doc: Omit): PieceInstance + insertPlannedPiece(doc: Omit): PlayoutPieceInstanceModel /** @deprecated HACK */ removePieceInstance(id: PieceInstanceId): boolean @@ -71,28 +74,17 @@ export interface PlayoutPartInstanceModel { setReportedStartedPlayback(time: Time): boolean setReportedStoppedPlayback(time: Time): boolean - setPieceInstancedPlannedStartedPlayback(pieceInstanceId: PieceInstanceId, time: Time): boolean - setPieceInstancedPlannedStoppedPlayback(pieceInstanceId: PieceInstanceId, time: Time | undefined): boolean - setPieceInstancedReportedStartedPlayback(pieceInstanceId: PieceInstanceId, time: Time): boolean - setPieceInstancedReportedStoppedPlayback(pieceInstanceId: PieceInstanceId, time: Time): boolean - validateScratchpadSegmentProperties(): void - preparePieceInstanceForHold(pieceInstanceId: PieceInstanceId): PieceInstanceInfiniteId - addHoldPieceInstance( - extendPieceInstance: ReadonlyDeep, + extendPieceInstance: PlayoutPieceInstanceModel, infiniteInstanceId: PieceInstanceInfiniteId - ): PieceInstance - - setPieceInstanceDuration(pieceInstanceId: PieceInstanceId, duration: Required['userDuration']): void + ): PlayoutPieceInstanceModel insertVirtualPiece( start: number, lifespan: PieceLifespan, sourceLayerId: string, outputLayerId: string - ): PieceInstance - - setPieceInstanceDisabled(pieceInstanceId: PieceInstanceId, disabled: boolean): void + ): PlayoutPieceInstanceModel } diff --git a/packages/job-worker/src/playout/model/PlayoutPieceInstanceModel.ts b/packages/job-worker/src/playout/model/PlayoutPieceInstanceModel.ts new file mode 100644 index 0000000000..b174f5ab20 --- /dev/null +++ b/packages/job-worker/src/playout/model/PlayoutPieceInstanceModel.ts @@ -0,0 +1,21 @@ +import { PieceInstanceInfiniteId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { ReadonlyDeep } from 'type-fest' +import { PieceInstance, PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { Time } from '@sofie-automation/blueprints-integration' + +export interface PlayoutPieceInstanceModel { + readonly PieceInstance: ReadonlyDeep + + updatePieceProps(props: Partial): void + + setPlannedStartedPlayback(time: Time): boolean + setPlannedStoppedPlayback(time: Time | undefined): boolean + setReportedStartedPlayback(time: Time): boolean + setReportedStoppedPlayback(time: Time): boolean + + prepareForHold(): PieceInstanceInfiniteId + + setDuration(duration: Required['userDuration']): void + + setDisabled(disabled: boolean): void +} diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts index 1535d70caf..3b25d3d67f 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts @@ -44,6 +44,7 @@ import { queuePartInstanceTimingEvent } from '../../timings/events' import { IS_PRODUCTION } from '../../../environment' import { DeferredAfterSaveFunction, DeferredFunction, PlayoutModel } from '../PlayoutModel' import { writePartInstancesAndPieceInstances, writeScratchpadSegments } from './SavePlayoutModel' +import { PlayoutPieceInstanceModel } from '../PlayoutPieceInstanceModel' /** * This is a cache used for playout operations. @@ -226,7 +227,7 @@ export class PlayoutModelImpl implements PlayoutModel { findPieceInstance( id: PieceInstanceId - ): { partInstance: PlayoutPartInstanceModel; pieceInstance: ReadonlyDeep } | undefined { + ): { partInstance: PlayoutPartInstanceModel; pieceInstance: PlayoutPieceInstanceModel } | undefined { for (const partInstance of this.LoadedPartInstances) { const pieceInstance = partInstance.getPieceInstance(id) if (pieceInstance) return { partInstance, pieceInstance } diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts index 8ee08d194d..db23472d8b 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts @@ -15,7 +15,6 @@ import { import { clone, getRandomId } from '@sofie-automation/corelib/dist/lib' import { getCurrentTime } from '../../../lib' import { setupPieceInstanceInfiniteProperties } from '../../pieces' -import { EmptyPieceTimelineObjectsBlob } from '@sofie-automation/corelib/dist/dataModel/Piece' import { calculatePartExpectedDurationWithPreroll, PartCalculatedTimings, @@ -29,15 +28,13 @@ import { } from '@sofie-automation/blueprints-integration' import { PlayoutPartInstanceModel } from '../PlayoutPartInstanceModel' import { protectString } from '@sofie-automation/corelib/dist/protectedString' - -interface PieceInstanceWrapper { - changed: boolean - doc: PieceInstance | null -} +import { PlayoutPieceInstanceModel } from '../PlayoutPieceInstanceModel' +import { PlayoutPieceInstanceModelImpl } from './PlayoutPieceInstanceModelImpl' +import { EmptyPieceTimelineObjectsBlob } from '@sofie-automation/corelib/dist/dataModel/Piece' export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { PartInstanceImpl: DBPartInstance - PieceInstancesImpl: Map + PieceInstancesImpl: Map #PartInstanceHasChanges = false get PartInstanceHasChanges(): boolean { @@ -46,7 +43,7 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { ChangedPieceInstanceIds(): PieceInstanceId[] { const result: PieceInstanceId[] = [] for (const [id, pieceInstance] of this.PieceInstancesImpl.entries()) { - if (pieceInstance.changed || !pieceInstance.doc) result.push(id) + if (!pieceInstance || pieceInstance.HasChanges) result.push(id) } return result } @@ -57,10 +54,10 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { this.#PartInstanceHasChanges = false for (const [id, value] of this.PieceInstancesImpl) { - if (!value.doc) { + if (!value) { this.PieceInstancesImpl.delete(id) - } else if (value.changed) { - value.changed = false + } else if (value.HasChanges) { + value.setDirty(false) } } } @@ -68,11 +65,11 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { get PartInstance(): ReadonlyDeep { return this.PartInstanceImpl } - get PieceInstances(): ReadonlyDeep[] { - const result: PieceInstance[] = [] + get PieceInstances(): PlayoutPieceInstanceModel[] { + const result: PlayoutPieceInstanceModel[] = [] for (const pieceWrapped of this.PieceInstancesImpl.values()) { - if (pieceWrapped.doc) result.push(pieceWrapped.doc) + if (pieceWrapped) result.push(pieceWrapped) } return result @@ -84,10 +81,7 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { this.PieceInstancesImpl = new Map() for (const pieceInstance of pieceInstances) { - this.PieceInstancesImpl.set(pieceInstance._id, { - doc: pieceInstance, - changed: hasChanges, - }) + this.PieceInstancesImpl.set(pieceInstance._id, new PlayoutPieceInstanceModelImpl(pieceInstance, hasChanges)) } } @@ -97,11 +91,20 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { * TODO - this has issues with deleting instances! */ clone(): PlayoutPartInstanceModel { - return new PlayoutPartInstanceModelImpl( - clone(this.PartInstanceImpl), - clone(this.PieceInstances), - this.#PartInstanceHasChanges - ) + const cloned = new PlayoutPartInstanceModelImpl(clone(this.PartInstanceImpl), [], this.#PartInstanceHasChanges) + + for (const [id, pieceInstance] of this.PieceInstancesImpl) { + if (!pieceInstance) { + cloned.PieceInstancesImpl.set(id, null) + } else { + cloned.PieceInstancesImpl.set( + id, + new PlayoutPieceInstanceModelImpl(clone(pieceInstance.PieceInstanceImpl), pieceInstance.HasChanges) + ) + } + } + + return cloned } setPlaylistActivationId(id: RundownPlaylistActivationId): void { @@ -110,9 +113,9 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { this.#PartInstanceHasChanges = true for (const pieceInstance of this.PieceInstancesImpl.values()) { - if (!pieceInstance.doc) continue - pieceInstance.changed = true - pieceInstance.doc.playlistActivationId = id + if (!pieceInstance) continue + pieceInstance.PieceInstanceImpl.playlistActivationId = id + pieceInstance.setDirty() } } @@ -121,14 +124,14 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { this.PartInstanceImpl.part.expectedDurationWithPreroll = calculatePartExpectedDurationWithPreroll( this.PartInstanceImpl.part, - this.PieceInstances.map((p) => p.piece) + this.PieceInstances.map((p) => p.PieceInstance.piece) ) } insertAdlibbedPiece( piece: Omit, fromAdlibId: PieceId | undefined - ): ReadonlyDeep { + ): PlayoutPieceInstanceModel { const pieceInstance: PieceInstance = { _id: protectString(`${this.PartInstance._id}_${piece._id}`), rundownId: this.PartInstance.rundownId, @@ -151,31 +154,25 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { setupPieceInstanceInfiniteProperties(pieceInstance) - this.PieceInstancesImpl.set(pieceInstance._id, { - doc: pieceInstance, - changed: true, - }) + const pieceInstanceModel = new PlayoutPieceInstanceModelImpl(pieceInstance, true) + this.PieceInstancesImpl.set(pieceInstance._id, pieceInstanceModel) - return pieceInstance + return pieceInstanceModel } replaceInfinitesFromPreviousPlayhead(pieces: PieceInstance[]): void { // TODO - this should do some validation/some of the wrapping from a Piece into a PieceInstance // Remove old ones - for (const piece of this.PieceInstancesImpl.values()) { - if (!piece.doc) continue + for (const [id, piece] of this.PieceInstancesImpl.entries()) { + if (!piece) continue - if (piece.doc.infinite?.fromPreviousPlayhead) { - piece.doc = null - piece.changed = true + if (piece.PieceInstance.infinite?.fromPreviousPlayhead) { + this.PieceInstancesImpl.set(id, null) } } for (const piece of pieces) { - this.PieceInstancesImpl.set(piece._id, { - doc: piece, - changed: true, - }) + this.PieceInstancesImpl.set(piece._id, new PlayoutPieceInstanceModelImpl(piece, true)) } } @@ -183,10 +180,10 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { this.PartInstanceImpl.reset = true for (const pieceInstance of this.PieceInstancesImpl.values()) { - if (!pieceInstance.doc) continue + if (!pieceInstance) continue - pieceInstance.changed = true - pieceInstance.doc.reset = true + pieceInstance.setDirty() + pieceInstance.PieceInstanceImpl.reset = true } this.#PartInstanceHasChanges = true @@ -262,35 +259,32 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { this.#PartInstanceHasChanges = true } - getPieceInstance(id: PieceInstanceId): ReadonlyDeep | undefined { - return this.PieceInstancesImpl.get(id)?.doc ?? undefined + getPieceInstance(id: PieceInstanceId): PlayoutPieceInstanceModel | undefined { + return this.PieceInstancesImpl.get(id) ?? undefined } - updatePieceProps(id: PieceInstanceId, props: Partial): void { + replacePieceInstance(doc: ReadonlyDeep): PlayoutPieceInstanceModel { // TODO - this is missing a lot of validation - const pieceInstance = this.PieceInstancesImpl.get(id) - if (!pieceInstance?.doc) throw new Error('Bad pieceinstance') - pieceInstance.changed = true - pieceInstance.doc.piece = { - ...pieceInstance.doc.piece, - ...props, + const existingPieceInstance = this.PieceInstancesImpl.get(doc._id) + if (existingPieceInstance) { + existingPieceInstance.setDirty() + existingPieceInstance.PieceInstanceImpl = { + ...existingPieceInstance.PieceInstanceImpl, + ...clone(doc), + } + return existingPieceInstance + } else { + const newPieceInstance = new PlayoutPieceInstanceModelImpl(clone(doc), true) + this.PieceInstancesImpl.set(newPieceInstance.PieceInstance._id, newPieceInstance) + return newPieceInstance } } - /** @deprecated HACK */ - replacePieceInstance(doc: ReadonlyDeep): void { - // TODO - this is missing a lot of validation - this.PieceInstancesImpl.set(doc._id, { - doc: clone(doc), - changed: true, - }) - } - /** @deprecated HACK * */ - insertPlannedPiece(piece: Omit): PieceInstance { + insertPlannedPiece(piece: Omit): PlayoutPieceInstanceModel { const pieceInstanceId = getPieceInstanceIdForPiece(this.PartInstance._id, piece._id) if (this.PieceInstancesImpl.has(pieceInstanceId)) throw new Error(`PieceInstance "${pieceInstanceId}" already exists`) @@ -309,11 +303,10 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { // Ensure the infinite-ness is setup correctly setupPieceInstanceInfiniteProperties(newPieceInstance) - this.PieceInstancesImpl.set(pieceInstanceId, { - doc: newPieceInstance, - changed: true, - }) - return newPieceInstance + const pieceInstanceModel = new PlayoutPieceInstanceModelImpl(newPieceInstance, true) + this.PieceInstancesImpl.set(pieceInstanceId, pieceInstanceModel) + + return pieceInstanceModel } /** @deprecated HACK */ @@ -321,8 +314,7 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { // TODO - this is missing a lot of validation const pieceInstanceWrapped = this.PieceInstancesImpl.get(id) if (pieceInstanceWrapped) { - pieceInstanceWrapped.changed = true - pieceInstanceWrapped.doc = null + this.PieceInstancesImpl.set(id, null) return true } return false @@ -331,10 +323,8 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { /** @deprecated HACK */ insertInfinitePieces(pieceInstances: PieceInstance[]): void { for (const pieceInstance of pieceInstances) { - this.PieceInstancesImpl.set(pieceInstance._id, { - doc: pieceInstance, - changed: true, - }) + const pieceInstanceModel = new PlayoutPieceInstanceModelImpl(pieceInstance, true) + this.PieceInstancesImpl.set(pieceInstance._id, pieceInstanceModel) } } @@ -389,61 +379,6 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { return false } - setPieceInstancedPlannedStartedPlayback(pieceInstanceId: PieceInstanceId, time: Time): boolean { - const pieceInstance = this.PieceInstancesImpl.get(pieceInstanceId) - if (!pieceInstance?.doc) throw new Error(`PieceInstance ${pieceInstanceId} not found`) // TODO - is this ok? - - if (pieceInstance.doc.plannedStartedPlayback !== time) { - pieceInstance.doc.plannedStartedPlayback = time - delete pieceInstance.doc.plannedStoppedPlayback - - pieceInstance.changed = true - - return true - } - return false - } - setPieceInstancedPlannedStoppedPlayback(pieceInstanceId: PieceInstanceId, time: Time | undefined): boolean { - const pieceInstance = this.PieceInstancesImpl.get(pieceInstanceId) - if (!pieceInstance?.doc) throw new Error(`PieceInstance ${pieceInstanceId} not found`) // TODO - is this ok? - - if (pieceInstance.doc.plannedStoppedPlayback !== time) { - pieceInstance.doc.plannedStoppedPlayback = time - - pieceInstance.changed = true - - return true - } - return false - } - setPieceInstancedReportedStartedPlayback(pieceInstanceId: PieceInstanceId, time: Time): boolean { - const pieceInstance = this.PieceInstancesImpl.get(pieceInstanceId) - if (!pieceInstance?.doc) throw new Error(`PieceInstance ${pieceInstanceId} not found`) // TODO - is this ok? - - if (pieceInstance.doc.reportedStartedPlayback !== time) { - pieceInstance.doc.reportedStartedPlayback = time - delete pieceInstance.doc.reportedStoppedPlayback - - pieceInstance.changed = true - - return true - } - return false - } - setPieceInstancedReportedStoppedPlayback(pieceInstanceId: PieceInstanceId, time: Time): boolean { - const pieceInstance = this.PieceInstancesImpl.get(pieceInstanceId) - if (!pieceInstance?.doc) throw new Error(`PieceInstance ${pieceInstanceId} not found`) // TODO - is this ok? - - if (pieceInstance.doc.reportedStoppedPlayback !== time) { - pieceInstance.doc.reportedStoppedPlayback = time - - pieceInstance.changed = true - - return true - } - return false - } - validateScratchpadSegmentProperties(): void { this.PartInstanceImpl.orphaned = 'adlib-part' @@ -457,69 +392,40 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { this.#PartInstanceHasChanges = true } - preparePieceInstanceForHold(pieceInstanceId: PieceInstanceId): PieceInstanceInfiniteId { - const pieceInstance = this.PieceInstancesImpl.get(pieceInstanceId) - if (!pieceInstance?.doc) throw new Error(`PieceInstance ${pieceInstanceId} not found`) // TODO - is this ok? - - const infiniteInstanceId: PieceInstanceInfiniteId = getRandomId() - pieceInstance.doc.infinite = { - infiniteInstanceId: infiniteInstanceId, - infiniteInstanceIndex: 0, - infinitePieceId: pieceInstance.doc.piece._id, - fromPreviousPart: false, - } - pieceInstance.changed = true - - return infiniteInstanceId - } - addHoldPieceInstance( - extendPieceInstance: ReadonlyDeep, + extendPieceInstance: PlayoutPieceInstanceModel, infiniteInstanceId: PieceInstanceInfiniteId - ): PieceInstance { + ): PlayoutPieceInstanceModel { // make the extension const newInstance: PieceInstance = { - _id: protectString(extendPieceInstance._id + '_hold'), - playlistActivationId: extendPieceInstance.playlistActivationId, - rundownId: extendPieceInstance.rundownId, + _id: protectString(extendPieceInstance.PieceInstance._id + '_hold'), + playlistActivationId: extendPieceInstance.PieceInstance.playlistActivationId, + rundownId: extendPieceInstance.PieceInstance.rundownId, partInstanceId: this.PartInstance._id, dynamicallyInserted: getCurrentTime(), piece: { - ...clone(extendPieceInstance.piece), + ...clone(extendPieceInstance.PieceInstance.piece), enable: { start: 0 }, extendOnHold: false, }, infinite: { infiniteInstanceId: infiniteInstanceId, infiniteInstanceIndex: 1, - infinitePieceId: extendPieceInstance.piece._id, + infinitePieceId: extendPieceInstance.PieceInstance.piece._id, fromPreviousPart: true, fromHold: true, }, // Preserve the timings from the playing instance - reportedStartedPlayback: extendPieceInstance.reportedStartedPlayback, - reportedStoppedPlayback: extendPieceInstance.reportedStoppedPlayback, - plannedStartedPlayback: extendPieceInstance.plannedStartedPlayback, - plannedStoppedPlayback: extendPieceInstance.plannedStoppedPlayback, + reportedStartedPlayback: extendPieceInstance.PieceInstance.reportedStartedPlayback, + reportedStoppedPlayback: extendPieceInstance.PieceInstance.reportedStoppedPlayback, + plannedStartedPlayback: extendPieceInstance.PieceInstance.plannedStartedPlayback, + plannedStoppedPlayback: extendPieceInstance.PieceInstance.plannedStoppedPlayback, } - this.PieceInstancesImpl.set(newInstance._id, { - doc: newInstance, - changed: true, - }) + const pieceInstanceModel = new PlayoutPieceInstanceModelImpl(newInstance, true) + this.PieceInstancesImpl.set(newInstance._id, pieceInstanceModel) - return newInstance - } - - setPieceInstanceDuration( - pieceInstanceId: PieceInstanceId, - duration: Required['userDuration'] - ): void { - const pieceInstance = this.PieceInstancesImpl.get(pieceInstanceId) - if (!pieceInstance?.doc) throw new Error(`PieceInstance ${pieceInstanceId} not found`) // TODO - is this ok? - - pieceInstance.changed = true - pieceInstance.doc.userDuration = duration + return pieceInstanceModel } insertVirtualPiece( @@ -527,7 +433,7 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { lifespan: PieceLifespan, sourceLayerId: string, outputLayerId: string - ): PieceInstance { + ): PlayoutPieceInstanceModel { const pieceId: PieceId = getRandomId() const newPieceInstance: PieceInstance = { _id: protectString(`${this.PartInstance._id}_${pieceId}`), @@ -554,19 +460,9 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { } setupPieceInstanceInfiniteProperties(newPieceInstance) - this.PieceInstancesImpl.set(newPieceInstance._id, { - doc: newPieceInstance, - changed: true, - }) - - return newPieceInstance - } - - setPieceInstanceDisabled(pieceInstanceId: PieceInstanceId, disabled: boolean): void { - const pieceInstance = this.PieceInstancesImpl.get(pieceInstanceId) - if (!pieceInstance?.doc) throw new Error(`PieceInstance ${pieceInstanceId} not found`) // TODO - is this ok? + const pieceInstanceModel = new PlayoutPieceInstanceModelImpl(newPieceInstance, true) + this.PieceInstancesImpl.set(newPieceInstance._id, pieceInstanceModel) - pieceInstance.changed = true - pieceInstance.doc.disabled = disabled + return pieceInstanceModel } } diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutPieceInstanceModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutPieceInstanceModelImpl.ts new file mode 100644 index 0000000000..2dcd3a77e3 --- /dev/null +++ b/packages/job-worker/src/playout/model/implementation/PlayoutPieceInstanceModelImpl.ts @@ -0,0 +1,113 @@ +import { PieceInstanceInfiniteId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { ReadonlyDeep } from 'type-fest' +import { PieceInstance, PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { clone, getRandomId } from '@sofie-automation/corelib/dist/lib' +import { Time } from '@sofie-automation/blueprints-integration' +import { PlayoutPieceInstanceModel } from '../PlayoutPieceInstanceModel' + +export class PlayoutPieceInstanceModelImpl implements PlayoutPieceInstanceModel { + PieceInstanceImpl: PieceInstance + + #HasChanges = false + get HasChanges(): boolean { + return this.#HasChanges + } + + setDirty(dirty = true): void { + this.#HasChanges = dirty + } + + get PieceInstance(): ReadonlyDeep { + return this.PieceInstanceImpl + } + + constructor(pieceInstances: PieceInstance, hasChanges: boolean) { + this.PieceInstanceImpl = pieceInstances + this.#HasChanges = hasChanges + } + + /** + * @deprecated + * What is the purpose of this? Without changing the ids it is going to clash with the old copy.. + * TODO - this has issues with deleting instances! + */ + clone(): PlayoutPieceInstanceModelImpl { + return new PlayoutPieceInstanceModelImpl(clone(this.PieceInstanceImpl), this.#HasChanges) + } + + updatePieceProps(props: Partial): void { + // TODO - this is missing a lot of validation + + this.#HasChanges = true + this.PieceInstanceImpl.piece = { + ...this.PieceInstanceImpl.piece, + ...props, + } + } + + setPlannedStartedPlayback(time: Time): boolean { + if (this.PieceInstanceImpl.plannedStartedPlayback !== time) { + this.PieceInstanceImpl.plannedStartedPlayback = time + delete this.PieceInstanceImpl.plannedStoppedPlayback + + this.#HasChanges = true + + return true + } + return false + } + setPlannedStoppedPlayback(time: Time | undefined): boolean { + if (this.PieceInstanceImpl.plannedStoppedPlayback !== time) { + this.PieceInstanceImpl.plannedStoppedPlayback = time + + this.#HasChanges = true + + return true + } + return false + } + setReportedStartedPlayback(time: Time): boolean { + if (this.PieceInstanceImpl.reportedStartedPlayback !== time) { + this.PieceInstanceImpl.reportedStartedPlayback = time + delete this.PieceInstanceImpl.reportedStoppedPlayback + + this.#HasChanges = true + + return true + } + return false + } + setReportedStoppedPlayback(time: Time): boolean { + if (this.PieceInstanceImpl.reportedStoppedPlayback !== time) { + this.PieceInstanceImpl.reportedStoppedPlayback = time + + this.#HasChanges = true + + return true + } + return false + } + + prepareForHold(): PieceInstanceInfiniteId { + const infiniteInstanceId: PieceInstanceInfiniteId = getRandomId() + this.PieceInstanceImpl.infinite = { + infiniteInstanceId: infiniteInstanceId, + infiniteInstanceIndex: 0, + infinitePieceId: this.PieceInstanceImpl.piece._id, + fromPreviousPart: false, + } + this.#HasChanges = true + + return infiniteInstanceId + } + + setDuration(duration: Required['userDuration']): void { + this.#HasChanges = true + this.PieceInstanceImpl.userDuration = duration + } + + setDisabled(disabled: boolean): void { + this.#HasChanges = true + this.PieceInstanceImpl.disabled = disabled + } +} diff --git a/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts b/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts index 434a859dd9..453b846fda 100644 --- a/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts +++ b/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts @@ -72,14 +72,14 @@ export function writePartInstancesAndPieceInstances( } for (const [pieceInstanceId, pieceInstance] of partInstance.PieceInstancesImpl.entries()) { - if (!pieceInstance.doc) { + if (!pieceInstance) { deletedPieceInstanceIds.push(pieceInstanceId) - } else if (pieceInstance.changed) { + } else if (pieceInstance.HasChanges) { // TODO - this does not perform any diffing pieceInstanceOps.push({ replaceOne: { filter: { _id: pieceInstanceId }, - replacement: pieceInstance.doc, + replacement: pieceInstance.PieceInstanceImpl, upsert: true, }, }) diff --git a/packages/job-worker/src/playout/pieces.ts b/packages/job-worker/src/playout/pieces.ts index efd7655acb..69dafc2bb4 100644 --- a/packages/job-worker/src/playout/pieces.ts +++ b/packages/job-worker/src/playout/pieces.ts @@ -10,6 +10,7 @@ import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' import { ReadonlyDeep } from 'type-fest' import { PlayoutPartInstanceModel } from './model/PlayoutPartInstanceModel' +import { PlayoutPieceInstanceModel } from './model/PlayoutPieceInstanceModel' /** * Approximate compare Piece start times (for use in .sort()) @@ -17,7 +18,11 @@ import { PlayoutPartInstanceModel } from './model/PlayoutPartInstanceModel' * @param b Second Piece * @param nowInPart Approximate time to substitute for 'now' */ -function comparePieceStart>(a: T, b: T, nowInPart: number): 0 | 1 | -1 { +export function comparePieceStart>( + a: T, + b: T, + nowInPart: number +): 0 | 1 | -1 { if (a.pieceType === IBlueprintPieceType.OutTransition && b.pieceType !== IBlueprintPieceType.OutTransition) { return 1 } else if (a.pieceType !== IBlueprintPieceType.OutTransition && b.pieceType === IBlueprintPieceType.OutTransition) { @@ -111,7 +116,7 @@ export function convertAdLibToPieceInstance( adLibPiece: AdLibPiece | Piece | BucketAdLib | PieceInstancePiece, partInstance: PlayoutPartInstanceModel, queue: boolean -): ReadonlyDeep { +): PlayoutPieceInstanceModel { const span = context.startSpan('convertAdLibToPieceInstance') let duration: number | undefined = undefined if ('expectedDuration' in adLibPiece && adLibPiece['expectedDuration']) { diff --git a/packages/job-worker/src/playout/resolvedPieces.ts b/packages/job-worker/src/playout/resolvedPieces.ts index f66eb57eff..c4d2b3d157 100644 --- a/packages/job-worker/src/playout/resolvedPieces.ts +++ b/packages/job-worker/src/playout/resolvedPieces.ts @@ -30,7 +30,11 @@ export function getResolvedPiecesForCurrentPartInstance( const partStarted = partInstance.PartInstance.timings?.plannedStartedPlayback const nowInPart = partStarted ? now - partStarted : 0 - const preprocessedPieces = processAndPrunePieceInstanceTimings(sourceLayers, partInstance.PieceInstances, nowInPart) + const preprocessedPieces = processAndPrunePieceInstanceTimings( + sourceLayers, + partInstance.PieceInstances.map((p) => p.PieceInstance), + nowInPart + ) return preprocessedPieces.map((instance) => resolvePrunedPieceInstance(nowInPart, instance)) } diff --git a/packages/job-worker/src/playout/take.ts b/packages/job-worker/src/playout/take.ts index 1090f6bf8e..03225b9603 100644 --- a/packages/job-worker/src/playout/take.ts +++ b/packages/job-worker/src/playout/take.ts @@ -436,13 +436,13 @@ export function updatePartInstanceOnTake( // calculate and cache playout timing properties, so that we don't depend on the previousPartInstance: const tmpTakePieces = processAndPrunePieceInstanceTimings( showStyle.sourceLayers, - takePartInstance.PieceInstances, + takePartInstance.PieceInstances.map((p) => p.PieceInstance), 0 ) const partPlayoutTimings = calculatePartTimings( playlist.holdState, currentPartInstance?.PartInstance?.part, - currentPartInstance?.PieceInstances?.map((p) => p.piece) ?? [], + currentPartInstance?.PieceInstances?.map((p) => p.PieceInstance.piece) ?? [], takePartInstance.PartInstance.part, tmpTakePieces.filter((p) => !p.infinite || p.infinite.infiniteInstanceIndex === 0).map((p) => p.piece) ) @@ -494,20 +494,23 @@ function startHold( const span = context.startSpan('startHold') // Make a copy of any item which is flagged as an 'infinite' extension - const pieceInstancesToCopy = holdFromPartInstance.PieceInstances.filter((p) => !!p.piece.extendOnHold) + const pieceInstancesToCopy = holdFromPartInstance.PieceInstances.filter((p) => !!p.PieceInstance.piece.extendOnHold) pieceInstancesToCopy.forEach((instance) => { - if (!instance.infinite) { + if (!instance.PieceInstance.infinite) { // mark current one as infinite - const infiniteInstanceId = holdFromPartInstance.preparePieceInstanceForHold(instance._id) + const infiniteInstanceId = instance.prepareForHold() // This gets deleted once the nextpart is activated, so it doesnt linger for long const extendedPieceInstance = holdToPartInstance.addHoldPieceInstance(instance, infiniteInstanceId) - const content = clone(instance.piece.content) as VTContent | undefined - if (content?.fileName && content.sourceDuration && instance.plannedStartedPlayback) { - content.seek = Math.min(content.sourceDuration, getCurrentTime() - instance.plannedStartedPlayback) + const content = clone(instance.PieceInstance.piece.content) as VTContent | undefined + if (content?.fileName && content.sourceDuration && instance.PieceInstance.plannedStartedPlayback) { + content.seek = Math.min( + content.sourceDuration, + getCurrentTime() - instance.PieceInstance.plannedStartedPlayback + ) } - holdToPartInstance.updatePieceProps(extendedPieceInstance._id, { content }) + extendedPieceInstance.updatePieceProps({ content }) } }) if (span) span.end() diff --git a/packages/job-worker/src/playout/timeline/generate.ts b/packages/job-worker/src/playout/timeline/generate.ts index cfdd7f243c..8021ae1c3d 100644 --- a/packages/job-worker/src/playout/timeline/generate.ts +++ b/packages/job-worker/src/playout/timeline/generate.ts @@ -274,7 +274,11 @@ function getPartInstanceTimelineInfo( if (partInstance) { const partStarted = partInstance.PartInstance.timings?.plannedStartedPlayback const nowInPart = partStarted === undefined ? 0 : currentTime - partStarted - const pieceInstances = processAndPrunePieceInstanceTimings(sourceLayers, partInstance.PieceInstances, nowInPart) + const pieceInstances = processAndPrunePieceInstanceTimings( + sourceLayers, + partInstance.PieceInstances.map((p) => p.PieceInstance), + nowInPart + ) return { partInstance: partInstance.PartInstance, diff --git a/packages/job-worker/src/playout/timeline/multi-gateway.ts b/packages/job-worker/src/playout/timeline/multi-gateway.ts index 454f8f6077..684fb03faf 100644 --- a/packages/job-worker/src/playout/timeline/multi-gateway.ts +++ b/packages/job-worker/src/playout/timeline/multi-gateway.ts @@ -1,7 +1,6 @@ import { Time } from '@sofie-automation/blueprints-integration' import { PieceInstanceInfiniteId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PeripheralDeviceType } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' -import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { TimelineObjRundown } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { normalizeArray } from '@sofie-automation/corelib/dist/lib' import { PieceTimelineMetadata } from './pieceGroup' @@ -13,7 +12,7 @@ import { RundownTimelineTimingContext, getInfinitePartGroupId } from './rundown' import { getExpectedLatency } from '@sofie-automation/corelib/dist/studio/playout' import { getPieceControlObjectId } from '@sofie-automation/corelib/dist/playout/ids' import { PlayoutPartInstanceModel } from '../model/PlayoutPartInstanceModel' -import { ReadonlyDeep } from 'type-fest' +import { PlayoutPieceInstanceModel } from '../model/PlayoutPieceInstanceModel' /** * We want it to be possible to generate a timeline without it containing any `start: 'now'`. @@ -153,10 +152,10 @@ function deNowifyCurrentPieces( // Ensure any pieces in the currentPartInstance have their now replaced for (const pieceInstance of currentPartInstance.PieceInstances) { - if (pieceInstance.piece.enable.start === 'now') { - currentPartInstance.updatePieceProps(pieceInstance._id, { + if (pieceInstance.PieceInstance.piece.enable.start === 'now') { + pieceInstance.updatePieceProps({ enable: { - ...pieceInstance.piece.enable, + ...pieceInstance.PieceInstance.piece.enable, start: nowInPart, }, }) @@ -178,19 +177,22 @@ function deNowifyCurrentPieces( // Ensure any pieces with an unconfirmed userDuration is confirmed for (const pieceInstance of currentPartInstance.PieceInstances) { - if (pieceInstance.userDuration && 'endRelativeToNow' in pieceInstance.userDuration) { - const relativeToNow = pieceInstance.userDuration.endRelativeToNow + if ( + pieceInstance.PieceInstance.userDuration && + 'endRelativeToNow' in pieceInstance.PieceInstance.userDuration + ) { + const relativeToNow = pieceInstance.PieceInstance.userDuration.endRelativeToNow const endRelativeToPart = relativeToNow + nowInPart - currentPartInstance.setPieceInstanceDuration(pieceInstance._id, { endRelativeToPart }) + pieceInstance.setDuration({ endRelativeToPart }) // Update the piece control obj - const controlObj = timelineObjsMap[getPieceControlObjectId(pieceInstance)] + const controlObj = timelineObjsMap[getPieceControlObjectId(pieceInstance.PieceInstance)] if (controlObj && !Array.isArray(controlObj.enable) && controlObj.enable.end === 'now') { controlObj.enable.end = endRelativeToPart } // If the piece is an infinite, there may be a now in the parent group - const infiniteGroup = timelineObjsMap[getInfinitePartGroupId(pieceInstance._id)] + const infiniteGroup = timelineObjsMap[getInfinitePartGroupId(pieceInstance.PieceInstance._id)] if (infiniteGroup && !Array.isArray(infiniteGroup.enable) && infiniteGroup.enable.end === 'now') { infiniteGroup.enable.end = targetNowTime + relativeToNow } @@ -210,9 +212,12 @@ function updatePlannedTimingsForPieceInstances( const pieceInstances = previousPartInstance.PieceInstances for (const pieceInstance of pieceInstances) { // Track the timings for the infinites - const plannedStartedPlayback = pieceInstance.plannedStartedPlayback - if (pieceInstance.infinite && plannedStartedPlayback) { - existingInfiniteTimings.set(pieceInstance.infinite.infiniteInstanceId, plannedStartedPlayback) + const plannedStartedPlayback = pieceInstance.PieceInstance.plannedStartedPlayback + if (pieceInstance.PieceInstance.infinite && plannedStartedPlayback) { + existingInfiniteTimings.set( + pieceInstance.PieceInstance.infinite.infiniteInstanceId, + plannedStartedPlayback + ) } } } @@ -220,78 +225,77 @@ function updatePlannedTimingsForPieceInstances( // Ensure any pieces have up to date timings for (const pieceInstance of currentPartInstance.PieceInstances) { setPlannedTimingsOnPieceInstance( - currentPartInstance, pieceInstance, partGroupTimings.currentStartTime, partGroupTimings.currentEndTime ) - preserveOrTrackInfiniteTimings(existingInfiniteTimings, timelineObjsMap, currentPartInstance, pieceInstance) + preserveOrTrackInfiniteTimings(existingInfiniteTimings, timelineObjsMap, pieceInstance) } const nextPartInstance = cache.NextPartInstance if (nextPartInstance && partGroupTimings.nextStartTime) { const nextPartGroupStartTime0 = partGroupTimings.nextStartTime for (const pieceInstance of nextPartInstance.PieceInstances) { - setPlannedTimingsOnPieceInstance(nextPartInstance, pieceInstance, nextPartGroupStartTime0, undefined) - preserveOrTrackInfiniteTimings(existingInfiniteTimings, timelineObjsMap, nextPartInstance, pieceInstance) + setPlannedTimingsOnPieceInstance(pieceInstance, nextPartGroupStartTime0, undefined) + preserveOrTrackInfiniteTimings(existingInfiniteTimings, timelineObjsMap, pieceInstance) } } } function setPlannedTimingsOnPieceInstance( - partInstance: PlayoutPartInstanceModel, - pieceInstance: ReadonlyDeep, + pieceInstance: PlayoutPieceInstanceModel, partPlannedStart: Time, partPlannedEnd: Time | undefined ): void { if ( - pieceInstance.infinite && - pieceInstance.infinite.infiniteInstanceIndex > 0 && - pieceInstance.plannedStartedPlayback + pieceInstance.PieceInstance.infinite && + pieceInstance.PieceInstance.infinite.infiniteInstanceIndex > 0 && + pieceInstance.PieceInstance.plannedStartedPlayback ) { // If not the start of an infinite chain, then the plannedStartedPlayback flows differently return } - if (typeof pieceInstance.piece.enable.start === 'number') { - const plannedStart = partPlannedStart + pieceInstance.piece.enable.start - partInstance.setPieceInstancedPlannedStartedPlayback(pieceInstance._id, plannedStart) + if (typeof pieceInstance.PieceInstance.piece.enable.start === 'number') { + const plannedStart = partPlannedStart + pieceInstance.PieceInstance.piece.enable.start + pieceInstance.setPlannedStartedPlayback(plannedStart) const userDurationEnd = - pieceInstance.userDuration && 'endRelativeToPart' in pieceInstance.userDuration - ? pieceInstance.userDuration.endRelativeToPart + pieceInstance.PieceInstance.userDuration && 'endRelativeToPart' in pieceInstance.PieceInstance.userDuration + ? pieceInstance.PieceInstance.userDuration.endRelativeToPart : null const plannedEnd = userDurationEnd ?? - (pieceInstance.piece.enable.duration ? plannedStart + pieceInstance.piece.enable.duration : partPlannedEnd) + (pieceInstance.PieceInstance.piece.enable.duration + ? plannedStart + pieceInstance.PieceInstance.piece.enable.duration + : partPlannedEnd) - partInstance.setPieceInstancedPlannedStoppedPlayback(pieceInstance._id, plannedEnd) + pieceInstance.setPlannedStoppedPlayback(plannedEnd) } } function preserveOrTrackInfiniteTimings( existingInfiniteTimings: Map, timelineObjsMap: Record, - partInstance: PlayoutPartInstanceModel, - pieceInstance: ReadonlyDeep + pieceInstance: PlayoutPieceInstanceModel ): void { - if (!pieceInstance.infinite) return + if (!pieceInstance.PieceInstance.infinite) return - const plannedStartedPlayback = existingInfiniteTimings.get(pieceInstance.infinite.infiniteInstanceId) + const plannedStartedPlayback = existingInfiniteTimings.get(pieceInstance.PieceInstance.infinite.infiniteInstanceId) if (plannedStartedPlayback) { // Found a value from the previousPartInstance, lets preserve it - partInstance.setPieceInstancedPlannedStartedPlayback(pieceInstance._id, plannedStartedPlayback) + pieceInstance.setPlannedStartedPlayback(plannedStartedPlayback) } else { - const plannedStartedPlayback = pieceInstance.plannedStartedPlayback + const plannedStartedPlayback = pieceInstance.PieceInstance.plannedStartedPlayback if (plannedStartedPlayback) { - existingInfiniteTimings.set(pieceInstance.infinite.infiniteInstanceId, plannedStartedPlayback) + existingInfiniteTimings.set(pieceInstance.PieceInstance.infinite.infiniteInstanceId, plannedStartedPlayback) } } // Update the timeline group - const startedPlayback = plannedStartedPlayback ?? pieceInstance.plannedStartedPlayback + const startedPlayback = plannedStartedPlayback ?? pieceInstance.PieceInstance.plannedStartedPlayback if (startedPlayback) { - const infinitePartGroupId = getInfinitePartGroupId(pieceInstance._id) + const infinitePartGroupId = getInfinitePartGroupId(pieceInstance.PieceInstance._id) const infinitePartGroupObj = timelineObjsMap[infinitePartGroupId] if ( infinitePartGroupObj && diff --git a/packages/job-worker/src/playout/timings/piecePlayback.ts b/packages/job-worker/src/playout/timings/piecePlayback.ts index 19fa51e1d7..2efa0ecd16 100644 --- a/packages/job-worker/src/playout/timings/piecePlayback.ts +++ b/packages/job-worker/src/playout/timings/piecePlayback.ts @@ -3,9 +3,8 @@ import { logger } from '../../logging' import { JobContext } from '../../jobs' import { PlayoutModel } from '../model/PlayoutModel' import { Time } from '@sofie-automation/blueprints-integration' -import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { PlayoutPartInstanceModel } from '../model/PlayoutPartInstanceModel' -import { ReadonlyDeep } from 'type-fest' +import { PlayoutPieceInstanceModel } from '../model/PlayoutPieceInstanceModel' /** * Set the playback of a piece is confirmed to have started @@ -44,7 +43,9 @@ export function onPiecePlaybackStarted( return } - const isPlaying = !!(pieceInstance.reportedStartedPlayback && !pieceInstance.reportedStoppedPlayback) + const isPlaying = !!( + pieceInstance.PieceInstance.reportedStartedPlayback && !pieceInstance.PieceInstance.reportedStoppedPlayback + ) if (!isPlaying) { logger.debug( `onPiecePlaybackStarted: Playout reports pieceInstance "${ @@ -90,7 +91,9 @@ export function onPiecePlaybackStopped( return } - const isPlaying = !!(pieceInstance.reportedStartedPlayback && !pieceInstance.reportedStoppedPlayback) + const isPlaying = !!( + pieceInstance.PieceInstance.reportedStartedPlayback && !pieceInstance.PieceInstance.reportedStoppedPlayback + ) if (isPlaying) { logger.debug( `onPiecePlaybackStopped: Playout reports pieceInstance "${ @@ -98,7 +101,7 @@ export function onPiecePlaybackStopped( }" has stopped playback on timestamp ${new Date(data.stoppedPlayback).toISOString()}` ) - reportPieceHasStopped(context, cache, partInstance, pieceInstance, data.stoppedPlayback) + reportPieceHasStopped(context, cache, pieceInstance, data.stoppedPlayback) } } @@ -113,32 +116,32 @@ function reportPieceHasStarted( _context: JobContext, cache: PlayoutModel, partInstance: PlayoutPartInstanceModel, - pieceInstance: ReadonlyDeep, + pieceInstance: PlayoutPieceInstanceModel, timestamp: Time ): void { - const timestampChanged = partInstance.setPieceInstancedReportedStartedPlayback(pieceInstance._id, timestamp) + const timestampChanged = pieceInstance.setReportedStartedPlayback(timestamp) if (timestampChanged) { if (!cache.isMultiGatewayMode) { - partInstance.setPieceInstancedPlannedStartedPlayback(pieceInstance._id, timestamp) + pieceInstance.setPlannedStartedPlayback(timestamp) } // Update the copy in the next-part if there is one, so that the infinite has the same start after a take const nextPartInstance = cache.NextPartInstance if ( - pieceInstance.infinite && + pieceInstance.PieceInstance.infinite && nextPartInstance && nextPartInstance.PartInstance._id !== partInstance.PartInstance._id ) { - const infiniteInstanceId = pieceInstance.infinite.infiniteInstanceId + const infiniteInstanceId = pieceInstance.PieceInstance.infinite.infiniteInstanceId for (const nextPieceInstance of nextPartInstance.PieceInstances) { if ( - !!nextPieceInstance.infinite && - nextPieceInstance.infinite.infiniteInstanceId === infiniteInstanceId + !!nextPieceInstance.PieceInstance.infinite && + nextPieceInstance.PieceInstance.infinite.infiniteInstanceId === infiniteInstanceId ) { - nextPartInstance.setPieceInstancedReportedStartedPlayback(nextPieceInstance._id, timestamp) + nextPieceInstance.setReportedStartedPlayback(timestamp) if (!cache.isMultiGatewayMode) { - nextPartInstance.setPieceInstancedPlannedStartedPlayback(nextPieceInstance._id, timestamp) + nextPieceInstance.setPlannedStartedPlayback(timestamp) } } } @@ -158,17 +161,16 @@ function reportPieceHasStarted( function reportPieceHasStopped( _context: JobContext, cache: PlayoutModel, - partInstance: PlayoutPartInstanceModel, - pieceInstance: ReadonlyDeep, + pieceInstance: PlayoutPieceInstanceModel, timestamp: Time ): void { - const timestampChanged = partInstance.setPieceInstancedReportedStoppedPlayback(pieceInstance._id, timestamp) + const timestampChanged = pieceInstance.setReportedStoppedPlayback(timestamp) if (timestampChanged) { if (!cache.isMultiGatewayMode) { - partInstance.setPieceInstancedPlannedStoppedPlayback(pieceInstance._id, timestamp) + pieceInstance.setPlannedStoppedPlayback(timestamp) } - cache.queuePartInstanceTimingEvent(partInstance.PartInstance._id) + cache.queuePartInstanceTimingEvent(pieceInstance.PieceInstance.partInstanceId) } } From d28511a0501254d9c432cf0a751b8b43e86f3661 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 11 Oct 2023 16:54:55 +0100 Subject: [PATCH 121/479] feat: rework CacheForPlayout as structured objects SOFIE-2513 --- .../__tests__/context-adlibActions.test.ts | 378 +++++----- .../context/RundownActivationContext.ts | 8 +- .../SyncIngestUpdateToPartInstanceContext.ts | 21 +- .../src/blueprints/context/adlibActions.ts | 95 +-- packages/job-worker/src/cache/CacheBase.ts | 5 + .../src/ingest/__tests__/ingest.test.ts | 30 +- .../src/ingest/__tests__/updateNext.test.ts | 4 +- packages/job-worker/src/ingest/commit.ts | 8 +- .../src/ingest/syncChangesToPartInstance.ts | 74 +- packages/job-worker/src/ingest/updateNext.ts | 22 +- packages/job-worker/src/peripheralDevice.ts | 7 +- .../src/playout/__tests__/actions.test.ts | 18 +- .../src/playout/__tests__/infinites.test.ts | 39 +- .../src/playout/__tests__/timeline.test.ts | 29 +- .../src/playout/activePlaylistActions.ts | 73 +- .../src/playout/activePlaylistJobs.ts | 48 +- .../job-worker/src/playout/adlibAction.ts | 42 +- packages/job-worker/src/playout/adlibJobs.ts | 114 +-- packages/job-worker/src/playout/adlibUtils.ts | 114 +-- packages/job-worker/src/playout/datastore.ts | 4 +- packages/job-worker/src/playout/debug.ts | 34 +- packages/job-worker/src/playout/holdJobs.ts | 32 +- packages/job-worker/src/playout/infinites.ts | 48 +- packages/job-worker/src/playout/lib.ts | 97 +-- packages/job-worker/src/playout/lock.ts | 22 +- .../lookahead/__tests__/lookahead.test.ts | 54 +- .../playout/lookahead/__tests__/util.test.ts | 50 +- .../job-worker/src/playout/lookahead/index.ts | 8 +- .../job-worker/src/playout/lookahead/util.ts | 12 +- .../src/playout/model/PlayoutModel.ts | 78 +- .../playout/model/PlayoutPartInstanceModel.ts | 121 ++-- .../model/PlayoutPieceInstanceModel.ts | 12 +- .../src/playout/model/PlayoutSegmentModel.ts | 4 +- .../model/implementation/LoadPlayoutModel.ts | 22 +- .../model/implementation/PlayoutModelImpl.ts | 680 ++++++++++-------- .../PlayoutPartInstanceModelImpl.ts | 550 +++++++------- .../PlayoutPieceInstanceModelImpl.ts | 137 ++-- .../implementation/PlayoutSegmentModelImpl.ts | 8 +- .../model/implementation/SavePlayoutModel.ts | 1 - .../job-worker/src/playout/moveNextPart.ts | 16 +- packages/job-worker/src/playout/pieces.ts | 37 +- packages/job-worker/src/playout/scratchpad.ts | 26 +- packages/job-worker/src/playout/setNext.ts | 129 ++-- .../job-worker/src/playout/setNextJobs.ts | 56 +- packages/job-worker/src/playout/take.ts | 142 ++-- .../src/playout/timeline/generate.ts | 71 +- .../src/playout/timeline/multi-gateway.ts | 30 +- .../src/playout/timeline/rundown.ts | 4 +- .../job-worker/src/playout/timelineJobs.ts | 28 +- .../job-worker/src/playout/timings/index.ts | 12 +- .../src/playout/timings/partPlayback.ts | 74 +- .../src/playout/timings/piecePlayback.ts | 40 +- .../playout/timings/timelineTriggerTime.ts | 10 +- packages/job-worker/src/rundown.ts | 8 +- packages/job-worker/src/rundownPlaylists.ts | 4 +- .../src/studio/StudioPlayoutModelImpl.ts | 2 +- packages/job-worker/src/studio/cleanup.ts | 6 +- packages/job-worker/src/studio/lock.ts | 14 +- 58 files changed, 1988 insertions(+), 1824 deletions(-) diff --git a/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts b/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts index 22c5614f5c..b4533808e9 100644 --- a/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts +++ b/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts @@ -11,7 +11,7 @@ import { isTooCloseToAutonext } from '../../playout/lib' import { PlayoutModel } from '../../playout/model/PlayoutModel' import { WatchedPackagesHelper } from '../context/watchedPackages' import { MockJobContext, setupDefaultJobEnvironment } from '../../__mocks__/context' -import { runJobWithPlayoutCache } from '../../playout/lock' +import { runJobWithPlayoutModel } from '../../playout/lock' import { defaultRundownPlaylist } from '../../__mocks__/defaultCollectionObjects' import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { clone, getRandomId, literal, normalizeArrayToMapFunc, omit } from '@sofie-automation/corelib/dist/lib' @@ -42,9 +42,12 @@ import * as PlayoutAdlib from '../../playout/adlibUtils' type TinnerStopPieces = jest.MockedFunction const innerStopPiecesMock = jest.spyOn(PlayoutAdlib, 'innerStopPieces') as TinnerStopPieces -const innerStartQueuedAdLibOrig = PlayoutAdlib.innerStartQueuedAdLib -type TinnerStartQueuedAdLib = jest.MockedFunction -const innerStartQueuedAdLibMock = jest.spyOn(PlayoutAdlib, 'innerStartQueuedAdLib') as TinnerStartQueuedAdLib +const insertQueuedPartWithPiecesOrig = PlayoutAdlib.insertQueuedPartWithPieces +type TinsertQueuedPartWithPieces = jest.MockedFunction +const insertQueuedPartWithPiecesMock = jest.spyOn( + PlayoutAdlib, + 'insertQueuedPartWithPieces' +) as TinsertQueuedPartWithPieces jest.mock('../../playout/resolvedPieces') import { getResolvedPiecesForCurrentPartInstance } from '../../playout/resolvedPieces' @@ -141,15 +144,10 @@ describe('Test blueprint api context', () => { ) } - // let context: MockJobContext - // beforeAll(async () => { - // context = await setupDefaultJobEnvironment() - // }) - - async function getActionExecutionContext(jobContext: JobContext, cache: PlayoutModel) { - const playlist = cache.Playlist + async function getActionExecutionContext(jobContext: JobContext, playoutModel: PlayoutModel) { + const playlist = playoutModel.Playlist expect(playlist).toBeTruthy() - const rundown = cache.Rundowns[0] + const rundown = playoutModel.Rundowns[0] expect(rundown).toBeTruthy() const activationId = playlist.activationId as RundownPlaylistActivationId @@ -169,7 +167,7 @@ describe('Test blueprint api context', () => { identifier: 'action', }, jobContext, - cache, + playoutModel, showStyle, showStyleConfig, rundown, @@ -185,12 +183,12 @@ describe('Test blueprint api context', () => { } } - async function wrapWithCache( + async function wrapWithPlayoutModel( context: JobContext, playlistId: RundownPlaylistId, - fcn: (cache: PlayoutModel) => Promise + fcn: (playoutModel: PlayoutModel) => Promise ): Promise { - return runJobWithPlayoutCache(context, { playlistId }, null, fcn) + return runJobWithPlayoutModel(context, { playlistId }, null, fcn) } async function setupMyDefaultRundown(): Promise<{ @@ -230,7 +228,7 @@ describe('Test blueprint api context', () => { async function saveAllToDatabase( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, allPartInstances: PlayoutPartInstanceModel[] ) { // We need to push changes back to 'mongo' for these tests @@ -240,7 +238,7 @@ describe('Test blueprint api context', () => { normalizeArrayToMapFunc(allPartInstances as PlayoutPartInstanceModelImpl[], (p) => p.PartInstance._id) ) ) - await cache.saveAllToDatabase() + await playoutModel.saveAllToDatabase() } async function setPartInstances( @@ -307,8 +305,8 @@ describe('Test blueprint api context', () => { test('invalid parameters', async () => { const { jobContext, playlistId } = await setupMyDefaultRundown() - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) // @ts-ignore await expect(context.getPartInstance()).rejects.toThrow('Unknown part "undefined"') @@ -324,20 +322,20 @@ describe('Test blueprint api context', () => { test('valid parameters', async () => { const { jobContext, playlistId, allPartInstances } = await setupMyDefaultRundown() - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) - expect(cache.LoadedPartInstances).toHaveLength(0) + expect(playoutModel.LoadedPartInstances).toHaveLength(0) await expect(context.getPartInstance('next')).resolves.toBeUndefined() await expect(context.getPartInstance('current')).resolves.toBeUndefined() }) await setPartInstances(jobContext, playlistId, allPartInstances[1], undefined) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) - expect(cache.LoadedPartInstances).toHaveLength(2) + expect(playoutModel.LoadedPartInstances).toHaveLength(2) // Check the current part await expect(context.getPartInstance('next')).resolves.toBeUndefined() @@ -347,10 +345,10 @@ describe('Test blueprint api context', () => { }) await setPartInstances(jobContext, playlistId, null, allPartInstances[2]) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) - expect(cache.LoadedPartInstances).toHaveLength(3) + expect(playoutModel.LoadedPartInstances).toHaveLength(3) // Now the next part await expect(context.getPartInstance('next')).resolves.toMatchObject({ @@ -364,8 +362,8 @@ describe('Test blueprint api context', () => { test('invalid parameters', async () => { const { jobContext, playlistId } = await setupMyDefaultRundown() - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) // @ts-ignore await expect(context.getPieceInstances()).rejects.toThrow('Unknown part "undefined"') @@ -381,20 +379,20 @@ describe('Test blueprint api context', () => { test('valid parameters', async () => { const { jobContext, playlistId, allPartInstances } = await setupMyDefaultRundown() - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) - expect(cache.LoadedPartInstances).toHaveLength(0) + expect(playoutModel.LoadedPartInstances).toHaveLength(0) await expect(context.getPieceInstances('next')).resolves.toHaveLength(0) await expect(context.getPieceInstances('current')).resolves.toHaveLength(0) }) await setPartInstances(jobContext, playlistId, allPartInstances[1], undefined) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) - expect(cache.LoadedPartInstances).toHaveLength(2) + expect(playoutModel.LoadedPartInstances).toHaveLength(2) // Check the current part await expect(context.getPieceInstances('next')).resolves.toHaveLength(0) @@ -402,10 +400,10 @@ describe('Test blueprint api context', () => { }) await setPartInstances(jobContext, playlistId, null, allPartInstances[2]) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) - expect(cache.LoadedPartInstances).toHaveLength(3) + expect(playoutModel.LoadedPartInstances).toHaveLength(3) // Now the next part await expect(context.getPieceInstances('next')).resolves.toHaveLength(1) @@ -417,8 +415,8 @@ describe('Test blueprint api context', () => { test('invalid parameters', async () => { const { jobContext, playlistId } = await setupMyDefaultRundown() - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) // @ts-ignore await expect(context.getResolvedPieceInstances()).rejects.toThrow('Unknown part "undefined"') @@ -436,10 +434,10 @@ describe('Test blueprint api context', () => { test('valid parameters', async () => { const { jobContext, playlistId, allPartInstances } = await setupMyDefaultRundown() - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) - expect(cache.LoadedPartInstances).toHaveLength(0) + expect(playoutModel.LoadedPartInstances).toHaveLength(0) expect(getResolvedPiecesForCurrentPartInstanceMock).toHaveBeenCalledTimes(0) @@ -476,10 +474,10 @@ describe('Test blueprint api context', () => { ) await setPartInstances(jobContext, playlistId, allPartInstances[1], undefined) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) - expect(cache.LoadedPartInstances).toHaveLength(2) + expect(playoutModel.LoadedPartInstances).toHaveLength(2) // Check the current part await expect(context.getResolvedPieceInstances('next')).resolves.toHaveLength(0) await expect( @@ -493,10 +491,10 @@ describe('Test blueprint api context', () => { getResolvedPiecesForCurrentPartInstanceMock.mockClear() await setPartInstances(jobContext, playlistId, null, allPartInstances[2]) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) - expect(cache.LoadedPartInstances).toHaveLength(3) + expect(playoutModel.LoadedPartInstances).toHaveLength(3) // Now the next part await expect( @@ -512,11 +510,11 @@ describe('Test blueprint api context', () => { test('invalid parameters', async () => { const { jobContext, playlistId } = await setupMyDefaultRundown() - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) // We need to push changes back to 'mongo' for these tests - await cache.saveAllToDatabase() + await playoutModel.saveAllToDatabase() // @ts-ignore await expect(context.findLastPieceOnLayer()).resolves.toBeUndefined() @@ -528,15 +526,15 @@ describe('Test blueprint api context', () => { test('basic and original only', async () => { const { jobContext, playlistId, allPartInstances } = await setupMyDefaultRundown() - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) allPartInstances[0].setTaken(getCurrentTime(), 0) // We need to push changes back to 'mongo' for these tests - await saveAllToDatabase(jobContext, cache, allPartInstances) + await saveAllToDatabase(jobContext, playoutModel, allPartInstances) - // const allPartInstances = cache.SortedLoadedPartInstances + // const allPartInstances = playoutModel.SortedLoadedPartInstances expect(allPartInstances).toHaveLength(5) const sourceLayerIds = Object.keys(context.showStyleCompound.sourceLayers) @@ -565,7 +563,7 @@ describe('Test blueprint api context', () => { ) insertedPieceInstance.setPlannedStartedPlayback(1000) // We need to push changes back to 'mongo' for these tests - await saveAllToDatabase(jobContext, cache, allPartInstances) + await saveAllToDatabase(jobContext, playoutModel, allPartInstances) await expect(context.findLastPieceOnLayer(sourceLayerIds[0])).resolves.toMatchObject({ _id: insertedPieceInstance.PieceInstance._id, @@ -594,7 +592,7 @@ describe('Test blueprint api context', () => { insertedPieceInstance2.setPlannedStartedPlayback(2000) // We need to push changes back to 'mongo' for these tests - await saveAllToDatabase(jobContext, cache, allPartInstances) + await saveAllToDatabase(jobContext, playoutModel, allPartInstances) await expect(context.findLastPieceOnLayer(sourceLayerIds[0])).resolves.toMatchObject({ _id: insertedPieceInstance2.PieceInstance._id, @@ -610,13 +608,13 @@ describe('Test blueprint api context', () => { await setPartInstances(jobContext, playlistId, allPartInstances[2], undefined) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) allPartInstances[0].setTaken(getCurrentTime(), 0) // We need to push changes back to 'mongo' for these tests - await saveAllToDatabase(jobContext, cache, allPartInstances) + await saveAllToDatabase(jobContext, playoutModel, allPartInstances) expect(allPartInstances).toHaveLength(5) @@ -663,7 +661,7 @@ describe('Test blueprint api context', () => { ) insertedPieceInstance2.setPlannedStartedPlayback(2000) // We need to push changes back to 'mongo' for these tests - await saveAllToDatabase(jobContext, cache, allPartInstances) + await saveAllToDatabase(jobContext, playoutModel, allPartInstances) // Check it await expect(context.findLastPieceOnLayer(sourceLayerIds[0])).resolves.toMatchObject({ @@ -682,13 +680,13 @@ describe('Test blueprint api context', () => { await setPartInstances(jobContext, playlistId, allPartInstances[2], undefined) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) allPartInstances[0].setTaken(getCurrentTime(), 0) // We need to push changes back to 'mongo' for these tests - await saveAllToDatabase(jobContext, cache, allPartInstances) + await saveAllToDatabase(jobContext, playoutModel, allPartInstances) expect(allPartInstances).toHaveLength(5) @@ -739,7 +737,7 @@ describe('Test blueprint api context', () => { ) insertedPieceInstance2.setPlannedStartedPlayback(2000) // We need to push changes back to 'mongo' for these tests - await saveAllToDatabase(jobContext, cache, allPartInstances) + await saveAllToDatabase(jobContext, playoutModel, allPartInstances) // Check it await expect(context.findLastPieceOnLayer(sourceLayerIds[0])).resolves.toMatchObject({ @@ -766,11 +764,11 @@ describe('Test blueprint api context', () => { test('No Current Part', async () => { const { jobContext, playlistId } = await setupMyDefaultRundown() - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) // We need to push changes back to 'mongo' for these tests - await cache.saveAllToDatabase() + await playoutModel.saveAllToDatabase() const sourceLayerIds = Object.keys(context.showStyleCompound.sourceLayers) expect(sourceLayerIds).toHaveLength(4) @@ -801,15 +799,15 @@ describe('Test blueprint api context', () => { // Set Part 1 as current part await setPartInstances(jobContext, playlistId, partInstances[0], undefined) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) const sourceLayerIds = Object.keys(context.showStyleCompound.sourceLayers) expect(sourceLayerIds).toHaveLength(4) const expectedPieceInstanceSourceLayer0 = pieceInstances.find( (p) => - p.partInstanceId === cache.Playlist.currentPartInfo?.partInstanceId && + p.partInstanceId === playoutModel.Playlist.currentPartInfo?.partInstanceId && p.piece.sourceLayerId === sourceLayerIds[0] ) expect(expectedPieceInstanceSourceLayer0).not.toBeUndefined() @@ -823,7 +821,7 @@ describe('Test blueprint api context', () => { const expectedPieceInstanceSourceLayer1 = pieceInstances.find( (p) => - p.partInstanceId === cache.Playlist.currentPartInfo?.partInstanceId && + p.partInstanceId === playoutModel.Playlist.currentPartInfo?.partInstanceId && p.piece.sourceLayerId === sourceLayerIds[1] ) expect(expectedPieceInstanceSourceLayer1).not.toBeUndefined() @@ -852,8 +850,8 @@ describe('Test blueprint api context', () => { // Set Part 1 as current part await setPartInstances(jobContext, playlistId, partInstances[0], undefined) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) const sourceLayerIds = Object.keys(context.showStyleCompound.sourceLayers) expect(sourceLayerIds).toHaveLength(4) @@ -884,15 +882,15 @@ describe('Test blueprint api context', () => { // Set Part 2 as current part await setPartInstances(jobContext, playlistId, partInstances[1], undefined) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) const sourceLayerIds = Object.keys(context.showStyleCompound.sourceLayers) expect(sourceLayerIds).toHaveLength(4) const expectedPieceInstanceSourceLayer0 = pieceInstances.find( (p) => - p.partInstanceId === cache.Playlist.currentPartInfo?.partInstanceId && + p.partInstanceId === playoutModel.Playlist.currentPartInfo?.partInstanceId && p.piece.sourceLayerId === sourceLayerIds[0] ) expect(expectedPieceInstanceSourceLayer0).not.toBeUndefined() @@ -924,8 +922,8 @@ describe('Test blueprint api context', () => { test('invalid parameters', async () => { const { jobContext, playlistId } = await setupMyDefaultRundown() - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) // @ts-ignore await expect(context.getPartInstanceForPreviousPiece()).rejects.toThrow( @@ -958,10 +956,10 @@ describe('Test blueprint api context', () => { const { jobContext, playlistId, allPartInstances } = await setupMyDefaultRundown() // Try with nothing in the cache - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) - expect(cache.LoadedPartInstances).toHaveLength(0) + expect(playoutModel.LoadedPartInstances).toHaveLength(0) await expect( context.getPartInstanceForPreviousPiece({ @@ -983,10 +981,10 @@ describe('Test blueprint api context', () => { // Again with stuff in the cache await setPartInstances(jobContext, playlistId, allPartInstances[1], undefined) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) - expect(cache.LoadedPartInstances).toHaveLength(2) + expect(playoutModel.LoadedPartInstances).toHaveLength(2) await expect( context.getPartInstanceForPreviousPiece({ @@ -1011,8 +1009,8 @@ describe('Test blueprint api context', () => { test('invalid parameters', async () => { const { jobContext, playlistId } = await setupMyDefaultRundown() - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) // @ts-ignore await expect(context.getPartForPreviousPiece()).rejects.toThrow( @@ -1045,10 +1043,10 @@ describe('Test blueprint api context', () => { const { jobContext, playlistId, allPartInstances } = await setupMyDefaultRundown() // Try with nothing in the cache - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) - expect(cache.LoadedPartInstances).toHaveLength(0) + expect(playoutModel.LoadedPartInstances).toHaveLength(0) const pieceInstance0 = (await jobContext.mockCollections.PieceInstances.findOne({ partInstanceId: allPartInstances[0].PartInstance._id, @@ -1085,10 +1083,10 @@ describe('Test blueprint api context', () => { await setPartInstances(jobContext, playlistId, allPartInstances[0], undefined) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) - const currentPartInstance = cache.CurrentPartInstance! + const currentPartInstance = playoutModel.CurrentPartInstance! expect(currentPartInstance).toBeTruthy() currentPartInstance.setTaken(getCurrentTime(), 0) @@ -1120,8 +1118,8 @@ describe('Test blueprint api context', () => { await setPartInstances(jobContext, playlistId, partInstance, undefined) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) postProcessPiecesMock.mockImplementationOnce(() => [ { @@ -1130,7 +1128,7 @@ describe('Test blueprint api context', () => { } as any, ]) - const currentPartInstance = cache.CurrentPartInstance! + const currentPartInstance = playoutModel.CurrentPartInstance! expect(currentPartInstance).toBeTruthy() currentPartInstance.setTaken(getCurrentTime(), 0) @@ -1138,7 +1136,7 @@ describe('Test blueprint api context', () => { const newPieceInstanceId = (await context.insertPiece('current', { externalId: 'input1' } as any)) ._id - expect(newPieceInstanceId).toMatch(/randomId([0-9]+)_part0_0_instance_randomId([0-9]+)/) + expect(newPieceInstanceId).toMatch(/randomId(\d+)_part0_0_instance_randomId(\d+)/) expect(postProcessPiecesMock).toHaveBeenCalledTimes(1) expect(postProcessPiecesMock).toHaveBeenCalledWith( expect.anything(), @@ -1152,7 +1150,7 @@ describe('Test blueprint api context', () => { expect(insertSpy).toHaveBeenCalledTimes(1) // check some properties not exposed to the blueprints - const newPieceInstance = cache.findPieceInstance(protectString(newPieceInstanceId)) + const newPieceInstance = playoutModel.findPieceInstance(protectString(newPieceInstanceId)) ?.pieceInstance as PlayoutPieceInstanceModel expect(newPieceInstance).toBeTruthy() expect(newPieceInstance.PieceInstance.dynamicallyInserted).toBeTruthy() @@ -1176,8 +1174,8 @@ describe('Test blueprint api context', () => { await setPartInstances(jobContext, playlistId, undefined, undefined, allPartInstances[0]) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) await expect(context.updatePieceInstance('abc', {})).rejects.toThrow( 'Some valid properties must be defined' @@ -1199,8 +1197,8 @@ describe('Test blueprint api context', () => { // Set a current part instance await setPartInstances(jobContext, playlistId, pieceInstance, undefined, pieceInstanceOther) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) await expect(context.updatePieceInstance('abc', { sourceLayerId: 'new' })).rejects.toThrow( 'PieceInstance could not be found' ) @@ -1214,8 +1212,8 @@ describe('Test blueprint api context', () => { // Set as next part instance await setPartInstances(jobContext, playlistId, null, pieceInstance, pieceInstanceOther) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) await expect(context.updatePieceInstance('abc', { sourceLayerId: 'new' })).rejects.toThrow( 'PieceInstance could not be found' ) @@ -1238,11 +1236,11 @@ describe('Test blueprint api context', () => { expect(pieceInstance0).toBeTruthy() await setPartInstances(jobContext, playlistId, pieceInstance0, undefined) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) // Ensure there are no pending updates already - for (const partInstance of cache.LoadedPartInstances) { + for (const partInstance of playoutModel.LoadedPartInstances) { expect((partInstance as PlayoutPartInstanceModelImpl).HasAnyChanges()).toBeFalsy() } @@ -1270,9 +1268,8 @@ describe('Test blueprint api context', () => { unprotectString(pieceInstance0._id), pieceInstance0Delta ) - const { pieceInstance: pieceInstance1, partInstance: partInstance1 } = cache.findPieceInstance( - pieceInstance0._id - )! + const { pieceInstance: pieceInstance1, partInstance: partInstance1 } = + playoutModel.findPieceInstance(pieceInstance0._id)! expect(pieceInstance1).toBeTruthy() expect(resultPiece).toEqual(convertPieceInstanceToBlueprints(pieceInstance1.PieceInstance)) @@ -1304,14 +1301,14 @@ describe('Test blueprint api context', () => { describe('queuePart', () => { beforeEach(() => { postProcessPiecesMock.mockClear() - innerStartQueuedAdLibMock.mockClear() + insertQueuedPartWithPiecesMock.mockClear() }) test('bad parameters', async () => { const { jobContext, playlistId, rundownId } = await setupMyDefaultRundown() - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) // No next-part // @ts-ignore @@ -1324,8 +1321,8 @@ describe('Test blueprint api context', () => { expect(partInstance).toBeTruthy() await setPartInstances(jobContext, playlistId, partInstance, undefined) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) // Next part has already been modified context.nextPartState = ActionPartChange.SAFE_CHANGE @@ -1339,46 +1336,26 @@ describe('Test blueprint api context', () => { 'New part must contain at least one piece' ) - // expect( - // context.queuePart( - // // @ts-ignore - // { - // floated: true, - // }, - // [{}] - // ).part.floated - // ).toBeFalsy() - // expect( - // context.queuePart( - // // @ts-ignore - // { - // invalid: true, - // }, - // [{}] - // ).part.invalid - // ).toBeFalsy() - expect(postProcessPiecesMock).toHaveBeenCalledTimes(0) - expect(innerStartQueuedAdLibMock).toHaveBeenCalledTimes(0) + expect(insertQueuedPartWithPiecesMock).toHaveBeenCalledTimes(0) postProcessPiecesMock.mockImplementationOnce(() => { throw new Error('Mock process error') }) await expect(context.queuePart({} as any, [{}] as any)).rejects.toThrow('Mock process error') expect(postProcessPiecesMock).toHaveBeenCalledTimes(1) - expect(innerStartQueuedAdLibMock).toHaveBeenCalledTimes(0) - - partInstance.part.autoNext = true - partInstance.part.expectedDuration = 700 - partInstance.timings = { - plannedStartedPlayback: getCurrentTime(), - plannedStoppedPlayback: undefined, - playOffset: 0, - take: undefined, - } - cache.replacePartInstance(new PlayoutPartInstanceModelImpl(partInstance, [], true)) + expect(insertQueuedPartWithPiecesMock).toHaveBeenCalledTimes(0) + + const partInstanceModel = playoutModel.getPartInstance(partInstance._id) as PlayoutPartInstanceModel + expect(partInstanceModel).toBeTruthy() - expect(isTooCloseToAutonext(partInstance, true)).toBeTruthy() + partInstanceModel.updatePartProps({ + autoNext: true, + expectedDuration: 700, + }) + partInstanceModel.setPlannedStartedPlayback(getCurrentTime()) + + expect(isTooCloseToAutonext(partInstanceModel.PartInstance, true)).toBeTruthy() await expect(context.queuePart({} as any, [{}] as any)).rejects.toThrow( 'Too close to an autonext to queue a part' ) @@ -1397,8 +1374,8 @@ describe('Test blueprint api context', () => { expect(partInstance).toBeTruthy() await setPartInstances(jobContext, playlistId, partInstance, undefined) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) const newPiece: IBlueprintPiece = { name: 'test piece', @@ -1417,20 +1394,22 @@ describe('Test blueprint api context', () => { } expect(postProcessPiecesMock).toHaveBeenCalledTimes(0) - expect(innerStartQueuedAdLibMock).toHaveBeenCalledTimes(0) + expect(insertQueuedPartWithPiecesMock).toHaveBeenCalledTimes(0) // Create it with most of the real flow postProcessPiecesMock.mockImplementationOnce(postProcessPiecesOrig) - innerStartQueuedAdLibMock.mockImplementationOnce(innerStartQueuedAdLibOrig) + insertQueuedPartWithPiecesMock.mockImplementationOnce(insertQueuedPartWithPiecesOrig) expect((await context.queuePart(newPart, [newPiece]))._id).toEqual( - cache.Playlist.nextPartInfo?.partInstanceId + playoutModel.Playlist.nextPartInfo?.partInstanceId ) expect(postProcessPiecesMock).toHaveBeenCalledTimes(1) - expect(innerStartQueuedAdLibMock).toHaveBeenCalledTimes(1) + expect(insertQueuedPartWithPiecesMock).toHaveBeenCalledTimes(1) // Verify some properties not exposed to the blueprints - const newPartInstance = cache.getPartInstance(cache.Playlist.nextPartInfo!.partInstanceId)! + const newPartInstance = playoutModel.getPartInstance( + playoutModel.Playlist.nextPartInfo!.partInstanceId + )! expect(newPartInstance).toBeTruthy() expect(newPartInstance.PartInstance.part._rank).toBeLessThan(9000) expect(newPartInstance.PartInstance.part._rank).toBeGreaterThan(partInstance.part._rank) @@ -1453,8 +1432,8 @@ describe('Test blueprint api context', () => { const { jobContext, playlistId } = await setupMyDefaultRundown() await setPartInstances(jobContext, playlistId, null, undefined) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) innerStopPiecesMock.mockClear() await expect(context.stopPiecesOnLayers(['lay1'], 34)).resolves.toEqual([]) @@ -1473,15 +1452,15 @@ describe('Test blueprint api context', () => { expect(currentPartInstance).toBeTruthy() await setPartInstances(jobContext, playlistId, currentPartInstance, undefined) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) innerStopPiecesMock.mockClear() let filter: (piece: PieceInstance) => boolean = null as any innerStopPiecesMock.mockImplementationOnce( - (context2, cache2, showStyleBase, partInstance, filter2, offset) => { + (context2, playoutModel2, showStyleBase, partInstance, filter2, offset) => { expect(context2).toBe(jobContext) - expect(cache2).toBe(cache) + expect(playoutModel2).toBe(playoutModel) expect(showStyleBase).toBeTruthy() expect(partInstance.PartInstance).toStrictEqual(currentPartInstance) expect(offset).toEqual(34) @@ -1512,8 +1491,8 @@ describe('Test blueprint api context', () => { const { jobContext, playlistId } = await setupMyDefaultRundown() await setPartInstances(jobContext, playlistId, null, undefined) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) innerStopPiecesMock.mockClear() await expect(context.stopPieceInstances(['lay1'], 34)).resolves.toEqual([]) @@ -1532,15 +1511,15 @@ describe('Test blueprint api context', () => { expect(currentPartInstance).toBeTruthy() await setPartInstances(jobContext, playlistId, currentPartInstance, undefined) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) innerStopPiecesMock.mockClear() let filter: (piece: PieceInstance) => boolean = null as any innerStopPiecesMock.mockImplementationOnce( - (context2, cache2, showStyleBase, partInstance, filter2, offset) => { + (context2, playoutModel2, showStyleBase, partInstance, filter2, offset) => { expect(context2).toBe(jobContext) - expect(cache2).toBe(cache) + expect(playoutModel2).toBe(playoutModel) expect(showStyleBase).toBeTruthy() expect(partInstance.PartInstance).toStrictEqual(currentPartInstance) expect(offset).toEqual(34) @@ -1572,17 +1551,17 @@ describe('Test blueprint api context', () => { current: number next: number } - function getPieceInstanceCounts(cache: PlayoutModel): PieceInstanceCounts { + function getPieceInstanceCounts(playoutModel: PlayoutModel): PieceInstanceCounts { let other = 0 - for (const partInstance of cache.OlderPartInstances) { + for (const partInstance of playoutModel.OlderPartInstances) { other += partInstance.PieceInstances.length } return { other, - previous: cache.PreviousPartInstance?.PieceInstances?.length ?? 0, - current: cache.CurrentPartInstance?.PieceInstances?.length ?? 0, - next: cache.NextPartInstance?.PieceInstances?.length ?? 0, + previous: playoutModel.PreviousPartInstance?.PieceInstances?.length ?? 0, + current: playoutModel.CurrentPartInstance?.PieceInstances?.length ?? 0, + next: playoutModel.NextPartInstance?.PieceInstances?.length ?? 0, } } @@ -1598,8 +1577,8 @@ describe('Test blueprint api context', () => { // No instance id await setPartInstances(jobContext, playlistId, undefined, null) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) // No instance id await expect(context.removePieceInstances('next', ['lay1'])).rejects.toThrow( @@ -1611,9 +1590,9 @@ describe('Test blueprint api context', () => { const partInstance = allPartInstances[0] await setPartInstances(jobContext, playlistId, undefined, partInstance) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) - const beforePieceInstancesCounts = getPieceInstanceCounts(cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) + const beforePieceInstancesCounts = getPieceInstanceCounts(playoutModel) expect(beforePieceInstancesCounts.previous).toEqual(0) expect(beforePieceInstancesCounts.current).toEqual(0) expect(beforePieceInstancesCounts.next).not.toEqual(0) @@ -1630,7 +1609,7 @@ describe('Test blueprint api context', () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion context.removePieceInstances('next', [unprotectString(pieceInstanceFromOther._id)]) ).resolves.toEqual([]) // Try and remove something belonging to a different part - expectCountsToEqual(getPieceInstanceCounts(cache), beforePieceInstancesCounts) + expectCountsToEqual(getPieceInstanceCounts(playoutModel), beforePieceInstancesCounts) }) }) @@ -1640,17 +1619,17 @@ describe('Test blueprint api context', () => { const partInstance = allPartInstances[0] await setPartInstances(jobContext, playlistId, undefined, partInstance) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) - const beforePieceInstancesCounts = getPieceInstanceCounts(cache) + const beforePieceInstancesCounts = getPieceInstanceCounts(playoutModel) expect(beforePieceInstancesCounts.previous).toEqual(0) expect(beforePieceInstancesCounts.current).toEqual(0) expect(beforePieceInstancesCounts.next).not.toEqual(0) expect(beforePieceInstancesCounts.other).toEqual(0) // Find the instance, and create its backing piece - const targetPieceInstance = cache.NextPartInstance!.PieceInstances[0] + const targetPieceInstance = playoutModel.NextPartInstance!.PieceInstances[0] expect(targetPieceInstance).toBeTruthy() await expect( @@ -1658,7 +1637,7 @@ describe('Test blueprint api context', () => { ).resolves.toEqual([unprotectString(targetPieceInstance.PieceInstance._id)]) // Ensure it was all removed - expect(cache.findPieceInstance(targetPieceInstance.PieceInstance._id)).toBeFalsy() + expect(playoutModel.findPieceInstance(targetPieceInstance.PieceInstance._id)).toBeFalsy() expect(context.nextPartState).toEqual(ActionPartChange.SAFE_CHANGE) }) }) @@ -1678,15 +1657,9 @@ describe('Test blueprint api context', () => { })) as DBPartInstance expect(partInstanceOther).toBeTruthy() - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) - await expect(context.updatePartInstance('current', {})).rejects.toThrow( - 'Some valid properties must be defined' - ) - await expect( - context.updatePartInstance('current', { _id: 'bad', nope: 'ok' } as any) - ).rejects.toThrow('Some valid properties must be defined') await expect(context.updatePartInstance('current', { title: 'new' })).rejects.toThrow( 'PartInstance could not be found' ) @@ -1694,8 +1667,15 @@ describe('Test blueprint api context', () => { // Set a current part instance await setPartInstances(jobContext, playlistId, partInstance, undefined) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) + await expect(context.updatePartInstance('current', {})).rejects.toThrow( + 'Some valid properties must be defined' + ) + await expect( + context.updatePartInstance('current', { _id: 'bad', nope: 'ok' } as any) + ).rejects.toThrow('Some valid properties must be defined') + await expect(context.updatePartInstance('next', { title: 'new' })).rejects.toThrow( 'PartInstance could not be found' ) @@ -1712,11 +1692,11 @@ describe('Test blueprint api context', () => { expect(partInstance0).toBeTruthy() await setPartInstances(jobContext, playlistId, undefined, partInstance0) - await wrapWithCache(jobContext, playlistId, async (cache) => { - const { context } = await getActionExecutionContext(jobContext, cache) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { context } = await getActionExecutionContext(jobContext, playoutModel) // Ensure there are no pending updates already - expect((cache.NextPartInstance! as PlayoutPartInstanceModelImpl).HasAnyChanges()).toBeFalsy() + expect((playoutModel.NextPartInstance! as PlayoutPartInstanceModelImpl).HasAnyChanges()).toBeFalsy() // Update it and expect it to match const partInstance0Before = clone(partInstance0) @@ -1728,7 +1708,7 @@ describe('Test blueprint api context', () => { badProperty: 9, // This will be dropped } const resultPart = await context.updatePartInstance('next', partInstance0Delta) - const partInstance1 = cache.NextPartInstance! as PlayoutPartInstanceModelImpl + const partInstance1 = playoutModel.NextPartInstance! as PlayoutPartInstanceModelImpl expect(partInstance1).toBeTruthy() expect(resultPart).toEqual(convertPartInstanceToBlueprints(partInstance1.PartInstance)) diff --git a/packages/job-worker/src/blueprints/context/RundownActivationContext.ts b/packages/job-worker/src/blueprints/context/RundownActivationContext.ts index e50a2b32d0..3e64bade68 100644 --- a/packages/job-worker/src/blueprints/context/RundownActivationContext.ts +++ b/packages/job-worker/src/blueprints/context/RundownActivationContext.ts @@ -8,12 +8,12 @@ import { RundownEventContext } from './RundownEventContext' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' export class RundownActivationContext extends RundownEventContext implements IRundownActivationContext { - private readonly _cache: PlayoutModel + private readonly _playoutModel: PlayoutModel private readonly _context: JobContext constructor( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, showStyleCompound: ReadonlyDeep, rundown: ReadonlyDeep ) { @@ -26,11 +26,11 @@ export class RundownActivationContext extends RundownEventContext implements IRu ) this._context = context - this._cache = cache + this._playoutModel = playoutModel } async listPlayoutDevices(): Promise { - return listPlayoutDevices(this._context, this._cache) + return listPlayoutDevices(this._context, this._playoutModel) } async executeTSRAction( diff --git a/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts b/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts index 371f627b3d..b5c712951d 100644 --- a/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts +++ b/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts @@ -20,7 +20,6 @@ import { import { postProcessPieces, postProcessTimelineObjects } from '../postProcess' import { IBlueprintPieceObjectsSampleKeys, - IBlueprintMutatablePartSampleKeys, convertPieceInstanceToBlueprints, convertPartInstanceToBlueprints, } from './lib' @@ -99,7 +98,7 @@ export class SyncIngestUpdateToPartInstanceContext ...proposedPieceInstance, piece: piece, } - this.partInstance.replacePieceInstance(newPieceInstance) + this.partInstance.mergeOrInsertPieceInstance(newPieceInstance) return convertPieceInstanceToBlueprints(newPieceInstance) } @@ -163,26 +162,20 @@ export class SyncIngestUpdateToPartInstanceContext return convertPieceInstanceToBlueprints(pieceInstance.PieceInstance) } updatePartInstance(updatePart: Partial): IBlueprintPartInstance { - // filter the submission to the allowed ones - const trimmedProps: Partial = _.pick(updatePart, [ - ...IBlueprintMutatablePartSampleKeys, - ]) - if (Object.keys(trimmedProps).length === 0) { - throw new Error(`Cannot update PartInstance. Some valid properties must be defined`) - } - if (!this.partInstance) throw new Error(`PartInstance has been removed`) // for autoNext, the new expectedDuration cannot be shorter than the time a part has been on-air for - if (trimmedProps.expectedDuration && (trimmedProps.autoNext ?? this.partInstance.PartInstance.part.autoNext)) { + if (updatePart.expectedDuration && (updatePart.autoNext ?? this.partInstance.PartInstance.part.autoNext)) { const onAir = this.partInstance.PartInstance.timings?.reportedStartedPlayback const minTime = Date.now() - (onAir ?? 0) + EXPECTED_INGEST_TO_PLAYOUT_TIME - if (onAir && minTime > trimmedProps.expectedDuration) { - trimmedProps.expectedDuration = minTime + if (onAir && minTime > updatePart.expectedDuration) { + updatePart.expectedDuration = minTime } } - this.partInstance.updatePartProps(trimmedProps) + if (!this.partInstance.updatePartProps(updatePart)) { + throw new Error(`Cannot update PartInstance. Some valid properties must be defined`) + } return convertPartInstanceToBlueprints(this.partInstance.PartInstance) } diff --git a/packages/job-worker/src/blueprints/context/adlibActions.ts b/packages/job-worker/src/blueprints/context/adlibActions.ts index 503a63308c..6aa94a37b1 100644 --- a/packages/job-worker/src/blueprints/context/adlibActions.ts +++ b/packages/job-worker/src/blueprints/context/adlibActions.ts @@ -39,7 +39,7 @@ import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceIns import { innerFindLastPieceOnLayer, innerFindLastScriptedPieceOnLayer, - innerStartQueuedAdLib, + insertQueuedPartWithPieces, innerStopPieces, } from '../../playout/adlibUtils' import { @@ -54,7 +54,6 @@ import { convertPieceToBlueprints, convertResolvedPieceInstanceToBlueprints, getMediaObjectDuration, - IBlueprintMutatablePartSampleKeys, IBlueprintPieceObjectsSampleKeys, } from './lib' import { postProcessPieces, postProcessTimelineObjects } from '../postProcess' @@ -122,7 +121,7 @@ export class DatastoreActionExecutionContext /** Actions */ export class ActionExecutionContext extends ShowStyleUserContext implements IActionExecutionContext, IEventContext { private readonly _context: JobContext - private readonly _cache: PlayoutModel + private readonly _playoutModel: PlayoutModel private readonly rundown: PlayoutRundownModel /** To be set by any mutation methods on this context. Indicates to core how extensive the changes are to the current partInstance */ @@ -135,7 +134,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct constructor( contextInfo: UserContextInfo, context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, showStyle: ReadonlyDeep, _showStyleBlueprintConfig: ProcessedShowStyleConfig, rundown: PlayoutRundownModel, @@ -143,7 +142,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct ) { super(contextInfo, context, showStyle, watchedPackages) this._context = context - this._cache = cache + this._playoutModel = playoutModel this.rundown = rundown this.takeAfterExecute = false } @@ -151,9 +150,9 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct private _getPartInstance(part: 'current' | 'next'): PlayoutPartInstanceModel | null { switch (part) { case 'current': - return this._cache.CurrentPartInstance + return this._playoutModel.CurrentPartInstance case 'next': - return this._cache.NextPartInstance + return this._playoutModel.NextPartInstance default: assertNever(part) logger.warn(`Blueprint action requested unknown PartInstance "${part}"`) @@ -201,15 +200,15 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct } } - if (options?.excludeCurrentPart && this._cache.Playlist.currentPartInfo) { - query['partInstanceId'] = { $ne: this._cache.Playlist.currentPartInfo.partInstanceId } + if (options?.excludeCurrentPart && this._playoutModel.Playlist.currentPartInfo) { + query['partInstanceId'] = { $ne: this._playoutModel.Playlist.currentPartInfo.partInstanceId } } const sourceLayerId = Array.isArray(sourceLayerId0) ? sourceLayerId0 : [sourceLayerId0] const lastPieceInstance = await innerFindLastPieceOnLayer( this._context, - this._cache, + this._playoutModel, sourceLayerId, options?.originalOnly || false, query @@ -234,13 +233,18 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct } } - if (options?.excludeCurrentPart && this._cache.CurrentPartInstance) { - query['startPartId'] = { $ne: this._cache.CurrentPartInstance.PartInstance.part._id } + if (options?.excludeCurrentPart && this._playoutModel.CurrentPartInstance) { + query['startPartId'] = { $ne: this._playoutModel.CurrentPartInstance.PartInstance.part._id } } const sourceLayerId = Array.isArray(sourceLayerId0) ? sourceLayerId0 : [sourceLayerId0] - const lastPiece = await innerFindLastScriptedPieceOnLayer(this._context, this._cache, sourceLayerId, query) + const lastPiece = await innerFindLastScriptedPieceOnLayer( + this._context, + this._playoutModel, + sourceLayerId, + query + ) return lastPiece && convertPieceToBlueprints(lastPiece) } @@ -252,13 +256,13 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct throw new Error('Cannot find PartInstance from invalid PieceInstance') } - const cached = this._cache.getPartInstance(partInstanceId) - if (cached) { - return convertPartInstanceToBlueprints(cached.PartInstance) + const loadedPartInstanceModel = this._playoutModel.getPartInstance(partInstanceId) + if (loadedPartInstanceModel) { + return convertPartInstanceToBlueprints(loadedPartInstanceModel.PartInstance) } - // It might be reset and so not in the cache - const rundownIds = this._cache.getRundownIds() + // It might be reset and so not in the loaded model + const rundownIds = this._playoutModel.getRundownIds() const oldInstance = await this._context.directCollections.PartInstances.findOne({ _id: partInstanceId, rundownId: { $in: rundownIds }, @@ -277,11 +281,11 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct const pieceDB = await this._context.directCollections.Pieces.findOne({ _id: protectString(piece._id), - startRundownId: { $in: this._cache.getRundownIds() }, + startRundownId: { $in: this._playoutModel.getRundownIds() }, }) if (!pieceDB) throw new Error(`Cannot find Piece ${piece._id}`) - const rundown = this._cache.getRundown(pieceDB.startRundownId) + const rundown = this._playoutModel.getRundown(pieceDB.startRundownId) const segment = rundown?.getSegment(pieceDB.startSegmentId) const part = segment?.getPart(pieceDB.startPartId) return part ? convertPartToBlueprints(part) : undefined @@ -293,7 +297,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct throw new Error('Cannot insert piece when no active part') } - const rundown = this._cache.getRundown(partInstance.PartInstance.rundownId) + const rundown = this._playoutModel.getRundown(partInstance.PartInstance.rundownId) if (!rundown) { throw new Error('Failed to find rundown of partInstance') } @@ -332,7 +336,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct throw new Error('Some valid properties must be defined') } - const foundPieceInstance = this._cache.findPieceInstance(protectString(pieceInstanceId)) + const foundPieceInstance = this._playoutModel.findPieceInstance(protectString(pieceInstanceId)) if (!foundPieceInstance) { throw new Error('PieceInstance could not be found') } @@ -344,11 +348,11 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct } const updatesCurrentPart: ActionPartChange = - pieceInstance.PieceInstance.partInstanceId === this._cache.Playlist.currentPartInfo?.partInstanceId + pieceInstance.PieceInstance.partInstanceId === this._playoutModel.Playlist.currentPartInfo?.partInstanceId ? ActionPartChange.SAFE_CHANGE : ActionPartChange.NONE const updatesNextPart: ActionPartChange = - pieceInstance.PieceInstance.partInstanceId === this._cache.Playlist.nextPartInfo?.partInstanceId + pieceInstance.PieceInstance.partInstanceId === this._playoutModel.Playlist.nextPartInfo?.partInstanceId ? ActionPartChange.SAFE_CHANGE : ActionPartChange.NONE if (!updatesCurrentPart && !updatesNextPart) { @@ -379,7 +383,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct return convertPieceInstanceToBlueprints(pieceInstance.PieceInstance) } async queuePart(rawPart: IBlueprintPart, rawPieces: IBlueprintPiece[]): Promise { - const currentPartInstance = this._cache.CurrentPartInstance + const currentPartInstance = this._playoutModel.CurrentPartInstance if (!currentPartInstance) { throw new Error('Cannot queue part when no current partInstance') } @@ -424,13 +428,16 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct throw new Error('Cannot queue a part which is not playable') } - const newPartInstance = this._cache.insertAdlibbedPartInstance(newPart) - for (const piece of pieces) { - newPartInstance.insertAdlibbedPiece(piece, undefined) - } - // Do the work - await innerStartQueuedAdLib(this._context, this._cache, this.rundown, currentPartInstance, newPartInstance) + const newPartInstance = await insertQueuedPartWithPieces( + this._context, + this._playoutModel, + this.rundown, + currentPartInstance, + newPart, + pieces, + undefined + ) this.nextPartState = ActionPartChange.SAFE_CHANGE this.queuedPartInstanceId = newPartInstance.PartInstance._id @@ -438,24 +445,20 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct return convertPartInstanceToBlueprints(newPartInstance.PartInstance) } async moveNextPart(partDelta: number, segmentDelta: number): Promise { - await moveNextPart(this._context, this._cache, partDelta, segmentDelta) + await moveNextPart(this._context, this._playoutModel, partDelta, segmentDelta) } async updatePartInstance( part: 'current' | 'next', props: Partial ): Promise { - // filter the submission to the allowed ones - const trimmedProps: Partial = _.pick(props, IBlueprintMutatablePartSampleKeys) - if (Object.keys(trimmedProps).length === 0) { - throw new Error('Some valid properties must be defined') - } - const partInstance = this._getPartInstance(part) if (!partInstance) { throw new Error('PartInstance could not be found') } - partInstance.updatePartProps(trimmedProps) + if (!partInstance.updatePartProps(props)) { + throw new Error('Some valid properties must be defined') + } this.nextPartState = Math.max( this.nextPartState, @@ -520,7 +523,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct if (time !== null && (time < getCurrentTime() || typeof time !== 'number')) throw new Error('Cannot block taking out of the current part, to a time in the past') - const partInstance = this._cache.CurrentPartInstance + const partInstance = this._playoutModel.CurrentPartInstance if (!partInstance) { throw new Error('Cannot block take when there is no part playing') } @@ -532,17 +535,17 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct filter: (pieceInstance: ReadonlyDeep) => boolean, timeOffset: number | undefined ) { - if (!this._cache.Playlist.currentPartInfo) { + if (!this._playoutModel.Playlist.currentPartInfo) { return [] } - const partInstance = this._cache.CurrentPartInstance + const partInstance = this._playoutModel.CurrentPartInstance if (!partInstance) { throw new Error('Cannot stop pieceInstances when no current partInstance') } const stoppedIds = innerStopPieces( this._context, - this._cache, + this._playoutModel, this.showStyleCompound.sourceLayers, partInstance, filter, @@ -561,7 +564,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct } async listPlayoutDevices(): Promise { - return listPlayoutDevices(this._context, this._cache) + return listPlayoutDevices(this._context, this._playoutModel) } async executeTSRAction( @@ -577,7 +580,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct const id = protectString(`${studioId}_${key}`) const collection = this._context.directCollections.TimelineDatastores - this._cache.deferAfterSave(async () => { + this._playoutModel.deferAfterSave(async () => { await collection.replace({ _id: id, studioId: studioId, @@ -596,7 +599,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct const id = getDatastoreId(studioId, key) const collection = this._context.directCollections.TimelineDatastores - this._cache.deferAfterSave(async () => { + this._playoutModel.deferAfterSave(async () => { await collection.remove({ _id: id }) }) } diff --git a/packages/job-worker/src/cache/CacheBase.ts b/packages/job-worker/src/cache/CacheBase.ts index 55651d552e..e6cf57e8c8 100644 --- a/packages/job-worker/src/cache/CacheBase.ts +++ b/packages/job-worker/src/cache/CacheBase.ts @@ -34,6 +34,11 @@ export interface ICacheBase2 { dispose(): void + /** + * Assert that no changes should have been made to the cache, will throw an Error otherwise. This can be used in + * place of `saveAllToDatabase()`, when the code controlling the cache expects no changes to have been made and any + * changes made are an error and will cause issues. + */ assertNoChanges(): void } diff --git a/packages/job-worker/src/ingest/__tests__/ingest.test.ts b/packages/job-worker/src/ingest/__tests__/ingest.test.ts index d08eb25d06..aac8492c3e 100644 --- a/packages/job-worker/src/ingest/__tests__/ingest.test.ts +++ b/packages/job-worker/src/ingest/__tests__/ingest.test.ts @@ -40,9 +40,9 @@ import { handleActivateRundownPlaylist } from '../../playout/activePlaylistJobs' import { PartInstanceId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { getSelectedPartInstances } from '../../playout/__tests__/lib' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' -import { runJobWithPlayoutCache } from '../../playout/lock' +import { runJobWithPlayoutModel } from '../../playout/lock' import { protectString } from '@sofie-automation/corelib/dist/protectedString' -import { innerStartQueuedAdLib } from '../../playout/adlibUtils' +import { insertQueuedPartWithPieces } from '../../playout/adlibUtils' import { IngestJobs, RemoveOrphanedSegmentsProps } from '@sofie-automation/corelib/dist/worker/ingest' import { removeRundownPlaylistFromDb } from './lib' import { UserErrorMessage } from '@sofie-automation/corelib/dist/error' @@ -1999,7 +1999,7 @@ describe('Test ingest actions for rundowns and segments', () => { }) const doQueuePart = async (): Promise => - runJobWithPlayoutCache( + runJobWithPlayoutModel( context, { playlistId: rundown.playlistId, @@ -2012,16 +2012,22 @@ describe('Test ingest actions for rundowns and segments', () => { const currentPartInstance = cache.CurrentPartInstance as PlayoutPartInstanceModel expect(currentPartInstance).toBeTruthy() - const newPartInstance = cache.insertAdlibbedPartInstance({ - _id: protectString(`after_${currentPartInstance.PartInstance._id}_part`), - _rank: 0, - externalId: `after_${currentPartInstance.PartInstance._id}_externalId`, - title: 'New part', - expectedDurationWithPreroll: undefined, - }) - // Simulate a queued part - await innerStartQueuedAdLib(context, cache, rundown0, currentPartInstance, newPartInstance) + const newPartInstance = await insertQueuedPartWithPieces( + context, + cache, + rundown0, + currentPartInstance, + { + _id: protectString(`after_${currentPartInstance.PartInstance._id}_part`), + _rank: 0, + externalId: `after_${currentPartInstance.PartInstance._id}_externalId`, + title: 'New part', + expectedDurationWithPreroll: undefined, + }, + [], + undefined + ) return newPartInstance.PartInstance._id } diff --git a/packages/job-worker/src/ingest/__tests__/updateNext.test.ts b/packages/job-worker/src/ingest/__tests__/updateNext.test.ts index 300eb75457..660207799f 100644 --- a/packages/job-worker/src/ingest/__tests__/updateNext.test.ts +++ b/packages/job-worker/src/ingest/__tests__/updateNext.test.ts @@ -7,7 +7,7 @@ import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { saveIntoDb } from '../../db/changes' import { ensureNextPartIsValid as ensureNextPartIsValidRaw } from '../updateNext' import { MockJobContext, setupDefaultJobEnvironment } from '../../__mocks__/context' -import { runJobWithPlayoutCache } from '../../playout/lock' +import { runJobWithPlayoutModel } from '../../playout/lock' jest.mock('../../playout/setNext') import { setNextPart } from '../../playout/setNext' @@ -344,7 +344,7 @@ describe('ensureNextPartIsValid', () => { }) } async function ensureNextPartIsValid() { - await runJobWithPlayoutCache(context, { playlistId: rundownPlaylistId }, null, async (cache) => + await runJobWithPlayoutModel(context, { playlistId: rundownPlaylistId }, null, async (cache) => ensureNextPartIsValidRaw(context, cache) ) } diff --git a/packages/job-worker/src/ingest/commit.ts b/packages/job-worker/src/ingest/commit.ts index 804d0fc6df..b9410f533d 100644 --- a/packages/job-worker/src/ingest/commit.ts +++ b/packages/job-worker/src/ingest/commit.ts @@ -23,7 +23,7 @@ import { getRundown } from './lib' import { JobContext } from '../jobs' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' -import { runJobWithPlaylistLock, runWithPlaylistCache } from '../playout/lock' +import { runJobWithPlaylistLock, runWithPlayoutModel } from '../playout/lock' import { removeSegmentContents } from './cleanup' import { CommitIngestData } from './lock' import { groupByToMap, groupByToMapFunc, normalizeArrayToMap } from '@sofie-automation/corelib/dist/lib' @@ -493,7 +493,7 @@ export async function updatePlayoutAfterChangingRundownInPlaylist( insertedRundown: ReadonlyDeep | null ): Promise { // ensure the 'old' playout is updated to remove any references to the rundown - await runWithPlaylistCache(context, playlist, playlistLock, null, async (playoutCache) => { + await runWithPlayoutModel(context, playlist, playlistLock, null, async (playoutCache) => { if (playoutCache.Rundowns.length === 0) { if (playoutCache.Playlist.activationId) throw new Error(`RundownPlaylist "${playoutCache.PlaylistId}" has no contents but is active...`) @@ -835,8 +835,8 @@ async function preserveUnsyncedPlayingSegmentContents( } } -async function validateScratchpad(_context: JobContext, cache: PlayoutModel) { - for (const rundown of cache.Rundowns) { +async function validateScratchpad(_context: JobContext, playoutModel: PlayoutModel) { + for (const rundown of playoutModel.Rundowns) { const scratchpadSegment = rundown.getScratchpadSegment() if (scratchpadSegment) { diff --git a/packages/job-worker/src/ingest/syncChangesToPartInstance.ts b/packages/job-worker/src/ingest/syncChangesToPartInstance.ts index 9ada58f6ae..253c4443a8 100644 --- a/packages/job-worker/src/ingest/syncChangesToPartInstance.ts +++ b/packages/job-worker/src/ingest/syncChangesToPartInstance.ts @@ -43,15 +43,15 @@ type SyncedInstance = { * Attempt to sync the current and next Part into their PartInstances * This defers out to the Blueprints to do the syncing * @param context Context of the job ebeing run - * @param cache Playout cache containing containing the Rundown being ingested + * @param playoutModel Playout cache containing containing the Rundown being ingested * @param ingestCache Ingest cache for the Rundown */ export async function syncChangesToPartInstances( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, ingestCache: ReadOnlyCache ): Promise { - if (cache.Playlist.activationId) { + if (playoutModel.Playlist.activationId) { // Get the final copy of the rundown const rundownWrapped = hackConvertIngestCacheToRundownWithSegments(ingestCache) @@ -62,9 +62,9 @@ export async function syncChangesToPartInstances( const blueprint = await context.getShowStyleBlueprint(showStyle._id) if (blueprint.blueprint.syncIngestUpdateToPartInstance) { - const currentPartInstance = cache.CurrentPartInstance - const nextPartInstance = cache.NextPartInstance - const previousPartInstance = cache.PreviousPartInstance + const currentPartInstance = playoutModel.CurrentPartInstance + const nextPartInstance = playoutModel.NextPartInstance + const previousPartInstance = playoutModel.PreviousPartInstance const instances: SyncedInstance[] = [] if (currentPartInstance) { @@ -75,14 +75,14 @@ export async function syncChangesToPartInstances( // it's ran first through the blueprints. if (currentPartInstance.PartInstance.orphaned === 'adlib-part') { const partAndPartInstance = findLastUnorphanedPartInstanceInSegment( - cache, + playoutModel, currentPartInstance.PartInstance ) if (partAndPartInstance) { insertToSyncedInstanceCandidates( context, instances, - cache, + playoutModel, ingestCache, partAndPartInstance.partInstance, null, @@ -95,7 +95,7 @@ export async function syncChangesToPartInstances( findPartAndInsertToSyncedInstanceCandidates( context, instances, - cache, + playoutModel, ingestCache, currentPartInstance, previousPartInstance, @@ -106,7 +106,7 @@ export async function syncChangesToPartInstances( findPartAndInsertToSyncedInstanceCandidates( context, instances, - cache, + playoutModel, ingestCache, nextPartInstance, currentPartInstance, @@ -131,7 +131,7 @@ export async function syncChangesToPartInstances( const proposedPieceInstances = getPieceInstancesForPart( context, - cache, + playoutModel, previousPartInstance, rundownWrapped, newPart ?? existingPartInstance.PartInstance.part, @@ -162,18 +162,18 @@ export async function syncChangesToPartInstances( ), } - const clonedPartInstance = existingPartInstance.clone() + const partInstanceSnapshot = existingPartInstance.snapshotMakeCopy() const syncContext = new SyncIngestUpdateToPartInstanceContext( context, { - name: `Update to ${clonedPartInstance.PartInstance.part.externalId}`, - identifier: `rundownId=${clonedPartInstance.PartInstance.part.rundownId},segmentId=${clonedPartInstance.PartInstance.part.segmentId}`, + name: `Update to ${existingPartInstance.PartInstance.part.externalId}`, + identifier: `rundownId=${existingPartInstance.PartInstance.part.rundownId},segmentId=${existingPartInstance.PartInstance.part.segmentId}`, }, context.studio, showStyle, rundownWrapped.Rundown, - clonedPartInstance, + existingPartInstance, proposedPieceInstances, playStatus ) @@ -186,16 +186,18 @@ export async function syncChangesToPartInstances( newResultData, playStatus ) - - // If the blueprint function throws, no changes will be synced to the cache: - // TODO - save clonedPartInstance - cache.replacePartInstance(clonedPartInstance) } catch (err) { logger.error(`Error in showStyleBlueprint.syncIngestUpdateToPartInstance: ${stringifyError(err)}`) + + // Operation failed, rollback the changes + existingPartInstance.snapshotRestore(partInstanceSnapshot) } if (playStatus === 'next') { - updateExpectedDurationWithPrerollForPartInstance(cache, clonedPartInstance.PartInstance._id) + updateExpectedDurationWithPrerollForPartInstance( + playoutModel, + existingPartInstance.PartInstance._id + ) } // Save notes: @@ -214,18 +216,22 @@ export async function syncChangesToPartInstances( if (newNotes.length) { // TODO - these dont get shown to the user currently // TODO - old notes from the sync may need to be pruned, or we will end up with duplicates and 'stuck' notes?+ - clonedPartInstance.appendNotes(newNotes) + existingPartInstance.appendNotes(newNotes) - validateScratchpartPartInstanceProperties(context, cache, existingPartInstance.PartInstance._id) + validateScratchpartPartInstanceProperties( + context, + playoutModel, + existingPartInstance.PartInstance._id + ) } - if (clonedPartInstance.PartInstance._id === cache.Playlist.currentPartInfo?.partInstanceId) { + if (existingPartInstance.PartInstance._id === playoutModel.Playlist.currentPartInfo?.partInstanceId) { // This should be run after 'current', before 'next': await syncPlayheadInfinitesForNextPartInstance( context, - cache, - cache.CurrentPartInstance, - cache.NextPartInstance + playoutModel, + playoutModel.CurrentPartInstance, + playoutModel.NextPartInstance ) } } @@ -241,7 +247,7 @@ export async function syncChangesToPartInstances( function insertToSyncedInstanceCandidates( context: JobContext, instances: SyncedInstance[], - cache: PlayoutModel, + playoutModel: PlayoutModel, ingestCache: ReadOnlyCache, thisPartInstance: PlayoutPartInstanceModel, previousPartInstance: PlayoutPartInstanceModel | null, @@ -255,7 +261,7 @@ function insertToSyncedInstanceCandidates( newPart: part, piecesThatMayBeActive: fetchPiecesThatMayBeActiveForPart( context, - cache, + playoutModel, ingestCache, part ?? thisPartInstance.PartInstance.part ), @@ -269,18 +275,18 @@ function insertToSyncedInstanceCandidates( function findPartAndInsertToSyncedInstanceCandidates( context: JobContext, instances: SyncedInstance[], - cache: PlayoutModel, + playoutModel: PlayoutModel, ingestCache: ReadOnlyCache, thisPartInstance: PlayoutPartInstanceModel, previousPartInstance: PlayoutPartInstanceModel | null, playStatus: PlayStatus ): void { - const newPart = cache.findPart(thisPartInstance.PartInstance.part._id) + const newPart = playoutModel.findPart(thisPartInstance.PartInstance.part._id) insertToSyncedInstanceCandidates( context, instances, - cache, + playoutModel, ingestCache, thisPartInstance, previousPartInstance, @@ -294,14 +300,14 @@ function findPartAndInsertToSyncedInstanceCandidates( * PartInstance that matches that Part. Returns them if found or returns `null` if it can't find anything. */ function findLastUnorphanedPartInstanceInSegment( - cache: PlayoutModel, + playoutModel: PlayoutModel, currentPartInstance: ReadonlyDeep ): { partInstance: PlayoutPartInstanceModel part: ReadonlyDeep } | null { // Find the "latest" (last played), non-orphaned PartInstance in this Segment, in this play-through - const previousPartInstance = cache.SortedLoadedPartInstances.reverse().find( + const previousPartInstance = playoutModel.SortedLoadedPartInstances.reverse().find( (p) => p.PartInstance.playlistActivationId === currentPartInstance.playlistActivationId && p.PartInstance.segmentId === currentPartInstance.segmentId && @@ -313,7 +319,7 @@ function findLastUnorphanedPartInstanceInSegment( if (!previousPartInstance) return null - const previousPart = cache.findPart(previousPartInstance.PartInstance.part._id) + const previousPart = playoutModel.findPart(previousPartInstance.PartInstance.part._id) if (!previousPart) return null return { diff --git a/packages/job-worker/src/ingest/updateNext.ts b/packages/job-worker/src/ingest/updateNext.ts index d1eda5f924..19c00b0425 100644 --- a/packages/job-worker/src/ingest/updateNext.ts +++ b/packages/job-worker/src/ingest/updateNext.ts @@ -10,16 +10,16 @@ import { updateTimeline } from '../playout/timeline/generate' * Make sure that the nextPartInstance for the current Playlist is still correct * This will often change the nextPartInstance * @param context Context of the job being run - * @param cache Playout Cache to operate on + * @param playoutModel Playout Cache to operate on */ -export async function ensureNextPartIsValid(context: JobContext, cache: PlayoutModel): Promise { +export async function ensureNextPartIsValid(context: JobContext, playoutModel: PlayoutModel): Promise { const span = context.startSpan('api.ingest.ensureNextPartIsValid') // Ensure the next-id is still valid - const playlist = cache.Playlist + const playlist = playoutModel.Playlist if (playlist?.activationId) { - const currentPartInstance = cache.CurrentPartInstance - const nextPartInstance = cache.NextPartInstance + const currentPartInstance = playoutModel.CurrentPartInstance + const nextPartInstance = playoutModel.NextPartInstance if ( playlist.nextPartInfo?.manuallySelected && @@ -38,8 +38,8 @@ export async function ensureNextPartIsValid(context: JobContext, cache: PlayoutM return } - const orderedSegments = cache.getAllOrderedSegments() - const orderedParts = cache.getAllOrderedParts() + const orderedSegments = playoutModel.getAllOrderedSegments() + const orderedParts = playoutModel.getAllOrderedParts() if (currentPartInstance && nextPartInstance) { // Check if the part is the same @@ -61,9 +61,9 @@ export async function ensureNextPartIsValid(context: JobContext, cache: PlayoutM !isPartPlayable(nextPartInstance.PartInstance.part) ) { // The 'new' next part is before the current next, so move the next point - await setNextPart(context, cache, newNextPart ?? null, false) + await setNextPart(context, playoutModel, newNextPart ?? null, false) - await updateTimeline(context, cache) + await updateTimeline(context, playoutModel) } } else if (!nextPartInstance || nextPartInstance.PartInstance.orphaned === 'deleted') { // Don't have a nextPart or it has been deleted, so autoselect something @@ -75,9 +75,9 @@ export async function ensureNextPartIsValid(context: JobContext, cache: PlayoutM orderedSegments, orderedParts ) - await setNextPart(context, cache, newNextPart ?? null, false) + await setNextPart(context, playoutModel, newNextPart ?? null, false) - await updateTimeline(context, cache) + await updateTimeline(context, playoutModel) } } diff --git a/packages/job-worker/src/peripheralDevice.ts b/packages/job-worker/src/peripheralDevice.ts index fb17055673..69aac9581f 100644 --- a/packages/job-worker/src/peripheralDevice.ts +++ b/packages/job-worker/src/peripheralDevice.ts @@ -188,9 +188,12 @@ async function executePeripheralDeviceGenericFunction( return result } -export async function listPlayoutDevices(context: JobContext, cache: PlayoutModel): Promise { +export async function listPlayoutDevices( + context: JobContext, + playoutModel: PlayoutModel +): Promise { const parentDevicesMap = normalizeArrayToMap( - cache.PeripheralDevices.filter( + playoutModel.PeripheralDevices.filter( (doc) => doc.studioId === context.studioId && doc.type === PeripheralDeviceType.PLAYOUT ), '_id' diff --git a/packages/job-worker/src/playout/__tests__/actions.test.ts b/packages/job-worker/src/playout/__tests__/actions.test.ts index 15bbc6fdb7..810a750ca1 100644 --- a/packages/job-worker/src/playout/__tests__/actions.test.ts +++ b/packages/job-worker/src/playout/__tests__/actions.test.ts @@ -4,7 +4,7 @@ import { removeRundownFromDb } from '../../rundownPlaylists' import { setupDefaultRundownPlaylist, setupMockShowStyleCompound } from '../../__mocks__/presetCollections' import { activateRundownPlaylist } from '../activePlaylistActions' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { runJobWithPlayoutCache } from '../lock' +import { runJobWithPlayoutModel } from '../lock' import { runWithRundownLock } from '../../ingest/lock' jest.mock('../../peripheralDevice') @@ -48,8 +48,8 @@ describe('Playout Actions', () => { expect(executePeripheralDeviceFunctionMock).toHaveBeenCalledTimes(0) // Activating a rundown, to rehearsal - await runJobWithPlayoutCache(context, { playlistId: playlistId0 }, null, async (cache) => - activateRundownPlaylist(context, cache, true) + await runJobWithPlayoutModel(context, { playlistId: playlistId0 }, null, async (playoutModel) => + activateRundownPlaylist(context, playoutModel, true) ) await expect(getPlaylist0()).resolves.toMatchObject({ activationId: expect.stringMatching(/^randomId/), @@ -57,8 +57,8 @@ describe('Playout Actions', () => { }) // Activating a rundown - await runJobWithPlayoutCache(context, { playlistId: playlistId0 }, null, async (cache) => - activateRundownPlaylist(context, cache, false) + await runJobWithPlayoutModel(context, { playlistId: playlistId0 }, null, async (playoutModel) => + activateRundownPlaylist(context, playoutModel, false) ) await expect(getPlaylist0()).resolves.toMatchObject({ activationId: expect.stringMatching(/^randomId/), @@ -66,8 +66,8 @@ describe('Playout Actions', () => { }) // Activating a rundown, back to rehearsal - await runJobWithPlayoutCache(context, { playlistId: playlistId0 }, null, async (cache) => - activateRundownPlaylist(context, cache, true) + await runJobWithPlayoutModel(context, { playlistId: playlistId0 }, null, async (playoutModel) => + activateRundownPlaylist(context, playoutModel, true) ) await expect(getPlaylist0()).resolves.toMatchObject({ activationId: expect.stringMatching(/^randomId/), @@ -78,8 +78,8 @@ describe('Playout Actions', () => { // Activating another rundown await expect( - runJobWithPlayoutCache(context, { playlistId: playlistId1 }, null, async (cache) => - activateRundownPlaylist(context, cache, false) + runJobWithPlayoutModel(context, { playlistId: playlistId1 }, null, async (playoutModel) => + activateRundownPlaylist(context, playoutModel, false) ) ).rejects.toMatchToString(/only one rundown can be active/i) diff --git a/packages/job-worker/src/playout/__tests__/infinites.test.ts b/packages/job-worker/src/playout/__tests__/infinites.test.ts index a9605f623b..7a9b226fd9 100644 --- a/packages/job-worker/src/playout/__tests__/infinites.test.ts +++ b/packages/job-worker/src/playout/__tests__/infinites.test.ts @@ -6,7 +6,7 @@ import { candidatePartIsAfterPreviewPartInstance } from '../infinites' import { setupDefaultRundownPlaylist, setupMockShowStyleCompound } from '../../__mocks__/presetCollections' import { getRandomId } from '@sofie-automation/corelib/dist/lib' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { runJobWithPlayoutCache } from '../lock' +import { runJobWithPlayoutModel } from '../lock' import { wrapPartToTemporaryInstance } from '../../__mocks__/partinstance' import { protectString } from '@sofie-automation/corelib/dist/protectedString' @@ -19,8 +19,11 @@ describe('canContinueAdlibOnEndInfinites', () => { await setupMockShowStyleCompound(context) }) - async function wrapWithCache( - fcn: (cache: PlayoutModel, playlist: SetRequired, 'activationId'>) => Promise + async function wrapWithPlayoutModel( + fcn: ( + playoutModel: PlayoutModel, + playlist: SetRequired, 'activationId'> + ) => Promise ): Promise { const defaultSetup = await setupDefaultRundownPlaylist(context) @@ -39,17 +42,17 @@ describe('canContinueAdlibOnEndInfinites', () => { const rundown = (await context.mockCollections.Rundowns.findOne(defaultSetup.rundownId)) as DBRundown expect(rundown).toBeTruthy() - return runJobWithPlayoutCache(context, { playlistId: tmpPlaylist._id }, null, async (cache) => { - const playlist = cache.Playlist as SetRequired, 'activationId'> + return runJobWithPlayoutModel(context, { playlistId: tmpPlaylist._id }, null, async (playoutModel) => { + const playlist = playoutModel.Playlist as SetRequired, 'activationId'> if (!playlist.activationId) throw new Error('Missing activationId') - return fcn(cache, playlist) + return fcn(playoutModel, playlist) }) } test('Basic case', async () => { - await wrapWithCache(async (cache, playlist) => { - const orderedSegments = cache.getAllOrderedSegments() - const orderedParts = cache.getAllOrderedParts() + await wrapWithPlayoutModel(async (playoutModel, playlist) => { + const orderedSegments = playoutModel.getAllOrderedSegments() + const orderedParts = playoutModel.getAllOrderedParts() expect(orderedParts.length).toBeGreaterThan(2) // At beginning @@ -95,9 +98,9 @@ describe('canContinueAdlibOnEndInfinites', () => { }) test('No previousPartInstance', async () => { - await wrapWithCache(async (cache, _playlist) => { - const orderedSegments = cache.getAllOrderedSegments() - const orderedParts = cache.getAllOrderedParts() + await wrapWithPlayoutModel(async (playoutModel, _playlist) => { + const orderedSegments = playoutModel.getAllOrderedSegments() + const orderedParts = playoutModel.getAllOrderedParts() expect( candidatePartIsAfterPreviewPartInstance(context, orderedSegments, undefined, orderedParts[1]) @@ -106,9 +109,9 @@ describe('canContinueAdlibOnEndInfinites', () => { }) test('Is before', async () => { - await wrapWithCache(async (cache, playlist) => { - const orderedSegments = cache.getAllOrderedSegments() - const orderedParts = cache.getAllOrderedParts() + await wrapWithPlayoutModel(async (playoutModel, playlist) => { + const orderedSegments = playoutModel.getAllOrderedSegments() + const orderedParts = playoutModel.getAllOrderedParts() expect(orderedParts.length).toBeGreaterThan(2) // At beginning @@ -144,9 +147,9 @@ describe('canContinueAdlibOnEndInfinites', () => { }) test('Orphaned PartInstance', async () => { - await wrapWithCache(async (cache, playlist) => { - const orderedSegments = cache.getAllOrderedSegments() - const orderedParts = cache.getAllOrderedParts() + await wrapWithPlayoutModel(async (playoutModel, playlist) => { + const orderedSegments = playoutModel.getAllOrderedSegments() + const orderedParts = playoutModel.getAllOrderedParts() expect(orderedParts.length).toBeGreaterThan(2) const candidatePart = { diff --git a/packages/job-worker/src/playout/__tests__/timeline.test.ts b/packages/job-worker/src/playout/__tests__/timeline.test.ts index 3d37124748..71112c6ef4 100644 --- a/packages/job-worker/src/playout/__tests__/timeline.test.ts +++ b/packages/job-worker/src/playout/__tests__/timeline.test.ts @@ -16,7 +16,7 @@ import { handleTakeNextPart } from '../take' import { handleActivateHold } from '../holdJobs' import { handleActivateRundownPlaylist, handleDeactivateRundownPlaylist } from '../activePlaylistJobs' import { fixSnapshot } from '../../__mocks__/helpers/snapshot' -import { runJobWithPlayoutCache } from '../lock' +import { runJobWithPlayoutModel } from '../lock' import { updateTimeline } from '../timeline/generate' import { getSelectedPartInstances, getSortedPartsForRundown } from './lib' import { PieceLifespan, IBlueprintPieceType, Time } from '@sofie-automation/blueprints-integration' @@ -436,14 +436,14 @@ async function doDeactivatePlaylist(context: MockJobContext, playlistId: Rundown /** perform an update of the timeline */ async function doUpdateTimeline(context: MockJobContext, playlistId: RundownPlaylistId, forceNowToTime?: Time) { - await runJobWithPlayoutCache( + await runJobWithPlayoutModel( context, { playlistId: playlistId, }, null, - async (cache) => { - await updateTimeline(context, cache, forceNowToTime) + async (playoutModel) => { + await updateTimeline(context, playoutModel, forceNowToTime) } ) } @@ -540,8 +540,8 @@ describe('Timeline', () => { // }) } - await runJobWithPlayoutCache(context, { playlistId: playlistId0 }, null, async (cache) => { - await updateTimeline(context, cache) + await runJobWithPlayoutModel(context, { playlistId: playlistId0 }, null, async (playoutModel) => { + await updateTimeline(context, playoutModel) }) expect(fixSnapshot(await context.directCollections.Timelines.findFetch())).toMatchSnapshot() @@ -1131,14 +1131,23 @@ describe('Timeline', () => { describe('Adlib pieces', () => { async function doStartAdlibPiece(playlistId: RundownPlaylistId, adlibSource: AdLibPiece) { - await runJobWithPlayoutCache(context, { playlistId }, null, async (cache) => { - const currentPartInstance = cache.CurrentPartInstance as PlayoutPartInstanceModel + await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => { + const currentPartInstance = playoutModel.CurrentPartInstance as PlayoutPartInstanceModel expect(currentPartInstance).toBeTruthy() - const rundown = cache.getRundown(currentPartInstance.PartInstance.rundownId) as PlayoutRundownModel + const rundown = playoutModel.getRundown( + currentPartInstance.PartInstance.rundownId + ) as PlayoutRundownModel expect(rundown).toBeTruthy() - return innerStartOrQueueAdLibPiece(context, cache, rundown, false, currentPartInstance, adlibSource) + return innerStartOrQueueAdLibPiece( + context, + playoutModel, + rundown, + false, + currentPartInstance, + adlibSource + ) }) } diff --git a/packages/job-worker/src/playout/activePlaylistActions.ts b/packages/job-worker/src/playout/activePlaylistActions.ts index 4674134796..7aa469b7a4 100644 --- a/packages/job-worker/src/playout/activePlaylistActions.ts +++ b/packages/job-worker/src/playout/activePlaylistActions.ts @@ -16,18 +16,18 @@ import { ReadonlyDeep } from 'type-fest' export async function activateRundownPlaylist( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, rehearsal: boolean ): Promise { - logger.info('Activating rundown ' + cache.Playlist._id + (rehearsal ? ' (Rehearsal)' : '')) + logger.info('Activating rundown ' + playoutModel.Playlist._id + (rehearsal ? ' (Rehearsal)' : '')) rehearsal = !!rehearsal - const wasActive = !!cache.Playlist.activationId + const wasActive = !!playoutModel.Playlist.activationId const anyOtherActiveRundowns = await getActiveRundownPlaylistsInStudioFromDb( context, context.studio._id, - cache.Playlist._id + playoutModel.Playlist._id ) if (anyOtherActiveRundowns.length) { // logger.warn('Only one rundown can be active at the same time. Active rundowns: ' + _.map(anyOtherActiveRundowns, rundown => rundown._id)) @@ -38,56 +38,56 @@ export async function activateRundownPlaylist( ) } - if (!cache.Playlist.activationId) { + if (!playoutModel.Playlist.activationId) { // Reset the playlist if it wasnt already active - await resetRundownPlaylist(context, cache) + await resetRundownPlaylist(context, playoutModel) } - const newActivationId = cache.activatePlaylist(rehearsal) + const newActivationId = playoutModel.activatePlaylist(rehearsal) let rundown: ReadonlyDeep | undefined - const currentPartInstance = cache.CurrentPartInstance + const currentPartInstance = playoutModel.CurrentPartInstance if (!currentPartInstance || currentPartInstance.PartInstance.reset) { - cache.clearSelectedPartInstances() + playoutModel.clearSelectedPartInstances() // If we are not playing anything, then regenerate the next part const firstPart = selectNextPart( context, - cache.Playlist, + playoutModel.Playlist, null, null, - cache.getAllOrderedSegments(), - cache.getAllOrderedParts() + playoutModel.getAllOrderedSegments(), + playoutModel.getAllOrderedParts() ) - await setNextPart(context, cache, firstPart, false) + await setNextPart(context, playoutModel, firstPart, false) if (firstPart) { - rundown = cache.getRundown(firstPart.part.rundownId)?.Rundown + rundown = playoutModel.getRundown(firstPart.part.rundownId)?.Rundown } } else { // Otherwise preserve the active partInstances - for (const partInstance of cache.SelectedPartInstances) { + for (const partInstance of playoutModel.SelectedPartInstances) { partInstance.setPlaylistActivationId(newActivationId) } - const nextPartInstance = cache.NextPartInstance + const nextPartInstance = playoutModel.NextPartInstance if (nextPartInstance) { - rundown = cache.getRundown(nextPartInstance.PartInstance.rundownId)?.Rundown + rundown = playoutModel.getRundown(nextPartInstance.PartInstance.rundownId)?.Rundown if (!rundown) throw new Error(`Could not find rundown "${nextPartInstance.PartInstance.rundownId}"`) } } - await updateTimeline(context, cache) + await updateTimeline(context, playoutModel) - cache.deferBeforeSave(async () => { + playoutModel.deferBeforeSave(async () => { 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) try { if (blueprint.blueprint.onRundownActivate) { - const blueprintContext = new RundownActivationContext(context, cache, showStyle, rundown) + const blueprintContext = new RundownActivationContext(context, playoutModel, showStyle, rundown) await blueprint.blueprint.onRundownActivate(blueprintContext, wasActive) } @@ -96,21 +96,21 @@ export async function activateRundownPlaylist( } }) } -export async function deactivateRundownPlaylist(context: JobContext, cache: PlayoutModel): Promise { - const rundown = await deactivateRundownPlaylistInner(context, cache) +export async function deactivateRundownPlaylist(context: JobContext, playoutModel: PlayoutModel): Promise { + const rundown = await deactivateRundownPlaylistInner(context, playoutModel) - await updateStudioTimeline(context, cache) + await updateStudioTimeline(context, playoutModel) - await cleanTimelineDatastore(context, cache) + await cleanTimelineDatastore(context, playoutModel) - cache.deferBeforeSave(async () => { + playoutModel.deferBeforeSave(async () => { if (rundown) { const showStyle = await context.getShowStyleCompound(rundown.showStyleVariantId, rundown.showStyleBaseId) const blueprint = await context.getShowStyleBlueprint(showStyle._id) try { if (blueprint.blueprint.onRundownDeActivate) { - const blueprintContext = new RundownActivationContext(context, cache, showStyle, rundown) + const blueprintContext = new RundownActivationContext(context, playoutModel, showStyle, rundown) await blueprint.blueprint.onRundownDeActivate(blueprintContext) } } catch (err) { @@ -121,23 +121,23 @@ export async function deactivateRundownPlaylist(context: JobContext, cache: Play } export async function deactivateRundownPlaylistInner( context: JobContext, - cache: PlayoutModel + playoutModel: PlayoutModel ): Promise | undefined> { const span = context.startSpan('deactivateRundownPlaylistInner') - logger.info(`Deactivating rundown playlist "${cache.Playlist._id}"`) + logger.info(`Deactivating rundown playlist "${playoutModel.Playlist._id}"`) - const currentPartInstance = cache.CurrentPartInstance - const nextPartInstance = cache.NextPartInstance + const currentPartInstance = playoutModel.CurrentPartInstance + const nextPartInstance = playoutModel.NextPartInstance let rundown: ReadonlyDeep | undefined if (currentPartInstance) { - rundown = cache.getRundown(currentPartInstance.PartInstance.rundownId)?.Rundown + rundown = playoutModel.getRundown(currentPartInstance.PartInstance.rundownId)?.Rundown - cache.deferAfterSave(async () => { + playoutModel.deferAfterSave(async () => { context .queueEventJob(EventsJobs.NotifyCurrentlyPlayingPart, { rundownId: currentPartInstance.PartInstance.rundownId, - isRehearsal: !!cache.Playlist.rehearsal, + isRehearsal: !!playoutModel.Playlist.rehearsal, partExternalId: null, }) .catch((e) => { @@ -145,13 +145,12 @@ export async function deactivateRundownPlaylistInner( }) }) } else if (nextPartInstance) { - rundown = cache.getRundown(nextPartInstance.PartInstance.rundownId)?.Rundown + rundown = playoutModel.getRundown(nextPartInstance.PartInstance.rundownId)?.Rundown } - cache.clearSelectedPartInstances() - cache.deactivatePlaylist() + playoutModel.deactivatePlaylist() - await setNextPart(context, cache, null, false) + await setNextPart(context, playoutModel, null, false) if (currentPartInstance) { // Set the current PartInstance as stopped diff --git a/packages/job-worker/src/playout/activePlaylistJobs.ts b/packages/job-worker/src/playout/activePlaylistJobs.ts index 73100d0e33..a3bd7be4a4 100644 --- a/packages/job-worker/src/playout/activePlaylistJobs.ts +++ b/packages/job-worker/src/playout/activePlaylistJobs.ts @@ -7,7 +7,7 @@ import { ResetRundownPlaylistProps, } from '@sofie-automation/corelib/dist/worker/studio' import { JobContext } from '../jobs' -import { runJobWithPlayoutCache } from './lock' +import { runJobWithPlayoutModel } from './lock' import { resetRundownPlaylist } from './lib' import { updateTimeline } from './timeline/generate' import { getActiveRundownPlaylistsInStudioFromDb } from '../studio/lib' @@ -43,19 +43,19 @@ export async function handlePrepareRundownPlaylistForBroadcast( context: JobContext, data: PrepareRundownForBroadcastProps ): Promise { - return runJobWithPlayoutCache( + return runJobWithPlayoutModel( context, data, - async (cache) => { - const playlist = cache.Playlist + async (playoutModel) => { + const playlist = playoutModel.Playlist if (playlist.activationId) throw UserError.create(UserErrorMessage.RundownAlreadyActive) await checkNoOtherPlaylistsActive(context, playlist) }, - async (cache) => { - await resetRundownPlaylist(context, cache) + async (playoutModel) => { + await resetRundownPlaylist(context, playoutModel) - await activateRundownPlaylist(context, cache, true) // Activate rundownPlaylist (rehearsal) + await activateRundownPlaylist(context, playoutModel, true) // Activate rundownPlaylist (rehearsal) } ) } @@ -66,11 +66,11 @@ export async function handlePrepareRundownPlaylistForBroadcast( * Optionally activate the rundown at the end. */ export async function handleResetRundownPlaylist(context: JobContext, data: ResetRundownPlaylistProps): Promise { - return runJobWithPlayoutCache( + return runJobWithPlayoutModel( context, data, - async (cache) => { - const playlist = cache.Playlist + async (playoutModel) => { + const playlist = playoutModel.Playlist if (playlist.activationId && !playlist.rehearsal && !context.studio.settings.allowRundownResetOnAir) { throw UserError.create(UserErrorMessage.RundownResetWhileActive) } @@ -87,7 +87,7 @@ export async function handleResetRundownPlaylist(context: JobContext, data: Rese // Try deactivating everything in parallel, although there should only ever be one active await Promise.allSettled( anyOtherActivePlaylists.map(async (otherRundownPlaylist) => - runJobWithPlayoutCache( + runJobWithPlayoutModel( context, // 'forceResetAndActivateRundownPlaylist', { playlistId: otherRundownPlaylist._id }, @@ -109,15 +109,15 @@ export async function handleResetRundownPlaylist(context: JobContext, data: Rese } } }, - async (cache) => { - await resetRundownPlaylist(context, cache) + async (playoutModel) => { + await resetRundownPlaylist(context, playoutModel) if (data.activate) { // Do the activation - await activateRundownPlaylist(context, cache, data.activate !== 'active') // Activate rundown - } else if (cache.Playlist.activationId) { + await activateRundownPlaylist(context, playoutModel, data.activate !== 'active') // Activate rundown + } else if (playoutModel.Playlist.activationId) { // Only update the timeline if this is the active playlist - await updateTimeline(context, cache) + await updateTimeline(context, playoutModel) } } ) @@ -130,16 +130,16 @@ export async function handleActivateRundownPlaylist( context: JobContext, data: ActivateRundownPlaylistProps ): Promise { - return runJobWithPlayoutCache( + return runJobWithPlayoutModel( context, // 'activateRundownPlaylist', data, - async (cache) => { - const playlist = cache.Playlist + async (playoutModel) => { + const playlist = playoutModel.Playlist await checkNoOtherPlaylistsActive(context, playlist) }, - async (cache) => { - await activateRundownPlaylist(context, cache, data.rehearsal) + async (playoutModel) => { + await activateRundownPlaylist(context, playoutModel, data.rehearsal) } ) } @@ -151,13 +151,13 @@ export async function handleDeactivateRundownPlaylist( context: JobContext, data: DeactivateRundownPlaylistProps ): Promise { - return runJobWithPlayoutCache( + return runJobWithPlayoutModel( context, // 'deactivateRundownPlaylist', data, null, - async (cache) => { - await deactivateRundownPlaylist(context, cache) + async (playoutModel) => { + await deactivateRundownPlaylist(context, playoutModel) } ) } diff --git a/packages/job-worker/src/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index def7b48810..ca0cf7cb11 100644 --- a/packages/job-worker/src/playout/adlibAction.ts +++ b/packages/job-worker/src/playout/adlibAction.ts @@ -21,7 +21,7 @@ import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/Rund import { logger } from '../logging' import { validateScratchpartPartInstanceProperties } from './scratchpad' import { PlayoutRundownModel } from './model/PlayoutRundownModel' -import { createPlayoutCachefromInitCache, loadPlayoutModelPreInit } from './model/implementation/LoadPlayoutModel' +import { createPlayoutModelfromInitModel, loadPlayoutModelPreInit } from './model/implementation/LoadPlayoutModel' /** * Execute an AdLib Action @@ -79,15 +79,15 @@ export async function handleExecuteAdlibAction( if (blueprint.blueprint.executeAction) { // load a full cache for the regular actions & executet the handler - const fullCache: PlayoutModel = await createPlayoutCachefromInitCache(context, initCache) + const playoutModel: PlayoutModel = await createPlayoutModelfromInitModel(context, initCache) - const fullRundown = fullCache.getRundown(rundown._id) + const fullRundown = playoutModel.getRundown(rundown._id) if (!fullRundown) throw new Error(`Rundown "${rundown._id}" missing between caches`) try { const res: ExecuteActionResult = await executeActionInner( context, - fullCache, + playoutModel, fullRundown, showStyle, blueprint, @@ -95,11 +95,11 @@ export async function handleExecuteAdlibAction( actionParameters ) - await fullCache.saveAllToDatabase() + await playoutModel.saveAllToDatabase() return res } catch (err) { - fullCache.dispose() + playoutModel.dispose() throw err } } @@ -123,7 +123,7 @@ export interface ExecuteActionParameters { export async function executeActionInner( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, rundown: PlayoutRundownModel, showStyle: ReadonlyDeep, blueprint: ReadonlyDeep, @@ -132,7 +132,7 @@ export async function executeActionInner( ): Promise { const now = getCurrentTime() - const playlist = cache.Playlist + const playlist = playoutModel.Playlist const actionContext = new ActionExecutionContext( { @@ -143,7 +143,7 @@ export async function executeActionInner( tempSendUserNotesIntoBlackHole: true, // TODO-CONTEXT store these notes }, context, - cache, + playoutModel, showStyle, context.getShowStyleBlueprintConfig(showStyle), rundown, @@ -171,7 +171,7 @@ export async function executeActionInner( throw UserError.fromUnknown(err, UserErrorMessage.InternalError) } - await applyAnyExecutionSideEffects(context, cache, actionContext, now) + await applyAnyExecutionSideEffects(context, playoutModel, actionContext, now) return { queuedPartInstanceId: actionContext.queuedPartInstanceId, @@ -181,7 +181,7 @@ export async function executeActionInner( async function applyAnyExecutionSideEffects( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, actionContext: ActionExecutionContext, now: number ) { @@ -191,36 +191,36 @@ async function applyAnyExecutionSideEffects( ) { await syncPlayheadInfinitesForNextPartInstance( context, - cache, - cache.CurrentPartInstance, - cache.NextPartInstance + playoutModel, + playoutModel.CurrentPartInstance, + playoutModel.NextPartInstance ) } if (actionContext.nextPartState !== ActionPartChange.NONE) { - const nextPartInstanceId = cache.Playlist.nextPartInfo?.partInstanceId + const nextPartInstanceId = playoutModel.Playlist.nextPartInfo?.partInstanceId if (nextPartInstanceId) { - updateExpectedDurationWithPrerollForPartInstance(cache, nextPartInstanceId) + updateExpectedDurationWithPrerollForPartInstance(playoutModel, nextPartInstanceId) - validateScratchpartPartInstanceProperties(context, cache, nextPartInstanceId) + validateScratchpartPartInstanceProperties(context, playoutModel, nextPartInstanceId) } } if (actionContext.currentPartState !== ActionPartChange.NONE) { - const currentPartInstanceId = cache.Playlist.currentPartInfo?.partInstanceId + const currentPartInstanceId = playoutModel.Playlist.currentPartInfo?.partInstanceId if (currentPartInstanceId) { - validateScratchpartPartInstanceProperties(context, cache, currentPartInstanceId) + validateScratchpartPartInstanceProperties(context, playoutModel, currentPartInstanceId) } } if (actionContext.takeAfterExecute) { - await performTakeToNextedPart(context, cache, now) + await performTakeToNextedPart(context, playoutModel, now) } else { if ( actionContext.currentPartState !== ActionPartChange.NONE || actionContext.nextPartState !== ActionPartChange.NONE ) { - await updateTimeline(context, cache) + await updateTimeline(context, playoutModel) } } } diff --git a/packages/job-worker/src/playout/adlibJobs.ts b/packages/job-worker/src/playout/adlibJobs.ts index b2c656859c..679a2e38e0 100644 --- a/packages/job-worker/src/playout/adlibJobs.ts +++ b/packages/job-worker/src/playout/adlibJobs.ts @@ -14,10 +14,10 @@ import { } from '@sofie-automation/corelib/dist/worker/studio' import { PlayoutModel } from './model/PlayoutModel' import { PlayoutPartInstanceModel } from './model/PlayoutPartInstanceModel' -import { runJobWithPlayoutCache } from './lock' +import { runJobWithPlayoutModel } from './lock' import { updateTimeline } from './timeline/generate' import { getCurrentTime } from '../lib' -import { comparePieceStart, convertAdLibToPieceInstance, convertPieceToAdLibPiece } from './pieces' +import { comparePieceStart, convertAdLibToGenericPiece, convertPieceToAdLibPiece } from './pieces' import { getResolvedPiecesForCurrentPartInstance } from './resolvedPieces' import { syncPlayheadInfinitesForNextPartInstance } from './infinites' import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/error' @@ -35,11 +35,11 @@ import { PlayoutPieceInstanceModel } from './model/PlayoutPieceInstanceModel' * Play an existing Piece in the Rundown as an AdLib */ export async function handleTakePieceAsAdlibNow(context: JobContext, data: TakePieceAsAdlibNowProps): Promise { - return runJobWithPlayoutCache( + return runJobWithPlayoutModel( context, data, - async (cache) => { - const playlist = cache.Playlist + async (playoutModel) => { + const playlist = playoutModel.Playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown) if (playlist.holdState === RundownHoldState.ACTIVE || playlist.holdState === RundownHoldState.PENDING) { throw UserError.create(UserErrorMessage.DuringHold) @@ -48,16 +48,18 @@ export async function handleTakePieceAsAdlibNow(context: JobContext, data: TakeP if (playlist.currentPartInfo?.partInstanceId !== data.partInstanceId) throw UserError.create(UserErrorMessage.AdlibCurrentPart) }, - async (cache) => { - const currentPartInstance = cache.CurrentPartInstance + async (playoutModel) => { + const currentPartInstance = playoutModel.CurrentPartInstance if (!currentPartInstance) throw UserError.create(UserErrorMessage.InactiveRundown) - const currentRundown = cache.getRundown(currentPartInstance.PartInstance.rundownId) + const currentRundown = playoutModel.getRundown(currentPartInstance.PartInstance.rundownId) if (!currentRundown) throw new Error(`Missing Rundown for PartInstance: ${currentPartInstance.PartInstance._id}`) - const rundownIds = cache.getRundownIds() + const rundownIds = playoutModel.getRundownIds() - const pieceInstanceToCopy = cache.findPieceInstance(data.pieceInstanceIdOrPieceIdToCopy as PieceInstanceId) + const pieceInstanceToCopy = playoutModel.findPieceInstance( + data.pieceInstanceIdOrPieceIdToCopy as PieceInstanceId + ) const pieceToCopy = pieceInstanceToCopy ? clone(pieceInstanceToCopy.pieceInstance.PieceInstance.piece) @@ -89,7 +91,7 @@ export async function handleTakePieceAsAdlibNow(context: JobContext, data: TakeP case IBlueprintDirectPlayType.AdLibPiece: await pieceTakeNowAsAdlib( context, - cache, + playoutModel, showStyleCompound, currentPartInstance, pieceToCopy, @@ -104,7 +106,7 @@ export async function handleTakePieceAsAdlibNow(context: JobContext, data: TakeP await executeActionInner( context, - cache, + playoutModel, currentRundown, showStyleCompound, blueprint, @@ -130,7 +132,7 @@ export async function handleTakePieceAsAdlibNow(context: JobContext, data: TakeP } async function pieceTakeNowAsAdlib( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, showStyleBase: ReadonlyDeep, currentPartInstance: PlayoutPartInstanceModel, pieceToCopy: PieceInstancePiece, @@ -138,7 +140,8 @@ async function pieceTakeNowAsAdlib( | { partInstance: PlayoutPartInstanceModel; pieceInstance: PlayoutPieceInstanceModel } | undefined ): Promise { - /*const newPieceInstance = */ convertAdLibToPieceInstance(context, pieceToCopy, currentPartInstance, false) + const genericAdlibPiece = convertAdLibToGenericPiece(pieceToCopy, false) + /*const newPieceInstance = */ currentPartInstance.insertAdlibbedPiece(genericAdlibPiece, pieceToCopy._id) // Disable the original piece if from the same Part if ( @@ -179,20 +182,25 @@ async function pieceTakeNowAsAdlib( pieceInstanceToCopy.pieceInstance.setDisabled(true) } - await syncPlayheadInfinitesForNextPartInstance(context, cache, cache.CurrentPartInstance, cache.NextPartInstance) + await syncPlayheadInfinitesForNextPartInstance( + context, + playoutModel, + playoutModel.CurrentPartInstance, + playoutModel.NextPartInstance + ) - await updateTimeline(context, cache) + await updateTimeline(context, playoutModel) } /** * Play an AdLib piece by its id */ export async function handleAdLibPieceStart(context: JobContext, data: AdlibPieceStartProps): Promise { - return runJobWithPlayoutCache( + return runJobWithPlayoutModel( context, data, - async (cache) => { - const playlist = cache.Playlist + async (playoutModel) => { + const playlist = playoutModel.Playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown) if (playlist.holdState === RundownHoldState.ACTIVE || playlist.holdState === RundownHoldState.PENDING) { throw UserError.create(UserErrorMessage.DuringHold) @@ -201,14 +209,14 @@ export async function handleAdLibPieceStart(context: JobContext, data: AdlibPiec if (!data.queue && playlist.currentPartInfo?.partInstanceId !== data.partInstanceId) throw UserError.create(UserErrorMessage.AdlibCurrentPart) }, - async (cache) => { - const partInstance = cache.getPartInstance(data.partInstanceId) + async (playoutModel) => { + const partInstance = playoutModel.getPartInstance(data.partInstanceId) if (!partInstance) throw new Error(`PartInstance "${data.partInstanceId}" not found!`) - const rundown = cache.getRundown(partInstance.PartInstance.rundownId) + const rundown = playoutModel.getRundown(partInstance.PartInstance.rundownId) if (!rundown) throw new Error(`Rundown "${partInstance.PartInstance.rundownId}" not found!`) // Rundows that share the same showstyle variant as the current rundown, so adlibs from these rundowns are safe to play - const safeRundownIds = cache.Rundowns.filter( + const safeRundownIds = playoutModel.Rundowns.filter( (rd) => rd.Rundown.showStyleVariantId === rundown.Rundown.showStyleVariantId ).map((r) => r.Rundown._id) @@ -257,7 +265,7 @@ export async function handleAdLibPieceStart(context: JobContext, data: AdlibPiec UserErrorMessage.AdlibUnplayable ) - await innerStartOrQueueAdLibPiece(context, cache, rundown, !!data.queue, partInstance, adLibPiece) + await innerStartOrQueueAdLibPiece(context, playoutModel, rundown, !!data.queue, partInstance, adLibPiece) } ) } @@ -269,22 +277,22 @@ export async function handleStartStickyPieceOnSourceLayer( context: JobContext, data: StartStickyPieceOnSourceLayerProps ): Promise { - return runJobWithPlayoutCache( + return runJobWithPlayoutModel( context, data, - async (cache) => { - const playlist = cache.Playlist + async (playoutModel) => { + const playlist = playoutModel.Playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown) if (playlist.holdState === RundownHoldState.ACTIVE || playlist.holdState === RundownHoldState.PENDING) { throw UserError.create(UserErrorMessage.DuringHold) } if (!playlist.currentPartInfo) throw UserError.create(UserErrorMessage.NoCurrentPart) }, - async (cache) => { - const currentPartInstance = cache.CurrentPartInstance + async (playoutModel) => { + const currentPartInstance = playoutModel.CurrentPartInstance if (!currentPartInstance) throw UserError.create(UserErrorMessage.NoCurrentPart) - const rundown = cache.getRundown(currentPartInstance.PartInstance.rundownId) + const rundown = playoutModel.getRundown(currentPartInstance.PartInstance.rundownId) if (!rundown) throw new Error(`Rundown "${currentPartInstance.PartInstance.rundownId}" not found!`) const showStyleBase = await context.getShowStyleBase(rundown.Rundown.showStyleBaseId) @@ -299,7 +307,7 @@ export async function handleStartStickyPieceOnSourceLayer( const lastPieceInstance = await innerFindLastPieceOnLayer( context, - cache, + playoutModel, [sourceLayer._id], sourceLayer.stickyOriginalOnly || false ) @@ -308,7 +316,7 @@ export async function handleStartStickyPieceOnSourceLayer( } const lastPiece = convertPieceToAdLibPiece(context, lastPieceInstance.piece) - await innerStartOrQueueAdLibPiece(context, cache, rundown, false, currentPartInstance, lastPiece) + await innerStartOrQueueAdLibPiece(context, playoutModel, rundown, false, currentPartInstance, lastPiece) } ) } @@ -321,31 +329,31 @@ export async function handleStopPiecesOnSourceLayers( data: StopPiecesOnSourceLayersProps ): Promise { if (data.sourceLayerIds.length === 0) return - return runJobWithPlayoutCache( + return runJobWithPlayoutModel( context, data, - async (cache) => { - const playlist = cache.Playlist + async (playoutModel) => { + const playlist = playoutModel.Playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown) if (playlist.holdState === RundownHoldState.ACTIVE || playlist.holdState === RundownHoldState.PENDING) { throw UserError.create(UserErrorMessage.DuringHold) } if (!playlist.currentPartInfo) throw UserError.create(UserErrorMessage.NoCurrentPart) }, - async (cache) => { - const partInstance = cache.getPartInstance(data.partInstanceId) + async (playoutModel) => { + const partInstance = playoutModel.getPartInstance(data.partInstanceId) if (!partInstance) throw new Error(`PartInstance "${data.partInstanceId}" not found!`) const lastStartedPlayback = partInstance.PartInstance.timings?.plannedStartedPlayback if (!lastStartedPlayback) throw new Error(`Part "${data.partInstanceId}" has yet to start playback!`) - const rundown = cache.getRundown(partInstance.PartInstance.rundownId) + const rundown = playoutModel.getRundown(partInstance.PartInstance.rundownId) if (!rundown) throw new Error(`Rundown "${partInstance.PartInstance.rundownId}" not found!`) const showStyleBase = await context.getShowStyleBase(rundown.Rundown.showStyleBaseId) const sourceLayerIds = new Set(data.sourceLayerIds) const changedIds = innerStopPieces( context, - cache, + playoutModel, showStyleBase.sourceLayers, partInstance, (pieceInstance) => sourceLayerIds.has(pieceInstance.piece.sourceLayerId), @@ -355,12 +363,12 @@ export async function handleStopPiecesOnSourceLayers( if (changedIds.length) { await syncPlayheadInfinitesForNextPartInstance( context, - cache, - cache.CurrentPartInstance, - cache.NextPartInstance + playoutModel, + playoutModel.CurrentPartInstance, + playoutModel.NextPartInstance ) - await updateTimeline(context, cache) + await updateTimeline(context, playoutModel) } } ) @@ -370,24 +378,24 @@ export async function handleStopPiecesOnSourceLayers( * Disable the next Piece which allows being disabled */ export async function handleDisableNextPiece(context: JobContext, data: DisableNextPieceProps): Promise { - return runJobWithPlayoutCache( + return runJobWithPlayoutModel( context, data, - async (cache) => { - const playlist = cache.Playlist + async (playoutModel) => { + const playlist = playoutModel.Playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown) if (!playlist.currentPartInfo) throw UserError.create(UserErrorMessage.NoCurrentPart) }, - async (cache) => { - const playlist = cache.Playlist + async (playoutModel) => { + const playlist = playoutModel.Playlist - const currentPartInstance = cache.CurrentPartInstance - const nextPartInstance = cache.NextPartInstance + const currentPartInstance = playoutModel.CurrentPartInstance + const nextPartInstance = playoutModel.NextPartInstance if (!currentPartInstance) throw new Error(`PartInstance "${playlist.currentPartInfo?.partInstanceId}" not found!`) - const rundown = cache.getRundown(currentPartInstance.PartInstance.rundownId) + const rundown = playoutModel.getRundown(currentPartInstance.PartInstance.rundownId) if (!rundown) throw new Error(`Rundown "${currentPartInstance.PartInstance.rundownId}" not found!`) const showStyleBase = await context.getShowStyleBase(rundown.Rundown.showStyleBaseId) @@ -461,9 +469,9 @@ export async function handleDisableNextPiece(context: JobContext, data: DisableN } if (disabledPiece) { - await updateTimeline(context, cache) + await updateTimeline(context, playoutModel) } else { - cache.assertNoChanges() + playoutModel.assertNoChanges() throw UserError.create(UserErrorMessage.DisableNoPieceFound) } diff --git a/packages/job-worker/src/playout/adlibUtils.ts b/packages/job-worker/src/playout/adlibUtils.ts index 8e9bfdf33e..02351eb57e 100644 --- a/packages/job-worker/src/playout/adlibUtils.ts +++ b/packages/job-worker/src/playout/adlibUtils.ts @@ -1,8 +1,8 @@ import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' -import { PartInstanceId, PieceInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PartInstanceId, PieceId, PieceInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' -import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { PieceInstance, PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { assertNever, getRandomId, getRank } from '@sofie-automation/corelib/dist/lib' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { getCurrentTime } from '../lib' @@ -14,7 +14,7 @@ import { getPieceInstancesForPart, syncPlayheadInfinitesForNextPartInstance, } from './infinites' -import { convertAdLibToPieceInstance } from './pieces' +import { convertAdLibToGenericPiece } from './pieces' import { getResolvedPiecesForCurrentPartInstance } from './resolvedPieces' import { updateTimeline } from './timeline/generate' import { PieceLifespan } from '@sofie-automation/blueprints-integration' @@ -26,6 +26,8 @@ import { calculateNowOffsetLatency } from './timeline/multi-gateway' import { logger } from '../logging' import { ReadonlyDeep } from 'type-fest' import { PlayoutRundownModel } from './model/PlayoutRundownModel' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' export async function innerStartOrQueueAdLibPiece( context: JobContext, @@ -38,25 +40,31 @@ export async function innerStartOrQueueAdLibPiece( const span = context.startSpan('innerStartOrQueueAdLibPiece') let queuedPartInstanceId: PartInstanceId | undefined if (queue || adLibPiece.toBeQueued) { - const newPartInstance = playoutModel.insertAdlibbedPartInstance({ + const adlibbedPart: Omit = { _id: getRandomId(), _rank: 99999, // Corrected in innerStartQueuedAdLib externalId: '', title: adLibPiece.name, expectedDuration: adLibPiece.expectedDuration, expectedDurationWithPreroll: adLibPiece.expectedDuration, // Filled in later - }) - - convertAdLibToPieceInstance(context, adLibPiece, newPartInstance, queue) - - newPartInstance.recalculateExpectedDurationWithPreroll() + } - await innerStartQueuedAdLib(context, playoutModel, rundown, currentPartInstance, newPartInstance) + const genericAdlibPiece = convertAdLibToGenericPiece(adLibPiece, true) + const newPartInstance = await insertQueuedPartWithPieces( + context, + playoutModel, + rundown, + currentPartInstance, + adlibbedPart, + [genericAdlibPiece], + adLibPiece._id + ) queuedPartInstanceId = newPartInstance.PartInstance._id // syncPlayheadInfinitesForNextPartInstance is handled by setNextPart } else { - convertAdLibToPieceInstance(context, adLibPiece, currentPartInstance, queue) + const genericAdlibPiece = convertAdLibToGenericPiece(adLibPiece, false) + currentPartInstance.insertAdlibbedPiece(genericAdlibPiece, adLibPiece._id) await syncPlayheadInfinitesForNextPartInstance( context, @@ -112,14 +120,14 @@ export async function innerFindLastPieceOnLayer( export async function innerFindLastScriptedPieceOnLayer( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, sourceLayerId: string[], customQuery?: MongoQuery ): Promise { const span = context.startSpan('innerFindLastScriptedPieceOnLayer') - const playlist = cache.Playlist - const rundownIds = cache.getRundownIds() + const playlist = playoutModel.Playlist + const rundownIds = playoutModel.getRundownIds() // TODO - this should throw instead of return more? @@ -127,7 +135,7 @@ export async function innerFindLastScriptedPieceOnLayer( return } - const currentPartInstance = cache.CurrentPartInstance?.PartInstance + const currentPartInstance = playoutModel.CurrentPartInstance?.PartInstance if (!currentPartInstance) { return @@ -145,7 +153,7 @@ export async function innerFindLastScriptedPieceOnLayer( }) const pieceIdSet = new Set(pieces.map((p) => p.startPartId)) - const part = cache + const part = playoutModel .getAllOrderedParts() .filter((p) => pieceIdSet.has(p._id) && p._rank <= currentPartInstance.part._rank) .reverse()[0] @@ -179,23 +187,22 @@ export async function innerFindLastScriptedPieceOnLayer( return fullPiece } -export async function innerStartQueuedAdLib( +function updateRankForAdlibbedPartInstance( context: JobContext, - cache: PlayoutModel, - rundown: PlayoutRundownModel, - currentPartInstance: PlayoutPartInstanceModel, + playoutModel: PlayoutModel, newPartInstance: PlayoutPartInstanceModel -): Promise { - const span = context.startSpan('innerStartQueuedAdLib') +) { + const currentPartInstance = playoutModel.CurrentPartInstance + if (!currentPartInstance) throw new Error('CurrentPartInstance not found') // Find the following part, so we can pick a good rank const followingPart = selectNextPart( context, - cache.Playlist, + playoutModel.Playlist, currentPartInstance.PartInstance, null, - cache.getAllOrderedSegments(), - cache.getAllOrderedParts(), + playoutModel.getAllOrderedSegments(), + playoutModel.getAllOrderedParts(), false // We want to insert it before any trailing invalid piece ) newPartInstance.setRank( @@ -205,34 +212,57 @@ export async function innerStartQueuedAdLib( ) ) - updatePartInstanceRanksAfterAdlib(cache, newPartInstance.PartInstance.segmentId) + updatePartInstanceRanksAfterAdlib(playoutModel, newPartInstance.PartInstance.segmentId) +} - // Find and insert any rundown defined infinites that we should inherit - const possiblePieces = await fetchPiecesThatMayBeActiveForPart( - context, - cache, - undefined, - newPartInstance.PartInstance.part - ) +export async function insertQueuedPartWithPieces( + context: JobContext, + playoutModel: PlayoutModel, + rundown: PlayoutRundownModel, + currentPartInstance: PlayoutPartInstanceModel, + newPart: Omit, + initialPieces: Omit[], + fromAdlibId: PieceId | undefined +): Promise { + const span = context.startSpan('insertQueuedPartWithPieces') + + const newPartFull: DBPart = { + ...newPart, + segmentId: currentPartInstance.PartInstance.segmentId, + rundownId: currentPartInstance.PartInstance.rundownId, + } + + // Find any rundown defined infinites that we should inherit + const possiblePieces = await fetchPiecesThatMayBeActiveForPart(context, playoutModel, undefined, newPartFull) const infinitePieceInstances = getPieceInstancesForPart( context, - cache, + playoutModel, currentPartInstance, rundown, - newPartInstance.PartInstance.part, + newPartFull, possiblePieces, - newPartInstance.PartInstance._id + protectString('') // Replaced inside playoutModel.insertAdlibbedPartInstance + ) + + const newPartInstance = playoutModel.createAdlibbedPartInstance( + newPart, + initialPieces, + fromAdlibId, + infinitePieceInstances ) - newPartInstance.insertInfinitePieces(infinitePieceInstances) - await setNextPart(context, cache, newPartInstance, false) + updateRankForAdlibbedPartInstance(context, playoutModel, newPartInstance) + + await setNextPart(context, playoutModel, newPartInstance, false) if (span) span.end() + + return newPartInstance } export function innerStopPieces( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, sourceLayers: SourceLayers, currentPartInstance: PlayoutPartInstanceModel, filter: (pieceInstance: ReadonlyDeep) => boolean, @@ -247,7 +277,7 @@ export function innerStopPieces( } const resolvedPieces = getResolvedPiecesForCurrentPartInstance(context, sourceLayers, currentPartInstance) - const offsetRelativeToNow = (timeOffset || 0) + (calculateNowOffsetLatency(context, cache, undefined) || 0) + const offsetRelativeToNow = (timeOffset || 0) + (calculateNowOffsetLatency(context, playoutModel, undefined) || 0) const stopAt = getCurrentTime() + offsetRelativeToNow const relativeStopAt = stopAt - lastStartedPlayback @@ -267,9 +297,9 @@ export function innerStopPieces( case PieceLifespan.OutOnRundownChange: { logger.info(`Blueprint action: Cropping PieceInstance "${pieceInstance._id}" to ${stopAt}`) - const pieceInstanceModel = cache.findPieceInstance(pieceInstance._id) + const pieceInstanceModel = playoutModel.findPieceInstance(pieceInstance._id) if (pieceInstanceModel) { - const newDuration: Required['userDuration'] = cache.isMultiGatewayMode + const newDuration: Required['userDuration'] = playoutModel.isMultiGatewayMode ? { endRelativeToNow: offsetRelativeToNow, } diff --git a/packages/job-worker/src/playout/datastore.ts b/packages/job-worker/src/playout/datastore.ts index abafbf9459..28206349a5 100644 --- a/packages/job-worker/src/playout/datastore.ts +++ b/packages/job-worker/src/playout/datastore.ts @@ -10,8 +10,8 @@ export function getDatastoreId(studioId: StudioId, key: string): TimelineDatasto } /** Remove documents in the TimelineDatastore collection where mode is temporary and has no references from the timeline */ -export async function cleanTimelineDatastore(context: JobContext, cache: PlayoutModel): Promise { - const timeline = cache.Timeline +export async function cleanTimelineDatastore(context: JobContext, playoutModel: PlayoutModel): Promise { + const timeline = playoutModel.Timeline if (!timeline) { return diff --git a/packages/job-worker/src/playout/debug.ts b/packages/job-worker/src/playout/debug.ts index 51f264f932..294bbfe315 100644 --- a/packages/job-worker/src/playout/debug.ts +++ b/packages/job-worker/src/playout/debug.ts @@ -2,12 +2,12 @@ import { DebugRegenerateNextPartInstanceProps, DebugSyncInfinitesForNextPartInstanceProps, } from '@sofie-automation/corelib/dist/worker/studio' -import { runJobWithStudioCache } from '../studio/lock' +import { runJobWithStudioPlayoutModel } from '../studio/lock' import { JobContext } from '../jobs' import { logger } from '../logging' import { syncPlayheadInfinitesForNextPartInstance } from './infinites' import { setNextPart } from './setNext' -import { runJobWithPlayoutCache } from './lock' +import { runJobWithPlayoutModel } from './lock' import { updateStudioTimeline, updateTimeline } from './timeline/generate' /** @@ -20,12 +20,12 @@ export async function handleDebugSyncPlayheadInfinitesForNextPartInstance( ): Promise { logger.info(`syncPlayheadInfinitesForNextPartInstance ${data.playlistId}`) - await runJobWithPlayoutCache(context, data, null, async (cache) => { + await runJobWithPlayoutModel(context, data, null, async (playoutModel) => { await syncPlayheadInfinitesForNextPartInstance( context, - cache, - cache.CurrentPartInstance, - cache.NextPartInstance + playoutModel, + playoutModel.CurrentPartInstance, + playoutModel.NextPartInstance ) }) } @@ -40,22 +40,22 @@ export async function handleDebugRegenerateNextPartInstance( ): Promise { logger.info('regenerateNextPartInstance') - await runJobWithPlayoutCache(context, data, null, async (cache) => { - const playlist = cache.Playlist + await runJobWithPlayoutModel(context, data, null, async (playoutModel) => { + const playlist = playoutModel.Playlist const originalNextPartInfo = playlist.nextPartInfo if (originalNextPartInfo && playlist.activationId) { - const nextPartInstance = cache.NextPartInstance - const part = nextPartInstance ? cache.findPart(nextPartInstance.PartInstance.part._id) : undefined + const nextPartInstance = playoutModel.NextPartInstance + const part = nextPartInstance ? playoutModel.findPart(nextPartInstance.PartInstance.part._id) : undefined if (part) { - await setNextPart(context, cache, null, false) + await setNextPart(context, playoutModel, null, false) await setNextPart( context, - cache, + playoutModel, { part: part, consumesQueuedSegmentId: false }, originalNextPartInfo.manuallySelected ) - await updateTimeline(context, cache) + await updateTimeline(context, playoutModel) } } }) @@ -67,10 +67,10 @@ export async function handleDebugRegenerateNextPartInstance( export async function handleDebugCrash(context: JobContext, data: DebugRegenerateNextPartInstanceProps): Promise { logger.info('debugCrash') - await runJobWithPlayoutCache(context, data, null, async (cache) => { + await runJobWithPlayoutModel(context, data, null, async (playoutModel) => { setTimeout(() => { //@ts-expect-error: 2339 - cache.callUndefined() + playoutModel.callUndefined() }, 10) }) } @@ -79,14 +79,14 @@ export async function handleDebugCrash(context: JobContext, data: DebugRegenerat * Debug: Regenerate the timeline for the Studio */ export async function handleDebugUpdateTimeline(context: JobContext, _data: void): Promise { - await runJobWithStudioCache(context, async (studioCache) => { + await runJobWithStudioPlayoutModel(context, async (studioCache) => { const activePlaylists = studioCache.getActiveRundownPlaylists() if (activePlaylists.length > 1) { throw new Error(`Too many active playlists`) } else if (activePlaylists.length > 0) { const playlist = activePlaylists[0] - await runJobWithPlayoutCache(context, { playlistId: playlist._id }, null, async (playoutCache) => { + await runJobWithPlayoutModel(context, { playlistId: playlist._id }, null, async (playoutCache) => { await updateTimeline(context, playoutCache) }) } else { diff --git a/packages/job-worker/src/playout/holdJobs.ts b/packages/job-worker/src/playout/holdJobs.ts index 01cb0bf5e3..08333c01ce 100644 --- a/packages/job-worker/src/playout/holdJobs.ts +++ b/packages/job-worker/src/playout/holdJobs.ts @@ -3,18 +3,18 @@ import { RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/Rundo import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/error' import { ActivateHoldProps, DeactivateHoldProps } from '@sofie-automation/corelib/dist/worker/studio' import { JobContext } from '../jobs' -import { runJobWithPlayoutCache } from './lock' +import { runJobWithPlayoutModel } from './lock' import { updateTimeline } from './timeline/generate' /** * Activate Hold */ export async function handleActivateHold(context: JobContext, data: ActivateHoldProps): Promise { - return runJobWithPlayoutCache( + return runJobWithPlayoutModel( context, data, - async (cache) => { - const playlist = cache.Playlist + async (playoutModel) => { + const playlist = playoutModel.Playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown) @@ -23,12 +23,12 @@ export async function handleActivateHold(context: JobContext, data: ActivateHold if (playlist.holdState) throw UserError.create(UserErrorMessage.HoldAlreadyActive) }, - async (cache) => { - const playlist = cache.Playlist - const currentPartInstance = cache.CurrentPartInstance + async (playoutModel) => { + const playlist = playoutModel.Playlist + const currentPartInstance = playoutModel.CurrentPartInstance if (!currentPartInstance) throw new Error(`PartInstance "${playlist.currentPartInfo?.partInstanceId}" not found!`) - const nextPartInstance = cache.NextPartInstance + const nextPartInstance = playoutModel.NextPartInstance if (!nextPartInstance) throw new Error(`PartInstance "${playlist.nextPartInfo?.partInstanceId}" not found!`) if ( @@ -48,9 +48,9 @@ export async function handleActivateHold(context: JobContext, data: ActivateHold ) if (hasDynamicallyInserted) throw UserError.create(UserErrorMessage.HoldAfterAdlib) - cache.setHoldState(RundownHoldState.PENDING) + playoutModel.setHoldState(RundownHoldState.PENDING) - await updateTimeline(context, cache) + await updateTimeline(context, playoutModel) } ) } @@ -59,21 +59,21 @@ export async function handleActivateHold(context: JobContext, data: ActivateHold * Deactivate Hold */ export async function handleDeactivateHold(context: JobContext, data: DeactivateHoldProps): Promise { - return runJobWithPlayoutCache( + return runJobWithPlayoutModel( context, data, - async (cache) => { - const playlist = cache.Playlist + async (playoutModel) => { + const playlist = playoutModel.Playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown) if (playlist.holdState !== RundownHoldState.PENDING) throw UserError.create(UserErrorMessage.HoldNotCancelable) }, - async (cache) => { - cache.setHoldState(RundownHoldState.NONE) + async (playoutModel) => { + playoutModel.setHoldState(RundownHoldState.NONE) - await updateTimeline(context, cache) + await updateTimeline(context, playoutModel) } ) } diff --git a/packages/job-worker/src/playout/infinites.ts b/packages/job-worker/src/playout/infinites.ts index a89e3e6182..05d8d98087 100644 --- a/packages/job-worker/src/playout/infinites.ts +++ b/packages/job-worker/src/playout/infinites.ts @@ -77,17 +77,17 @@ export function candidatePartIsAfterPreviewPartInstance( * Get the ids of parts, segments and rundowns before a given part in the playlist. * Note: this will return no segments and rundowns if the part is in the scratchpad */ -function getIdsBeforeThisPart(context: JobContext, cache: PlayoutModel, nextPart: ReadonlyDeep) { +function getIdsBeforeThisPart(context: JobContext, playoutModel: PlayoutModel, nextPart: ReadonlyDeep) { const span = context.startSpan('getIdsBeforeThisPart') - const currentRundown = cache.getRundown(nextPart.rundownId) + const currentRundown = playoutModel.getRundown(nextPart.rundownId) const currentSegment = currentRundown?.getSegment(nextPart.segmentId) // Get the normal parts const partsBeforeThisInSegment = currentSegment?.Parts?.filter((p) => p._rank < nextPart._rank) ?? [] // Find any orphaned parts - const partInstancesBeforeThisInSegment = cache.LoadedPartInstances.filter( + const partInstancesBeforeThisInSegment = playoutModel.LoadedPartInstances.filter( (p) => p.PartInstance.segmentId === nextPart.segmentId && !!p.PartInstance.orphaned && @@ -120,8 +120,8 @@ function getIdsBeforeThisPart(context: JobContext, cache: PlayoutModel, nextPart : [] const sortedRundownIds = sortRundownIDsInPlaylist( - cache.Playlist.rundownIdsInOrder, - cache.Rundowns.map((rd) => rd.Rundown._id) + playoutModel.Playlist.rundownIdsInOrder, + playoutModel.Rundowns.map((rd) => rd.Rundown._id) ) const currentRundownIndex = sortedRundownIds.indexOf(nextPart.rundownId) const rundownsToReceiveOnShowStyleEndFrom = @@ -140,14 +140,14 @@ function getIdsBeforeThisPart(context: JobContext, cache: PlayoutModel, nextPart * Find all infinite Pieces that _may_ be active in the given Part, which will be continued from a previous part * Either search a provided ingest cache, or the database for these Pieces * @param context Context of the current job - * @param cache Playout cache for the current playlist + * @param playoutModel Playout cache for the current playlist * @param unsavedIngestCache If an ingest cache is loaded, we can search that instead of mongo * @param part The Part to get the Pieces for * @returns Array of Piece */ export async function fetchPiecesThatMayBeActiveForPart( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, unsavedIngestCache: Omit, 'Rundown'> | undefined, part: ReadonlyDeep ): Promise { @@ -165,7 +165,7 @@ export async function fetchPiecesThatMayBeActiveForPart( // Figure out the ids of everything else we will have to search through const { partsToReceiveOnSegmentEndFrom, segmentsToReceiveOnRundownEndFrom, rundownsToReceiveOnShowStyleEndFrom } = - getIdsBeforeThisPart(context, cache, part) + getIdsBeforeThisPart(context, playoutModel, part) if (unsavedIngestCache?.RundownId === part.rundownId) { // Find pieces for the current rundown @@ -210,33 +210,33 @@ export async function fetchPiecesThatMayBeActiveForPart( /** * Update the onChange infinites for the nextPartInstance to be up to date with the ones on the currentPartInstance * @param context Context for the current job - * @param cache Playout cache for the current playlist + * @param playoutModel Playout cache for the current playlist */ export async function syncPlayheadInfinitesForNextPartInstance( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, fromPartInstance: PlayoutPartInstanceModel | null, toPartInstance: PlayoutPartInstanceModel | null ): Promise { const span = context.startSpan('syncPlayheadInfinitesForNextPartInstance') if (toPartInstance && fromPartInstance) { - const playlist = cache.Playlist + const playlist = playoutModel.Playlist if (!playlist.activationId) throw new Error(`RundownPlaylist "${playlist._id}" is not active`) const { partsToReceiveOnSegmentEndFrom, segmentsToReceiveOnRundownEndFrom, rundownsToReceiveOnShowStyleEndFrom, - } = getIdsBeforeThisPart(context, cache, toPartInstance.PartInstance.part) + } = getIdsBeforeThisPart(context, playoutModel, toPartInstance.PartInstance.part) - const currentRundown = cache.getRundown(fromPartInstance.PartInstance.rundownId) + const currentRundown = playoutModel.getRundown(fromPartInstance.PartInstance.rundownId) if (!currentRundown) throw new Error(`Rundown "${fromPartInstance.PartInstance.rundownId}" not found!`) const currentSegment = currentRundown.getSegment(fromPartInstance.PartInstance.segmentId) if (!currentSegment) throw new Error(`Segment "${fromPartInstance.PartInstance.segmentId}" not found!`) - const nextRundown = cache.getRundown(toPartInstance.PartInstance.rundownId) + const nextRundown = playoutModel.getRundown(toPartInstance.PartInstance.rundownId) if (!nextRundown) throw new Error(`Rundown "${toPartInstance.PartInstance.rundownId}" not found!`) const nextSegment = nextRundown.getSegment(toPartInstance.PartInstance.segmentId) @@ -246,7 +246,7 @@ export async function syncPlayheadInfinitesForNextPartInstance( const nextPartIsAfterCurrentPart = candidatePartIsAfterPreviewPartInstance( context, - cache.getAllOrderedSegments(), + playoutModel.getAllOrderedSegments(), fromPartInstance.PartInstance, toPartInstance.PartInstance.part ) @@ -260,7 +260,7 @@ export async function syncPlayheadInfinitesForNextPartInstance( true ) - const rundownIdsToShowstyleIds = getShowStyleIdsRundownMapping(cache.Rundowns) + const rundownIdsToShowstyleIds = getShowStyleIdsRundownMapping(playoutModel.Rundowns) const infinites = libgetPlayheadTrackingInfinitesForPart( playlist.activationId, @@ -287,7 +287,7 @@ export async function syncPlayheadInfinitesForNextPartInstance( /** * Calculate all of the onEnd PieceInstances for a PartInstance * @param context Context for the running job - * @param cache Playout cache for the current playlist + * @param playoutModel Playout cache for the current playlist * @param playingPartInstance The current PartInstance, if there is one * @param rundown The Rundown the Part belongs to * @param part The Part the PartInstance is based on @@ -297,7 +297,7 @@ export async function syncPlayheadInfinitesForNextPartInstance( */ export function getPieceInstancesForPart( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, playingPartInstance: PlayoutPartInstanceModel | null, rundown: PlayoutRundownModel, part: ReadonlyDeep, @@ -306,26 +306,26 @@ export function getPieceInstancesForPart( ): PieceInstance[] { const span = context.startSpan('getPieceInstancesForPart') const { partsToReceiveOnSegmentEndFrom, segmentsToReceiveOnRundownEndFrom, rundownsToReceiveOnShowStyleEndFrom } = - getIdsBeforeThisPart(context, cache, part) + getIdsBeforeThisPart(context, playoutModel, part) - const playlist = cache.Playlist + const playlist = playoutModel.Playlist if (!playlist.activationId) throw new Error(`RundownPlaylist "${playlist._id}" is not active`) const playingPieceInstances = playingPartInstance?.PieceInstances ?? [] const nextPartIsAfterCurrentPart = candidatePartIsAfterPreviewPartInstance( context, - cache.getAllOrderedSegments(), + playoutModel.getAllOrderedSegments(), playingPartInstance?.PartInstance, part ) - const rundownIdsToShowstyleIds = getShowStyleIdsRundownMapping(cache.Rundowns) + const rundownIdsToShowstyleIds = getShowStyleIdsRundownMapping(playoutModel.Rundowns) let playingRundown: PlayoutRundownModel | undefined let playingSegment: PlayoutSegmentModel | undefined if (playingPartInstance) { - playingRundown = cache.getRundown(playingPartInstance.PartInstance.rundownId) + playingRundown = playoutModel.getRundown(playingPartInstance.PartInstance.rundownId) if (!playingRundown) throw new Error(`Rundown "${playingPartInstance.PartInstance.rundownId}" not found!`) playingSegment = playingRundown.getSegment(playingPartInstance.PartInstance.segmentId) @@ -348,7 +348,7 @@ export function getPieceInstancesForPart( rundownsToReceiveOnShowStyleEndFrom, rundownIdsToShowstyleIds, possiblePieces, - cache.getAllOrderedParts().map((p) => p._id), + playoutModel.getAllOrderedParts().map((p) => p._id), newInstanceId, nextPartIsAfterCurrentPart, false diff --git a/packages/job-worker/src/playout/lib.ts b/packages/job-worker/src/playout/lib.ts index 96c9f33aff..caf1d6ec2b 100644 --- a/packages/job-worker/src/playout/lib.ts +++ b/packages/job-worker/src/playout/lib.ts @@ -18,49 +18,49 @@ import { selectNextPart } from './selectNextPart' * Reset the rundownPlaylist (all of the rundowns within the playlist): * Remove all dynamically inserted/updated pieces, parts, timings etc.. */ -export async function resetRundownPlaylist(context: JobContext, cache: PlayoutModel): Promise { - logger.info('resetRundownPlaylist ' + cache.Playlist._id) +export async function resetRundownPlaylist(context: JobContext, playoutModel: PlayoutModel): Promise { + logger.info('resetRundownPlaylist ' + playoutModel.Playlist._id) // Remove all dunamically inserted pieces (adlibs etc) // const rundownIds = new Set((cache.getRundownIds())) - removePartInstancesWithPieceInstances(context, cache, { rehearsal: true }) - resetPartInstancesWithPieceInstances(context, cache) + playoutModel.resetPlaylist(!!playoutModel.Playlist.activationId) + + playoutModel.removeAllRehearsalPartInstances() + resetPartInstancesWithPieceInstances(context, playoutModel) // Remove the scratchpad - for (const rundown of cache.Rundowns) { + for (const rundown of playoutModel.Rundowns) { rundown.removeScratchpadSegment() } - cache.resetPlaylist(!!cache.Playlist.activationId) - - if (cache.Playlist.activationId) { + if (playoutModel.Playlist.activationId) { // put the first on queue: const firstPart = selectNextPart( context, - cache.Playlist, + playoutModel.Playlist, null, null, - cache.getAllOrderedSegments(), - cache.getAllOrderedParts() + playoutModel.getAllOrderedSegments(), + playoutModel.getAllOrderedParts() ) - await setNextPart(context, cache, firstPart, false) + await setNextPart(context, playoutModel, firstPart, false) } else { - await setNextPart(context, cache, null, false) + await setNextPart(context, playoutModel, null, false) } } /** * Reset selected or all partInstances with their pieceInstances - * @param cache + * @param playoutModel * @param selector if not provided, all partInstances will be reset */ export function resetPartInstancesWithPieceInstances( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, selector?: MongoQuery ): void { const partInstanceIdsToReset: PartInstanceId[] = [] - for (const partInstance of cache.LoadedPartInstances) { + for (const partInstance of playoutModel.LoadedPartInstances) { if (!partInstance.PartInstance.reset && (!selector || mongoWhere(partInstance.PartInstance, selector))) { partInstance.markAsReset() partInstanceIdsToReset.push(partInstance.PartInstance._id) @@ -68,9 +68,9 @@ export function resetPartInstancesWithPieceInstances( } // Defer ones which arent loaded - cache.deferAfterSave(async (cache) => { - const rundownIds = cache.getRundownIds() - const partInstanceIdsInCache = cache.LoadedPartInstances.map((p) => p.PartInstance._id) + playoutModel.deferAfterSave(async (playoutModel) => { + const rundownIds = playoutModel.getRundownIds() + const partInstanceIdsInCache = playoutModel.LoadedPartInstances.map((p) => p.PartInstance._id) // Find all the partInstances which are not loaded, but should be reset const resetInDb = await context.directCollections.PartInstances.findFetch( @@ -123,61 +123,6 @@ export function resetPartInstancesWithPieceInstances( }) } -/** - * Remove selected partInstances with their pieceInstances - */ -function removePartInstancesWithPieceInstances( - context: JobContext, - cache: PlayoutModel, - selector: MongoQuery -): void { - const partInstancesToRemove: PartInstanceId[] = [] - for (const partInstance of cache.OlderPartInstances) { - if (mongoWhere(partInstance.PartInstance, selector)) { - cache.removePartInstance(partInstance.PartInstance._id) - } - } - - // Defer ones which arent loaded - cache.deferAfterSave(async (cache) => { - const rundownIds = cache.getRundownIds() - // We need to keep any for PartInstances which are still existent in the cache (as they werent removed) - const partInstanceIdsInCache = cache.LoadedPartInstances.map((p) => p.PartInstance._id) - - // Find all the partInstances which are not loaded, but should be removed - const removeFromDb = await context.directCollections.PartInstances.findFetch( - { - $and: [ - selector, - { - // Not any which are in the cache, as they have already been done if needed - _id: { $nin: partInstanceIdsInCache }, - rundownId: { $in: rundownIds }, - }, - ], - }, - { projection: { _id: 1 } } - ).then((ps) => ps.map((p) => p._id)) - - // Do the remove - const allToRemove = [...removeFromDb, ...partInstancesToRemove] - await Promise.all([ - removeFromDb.length > 0 - ? context.directCollections.PartInstances.remove({ - _id: { $in: removeFromDb }, - rundownId: { $in: rundownIds }, - }) - : undefined, - allToRemove.length > 0 - ? context.directCollections.PieceInstances.remove({ - partInstanceId: { $in: allToRemove }, - rundownId: { $in: rundownIds }, - }) - : undefined, - ]) - }) -} - export function substituteObjectIds( rawEnable: TSR.Timeline.TimelineEnable | TSR.Timeline.TimelineEnable[], idMap: { [oldId: string]: string | undefined } @@ -280,10 +225,10 @@ export function isTooCloseToAutonext( * The value is used by the UI to approximate the duration of a PartInstance as it will be played out */ export function updateExpectedDurationWithPrerollForPartInstance( - cache: PlayoutModel, + playoutModel: PlayoutModel, partInstanceId: PartInstanceId ): void { - const nextPartInstance = cache.getPartInstance(partInstanceId) + const nextPartInstance = playoutModel.getPartInstance(partInstanceId) if (nextPartInstance) { // Update expectedDurationWithPreroll of the next part instance, as it may have changed and is used by the ui until it is taken nextPartInstance.recalculateExpectedDurationWithPreroll() diff --git a/packages/job-worker/src/playout/lock.ts b/packages/job-worker/src/playout/lock.ts index a95a4bf73a..4015b74aea 100644 --- a/packages/job-worker/src/playout/lock.ts +++ b/packages/job-worker/src/playout/lock.ts @@ -6,17 +6,17 @@ import { ReadonlyDeep } from 'type-fest' import { JobContext } from '../jobs' import { PlaylistLock } from '../jobs/lock' import { PlayoutModel, PlayoutModelPreInit } from './model/PlayoutModel' -import { createPlayoutCachefromInitCache, loadPlayoutModelPreInit } from './model/implementation/LoadPlayoutModel' +import { createPlayoutModelfromInitModel, loadPlayoutModelPreInit } from './model/implementation/LoadPlayoutModel' /** * Run a typical playout job - * This means loading the playout cache in stages, doing some calculations and saving the result + * This means loading the playout model in stages, doing some calculations and saving the result */ -export async function runJobWithPlayoutCache( +export async function runJobWithPlayoutModel( context: JobContext, data: RundownPlayoutPropsBase, - preInitFcn: null | ((cache: PlayoutModelPreInit) => Promise | void), - fcn: (cache: PlayoutModel) => Promise | TRes + preInitFcn: null | ((playoutModel: PlayoutModelPreInit) => Promise | void), + fcn: (playoutModel: PlayoutModel) => Promise | TRes ): Promise { if (!data.playlistId) { throw new Error(`Job is missing playlistId`) @@ -29,7 +29,7 @@ export async function runJobWithPlayoutCache( throw new Error(`Job playlist "${data.playlistId}" not found or for another studio`) } - return runWithPlaylistCache(context, playlist, playlistLock, preInitFcn, fcn) + return runWithPlayoutModel(context, playlist, playlistLock, preInitFcn, fcn) }) } @@ -75,12 +75,12 @@ export async function runWithPlaylistLock( } } -export async function runWithPlaylistCache( +export async function runWithPlayoutModel( context: JobContext, playlist: ReadonlyDeep, lock: PlaylistLock, - preInitFcn: null | ((cache: PlayoutModelPreInit) => Promise | void), - fcn: (cache: PlayoutModel) => Promise | TRes + preInitFcn: null | ((playoutModel: PlayoutModelPreInit) => Promise | void), + fcn: (playoutModel: PlayoutModel) => Promise | TRes ): Promise { const initCache = await loadPlayoutModelPreInit(context, lock, playlist, false) @@ -88,11 +88,11 @@ export async function runWithPlaylistCache( await preInitFcn(initCache) } - const fullCache = await createPlayoutCachefromInitCache(context, initCache) + const fullCache = await createPlayoutModelfromInitModel(context, initCache) try { const res = await fcn(fullCache) - logger.silly('runWithPlaylistCache: saveAllToDatabase') + logger.silly('runWithPlayoutModel: saveAllToDatabase') await fullCache.saveAllToDatabase() return res diff --git a/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts b/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts index 8e4edbbee0..7f69132be5 100644 --- a/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts +++ b/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts @@ -9,7 +9,7 @@ import { SelectedPartInstancesTimelineInfo } from '../../timeline/generate' import { getLookeaheadObjects } from '..' import { LookaheadMode, PlaylistTimingType, TSR } from '@sofie-automation/blueprints-integration' import { setupDefaultJobEnvironment, MockJobContext } from '../../../__mocks__/context' -import { runJobWithPlayoutCache } from '../../../playout/lock' +import { runJobWithPlayoutModel } from '../../../playout/lock' import { defaultRundownPlaylist } from '../../../__mocks__/defaultCollectionObjects' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' @@ -163,8 +163,8 @@ describe('Lookahead', () => { const fakeParts = partIds.map((p) => ({ part: { _id: p } as any, usesInTransition: true, pieces: [] })) getOrderedPartsAfterPlayheadMock.mockReturnValueOnce(fakeParts.map((p) => p.part)) - const res = await runJobWithPlayoutCache(context, { playlistId }, null, async (cache) => - getLookeaheadObjects(context, cache, partInstancesInfo) + const res = await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => + getLookeaheadObjects(context, playoutModel, partInstancesInfo) ) expect(res).toHaveLength(0) @@ -206,8 +206,8 @@ describe('Lookahead', () => { ], })) - const res = await runJobWithPlayoutCache(context, { playlistId }, null, async (cache) => - getLookeaheadObjects(context, cache, partInstancesInfo) + const res = await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => + getLookeaheadObjects(context, playoutModel, partInstancesInfo) ) expect(res).toMatchSnapshot() @@ -226,8 +226,8 @@ describe('Lookahead', () => { studio.mappingsWithOverrides.defaults['PRELOAD'].lookaheadMaxSearchDistance = 0 context.setStudio(studio) } - await runJobWithPlayoutCache(context, { playlistId }, null, async (cache) => - getLookeaheadObjects(context, cache, partInstancesInfo) + await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => + getLookeaheadObjects(context, playoutModel, partInstancesInfo) ) expect(getOrderedPartsAfterPlayheadMock).toHaveBeenCalledTimes(1) expect(getOrderedPartsAfterPlayheadMock).toHaveBeenCalledWith(context, expect.anything(), 0) @@ -240,8 +240,8 @@ describe('Lookahead', () => { studio.mappingsWithOverrides.defaults['PRELOAD'].lookaheadMaxSearchDistance = 2000 context.setStudio(studio) } - await runJobWithPlayoutCache(context, { playlistId }, null, async (cache) => - getLookeaheadObjects(context, cache, partInstancesInfo) + await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => + getLookeaheadObjects(context, playoutModel, partInstancesInfo) ) expect(getOrderedPartsAfterPlayheadMock).toHaveBeenCalledTimes(1) expect(getOrderedPartsAfterPlayheadMock).toHaveBeenCalledWith(context, expect.anything(), 2000) @@ -254,8 +254,8 @@ describe('Lookahead', () => { studio.mappingsWithOverrides.defaults['PRELOAD'].lookaheadMaxSearchDistance = -1 context.setStudio(studio) } - await runJobWithPlayoutCache(context, { playlistId }, null, async (cache) => - getLookeaheadObjects(context, cache, partInstancesInfo) + await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => + getLookeaheadObjects(context, playoutModel, partInstancesInfo) ) expect(getOrderedPartsAfterPlayheadMock).toHaveBeenCalledTimes(1) expect(getOrderedPartsAfterPlayheadMock).toHaveBeenCalledWith(context, expect.anything(), 10) @@ -286,8 +286,8 @@ describe('Lookahead', () => { } // With a previous - await runJobWithPlayoutCache(context, { playlistId }, null, async (cache) => - getLookeaheadObjects(context, cache, partInstancesInfo) + await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => + getLookeaheadObjects(context, playoutModel, partInstancesInfo) ) await expectLookaheadForLayerMock(playlistId, [], expectedPrevious, fakeParts) @@ -306,8 +306,8 @@ describe('Lookahead', () => { allPieces: partInstancesInfo.current.pieceInstances, calculatedTimings: partInstancesInfo.current.calculatedTimings, } - await runJobWithPlayoutCache(context, { playlistId }, null, async (cache) => - getLookeaheadObjects(context, cache, partInstancesInfo) + await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => + getLookeaheadObjects(context, playoutModel, partInstancesInfo) ) await expectLookaheadForLayerMock(playlistId, [expectedCurrent], expectedPrevious, fakeParts) @@ -326,16 +326,16 @@ describe('Lookahead', () => { allPieces: partInstancesInfo.next.pieceInstances, calculatedTimings: partInstancesInfo.next.calculatedTimings, } - await runJobWithPlayoutCache(context, { playlistId }, null, async (cache) => - getLookeaheadObjects(context, cache, partInstancesInfo) + await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => + getLookeaheadObjects(context, playoutModel, partInstancesInfo) ) await expectLookaheadForLayerMock(playlistId, [expectedCurrent, expectedNext], expectedPrevious, fakeParts) // current has autonext ;(partInstancesInfo.current.partInstance.part as DBPart).autoNext = true expectedNext.onTimeline = true - await runJobWithPlayoutCache(context, { playlistId }, null, async (cache) => - getLookeaheadObjects(context, cache, partInstancesInfo) + await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => + getLookeaheadObjects(context, playoutModel, partInstancesInfo) ) await expectLookaheadForLayerMock(playlistId, [expectedCurrent, expectedNext], expectedPrevious, fakeParts) }) @@ -382,9 +382,9 @@ describe('Lookahead', () => { // // pieceMap should have come through valid // ;( - // wrapWithCacheForRundownPlaylist(playlist, async (cache) => { - // await getLookeaheadObjects(context, cache, env.studio, playlist, partInstancesInfo) - // expect(cache.Pieces.initialized).toBeFalsy() + // wrapWithCacheForRundownPlaylist(playlist, async (playoutModel) => { + // await getLookeaheadObjects(context, playoutModel, env.studio, playlist, partInstancesInfo) + // expect(playoutModel.Pieces.initialized).toBeFalsy() // }) // ) // await expectLookaheadForLayerMock(playlist, [], undefined, fakeParts, pieceMap) @@ -392,18 +392,18 @@ describe('Lookahead', () => { // // Use the modified cache values // const removedIds: PieceId[] = protectStringArray(['piece_1_0', 'piece_4_0']) // ;( - // wrapWithCacheForRundownPlaylist(playlist, async (cache) => { + // wrapWithCacheForRundownPlaylist(playlist, async (playoutModel) => { // expect( - // cache.Pieces.update(removedIds[0], { + // playoutModel.Pieces.update(removedIds[0], { // $set: { // invalid: true, // }, // }) // ).toEqual(1) - // cache.Pieces.remove(removedIds[1]) - // expect(cache.Pieces.initialized).toBeTruthy() + // playoutModel.Pieces.remove(removedIds[1]) + // expect(playoutModel.Pieces.initialized).toBeTruthy() - // await getLookeaheadObjects(context, cache, env.studio, playlist, partInstancesInfo) + // await getLookeaheadObjects(context, playoutModel, env.studio, playlist, partInstancesInfo) // }) // ) // const pieceMap2 = new Map() diff --git a/packages/job-worker/src/playout/lookahead/__tests__/util.test.ts b/packages/job-worker/src/playout/lookahead/__tests__/util.test.ts index e63e1b5173..862a929fb7 100644 --- a/packages/job-worker/src/playout/lookahead/__tests__/util.test.ts +++ b/packages/job-worker/src/playout/lookahead/__tests__/util.test.ts @@ -6,7 +6,7 @@ import { getCurrentTime } from '../../../lib' import { LookaheadMode, PlaylistTimingType, TSR } from '@sofie-automation/blueprints-integration' import { getOrderedPartsAfterPlayhead } from '../util' import { MockJobContext, setupDefaultJobEnvironment } from '../../../__mocks__/context' -import { runJobWithPlayoutCache } from '../../../playout/lock' +import { runJobWithPlayoutModel } from '../../../playout/lock' import { defaultRundownPlaylist } from '../../../__mocks__/defaultCollectionObjects' import _ = require('underscore') import { wrapPartToTemporaryInstance } from '../../../__mocks__/partinstance' @@ -136,8 +136,8 @@ describe('getOrderedPartsAfterPlayhead', () => { ]) }) test('all parts come back', async () => { - const parts = await runJobWithPlayoutCache(context, { playlistId }, null, async (cache) => - getOrderedPartsAfterPlayhead(context, cache, 100) + const parts = await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => + getOrderedPartsAfterPlayhead(context, playoutModel, 100) ) expect(parts.map((p) => p._id)).toEqual(partIds) @@ -162,15 +162,15 @@ describe('getOrderedPartsAfterPlayhead', () => { }, }) - const parts = await runJobWithPlayoutCache(context, { playlistId }, null, async (cache) => - getOrderedPartsAfterPlayhead(context, cache, 100) + const parts = await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => + getOrderedPartsAfterPlayhead(context, playoutModel, 100) ) // Should not have the first expect(parts.map((p) => p._id)).toEqual(partIds.slice(1)) // Try with a limit - const parts2 = await runJobWithPlayoutCache(context, { playlistId }, null, async (cache) => - getOrderedPartsAfterPlayhead(context, cache, 5) + const parts2 = await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => + getOrderedPartsAfterPlayhead(context, playoutModel, 5) ) // Should not have the first expect(parts2.map((p) => p._id)).toEqual(partIds.slice(1, 6)) @@ -195,15 +195,15 @@ describe('getOrderedPartsAfterPlayhead', () => { }, }) - const parts = await runJobWithPlayoutCache(context, { playlistId }, null, async (cache) => - getOrderedPartsAfterPlayhead(context, cache, 100) + const parts = await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => + getOrderedPartsAfterPlayhead(context, playoutModel, 100) ) // Should not have the first expect(parts.map((p) => p._id)).toEqual(partIds.slice(1)) // Try with a limit - const parts2 = await runJobWithPlayoutCache(context, { playlistId }, null, async (cache) => - getOrderedPartsAfterPlayhead(context, cache, 5) + const parts2 = await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => + getOrderedPartsAfterPlayhead(context, playoutModel, 5) ) // Should not have the first expect(parts2.map((p) => p._id)).toEqual(partIds.slice(1, 6)) @@ -228,16 +228,16 @@ describe('getOrderedPartsAfterPlayhead', () => { }, }) - const parts = await runJobWithPlayoutCache(context, { playlistId }, null, async (cache) => - getOrderedPartsAfterPlayhead(context, cache, 100) + const parts = await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => + getOrderedPartsAfterPlayhead(context, playoutModel, 100) ) // Should be empty expect(parts.map((p) => p._id)).toEqual([]) // Playlist could loop await context.mockCollections.RundownPlaylists.update(playlistId, { $set: { loop: true } }) - const parts2 = await runJobWithPlayoutCache(context, { playlistId }, null, async (cache) => - getOrderedPartsAfterPlayhead(context, cache, 5) + const parts2 = await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => + getOrderedPartsAfterPlayhead(context, playoutModel, 5) ) // Should be empty expect(parts2.map((p) => p._id)).toEqual(partIds.slice(0, 5)) @@ -251,8 +251,8 @@ describe('getOrderedPartsAfterPlayhead', () => { $set: { invalid: true }, } ) - const parts3 = await runJobWithPlayoutCache(context, { playlistId }, null, async (cache) => - getOrderedPartsAfterPlayhead(context, cache, 5) + const parts3 = await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => + getOrderedPartsAfterPlayhead(context, playoutModel, 5) ) // Should be empty expect(parts3.map((p) => p._id)).toEqual([partIds[0], ...partIds.slice(2, 4), ...partIds.slice(5, 7)]) @@ -286,8 +286,8 @@ describe('getOrderedPartsAfterPlayhead', () => { }, }) - const parts = await runJobWithPlayoutCache(context, { playlistId }, null, async (cache) => - getOrderedPartsAfterPlayhead(context, cache, 5) + const parts = await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => + getOrderedPartsAfterPlayhead(context, playoutModel, 5) ) // Should not have the first expect(parts.map((p) => p._id)).toEqual([partIds[5], partIds[6], partIds[8], partIds[9], partIds[10]]) @@ -314,8 +314,8 @@ describe('getOrderedPartsAfterPlayhead', () => { // Change next segment await context.mockCollections.RundownPlaylists.update(playlistId, { $set: { queuedSegmentId: segmentId2 } }) - const parts = await runJobWithPlayoutCache(context, { playlistId }, null, async (cache) => - getOrderedPartsAfterPlayhead(context, cache, 10) + const parts = await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => + getOrderedPartsAfterPlayhead(context, playoutModel, 10) ) expect(parts.map((p) => p._id)).toEqual([...partIds.slice(1, 5), ...partIds.slice(8)]) @@ -328,8 +328,8 @@ describe('getOrderedPartsAfterPlayhead', () => { $set: { invalid: true }, } ) - const parts2 = await runJobWithPlayoutCache(context, { playlistId }, null, async (cache) => - getOrderedPartsAfterPlayhead(context, cache, 10) + const parts2 = await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => + getOrderedPartsAfterPlayhead(context, playoutModel, 10) ) expect(parts2.map((p) => p._id)).toEqual([...partIds.slice(1, 5), ...partIds.slice(9)]) @@ -342,8 +342,8 @@ describe('getOrderedPartsAfterPlayhead', () => { $set: { invalid: true }, } ) - const parts3 = await runJobWithPlayoutCache(context, { playlistId }, null, async (cache) => - getOrderedPartsAfterPlayhead(context, cache, 10) + const parts3 = await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => + getOrderedPartsAfterPlayhead(context, playoutModel, 10) ) expect(parts3.map((p) => p._id)).toEqual(partIds.slice(1, 8)) }) diff --git a/packages/job-worker/src/playout/lookahead/index.ts b/packages/job-worker/src/playout/lookahead/index.ts index 826b37ddc8..5cf9761caf 100644 --- a/packages/job-worker/src/playout/lookahead/index.ts +++ b/packages/job-worker/src/playout/lookahead/index.ts @@ -45,7 +45,7 @@ type ValidLookaheadMode = LookaheadMode.PRELOAD | LookaheadMode.WHEN_CLEAR export async function getLookeaheadObjects( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, partInstancesInfo0: SelectedPartInstancesTimelineInfo ): Promise> { const span = context.startSpan('getLookeaheadObjects') @@ -59,10 +59,10 @@ export async function getLookeaheadObjects( } const maxLookaheadDistance = findLargestLookaheadDistance(mappingsToConsider) - const orderedPartsFollowingPlayhead = getOrderedPartsAfterPlayhead(context, cache, maxLookaheadDistance) + const orderedPartsFollowingPlayhead = getOrderedPartsAfterPlayhead(context, playoutModel, maxLookaheadDistance) const piecesToSearchQuery: FilterQuery = { - startRundownId: { $in: cache.getRundownIds() }, + startRundownId: { $in: playoutModel.getRundownIds() }, startPartId: { $in: orderedPartsFollowingPlayhead.map((p) => p._id) }, invalid: { $ne: true }, } @@ -165,7 +165,7 @@ export async function getLookeaheadObjects( const lookaheadObjs = findLookaheadForLayer( context, - cache.Playlist.currentPartInfo?.partInstanceId ?? null, + playoutModel.Playlist.currentPartInfo?.partInstanceId ?? null, partInstancesInfo, previousPartInfo, orderedPartInfos, diff --git a/packages/job-worker/src/playout/lookahead/util.ts b/packages/job-worker/src/playout/lookahead/util.ts index 48694985bc..5cf54c020e 100644 --- a/packages/job-worker/src/playout/lookahead/util.ts +++ b/packages/job-worker/src/playout/lookahead/util.ts @@ -37,7 +37,7 @@ export function isPieceInstance(piece: Piece | PieceInstance | PieceInstancePiec */ export function getOrderedPartsAfterPlayhead( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, partCount: number ): ReadonlyDeep[] { if (partCount <= 0) { @@ -45,11 +45,11 @@ export function getOrderedPartsAfterPlayhead( } const span = context.startSpan('getOrderedPartsAfterPlayhead') - const playlist = cache.Playlist - const orderedSegments = cache.getAllOrderedSegments() - const orderedParts = cache.getAllOrderedParts() - const currentPartInstance = cache.CurrentPartInstance?.PartInstance - const nextPartInstance = cache.NextPartInstance?.PartInstance + const playlist = playoutModel.Playlist + const orderedSegments = playoutModel.getAllOrderedSegments() + const orderedParts = playoutModel.getAllOrderedParts() + const currentPartInstance = playoutModel.CurrentPartInstance?.PartInstance + const nextPartInstance = playoutModel.NextPartInstance?.PartInstance // If the nextPartInstance consumes the const alreadyConsumedQueuedSegmentId = diff --git a/packages/job-worker/src/playout/model/PlayoutModel.ts b/packages/job-worker/src/playout/model/PlayoutModel.ts index c34a8ea790..a87c722c82 100644 --- a/packages/job-worker/src/playout/model/PlayoutModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutModel.ts @@ -1,6 +1,7 @@ import { PartId, PartInstanceId, + PieceId, PieceInstanceId, RundownId, RundownPlaylistActivationId, @@ -17,7 +18,7 @@ import { import { ReadonlyDeep } from 'type-fest' import { StudioPlayoutModelBase, StudioPlayoutModelBaseReadonly } from '../../studio/StudioPlayoutModel' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' -import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { PieceInstance, PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { PlaylistLock } from '../../jobs/lock' import { PlayoutRundownModel } from './PlayoutRundownModel' import { PlayoutSegmentModel } from './PlayoutSegmentModel' @@ -26,8 +27,8 @@ import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/Perip import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { PlayoutPieceInstanceModel } from './PlayoutPieceInstanceModel' -export type DeferredFunction = (cache: PlayoutModel) => void | Promise -export type DeferredAfterSaveFunction = (cache: PlayoutModelReadonly) => void | Promise +export type DeferredFunction = (playoutModel: PlayoutModel) => void | Promise +export type DeferredAfterSaveFunction = (playoutModel: PlayoutModelReadonly) => void | Promise export interface PlayoutModelPreInit { readonly PlaylistId: RundownPlaylistId @@ -42,7 +43,6 @@ export interface PlayoutModelPreInit { } export interface PlayoutModelReadonly extends StudioPlayoutModelBaseReadonly { - readonly isPlayout: true readonly PlaylistId: RundownPlaylistId readonly PlaylistLock: PlaylistLock @@ -50,8 +50,6 @@ export interface PlayoutModelReadonly extends StudioPlayoutModelBaseReadonly { get Playlist(): ReadonlyDeep get Rundowns(): readonly PlayoutRundownModel[] - get HackDeletedPartInstanceIds(): PartInstanceId[] - get OlderPartInstances(): PlayoutPartInstanceModel[] get PreviousPartInstance(): PlayoutPartInstanceModel | null get CurrentPartInstance(): PlayoutPartInstanceModel | null @@ -81,37 +79,37 @@ export interface PlayoutModelReadonly extends StudioPlayoutModelBaseReadonly { } export interface PlayoutModel extends PlayoutModelReadonly, StudioPlayoutModelBase, ICacheBase2 { + /** + * Temporary hack for debug logging + */ + get HackDeletedPartInstanceIds(): PartInstanceId[] + + activatePlaylist(rehearsal: boolean): RundownPlaylistActivationId + + clearSelectedPartInstances(): void + + createAdlibbedPartInstance( + part: Omit, + pieces: Omit[], + fromAdlibId: PieceId | undefined, + infinitePieceInstances: PieceInstance[] + ): PlayoutPartInstanceModel + createInstanceForPart(nextPart: ReadonlyDeep, pieceInstances: PieceInstance[]): PlayoutPartInstanceModel - insertAdlibbedPartInstance(part: Omit): PlayoutPartInstanceModel - insertScratchpadPartInstance( + + createScratchpadPartInstance( rundown: PlayoutRundownModel, part: Omit ): PlayoutPartInstanceModel - /** - * HACK: This allows for taking a copy of a `PartInstanceWithPieces` for use in `syncChangesToPartInstances`. - * This lets us discard the changes if the blueprint call throws. - * We should look at avoiding this messy/dangerous method, and find a better way to do this - */ - replacePartInstance(partInstance: PlayoutPartInstanceModel): void - /** @deprecated HACK */ - removePartInstance(id: PartInstanceId): void - - setHoldState(newState: RundownHoldState): void - setQueuedSegment(segment: PlayoutSegmentModel | null): void - cycleSelectedPartInstances(): void - setRundownStartedPlayback(rundownId: RundownId, timestamp: number): void - setPartInstanceAsNext( - partInstance: PlayoutPartInstanceModel | null, - setManually: boolean, - consumesQueuedSegmentId: boolean, - nextTimeOffset?: number - ): void - clearSelectedPartInstances(): void - activatePlaylist(rehearsal: boolean): RundownPlaylistActivationId deactivatePlaylist(): void + + queuePartInstanceTimingEvent(partInstanceId: PartInstanceId): void + + removeAllRehearsalPartInstances(): void + removeUntakenPartInstances(): void /** @@ -119,25 +117,31 @@ export interface PlayoutModel extends PlayoutModelReadonly, StudioPlayoutModelBa */ resetPlaylist(regenerateActivationId: boolean): void + setHoldState(newState: RundownHoldState): void + setOnTimelineGenerateResult( persistentState: unknown | undefined, assignedAbSessions: Record, trackedAbSessions: ABSessionInfo[] ): void - queuePartInstanceTimingEvent(partInstanceId: PartInstanceId): void + setPartInstanceAsNext( + partInstance: PlayoutPartInstanceModel | null, + setManually: boolean, + consumesQueuedSegmentId: boolean, + nextTimeOffset?: number + ): void + + setQueuedSegment(segment: PlayoutSegmentModel | null): void + + setRundownStartedPlayback(rundownId: RundownId, timestamp: number): void + + /** Lifecycle */ /** @deprecated */ deferBeforeSave(fcn: DeferredFunction): void /** @deprecated */ deferAfterSave(fcn: DeferredAfterSaveFunction): void - /** - * Assert that no changes should have been made to the cache, will throw an Error otherwise. This can be used in - * place of `saveAllToDatabase()`, when the code controlling the cache expects no changes to have been made and any - * changes made are an error and will cause issues. - */ - assertNoChanges(): void - saveAllToDatabase(): Promise } diff --git a/packages/job-worker/src/playout/model/PlayoutPartInstanceModel.ts b/packages/job-worker/src/playout/model/PlayoutPartInstanceModel.ts index 5b0b151b75..e39b324cfb 100644 --- a/packages/job-worker/src/playout/model/PlayoutPartInstanceModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutPartInstanceModel.ts @@ -12,79 +12,112 @@ import { IBlueprintMutatablePart, PieceLifespan, Time } from '@sofie-automation/ import { PartCalculatedTimings } from '@sofie-automation/corelib/dist/playout/timings' import { PlayoutPieceInstanceModel } from './PlayoutPieceInstanceModel' +/** + * Token returned when making a backup copy of a PlayoutPartInstanceModel + * The contents of this type is opaque and will vary fully across implementations + */ +export interface PlayoutPartInstanceModelSnapshot { + __isPlayoutPartInstanceModelBackup: true +} + export interface PlayoutPartInstanceModel { readonly PartInstance: ReadonlyDeep readonly PieceInstances: PlayoutPieceInstanceModel[] - clone(): PlayoutPartInstanceModel - - setPlaylistActivationId(id: RundownPlaylistActivationId): void - - insertAdlibbedPiece( - piece: Omit, - fromAdlibId: PieceId | undefined - ): PlayoutPieceInstanceModel - - recalculateExpectedDurationWithPreroll(): void + /** + * Take a snapshot of the current state of this PlayoutPartInstanceModel + * This can be restored with `snapshotRestore` to rollback to a previous state of the model + */ + snapshotMakeCopy(): PlayoutPartInstanceModelSnapshot - replaceInfinitesFromPreviousPlayhead(pieces: PieceInstance[]): void + /** + * Restore a snapshot of this PlayoutPartInstanceModel, to rollback to a previous state + * Note: It is only possible to restore each snapshot once. + * Note: Any references to child `PlayoutPieceInstanceModel` or `DBPartInstance` may no longer be valid after this operation + * @param snapshot Snapshot to restore + */ + snapshotRestore(snapshot: PlayoutPartInstanceModelSnapshot): void - markAsReset(): void + appendNotes(notes: PartNote[]): void blockTakeUntil(timestamp: Time | null): void clearPlannedTimings(): void - setRank(rank: number): void + getPieceInstance(id: PieceInstanceId): PlayoutPieceInstanceModel | undefined - setOrphaned(orphaned: 'adlib-part' | 'deleted' | undefined): void + insertAdlibbedPiece( + piece: Omit, + fromAdlibId: PieceId | undefined + ): PlayoutPieceInstanceModel - setTaken(takeTime: number, playOffset: number): void + insertHoldPieceInstance( + extendPieceInstance: PlayoutPieceInstanceModel, + infiniteInstanceId: PieceInstanceInfiniteId + ): PlayoutPieceInstanceModel - storePlayoutTimingsAndPreviousEndState( - partPlayoutTimings: PartCalculatedTimings, - previousPartEndState: unknown - ): void + /** + * Insert a Piece as if it were originally planned at the time of ingest + * This is a weird operation to have for playout, but it is a needed part of the SyncIngestChanges flow + * @param piece Piece to insert into this PartInstance + * @returns The inserted PlayoutPieceInstanceModel + */ + insertPlannedPiece(piece: Omit): PlayoutPieceInstanceModel - appendNotes(notes: PartNote[]): void + insertVirtualPiece( + start: number, + lifespan: PieceLifespan, + sourceLayerId: string, + outputLayerId: string + ): PlayoutPieceInstanceModel - updatePartProps(props: Partial): void + markAsReset(): void - getPieceInstance(id: PieceInstanceId): PlayoutPieceInstanceModel | undefined + recalculateExpectedDurationWithPreroll(): void /** - * Replace a PieceInstance with a new version. - * If there is an existing PieceInstance with the same id, it will be merged onto that - * Note: this will replace any playout owned properties too - * @param doc + * Remove a PieceInstance from the model. + * This is a slightly dangerous operation to have, as it could remove a PieceInstance which will be readded by the ingest or SyncIngestChanges logic + * @param id Piece to remove from this PartInstance + * @returns Whether the PieceInstance was found and removed */ - replacePieceInstance(doc: ReadonlyDeep): PlayoutPieceInstanceModel + removePieceInstance(id: PieceInstanceId): boolean - /** @deprecated HACK */ - insertPlannedPiece(doc: Omit): PlayoutPieceInstanceModel + replaceInfinitesFromPreviousPlayhead(pieceInstances: PieceInstance[]): void - /** @deprecated HACK */ - removePieceInstance(id: PieceInstanceId): boolean + /** + * Merge a PieceInstance with a new version, or insert as a new PieceInstance. + * If there is an existing PieceInstance with the same id, it will be merged onto that + * Note: this can replace any playout owned properties too + * @param pieceInstance Replacement PieceInstance to use + * @returns The inserted PlayoutPieceInstanceModel + */ + mergeOrInsertPieceInstance(pieceInstance: ReadonlyDeep): PlayoutPieceInstanceModel - /** @deprecated HACK */ - insertInfinitePieces(pieceInstances: PieceInstance[]): void + setOrphaned(orphaned: 'adlib-part' | 'deleted' | undefined): void + + setPlaylistActivationId(id: RundownPlaylistActivationId): void setPlannedStartedPlayback(time: Time | undefined): void setPlannedStoppedPlayback(time: Time): void setReportedStartedPlayback(time: Time): boolean setReportedStoppedPlayback(time: Time): boolean - validateScratchpadSegmentProperties(): void + setRank(rank: number): void - addHoldPieceInstance( - extendPieceInstance: PlayoutPieceInstanceModel, - infiniteInstanceId: PieceInstanceInfiniteId - ): PlayoutPieceInstanceModel + setTaken(takeTime: number, playOffset: number): void - insertVirtualPiece( - start: number, - lifespan: PieceLifespan, - sourceLayerId: string, - outputLayerId: string - ): PlayoutPieceInstanceModel + storePlayoutTimingsAndPreviousEndState( + partPlayoutTimings: PartCalculatedTimings, + previousPartEndState: unknown + ): void + + /** + * + * @param props + * @returns True if any valid properties were provided + */ + updatePartProps(props: Partial): boolean + + validateScratchpadSegmentProperties(): void } diff --git a/packages/job-worker/src/playout/model/PlayoutPieceInstanceModel.ts b/packages/job-worker/src/playout/model/PlayoutPieceInstanceModel.ts index b174f5ab20..224050d533 100644 --- a/packages/job-worker/src/playout/model/PlayoutPieceInstanceModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutPieceInstanceModel.ts @@ -6,16 +6,16 @@ import { Time } from '@sofie-automation/blueprints-integration' export interface PlayoutPieceInstanceModel { readonly PieceInstance: ReadonlyDeep - updatePieceProps(props: Partial): void + prepareForHold(): PieceInstanceInfiniteId + + setDisabled(disabled: boolean): void + + setDuration(duration: Required['userDuration']): void setPlannedStartedPlayback(time: Time): boolean setPlannedStoppedPlayback(time: Time | undefined): boolean setReportedStartedPlayback(time: Time): boolean setReportedStoppedPlayback(time: Time): boolean - prepareForHold(): PieceInstanceInfiniteId - - setDuration(duration: Required['userDuration']): void - - setDisabled(disabled: boolean): void + updatePieceProps(props: Partial): void } diff --git a/packages/job-worker/src/playout/model/PlayoutSegmentModel.ts b/packages/job-worker/src/playout/model/PlayoutSegmentModel.ts index fa8bdd8665..880c234282 100644 --- a/packages/job-worker/src/playout/model/PlayoutSegmentModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutSegmentModel.ts @@ -12,7 +12,7 @@ export interface PlayoutSegmentModel { */ readonly Parts: ReadonlyDeep - getPartIds(): PartId[] - getPart(id: PartId): ReadonlyDeep | undefined + + getPartIds(): PartId[] } diff --git a/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts b/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts index 03ec62695d..2e6b3cb3e5 100644 --- a/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts +++ b/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts @@ -111,31 +111,31 @@ async function loadInitData( return [peripheralDevices, reloadedPlaylist, rundowns] } -export async function createPlayoutCachefromInitCache( +export async function createPlayoutModelfromInitModel( context: JobContext, - initCache: PlayoutModelPreInit + initModel: PlayoutModelPreInit ): Promise { const span = context.startSpan('CacheForPlayout.fromInit') - if (span) span.setLabel('playlistId', unprotectString(initCache.PlaylistId)) + if (span) span.setLabel('playlistId', unprotectString(initModel.PlaylistId)) - if (!initCache.PlaylistLock.isLocked) { + if (!initModel.PlaylistLock.isLocked) { throw new Error('Cannot create cache with released playlist lock') } - const rundownIds = initCache.Rundowns.map((r) => r._id) + const rundownIds = initModel.Rundowns.map((r) => r._id) const [partInstances, rundownsWithContent, timeline] = await Promise.all([ - loadPartInstances(context, initCache.Playlist, rundownIds), - loadRundowns(context, null, initCache.Rundowns), + loadPartInstances(context, initModel.Playlist, rundownIds), + loadRundowns(context, null, initModel.Rundowns), loadTimeline(context), ]) const res = new PlayoutModelImpl( context, - initCache.PlaylistLock, - initCache.PlaylistId, - initCache.PeripheralDevices, - clone(initCache.Playlist), + initModel.PlaylistLock, + initModel.PlaylistId, + initModel.PeripheralDevices, + clone(initModel.Playlist), partInstances, rundownsWithContent, timeline diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts index 3b25d3d67f..176c12c3f9 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts @@ -1,6 +1,7 @@ import { PartId, PartInstanceId, + PieceId, PieceInstanceId, RundownId, RundownPlaylistActivationId, @@ -20,7 +21,11 @@ import { ReadonlyDeep } from 'type-fest' import { JobContext } from '../../../jobs' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' -import { getPieceInstanceIdForPiece, PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { + getPieceInstanceIdForPiece, + PieceInstance, + PieceInstancePiece, +} from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { serializeTimelineBlob, TimelineComplete, @@ -42,54 +47,55 @@ import { getCurrentTime } from '../../../lib' import { protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { queuePartInstanceTimingEvent } from '../../timings/events' import { IS_PRODUCTION } from '../../../environment' -import { DeferredAfterSaveFunction, DeferredFunction, PlayoutModel } from '../PlayoutModel' +import { DeferredAfterSaveFunction, DeferredFunction, PlayoutModel, PlayoutModelReadonly } from '../PlayoutModel' import { writePartInstancesAndPieceInstances, writeScratchpadSegments } from './SavePlayoutModel' import { PlayoutPieceInstanceModel } from '../PlayoutPieceInstanceModel' -/** - * This is a cache used for playout operations. - * It contains everything that is needed to generate the timeline, and everything except for pieces needed to update the partinstances. - * Anything not in this cache should not be needed often, and only for specific operations (eg, AdlibActions needed to run one). - */ -export class PlayoutModelImpl implements PlayoutModel { - #deferredBeforeSaveFunctions: DeferredFunction[] = [] - #deferredAfterSaveFunctions: DeferredAfterSaveFunction[] = [] - #disposed = false - - public readonly isPlayout = true +export class PlayoutModelReadonlyImpl implements PlayoutModelReadonly { public readonly PlaylistId: RundownPlaylistId public readonly PlaylistLock: PlaylistLock public readonly PeripheralDevices: ReadonlyDeep - #PlaylistHasChanged = false - readonly #Playlist: DBRundownPlaylist + protected readonly PlaylistImpl: DBRundownPlaylist public get Playlist(): ReadonlyDeep { - return this.#Playlist + return this.PlaylistImpl } - readonly #Rundowns: readonly PlayoutRundownModelImpl[] + protected readonly RundownsImpl: readonly PlayoutRundownModelImpl[] public get Rundowns(): readonly PlayoutRundownModel[] { - return this.#Rundowns + return this.RundownsImpl } - #TimelineHasChanged = false - #Timeline: TimelineComplete | null + protected TimelineImpl: TimelineComplete | null public get Timeline(): TimelineComplete | null { - return this.#Timeline + return this.TimelineImpl } - #PendingPartInstanceTimingEvents = new Set() + protected AllPartInstances: Map - #AllPartInstances: Map + public constructor( + protected readonly context: JobContext, + playlistLock: PlaylistLock, + playlistId: RundownPlaylistId, + peripheralDevices: ReadonlyDeep, + playlist: DBRundownPlaylist, + partInstances: PlayoutPartInstanceModelImpl[], + rundowns: PlayoutRundownModelImpl[], + timeline: TimelineComplete | undefined + ) { + this.PlaylistId = playlistId + this.PlaylistLock = playlistLock - get HackDeletedPartInstanceIds(): PartInstanceId[] { - const result: PartInstanceId[] = [] - for (const [id, doc] of this.#AllPartInstances) { - if (!doc) result.push(id) - } - return result + this.PeripheralDevices = peripheralDevices + this.PlaylistImpl = playlist + + this.RundownsImpl = rundowns + + this.TimelineImpl = timeline ?? null + + this.AllPartInstances = normalizeArrayToMapFunc(partInstances, (p) => p.PartInstance._id) } public get OlderPartInstances(): PlayoutPartInstanceModel[] { @@ -101,19 +107,19 @@ export class PlayoutModelImpl implements PlayoutModel { } public get PreviousPartInstance(): PlayoutPartInstanceModel | null { if (!this.Playlist.previousPartInfo?.partInstanceId) return null - const partInstance = this.#AllPartInstances.get(this.Playlist.previousPartInfo.partInstanceId) + const partInstance = this.AllPartInstances.get(this.Playlist.previousPartInfo.partInstanceId) if (!partInstance) return null // throw new Error('PreviousPartInstance is missing') return partInstance } public get CurrentPartInstance(): PlayoutPartInstanceModel | null { if (!this.Playlist.currentPartInfo?.partInstanceId) return null - const partInstance = this.#AllPartInstances.get(this.Playlist.currentPartInfo.partInstanceId) + const partInstance = this.AllPartInstances.get(this.Playlist.currentPartInfo.partInstanceId) if (!partInstance) return null // throw new Error('CurrentPartInstance is missing') return partInstance } public get NextPartInstance(): PlayoutPartInstanceModel | null { if (!this.Playlist.nextPartInfo?.partInstanceId) return null - const partInstance = this.#AllPartInstances.get(this.Playlist.nextPartInfo.partInstanceId) + const partInstance = this.AllPartInstances.get(this.Playlist.nextPartInfo.partInstanceId) if (!partInstance) return null // throw new Error('NextPartInstance is missing') return partInstance } @@ -131,7 +137,7 @@ export class PlayoutModelImpl implements PlayoutModel { } public get LoadedPartInstances(): PlayoutPartInstanceModel[] { - return Array.from(this.#AllPartInstances.values()).filter((v): v is PlayoutPartInstanceModelImpl => v !== null) + return Array.from(this.AllPartInstances.values()).filter((v): v is PlayoutPartInstanceModelImpl => v !== null) } public get SortedLoadedPartInstances(): PlayoutPartInstanceModel[] { @@ -142,47 +148,7 @@ export class PlayoutModelImpl implements PlayoutModel { } public getPartInstance(partInstanceId: PartInstanceId): PlayoutPartInstanceModel | undefined { - return this.#AllPartInstances.get(partInstanceId) ?? undefined - } - - public constructor( - protected readonly context: JobContext, - playlistLock: PlaylistLock, - playlistId: RundownPlaylistId, - peripheralDevices: ReadonlyDeep, - playlist: DBRundownPlaylist, - partInstances: PlayoutPartInstanceModelImpl[], - rundowns: PlayoutRundownModelImpl[], - timeline: TimelineComplete | undefined - ) { - context.trackCache(this) - - this.PlaylistId = playlistId - this.PlaylistLock = playlistLock - - this.PeripheralDevices = peripheralDevices - this.#Playlist = playlist - - this.#Rundowns = rundowns - - this.#Timeline = timeline ?? null - - this.#AllPartInstances = normalizeArrayToMapFunc(partInstances, (p) => p.PartInstance._id) - } - - public get DisplayName(): string { - return `CacheForPlayout "${this.PlaylistId}"` - } - - setTimeline(timelineObjs: TimelineObjGeneric[], generationVersions: TimelineCompleteGenerationVersions): void { - this.#Timeline = { - _id: this.context.studioId, - timelineHash: getRandomId(), // randomized on every timeline change - generated: getCurrentTime(), - timelineBlob: serializeTimelineBlob(timelineObjs), - generationVersions: generationVersions, - } - this.#TimelineHasChanged = true + return this.AllPartInstances.get(partInstanceId) ?? undefined } /** @@ -236,45 +202,98 @@ export class PlayoutModelImpl implements PlayoutModel { return undefined } - createInstanceForPart(nextPart: ReadonlyDeep, pieceInstances: PieceInstance[]): PlayoutPartInstanceModel { - const playlistActivationId = this.Playlist.activationId - if (!playlistActivationId) throw new Error(`Playlist is not active`) + #isMultiGatewayMode: boolean | undefined = undefined + public get isMultiGatewayMode(): boolean { + if (this.#isMultiGatewayMode === undefined) { + if (this.context.studio.settings.forceMultiGatewayMode) { + this.#isMultiGatewayMode = true + } else { + const playoutDevices = this.PeripheralDevices.filter( + (device) => device.type === PeripheralDeviceType.PLAYOUT + ) + this.#isMultiGatewayMode = playoutDevices.length > 1 + } + } + return this.#isMultiGatewayMode + } +} - const currentPartInstance = this.CurrentPartInstance +/** + * This is a cache used for playout operations. + * It contains everything that is needed to generate the timeline, and everything except for pieces needed to update the partinstances. + * Anything not in this cache should not be needed often, and only for specific operations (eg, AdlibActions needed to run one). + */ +export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements PlayoutModel { + #deferredBeforeSaveFunctions: DeferredFunction[] = [] + #deferredAfterSaveFunctions: DeferredAfterSaveFunction[] = [] + #disposed = false - const newTakeCount = currentPartInstance ? currentPartInstance.PartInstance.takeCount + 1 : 0 // Increment - const segmentPlayoutId: SegmentPlayoutId = - currentPartInstance && nextPart.segmentId === currentPartInstance.PartInstance.segmentId - ? currentPartInstance.PartInstance.segmentPlayoutId - : getRandomId() + #PlaylistHasChanged = false + #TimelineHasChanged = false - const newPartInstance: DBPartInstance = { - _id: protectString(`${nextPart._id}_${getRandomId()}`), - rundownId: nextPart.rundownId, - segmentId: nextPart.segmentId, - playlistActivationId: playlistActivationId, - segmentPlayoutId, - takeCount: newTakeCount, - rehearsal: !!this.Playlist.rehearsal, - part: clone(nextPart), - timings: { - setAsNext: getCurrentTime(), - }, - } + #PendingPartInstanceTimingEvents = new Set() - for (const pieceInstance of pieceInstances) { - // TODO - should these be PieceInstance already, or should that be handled here? - pieceInstance._id = getPieceInstanceIdForPiece(newPartInstance._id, pieceInstance.piece._id) - pieceInstance.partInstanceId = newPartInstance._id + get HackDeletedPartInstanceIds(): PartInstanceId[] { + const result: PartInstanceId[] = [] + for (const [id, doc] of this.AllPartInstances) { + if (!doc) result.push(id) } + return result + } - const partWithPieces = new PlayoutPartInstanceModelImpl(newPartInstance, pieceInstances, true) - this.#AllPartInstances.set(newPartInstance._id, partWithPieces) + public constructor( + context: JobContext, + playlistLock: PlaylistLock, + playlistId: RundownPlaylistId, + peripheralDevices: ReadonlyDeep, + playlist: DBRundownPlaylist, + partInstances: PlayoutPartInstanceModelImpl[], + rundowns: PlayoutRundownModelImpl[], + timeline: TimelineComplete | undefined + ) { + super(context, playlistLock, playlistId, peripheralDevices, playlist, partInstances, rundowns, timeline) + context.trackCache(this) + } - return partWithPieces + public get DisplayName(): string { + return `PlayoutModel "${this.PlaylistId}"` + } + + activatePlaylist(rehearsal: boolean): RundownPlaylistActivationId { + this.PlaylistImpl.activationId = getRandomId() + this.PlaylistImpl.rehearsal = rehearsal + + this.#PlaylistHasChanged = true + + return this.PlaylistImpl.activationId + } + + clearSelectedPartInstances(): void { + this.PlaylistImpl.currentPartInfo = null + this.PlaylistImpl.nextPartInfo = null + this.PlaylistImpl.previousPartInfo = null + this.PlaylistImpl.holdState = RundownHoldState.NONE + + delete this.PlaylistImpl.lastTakeTime + delete this.PlaylistImpl.queuedSegmentId + + this.#PlaylistHasChanged = true } - insertAdlibbedPartInstance(part: Omit): PlayoutPartInstanceModel { + #fixupPieceInstancesForPartInstance(partInstance: DBPartInstance, pieceInstances: PieceInstance[]): void { + for (const pieceInstance of pieceInstances) { + // Future: should these be PieceInstance already, or should that be handled here? + pieceInstance._id = getPieceInstanceIdForPiece(partInstance._id, pieceInstance.piece._id) + pieceInstance.partInstanceId = partInstance._id + } + } + + createAdlibbedPartInstance( + part: Omit, + pieces: Omit[], + fromAdlibId: PieceId | undefined, + infinitePieceInstances: PieceInstance[] + ): PlayoutPartInstanceModel { const currentPartInstance = this.CurrentPartInstance if (!currentPartInstance) throw new Error('No currentPartInstance') @@ -294,13 +313,58 @@ export class PlayoutModelImpl implements PlayoutModel { }, } - const partWithPieces = new PlayoutPartInstanceModelImpl(newPartInstance, [], true) - this.#AllPartInstances.set(newPartInstance._id, partWithPieces) + this.#fixupPieceInstancesForPartInstance(newPartInstance, infinitePieceInstances) + + const partInstance = new PlayoutPartInstanceModelImpl(newPartInstance, infinitePieceInstances, true) + + for (const piece of pieces) { + partInstance.insertAdlibbedPiece(piece, fromAdlibId) + } + + partInstance.recalculateExpectedDurationWithPreroll() + + this.AllPartInstances.set(newPartInstance._id, partInstance) + + return partInstance + } + + createInstanceForPart(nextPart: ReadonlyDeep, pieceInstances: PieceInstance[]): PlayoutPartInstanceModel { + const playlistActivationId = this.Playlist.activationId + if (!playlistActivationId) throw new Error(`Playlist is not active`) + + const currentPartInstance = this.CurrentPartInstance + + const newTakeCount = currentPartInstance ? currentPartInstance.PartInstance.takeCount + 1 : 0 // Increment + const segmentPlayoutId: SegmentPlayoutId = + currentPartInstance && nextPart.segmentId === currentPartInstance.PartInstance.segmentId + ? currentPartInstance.PartInstance.segmentPlayoutId + : getRandomId() + + const newPartInstance: DBPartInstance = { + _id: protectString(`${nextPart._id}_${getRandomId()}`), + rundownId: nextPart.rundownId, + segmentId: nextPart.segmentId, + playlistActivationId: playlistActivationId, + segmentPlayoutId, + takeCount: newTakeCount, + rehearsal: !!this.Playlist.rehearsal, + part: clone(nextPart), + timings: { + setAsNext: getCurrentTime(), + }, + } + + this.#fixupPieceInstancesForPartInstance(newPartInstance, pieceInstances) - return partWithPieces + const partInstance = new PlayoutPartInstanceModelImpl(newPartInstance, pieceInstances, true) + partInstance.recalculateExpectedDurationWithPreroll() + + this.AllPartInstances.set(newPartInstance._id, partInstance) + + return partInstance } - insertScratchpadPartInstance( + createScratchpadPartInstance( rundown: PlayoutRundownModel, part: Omit ): PlayoutPartInstanceModel { @@ -331,85 +395,186 @@ export class PlayoutModelImpl implements PlayoutModel { }, } - const partWithPieces = new PlayoutPartInstanceModelImpl(newPartInstance, [], true) - this.#AllPartInstances.set(newPartInstance._id, partWithPieces) + const partInstance = new PlayoutPartInstanceModelImpl(newPartInstance, [], true) + partInstance.recalculateExpectedDurationWithPreroll() - return partWithPieces - } + this.AllPartInstances.set(newPartInstance._id, partInstance) - /** - * HACK: This allows for taking a copy of a `PartInstanceWithPieces` for use in `syncChangesToPartInstances`. - * This lets us discard the changes if the blueprint call throws. - * We should look at avoiding this messy/dangerous method, and find a better way to do this - */ - replacePartInstance(partInstance: PlayoutPartInstanceModel): void { - if (!(partInstance instanceof PlayoutPartInstanceModelImpl)) - throw new Error(`Expected PartInstanceWithPiecesImpl`) + return partInstance + } - const currentPartInstance = this.CurrentPartInstance - const nextPartInstance = this.NextPartInstance + cycleSelectedPartInstances(): void { + this.PlaylistImpl.previousPartInfo = this.PlaylistImpl.currentPartInfo + this.PlaylistImpl.currentPartInfo = this.PlaylistImpl.nextPartInfo + this.PlaylistImpl.nextPartInfo = null + this.PlaylistImpl.lastTakeTime = getCurrentTime() - if ( - (currentPartInstance && partInstance.PartInstance._id === currentPartInstance.PartInstance._id) || - (nextPartInstance && partInstance.PartInstance._id === nextPartInstance.PartInstance._id) - ) { - this.#AllPartInstances.set(partInstance.PartInstance._id, partInstance) + if (!this.PlaylistImpl.holdState || this.PlaylistImpl.holdState === RundownHoldState.COMPLETE) { + this.PlaylistImpl.holdState = RundownHoldState.NONE } else { - throw new Error('Cannot call `replacePartInstance` for arbitrary PartInstances') + this.PlaylistImpl.holdState = this.PlaylistImpl.holdState + 1 } + + this.#PlaylistHasChanged = true } - /** @deprecated HACK */ - removePartInstance(id: PartInstanceId): void { - if (this.SelectedPartInstanceIds.includes(id)) - throw new Error('Cannot call removePartInstance for one of the selected PartInstances') + deactivatePlaylist(): void { + delete this.PlaylistImpl.activationId + + this.clearSelectedPartInstances() - this.#AllPartInstances.set(id, null) + this.#PlaylistHasChanged = true } - setHoldState(newState: RundownHoldState): void { - // TODO some validation? - this.#Playlist.holdState = newState + queuePartInstanceTimingEvent(partInstanceId: PartInstanceId): void { + this.#PendingPartInstanceTimingEvents.add(partInstanceId) + } - this.#PlaylistHasChanged = true + removeAllRehearsalPartInstances(): void { + const partInstancesToRemove: PartInstanceId[] = [] + + for (const [id, partInstance] of this.AllPartInstances.entries()) { + if (partInstance?.PartInstance.rehearsal) { + this.AllPartInstances.set(id, null) + partInstancesToRemove.push(id) + } + } + + // Defer ones which arent loaded + this.deferAfterSave(async (playoutModel) => { + const rundownIds = playoutModel.getRundownIds() + // We need to keep any for PartInstances which are still existent in the cache (as they werent removed) + const partInstanceIdsInCache = playoutModel.LoadedPartInstances.map((p) => p.PartInstance._id) + + // Find all the partInstances which are not loaded, but should be removed + const removeFromDb = await this.context.directCollections.PartInstances.findFetch( + { + // Not any which are in the cache, as they have already been done if needed + _id: { $nin: partInstanceIdsInCache }, + rundownId: { $in: rundownIds }, + rehearsal: true, + }, + { projection: { _id: 1 } } + ).then((ps) => ps.map((p) => p._id)) + + // Do the remove + const allToRemove = [...removeFromDb, ...partInstancesToRemove] + await Promise.all([ + removeFromDb.length > 0 + ? this.context.directCollections.PartInstances.remove({ + _id: { $in: removeFromDb }, + rundownId: { $in: rundownIds }, + }) + : undefined, + allToRemove.length > 0 + ? this.context.directCollections.PieceInstances.remove({ + partInstanceId: { $in: allToRemove }, + rundownId: { $in: rundownIds }, + }) + : undefined, + ]) + }) } - setQueuedSegment(segment: PlayoutSegmentModel | null): void { - // TODO some validation? - this.#Playlist.queuedSegmentId = segment?.Segment?._id ?? undefined + removeUntakenPartInstances(): void { + for (const partInstance of this.OlderPartInstances) { + if (!partInstance.PartInstance.isTaken) { + this.AllPartInstances.set(partInstance.PartInstance._id, null) + } + } + } + + /** + * Reset the playlist for playout + */ + resetPlaylist(regenerateActivationId: boolean): void { + this.PlaylistImpl.previousPartInfo = null + this.PlaylistImpl.currentPartInfo = null + this.PlaylistImpl.nextPartInfo = null + this.PlaylistImpl.holdState = RundownHoldState.NONE + this.PlaylistImpl.resetTime = getCurrentTime() + + delete this.PlaylistImpl.lastTakeTime + delete this.PlaylistImpl.startedPlayback + delete this.PlaylistImpl.rundownsStartedPlayback + delete this.PlaylistImpl.previousPersistentState + delete this.PlaylistImpl.trackedAbSessions + delete this.PlaylistImpl.queuedSegmentId + + if (regenerateActivationId) this.PlaylistImpl.activationId = getRandomId() this.#PlaylistHasChanged = true } - cycleSelectedPartInstances(): void { - this.#Playlist.previousPartInfo = this.#Playlist.currentPartInfo - this.#Playlist.currentPartInfo = this.#Playlist.nextPartInfo - this.#Playlist.nextPartInfo = null - this.#Playlist.lastTakeTime = getCurrentTime() + async saveAllToDatabase(): Promise { + if (this.#disposed) { + throw new Error('Cannot save disposed PlayoutModel') + } - if (!this.#Playlist.holdState || this.#Playlist.holdState === RundownHoldState.COMPLETE) { - this.#Playlist.holdState = RundownHoldState.NONE - } else { - this.#Playlist.holdState = this.#Playlist.holdState + 1 + // TODO - ideally we should make sure to preserve the lock during this operation + if (!this.PlaylistLock.isLocked) { + throw new Error('Cannot save changes with released playlist lock') } - this.#PlaylistHasChanged = true - } + const span = this.context.startSpan('PlayoutModelImpl.saveAllToDatabase') - setRundownStartedPlayback(rundownId: RundownId, timestamp: number): void { - if (!this.#Playlist.rundownsStartedPlayback) { - this.#Playlist.rundownsStartedPlayback = {} + // Execute cache.deferBeforeSave()'s + for (const fn of this.#deferredBeforeSaveFunctions) { + await fn(this as any) } + this.#deferredBeforeSaveFunctions.length = 0 // clear the array - // If the partInstance is "untimed", it will not update the playlist's startedPlayback and will not count time in the GUI: - const rundownIdStr = unprotectString(rundownId) - if (!this.#Playlist.rundownsStartedPlayback[rundownIdStr]) { - this.#Playlist.rundownsStartedPlayback[rundownIdStr] = timestamp + // Prioritise the timeline for publication reasons + if (this.#TimelineHasChanged && this.TimelineImpl) { + await this.context.directCollections.Timelines.replace(this.TimelineImpl) + if (!process.env.JEST_WORKER_ID) { + // Wait a little bit before saving the rest. + // The idea is that this allows for the high priority publications to update (such as the Timeline), + // sending the updated timeline to Playout-gateway + await sleep(2) + } + } + this.#TimelineHasChanged = false + + await Promise.all([ + this.#PlaylistHasChanged + ? this.context.directCollections.RundownPlaylists.replace(this.PlaylistImpl) + : undefined, + ...writePartInstancesAndPieceInstances(this.context, this.AllPartInstances), + writeScratchpadSegments(this.context, this.RundownsImpl), + ]) + + this.#PlaylistHasChanged = false + + // Execute cache.deferAfterSave()'s + for (const fn of this.#deferredAfterSaveFunctions) { + await fn(this as any) } + this.#deferredAfterSaveFunctions.length = 0 // clear the array - if (!this.#Playlist.startedPlayback) { - this.#Playlist.startedPlayback = timestamp + for (const partInstanceId of this.#PendingPartInstanceTimingEvents) { + // Run in the background, we don't want to hold onto the lock to do this + queuePartInstanceTimingEvent(this.context, this.PlaylistId, partInstanceId) } + this.#PendingPartInstanceTimingEvents.clear() + + if (span) span.end() + } + + setHoldState(newState: RundownHoldState): void { + this.PlaylistImpl.holdState = newState + + this.#PlaylistHasChanged = true + } + + setOnTimelineGenerateResult( + persistentState: unknown | undefined, + assignedAbSessions: Record, + trackedAbSessions: ABSessionInfo[] + ): void { + this.PlaylistImpl.previousPersistentState = persistentState + this.PlaylistImpl.assignedAbSessions = assignedAbSessions + this.PlaylistImpl.trackedAbSessions = trackedAbSessions this.#PlaylistHasChanged = true } @@ -421,112 +586,75 @@ export class PlayoutModelImpl implements PlayoutModel { nextTimeOffset?: number ): void { if (partInstance) { - const storedPartInstance = this.#AllPartInstances.get(partInstance.PartInstance._id) + const storedPartInstance = this.AllPartInstances.get(partInstance.PartInstance._id) if (!storedPartInstance) throw new Error(`PartInstance being set as next was not constructed correctly`) // Make sure we were given the exact same object if (storedPartInstance !== partInstance) throw new Error(`PartInstance being set as next is not current`) } if (partInstance) { - this.#Playlist.nextPartInfo = literal({ + this.PlaylistImpl.nextPartInfo = literal({ partInstanceId: partInstance.PartInstance._id, rundownId: partInstance.PartInstance.rundownId, manuallySelected: !!(setManually || partInstance.PartInstance.orphaned), consumesQueuedSegmentId, }) - this.#Playlist.nextTimeOffset = nextTimeOffset || null + this.PlaylistImpl.nextTimeOffset = nextTimeOffset || null } else { - this.#Playlist.nextPartInfo = null - this.#Playlist.nextTimeOffset = null + this.PlaylistImpl.nextPartInfo = null + this.PlaylistImpl.nextTimeOffset = null } this.#PlaylistHasChanged = true } - clearSelectedPartInstances(): void { - this.#Playlist.currentPartInfo = null - this.#Playlist.nextPartInfo = null - this.#Playlist.previousPartInfo = null - this.#Playlist.holdState = RundownHoldState.NONE - - delete this.#Playlist.lastTakeTime - delete this.#Playlist.queuedSegmentId + setQueuedSegment(segment: PlayoutSegmentModel | null): void { + this.PlaylistImpl.queuedSegmentId = segment?.Segment?._id ?? undefined this.#PlaylistHasChanged = true } - activatePlaylist(rehearsal: boolean): RundownPlaylistActivationId { - this.#Playlist.activationId = getRandomId() - this.#Playlist.rehearsal = rehearsal - - this.#PlaylistHasChanged = true - - return this.#Playlist.activationId - } + setRundownStartedPlayback(rundownId: RundownId, timestamp: number): void { + if (!this.PlaylistImpl.rundownsStartedPlayback) { + this.PlaylistImpl.rundownsStartedPlayback = {} + } - deactivatePlaylist(): void { - delete this.#Playlist.activationId + // If the partInstance is "untimed", it will not update the playlist's startedPlayback and will not count time in the GUI: + const rundownIdStr = unprotectString(rundownId) + if (!this.PlaylistImpl.rundownsStartedPlayback[rundownIdStr]) { + this.PlaylistImpl.rundownsStartedPlayback[rundownIdStr] = timestamp + } - // this.clearSelectedPartInstances() // TODO? + if (!this.PlaylistImpl.startedPlayback) { + this.PlaylistImpl.startedPlayback = timestamp + } this.#PlaylistHasChanged = true } - removeUntakenPartInstances(): void { - for (const partInstance of this.OlderPartInstances) { - if (!partInstance.PartInstance.isTaken) { - this.#AllPartInstances.set(partInstance.PartInstance._id, null) - } + setTimeline(timelineObjs: TimelineObjGeneric[], generationVersions: TimelineCompleteGenerationVersions): void { + this.TimelineImpl = { + _id: this.context.studioId, + timelineHash: getRandomId(), // randomized on every timeline change + generated: getCurrentTime(), + timelineBlob: serializeTimelineBlob(timelineObjs), + generationVersions: generationVersions, } + this.#TimelineHasChanged = true } - /** - * Reset the playlist for playout - */ - resetPlaylist(regenerateActivationId: boolean): void { - this.#Playlist.previousPartInfo = null - this.#Playlist.currentPartInfo = null - this.#Playlist.holdState = RundownHoldState.NONE - this.#Playlist.resetTime = getCurrentTime() - - delete this.#Playlist.lastTakeTime - delete this.#Playlist.startedPlayback - delete this.#Playlist.rundownsStartedPlayback - delete this.#Playlist.previousPersistentState - delete this.#Playlist.trackedAbSessions - delete this.#Playlist.queuedSegmentId - - if (regenerateActivationId) this.#Playlist.activationId = getRandomId() - - this.#PlaylistHasChanged = true - } - - setOnTimelineGenerateResult( - persistentState: unknown | undefined, - assignedAbSessions: Record, - trackedAbSessions: ABSessionInfo[] - ): void { - this.#Playlist.previousPersistentState = persistentState - this.#Playlist.assignedAbSessions = assignedAbSessions - this.#Playlist.trackedAbSessions = trackedAbSessions + /** Lifecycle */ - this.#PlaylistHasChanged = true + /** @deprecated */ + deferBeforeSave(fcn: DeferredFunction): void { + this.#deferredBeforeSaveFunctions.push(fcn) } - - queuePartInstanceTimingEvent(partInstanceId: PartInstanceId): void { - this.#PendingPartInstanceTimingEvents.add(partInstanceId) + /** @deprecated */ + deferAfterSave(fcn: DeferredAfterSaveFunction): void { + this.#deferredAfterSaveFunctions.push(fcn) } - /** - * Discards all documents in this cache, and marks it as unusable - */ - dispose(): void { - this.#disposed = true - - // Discard any hooks too - this.#deferredAfterSaveFunctions.length = 0 - this.#deferredBeforeSaveFunctions.length = 0 - } + /** ICacheBase */ /** * Assert that no changes should have been made to the cache, will throw an Error otherwise. This can be used in @@ -568,11 +696,11 @@ export class PlayoutModelImpl implements PlayoutModel { if (this.#PlaylistHasChanged) logOrThrowError(new Error(`Failed no changes in cache assertion, Playlist has been changed`)) - if (this.#Rundowns.find((rd) => rd.ScratchPadSegmentHasChanged)) + if (this.RundownsImpl.find((rd) => rd.ScratchPadSegmentHasChanged)) logOrThrowError(new Error(`Failed no changes in cache assertion, a scratchpad Segment has been changed`)) if ( - Array.from(this.#AllPartInstances.values()).find( + Array.from(this.AllPartInstances.values()).find( (part) => !part || part.PartInstanceHasChanges || part.ChangedPieceInstanceIds().length > 0 ) ) @@ -581,82 +709,14 @@ export class PlayoutModelImpl implements PlayoutModel { if (span) span.end() } - /** @deprecated */ - deferBeforeSave(fcn: DeferredFunction): void { - this.#deferredBeforeSaveFunctions.push(fcn) - } - /** @deprecated */ - deferAfterSave(fcn: DeferredAfterSaveFunction): void { - this.#deferredAfterSaveFunctions.push(fcn) - } - - async saveAllToDatabase(): Promise { - if (this.#disposed) { - throw new Error('Cannot save disposed PlayoutModel') - } - - // TODO - ideally we should make sure to preserve the lock during this operation - if (!this.PlaylistLock.isLocked) { - throw new Error('Cannot save changes with released playlist lock') - } - - const span = this.context.startSpan('PlayoutModelImpl.saveAllToDatabase') - - // Execute cache.deferBeforeSave()'s - for (const fn of this.#deferredBeforeSaveFunctions) { - await fn(this as any) - } - this.#deferredBeforeSaveFunctions.length = 0 // clear the array - - // Prioritise the timeline for publication reasons - if (this.#TimelineHasChanged && this.#Timeline) { - await this.context.directCollections.Timelines.replace(this.#Timeline) - if (!process.env.JEST_WORKER_ID) { - // Wait a little bit before saving the rest. - // The idea is that this allows for the high priority publications to update (such as the Timeline), - // sending the updated timeline to Playout-gateway - await sleep(2) - } - } - this.#TimelineHasChanged = false - - await Promise.all([ - this.#PlaylistHasChanged - ? this.context.directCollections.RundownPlaylists.replace(this.#Playlist) - : undefined, - ...writePartInstancesAndPieceInstances(this.context, this.#AllPartInstances), - writeScratchpadSegments(this.context, this.#Rundowns), - ]) - - this.#PlaylistHasChanged = false - - // Execute cache.deferAfterSave()'s - for (const fn of this.#deferredAfterSaveFunctions) { - await fn(this as any) - } - this.#deferredAfterSaveFunctions.length = 0 // clear the array - - for (const partInstanceId of this.#PendingPartInstanceTimingEvents) { - // Run in the background, we don't want to hold onto the lock to do this - queuePartInstanceTimingEvent(this.context, this.PlaylistId, partInstanceId) - } - this.#PendingPartInstanceTimingEvents.clear() - - if (span) span.end() - } + /** + * Discards all documents in this cache, and marks it as unusable + */ + dispose(): void { + this.#disposed = true - #isMultiGatewayMode: boolean | undefined = undefined - public get isMultiGatewayMode(): boolean { - if (this.#isMultiGatewayMode === undefined) { - if (this.context.studio.settings.forceMultiGatewayMode) { - this.#isMultiGatewayMode = true - } else { - const playoutDevices = this.PeripheralDevices.filter( - (device) => device.type === PeripheralDeviceType.PLAYOUT - ) - this.#isMultiGatewayMode = playoutDevices.length > 1 - } - } - return this.#isMultiGatewayMode + // Discard any hooks too + this.#deferredAfterSaveFunctions.length = 0 + this.#deferredBeforeSaveFunctions.length = 0 } } diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts index db23472d8b..f049c8e45d 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts @@ -26,16 +26,100 @@ import { PieceLifespan, Time, } from '@sofie-automation/blueprints-integration' -import { PlayoutPartInstanceModel } from '../PlayoutPartInstanceModel' +import { PlayoutPartInstanceModel, PlayoutPartInstanceModelSnapshot } from '../PlayoutPartInstanceModel' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { PlayoutPieceInstanceModel } from '../PlayoutPieceInstanceModel' import { PlayoutPieceInstanceModelImpl } from './PlayoutPieceInstanceModelImpl' import { EmptyPieceTimelineObjectsBlob } from '@sofie-automation/corelib/dist/dataModel/Piece' +import _ = require('underscore') +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import { IBlueprintMutatablePartSampleKeys } from '../../../blueprints/context/lib' +interface PlayoutPieceInstanceModelSnapshotImpl { + PieceInstance: PieceInstance + HasChanges: boolean +} +class PlayoutPartInstanceModelSnapshotImpl implements PlayoutPartInstanceModelSnapshot { + readonly __isPlayoutPartInstanceModelBackup = true + + isRestored = false + + readonly PartInstance: DBPartInstance + readonly PartInstanceHasChanges: boolean + readonly PieceInstances: ReadonlyMap + + constructor(copyFrom: PlayoutPartInstanceModelImpl) { + this.PartInstance = clone(copyFrom.PartInstanceImpl) + this.PartInstanceHasChanges = copyFrom.PartInstanceHasChanges + + const pieceInstances = new Map() + for (const [pieceInstanceId, pieceInstance] of copyFrom.PieceInstancesImpl) { + if (pieceInstance) { + pieceInstances.set(pieceInstanceId, { + PieceInstance: clone(pieceInstance.PieceInstanceImpl), + HasChanges: pieceInstance.HasChanges, + }) + } else { + pieceInstances.set(pieceInstanceId, null) + } + } + this.PieceInstances = pieceInstances + } +} export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { PartInstanceImpl: DBPartInstance PieceInstancesImpl: Map + #setPartInstanceValue(key: T, newValue: DBPartInstance[T]): void { + if (newValue === undefined) { + delete this.PartInstanceImpl[key] + } else { + this.PartInstanceImpl[key] = newValue + } + + this.#PartInstanceHasChanges = true + } + #compareAndSetPartInstanceValue( + key: T, + newValue: DBPartInstance[T], + deepEqual = false + ): boolean { + const oldValue = this.PartInstanceImpl[key] + + const areEqual = deepEqual ? _.isEqual(oldValue, newValue) : oldValue === newValue + + if (!areEqual) { + this.#setPartInstanceValue(key, newValue) + + return true + } else { + return false + } + } + + #setPartValue(key: T, newValue: DBPart[T]): void { + if (newValue === undefined) { + delete this.PartInstanceImpl.part[key] + } else { + this.PartInstanceImpl.part[key] = newValue + } + + this.#PartInstanceHasChanges = true + } + #compareAndSetPartValue(key: T, newValue: DBPart[T], deepEqual = false): boolean { + const oldValue = this.PartInstanceImpl.part[key] + + const areEqual = deepEqual ? _.isEqual(oldValue, newValue) : oldValue === newValue + + if (!areEqual) { + this.#setPartValue(key, newValue) + + return true + } else { + return false + } + } + #PartInstanceHasChanges = false get PartInstanceHasChanges(): boolean { return this.#PartInstanceHasChanges @@ -57,7 +141,7 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { if (!value) { this.PieceInstancesImpl.delete(id) } else if (value.HasChanges) { - value.setDirty(false) + value.clearChangedFlag() } } } @@ -85,47 +169,55 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { } } - /** - * @deprecated - * What is the purpose of this? Without changing the ids it is going to clash with the old copy.. - * TODO - this has issues with deleting instances! - */ - clone(): PlayoutPartInstanceModel { - const cloned = new PlayoutPartInstanceModelImpl(clone(this.PartInstanceImpl), [], this.#PartInstanceHasChanges) + snapshotMakeCopy(): PlayoutPartInstanceModelSnapshot { + return new PlayoutPartInstanceModelSnapshotImpl(this) + } + + snapshotRestore(snapshot: PlayoutPartInstanceModelSnapshot): void { + if (!(snapshot instanceof PlayoutPartInstanceModelSnapshotImpl)) + throw new Error(`Cannot restore a Snapshot from an different Model`) - for (const [id, pieceInstance] of this.PieceInstancesImpl) { - if (!pieceInstance) { - cloned.PieceInstancesImpl.set(id, null) - } else { - cloned.PieceInstancesImpl.set( - id, - new PlayoutPieceInstanceModelImpl(clone(pieceInstance.PieceInstanceImpl), pieceInstance.HasChanges) + if (snapshot.PartInstance._id !== this.PartInstance._id) + throw new Error(`Cannot restore a Snapshot from an different PartInstance`) + + if (snapshot.isRestored) throw new Error(`Cannot restore a Snapshot which has already been restored`) + snapshot.isRestored = true + + this.PartInstanceImpl = snapshot.PartInstance + this.#PartInstanceHasChanges = snapshot.PartInstanceHasChanges + this.PieceInstancesImpl.clear() + for (const [pieceInstanceId, pieceInstance] of snapshot.PieceInstances) { + if (pieceInstance) { + this.PieceInstancesImpl.set( + pieceInstanceId, + new PlayoutPieceInstanceModelImpl(pieceInstance.PieceInstance, pieceInstance.HasChanges) ) + } else { + this.PieceInstancesImpl.set(pieceInstanceId, null) } } + } - return cloned + appendNotes(notes: PartNote[]): void { + this.#setPartValue('notes', [...(this.PartInstanceImpl.part.notes ?? []), ...clone(notes)]) } - setPlaylistActivationId(id: RundownPlaylistActivationId): void { - this.PartInstanceImpl.playlistActivationId = id + blockTakeUntil(timestamp: Time | null): void { + this.#compareAndSetPartInstanceValue('blockTakeUntil', timestamp ?? undefined) + } - this.#PartInstanceHasChanges = true + clearPlannedTimings(): void { + const timings = { ...this.PartInstanceImpl.timings } + if (timings.plannedStartedPlayback) { + delete timings.plannedStartedPlayback + delete timings.plannedStoppedPlayback - for (const pieceInstance of this.PieceInstancesImpl.values()) { - if (!pieceInstance) continue - pieceInstance.PieceInstanceImpl.playlistActivationId = id - pieceInstance.setDirty() + this.#compareAndSetPartInstanceValue('timings', timings, true) } } - recalculateExpectedDurationWithPreroll(): void { - this.#PartInstanceHasChanges = true - - this.PartInstanceImpl.part.expectedDurationWithPreroll = calculatePartExpectedDurationWithPreroll( - this.PartInstanceImpl.part, - this.PieceInstances.map((p) => p.PieceInstance.piece) - ) + getPieceInstance(id: PieceInstanceId): PlayoutPieceInstanceModel | undefined { + return this.PieceInstancesImpl.get(id) ?? undefined } insertAdlibbedPiece( @@ -160,119 +252,170 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { return pieceInstanceModel } - replaceInfinitesFromPreviousPlayhead(pieces: PieceInstance[]): void { - // TODO - this should do some validation/some of the wrapping from a Piece into a PieceInstance - // Remove old ones - for (const [id, piece] of this.PieceInstancesImpl.entries()) { - if (!piece) continue - - if (piece.PieceInstance.infinite?.fromPreviousPlayhead) { - this.PieceInstancesImpl.set(id, null) - } + insertHoldPieceInstance( + extendPieceInstance: PlayoutPieceInstanceModel, + infiniteInstanceId: PieceInstanceInfiniteId + ): PlayoutPieceInstanceModel { + // make the extension + const newInstance: PieceInstance = { + _id: protectString(extendPieceInstance.PieceInstance._id + '_hold'), + playlistActivationId: extendPieceInstance.PieceInstance.playlistActivationId, + rundownId: extendPieceInstance.PieceInstance.rundownId, + partInstanceId: this.PartInstance._id, + dynamicallyInserted: getCurrentTime(), + piece: { + ...clone(extendPieceInstance.PieceInstance.piece), + enable: { start: 0 }, + extendOnHold: false, + }, + infinite: { + infiniteInstanceId: infiniteInstanceId, + infiniteInstanceIndex: 1, + infinitePieceId: extendPieceInstance.PieceInstance.piece._id, + fromPreviousPart: true, + fromHold: true, + }, + // Preserve the timings from the playing instance + reportedStartedPlayback: extendPieceInstance.PieceInstance.reportedStartedPlayback, + reportedStoppedPlayback: extendPieceInstance.PieceInstance.reportedStoppedPlayback, + plannedStartedPlayback: extendPieceInstance.PieceInstance.plannedStartedPlayback, + plannedStoppedPlayback: extendPieceInstance.PieceInstance.plannedStoppedPlayback, } - for (const piece of pieces) { - this.PieceInstancesImpl.set(piece._id, new PlayoutPieceInstanceModelImpl(piece, true)) - } - } + const pieceInstanceModel = new PlayoutPieceInstanceModelImpl(newInstance, true) + this.PieceInstancesImpl.set(newInstance._id, pieceInstanceModel) - markAsReset(): void { - this.PartInstanceImpl.reset = true + return pieceInstanceModel + } - for (const pieceInstance of this.PieceInstancesImpl.values()) { - if (!pieceInstance) continue + insertPlannedPiece(piece: Omit): PlayoutPieceInstanceModel { + const pieceInstanceId = getPieceInstanceIdForPiece(this.PartInstance._id, piece._id) + if (this.PieceInstancesImpl.has(pieceInstanceId)) + throw new Error(`PieceInstance "${pieceInstanceId}" already exists`) - pieceInstance.setDirty() - pieceInstance.PieceInstanceImpl.reset = true + const newPieceInstance: PieceInstance = { + _id: getPieceInstanceIdForPiece(this.PartInstance._id, piece._id), + rundownId: this.PartInstance.rundownId, + playlistActivationId: this.PartInstance.playlistActivationId, + partInstanceId: this.PartInstance._id, + piece: { + ...piece, + startPartId: this.PartInstance.part._id, + }, } - this.#PartInstanceHasChanges = true - } + // Ensure the infinite-ness is setup correctly + setupPieceInstanceInfiniteProperties(newPieceInstance) - blockTakeUntil(timestamp: Time | null): void { - if (timestamp) { - this.PartInstanceImpl.blockTakeUntil = timestamp - } else { - delete this.PartInstanceImpl.blockTakeUntil - } + const pieceInstanceModel = new PlayoutPieceInstanceModelImpl(newPieceInstance, true) + this.PieceInstancesImpl.set(pieceInstanceId, pieceInstanceModel) - this.#PartInstanceHasChanges = true + return pieceInstanceModel } - clearPlannedTimings(): void { - if (this.PartInstanceImpl.timings?.plannedStartedPlayback) { - delete this.PartInstanceImpl.timings.plannedStartedPlayback - delete this.PartInstanceImpl.timings.plannedStoppedPlayback + insertVirtualPiece( + start: number, + lifespan: PieceLifespan, + sourceLayerId: string, + outputLayerId: string + ): PlayoutPieceInstanceModel { + const pieceId: PieceId = getRandomId() + const newPieceInstance: PieceInstance = { + _id: protectString(`${this.PartInstance._id}_${pieceId}`), + rundownId: this.PartInstance.rundownId, + playlistActivationId: this.PartInstance.playlistActivationId, + partInstanceId: this.PartInstance._id, + piece: { + _id: pieceId, + externalId: '-', + enable: { start: start }, + lifespan: lifespan, + sourceLayerId: sourceLayerId, + outputLayerId: outputLayerId, + invalid: false, + name: '', + startPartId: this.PartInstance.part._id, + pieceType: IBlueprintPieceType.Normal, + virtual: true, + content: {}, + timelineObjectsString: EmptyPieceTimelineObjectsBlob, + }, - this.#PartInstanceHasChanges = true + dynamicallyInserted: getCurrentTime(), } - } + setupPieceInstanceInfiniteProperties(newPieceInstance) - setRank(rank: number): void { - this.PartInstanceImpl.part._rank = rank + const pieceInstanceModel = new PlayoutPieceInstanceModelImpl(newPieceInstance, true) + this.PieceInstancesImpl.set(newPieceInstance._id, pieceInstanceModel) - this.#PartInstanceHasChanges = true + return pieceInstanceModel } - setOrphaned(orphaned: 'adlib-part' | 'deleted' | undefined): void { - this.PartInstanceImpl.orphaned = orphaned - - this.#PartInstanceHasChanges = true - } + markAsReset(): void { + this.#compareAndSetPartInstanceValue('reset', true) - setTaken(takeTime: number, playOffset: number): void { - this.PartInstanceImpl.isTaken = true + for (const pieceInstance of this.PieceInstancesImpl.values()) { + if (!pieceInstance) continue - const timings = this.PartInstanceImpl.timings ?? {} - this.PartInstanceImpl.timings = timings + pieceInstance.compareAndSetPieceInstanceValue('reset', true) + } + } - timings.take = takeTime - timings.playOffset = playOffset + recalculateExpectedDurationWithPreroll(): void { + const newDuration = calculatePartExpectedDurationWithPreroll( + this.PartInstanceImpl.part, + this.PieceInstances.map((p) => p.PieceInstance.piece) + ) - this.#PartInstanceHasChanges = true + this.#compareAndSetPartValue('expectedDurationWithPreroll', newDuration) } - storePlayoutTimingsAndPreviousEndState( - partPlayoutTimings: PartCalculatedTimings, - previousPartEndState: unknown - ): void { - this.PartInstanceImpl.isTaken = true + removePieceInstance(id: PieceInstanceId): boolean { + // Future: should this limit what can be removed based on type/infinite - this.PartInstanceImpl.partPlayoutTimings = partPlayoutTimings - this.PartInstanceImpl.previousPartEndState = previousPartEndState + const pieceInstanceWrapped = this.PieceInstancesImpl.get(id) + if (pieceInstanceWrapped) { + this.PieceInstancesImpl.set(id, null) + return true + } + + return false } - appendNotes(notes: PartNote[]): void { - if (!this.PartInstanceImpl.part.notes) this.PartInstanceImpl.part.notes = [] - this.PartInstanceImpl.part.notes.push(...clone(notes)) + replaceInfinitesFromPreviousPlayhead(pieceInstances: PieceInstance[]): void { + // Future: this should do some of the wrapping from a Piece into a PieceInstance - this.#PartInstanceHasChanges = true - } + // Remove old infinite pieces + for (const [id, piece] of this.PieceInstancesImpl.entries()) { + if (!piece) continue - updatePartProps(props: Partial): void { - // TODO - this is missing a lot of validation - this.PartInstanceImpl.part = { - ...this.PartInstanceImpl.part, - ...props, + if (piece.PieceInstance.infinite?.fromPreviousPlayhead) { + this.PieceInstancesImpl.set(id, null) + } } - this.#PartInstanceHasChanges = true - } + for (const pieceInstance of pieceInstances) { + if (this.PieceInstancesImpl.has(pieceInstance._id)) + throw new Error( + `Cannot replace infinite PieceInstance "${pieceInstance._id}" as it replaces a non-infinite` + ) - getPieceInstance(id: PieceInstanceId): PlayoutPieceInstanceModel | undefined { - return this.PieceInstancesImpl.get(id) ?? undefined + if (!pieceInstance.infinite?.fromPreviousPlayhead) + throw new Error(`Cannot insert non-infinite PieceInstance "${pieceInstance._id}" as an infinite`) + + // Future: should this do any deeper validation of the PieceInstances? + + this.PieceInstancesImpl.set(pieceInstance._id, new PlayoutPieceInstanceModelImpl(pieceInstance, true)) + } } - replacePieceInstance(doc: ReadonlyDeep): PlayoutPieceInstanceModel { - // TODO - this is missing a lot of validation + mergeOrInsertPieceInstance(doc: ReadonlyDeep): PlayoutPieceInstanceModel { + // Future: this should do some validation of the new PieceInstance const existingPieceInstance = this.PieceInstancesImpl.get(doc._id) if (existingPieceInstance) { - existingPieceInstance.setDirty() - existingPieceInstance.PieceInstanceImpl = { - ...existingPieceInstance.PieceInstanceImpl, - ...clone(doc), - } + existingPieceInstance.mergeProperties(doc) + return existingPieceInstance } else { const newPieceInstance = new PlayoutPieceInstanceModelImpl(clone(doc), true) @@ -281,188 +424,111 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { } } - /** @deprecated HACK - * - */ - insertPlannedPiece(piece: Omit): PlayoutPieceInstanceModel { - const pieceInstanceId = getPieceInstanceIdForPiece(this.PartInstance._id, piece._id) - if (this.PieceInstancesImpl.has(pieceInstanceId)) - throw new Error(`PieceInstance "${pieceInstanceId}" already exists`) - - const newPieceInstance: PieceInstance = { - _id: getPieceInstanceIdForPiece(this.PartInstance._id, piece._id), - rundownId: this.PartInstance.rundownId, - playlistActivationId: this.PartInstance.playlistActivationId, - partInstanceId: this.PartInstance._id, - piece: { - ...piece, - startPartId: this.PartInstance.part._id, - }, - } - - // Ensure the infinite-ness is setup correctly - setupPieceInstanceInfiniteProperties(newPieceInstance) - - const pieceInstanceModel = new PlayoutPieceInstanceModelImpl(newPieceInstance, true) - this.PieceInstancesImpl.set(pieceInstanceId, pieceInstanceModel) - - return pieceInstanceModel + setOrphaned(orphaned: 'adlib-part' | 'deleted' | undefined): void { + this.#compareAndSetPartInstanceValue('orphaned', orphaned) } - /** @deprecated HACK */ - removePieceInstance(id: PieceInstanceId): boolean { - // TODO - this is missing a lot of validation - const pieceInstanceWrapped = this.PieceInstancesImpl.get(id) - if (pieceInstanceWrapped) { - this.PieceInstancesImpl.set(id, null) - return true - } - return false - } + setPlaylistActivationId(id: RundownPlaylistActivationId): void { + this.#compareAndSetPartInstanceValue('playlistActivationId', id) - /** @deprecated HACK */ - insertInfinitePieces(pieceInstances: PieceInstance[]): void { - for (const pieceInstance of pieceInstances) { - const pieceInstanceModel = new PlayoutPieceInstanceModelImpl(pieceInstance, true) - this.PieceInstancesImpl.set(pieceInstance._id, pieceInstanceModel) + for (const pieceInstance of this.PieceInstancesImpl.values()) { + if (!pieceInstance) continue + pieceInstance.compareAndSetPieceInstanceValue('playlistActivationId', id) } } setPlannedStartedPlayback(time: Time | undefined): void { - const timings = this.PartInstanceImpl.timings ?? {} + const timings = { ...this.PartInstanceImpl.timings } + timings.plannedStartedPlayback = time + delete timings.plannedStoppedPlayback - if (timings.plannedStartedPlayback !== time) { - timings.plannedStartedPlayback = time - delete timings.plannedStoppedPlayback - - this.PartInstanceImpl.timings = timings - this.#PartInstanceHasChanges = true - } + this.#compareAndSetPartInstanceValue('timings', timings, true) } setPlannedStoppedPlayback(time: Time): void { - const timings = this.PartInstanceImpl.timings + const timings = { ...this.PartInstanceImpl.timings } if (timings?.plannedStartedPlayback && !timings.plannedStoppedPlayback) { timings.plannedStoppedPlayback = time timings.duration = time - timings.plannedStartedPlayback - this.#PartInstanceHasChanges = true + this.#compareAndSetPartInstanceValue('timings', timings, true) } } setReportedStartedPlayback(time: Time): boolean { - const timings = this.PartInstanceImpl.timings ?? {} + const timings = { ...this.PartInstanceImpl.timings } if (!timings.reportedStartedPlayback) { timings.reportedStartedPlayback = time delete timings.plannedStoppedPlayback delete timings.duration - this.PartInstanceImpl.timings = timings - this.#PartInstanceHasChanges = true - - return true + return this.#compareAndSetPartInstanceValue('timings', timings, true) } return false } setReportedStoppedPlayback(time: number): boolean { - const timings = this.PartInstanceImpl.timings ?? {} + const timings = { ...this.PartInstanceImpl.timings } if (!timings.reportedStoppedPlayback) { timings.reportedStoppedPlayback = time timings.duration = time - (timings.reportedStartedPlayback || time) - this.PartInstanceImpl.timings = timings - this.#PartInstanceHasChanges = true - - return true + return this.#compareAndSetPartInstanceValue('timings', timings, true) } return false } - validateScratchpadSegmentProperties(): void { - this.PartInstanceImpl.orphaned = 'adlib-part' + setRank(rank: number): void { + this.#compareAndSetPartValue('_rank', rank) + } - // Autonext isn't allowed to begin with, to avoid accidentally exiting the scratchpad - delete this.PartInstanceImpl.part.autoNext + setTaken(takeTime: number, playOffset: number): void { + this.#compareAndSetPartInstanceValue('isTaken', true) - // Force this to not affect rundown timing - this.PartInstanceImpl.part.untimed = true + const timings = { ...this.PartInstanceImpl.timings } + timings.take = takeTime + timings.playOffset = playOffset - // TODO - more intelligent - this.#PartInstanceHasChanges = true + this.#compareAndSetPartInstanceValue('timings', timings, true) } - addHoldPieceInstance( - extendPieceInstance: PlayoutPieceInstanceModel, - infiniteInstanceId: PieceInstanceInfiniteId - ): PlayoutPieceInstanceModel { - // make the extension - const newInstance: PieceInstance = { - _id: protectString(extendPieceInstance.PieceInstance._id + '_hold'), - playlistActivationId: extendPieceInstance.PieceInstance.playlistActivationId, - rundownId: extendPieceInstance.PieceInstance.rundownId, - partInstanceId: this.PartInstance._id, - dynamicallyInserted: getCurrentTime(), - piece: { - ...clone(extendPieceInstance.PieceInstance.piece), - enable: { start: 0 }, - extendOnHold: false, - }, - infinite: { - infiniteInstanceId: infiniteInstanceId, - infiniteInstanceIndex: 1, - infinitePieceId: extendPieceInstance.PieceInstance.piece._id, - fromPreviousPart: true, - fromHold: true, - }, - // Preserve the timings from the playing instance - reportedStartedPlayback: extendPieceInstance.PieceInstance.reportedStartedPlayback, - reportedStoppedPlayback: extendPieceInstance.PieceInstance.reportedStoppedPlayback, - plannedStartedPlayback: extendPieceInstance.PieceInstance.plannedStartedPlayback, - plannedStoppedPlayback: extendPieceInstance.PieceInstance.plannedStoppedPlayback, - } - - const pieceInstanceModel = new PlayoutPieceInstanceModelImpl(newInstance, true) - this.PieceInstancesImpl.set(newInstance._id, pieceInstanceModel) + storePlayoutTimingsAndPreviousEndState( + partPlayoutTimings: PartCalculatedTimings, + previousPartEndState: unknown + ): void { + this.#compareAndSetPartInstanceValue('isTaken', true) - return pieceInstanceModel + // TODO: should this do a comparison? + this.#setPartInstanceValue('partPlayoutTimings', partPlayoutTimings) + this.#setPartInstanceValue('previousPartEndState', previousPartEndState) } - insertVirtualPiece( - start: number, - lifespan: PieceLifespan, - sourceLayerId: string, - outputLayerId: string - ): PlayoutPieceInstanceModel { - const pieceId: PieceId = getRandomId() - const newPieceInstance: PieceInstance = { - _id: protectString(`${this.PartInstance._id}_${pieceId}`), - rundownId: this.PartInstance.rundownId, - playlistActivationId: this.PartInstance.playlistActivationId, - partInstanceId: this.PartInstance._id, - piece: { - _id: pieceId, - externalId: '-', - enable: { start: start }, - lifespan: lifespan, - sourceLayerId: sourceLayerId, - outputLayerId: outputLayerId, - invalid: false, - name: '', - startPartId: this.PartInstance.part._id, - pieceType: IBlueprintPieceType.Normal, - virtual: true, - content: {}, - timelineObjectsString: EmptyPieceTimelineObjectsBlob, + updatePartProps(props: Partial): boolean { + // Future: this could do some better validation + + // filter the submission to the allowed ones + const trimmedProps: Partial = _.pick(props, [...IBlueprintMutatablePartSampleKeys]) + if (Object.keys(trimmedProps).length === 0) return false + + this.#compareAndSetPartInstanceValue( + 'part', + { + ...this.PartInstanceImpl.part, + ...trimmedProps, }, + true + ) - dynamicallyInserted: getCurrentTime(), - } - setupPieceInstanceInfiniteProperties(newPieceInstance) + return true + } - const pieceInstanceModel = new PlayoutPieceInstanceModelImpl(newPieceInstance, true) - this.PieceInstancesImpl.set(newPieceInstance._id, pieceInstanceModel) + validateScratchpadSegmentProperties(): void { + this.#compareAndSetPartInstanceValue('orphaned', 'adlib-part') - return pieceInstanceModel + // Autonext isn't allowed to begin with, to avoid accidentally exiting the scratchpad + this.#compareAndSetPartValue('autoNext', undefined) + + // Force this to not affect rundown timing + this.#compareAndSetPartValue('untimed', true) } } diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutPieceInstanceModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutPieceInstanceModelImpl.ts index 2dcd3a77e3..45bc20a7e4 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutPieceInstanceModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutPieceInstanceModelImpl.ts @@ -4,17 +4,46 @@ import { PieceInstance, PieceInstancePiece } from '@sofie-automation/corelib/dis import { clone, getRandomId } from '@sofie-automation/corelib/dist/lib' import { Time } from '@sofie-automation/blueprints-integration' import { PlayoutPieceInstanceModel } from '../PlayoutPieceInstanceModel' +import _ = require('underscore') export class PlayoutPieceInstanceModelImpl implements PlayoutPieceInstanceModel { PieceInstanceImpl: PieceInstance + setPieceInstanceValue(key: T, newValue: PieceInstance[T]): void { + if (newValue === undefined) { + delete this.PieceInstanceImpl[key] + } else { + this.PieceInstanceImpl[key] = newValue + } + + this.#HasChanges = true + } + + compareAndSetPieceInstanceValue( + key: T, + newValue: PieceInstance[T], + deepEqual = false + ): boolean { + const oldValue = this.PieceInstanceImpl[key] + + const areEqual = deepEqual ? _.isEqual(oldValue, newValue) : oldValue === newValue + + if (!areEqual) { + this.setPieceInstanceValue(key, newValue) + + return true + } else { + return false + } + } + #HasChanges = false get HasChanges(): boolean { return this.#HasChanges } - setDirty(dirty = true): void { - this.#HasChanges = dirty + clearChangedFlag(): void { + this.#HasChanges = false } get PieceInstance(): ReadonlyDeep { @@ -26,88 +55,60 @@ export class PlayoutPieceInstanceModelImpl implements PlayoutPieceInstanceModel this.#HasChanges = hasChanges } - /** - * @deprecated - * What is the purpose of this? Without changing the ids it is going to clash with the old copy.. - * TODO - this has issues with deleting instances! - */ - clone(): PlayoutPieceInstanceModelImpl { - return new PlayoutPieceInstanceModelImpl(clone(this.PieceInstanceImpl), this.#HasChanges) - } - - updatePieceProps(props: Partial): void { - // TODO - this is missing a lot of validation - - this.#HasChanges = true - this.PieceInstanceImpl.piece = { - ...this.PieceInstanceImpl.piece, - ...props, - } - } - - setPlannedStartedPlayback(time: Time): boolean { - if (this.PieceInstanceImpl.plannedStartedPlayback !== time) { - this.PieceInstanceImpl.plannedStartedPlayback = time - delete this.PieceInstanceImpl.plannedStoppedPlayback - - this.#HasChanges = true - - return true + mergeProperties(pieceInstance: ReadonlyDeep): void { + this.PieceInstanceImpl = { + ...this.PieceInstanceImpl, + ...clone(pieceInstance), } - return false - } - setPlannedStoppedPlayback(time: Time | undefined): boolean { - if (this.PieceInstanceImpl.plannedStoppedPlayback !== time) { - this.PieceInstanceImpl.plannedStoppedPlayback = time - - this.#HasChanges = true - return true - } - return false - } - setReportedStartedPlayback(time: Time): boolean { - if (this.PieceInstanceImpl.reportedStartedPlayback !== time) { - this.PieceInstanceImpl.reportedStartedPlayback = time - delete this.PieceInstanceImpl.reportedStoppedPlayback - - this.#HasChanges = true - - return true - } - return false - } - setReportedStoppedPlayback(time: Time): boolean { - if (this.PieceInstanceImpl.reportedStoppedPlayback !== time) { - this.PieceInstanceImpl.reportedStoppedPlayback = time - - this.#HasChanges = true - - return true - } - return false + this.#HasChanges = true } prepareForHold(): PieceInstanceInfiniteId { const infiniteInstanceId: PieceInstanceInfiniteId = getRandomId() - this.PieceInstanceImpl.infinite = { + this.setPieceInstanceValue('infinite', { infiniteInstanceId: infiniteInstanceId, infiniteInstanceIndex: 0, infinitePieceId: this.PieceInstanceImpl.piece._id, fromPreviousPart: false, - } - this.#HasChanges = true + }) return infiniteInstanceId } + setDisabled(disabled: boolean): void { + this.compareAndSetPieceInstanceValue('disabled', disabled) + } + setDuration(duration: Required['userDuration']): void { - this.#HasChanges = true - this.PieceInstanceImpl.userDuration = duration + this.compareAndSetPieceInstanceValue('userDuration', duration, true) } - setDisabled(disabled: boolean): void { - this.#HasChanges = true - this.PieceInstanceImpl.disabled = disabled + setPlannedStartedPlayback(time: Time): boolean { + this.compareAndSetPieceInstanceValue('plannedStoppedPlayback', undefined) + return this.compareAndSetPieceInstanceValue('plannedStartedPlayback', time) + } + setPlannedStoppedPlayback(time: Time | undefined): boolean { + return this.compareAndSetPieceInstanceValue('plannedStoppedPlayback', time) + } + setReportedStartedPlayback(time: Time): boolean { + this.compareAndSetPieceInstanceValue('reportedStoppedPlayback', undefined) + return this.compareAndSetPieceInstanceValue('reportedStartedPlayback', time) + } + setReportedStoppedPlayback(time: Time): boolean { + return this.compareAndSetPieceInstanceValue('reportedStoppedPlayback', time) + } + + updatePieceProps(props: Partial): void { + // TODO - this is missing a lot of validation + + this.compareAndSetPieceInstanceValue( + 'piece', + { + ...this.PieceInstanceImpl.piece, + ...props, + }, + true + ) } } diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutSegmentModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutSegmentModelImpl.ts index 635ca3016b..f2d5fc0bef 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutSegmentModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutSegmentModelImpl.ts @@ -19,14 +19,14 @@ export class PlayoutSegmentModelImpl implements PlayoutSegmentModel { this.Parts = parts } - getPartIds(): PartId[] { - return this.Parts.map((part) => part._id) - } - getPart(id: PartId): ReadonlyDeep | undefined { return this.Parts.find((part) => part._id === id) } + getPartIds(): PartId[] { + return this.Parts.map((part) => part._id) + } + // Internal mutation hack setScratchpadRank(rank: number): void { if (this.#Segment.orphaned !== SegmentOrphanedReason.SCRATCHPAD) diff --git a/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts b/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts index 453b846fda..dc1badd30b 100644 --- a/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts +++ b/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts @@ -75,7 +75,6 @@ export function writePartInstancesAndPieceInstances( if (!pieceInstance) { deletedPieceInstanceIds.push(pieceInstanceId) } else if (pieceInstance.HasChanges) { - // TODO - this does not perform any diffing pieceInstanceOps.push({ replaceOne: { filter: { _id: pieceInstanceId }, diff --git a/packages/job-worker/src/playout/moveNextPart.ts b/packages/job-worker/src/playout/moveNextPart.ts index 528a76c2ce..7b66e5a154 100644 --- a/packages/job-worker/src/playout/moveNextPart.ts +++ b/packages/job-worker/src/playout/moveNextPart.ts @@ -11,22 +11,22 @@ import { ReadonlyDeep } from 'type-fest' export async function moveNextPart( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, partDelta: number, segmentDelta: number ): Promise { - const playlist = cache.Playlist + const playlist = playoutModel.Playlist - const currentPartInstance = cache.CurrentPartInstance?.PartInstance - const nextPartInstance = cache.NextPartInstance?.PartInstance + const currentPartInstance = playoutModel.CurrentPartInstance?.PartInstance + const nextPartInstance = playoutModel.NextPartInstance?.PartInstance const refPartInstance = nextPartInstance ?? currentPartInstance const refPart = refPartInstance?.part if (!refPart || !refPartInstance) throw new Error(`RundownPlaylist "${playlist._id}" has no next and no current part!`) - const rawSegments = cache.getAllOrderedSegments() - const rawParts = cache.getAllOrderedParts() + const rawSegments = playoutModel.getAllOrderedSegments() + const rawParts = playoutModel.getAllOrderedParts() if (segmentDelta) { // Ignores horizontalDelta @@ -69,7 +69,7 @@ export async function moveNextPart( // TODO - looping playlists if (selectedPart) { // Switch to that part - await setNextPartFromPart(context, cache, selectedPart, true) + await setNextPartFromPart(context, playoutModel, selectedPart, true) return selectedPart._id } else { // Nothing looked valid so do nothing @@ -101,7 +101,7 @@ export async function moveNextPart( if (targetPart) { // Switch to that part - await setNextPartFromPart(context, cache, targetPart, true) + await setNextPartFromPart(context, playoutModel, targetPart, true) return targetPart._id } else { // Nothing looked valid so do nothing diff --git a/packages/job-worker/src/playout/pieces.ts b/packages/job-worker/src/playout/pieces.ts index 69dafc2bb4..a30af7f8c2 100644 --- a/packages/job-worker/src/playout/pieces.ts +++ b/packages/job-worker/src/playout/pieces.ts @@ -9,8 +9,6 @@ import _ = require('underscore') import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' import { ReadonlyDeep } from 'type-fest' -import { PlayoutPartInstanceModel } from './model/PlayoutPartInstanceModel' -import { PlayoutPieceInstanceModel } from './model/PlayoutPieceInstanceModel' /** * Approximate compare Piece start times (for use in .sort()) @@ -108,16 +106,13 @@ export function convertPieceToAdLibPiece(context: JobContext, piece: PieceInstan * @param playlistActivationId ActivationId for the active current playlist * @param adLibPiece The piece or AdLibPiece to convert * @param partInstance The PartInstance the Adlibbed PieceInstance will belong to - * @param queue Whether this is being queued as a new PartInstance, or adding to the already playing PartInstance + * @param isBeingQueued Whether this is being queued as a new PartInstance, or adding to the already playing PartInstance * @returns The PieceInstance that was constructed */ -export function convertAdLibToPieceInstance( - context: JobContext, +export function convertAdLibToGenericPiece( adLibPiece: AdLibPiece | Piece | BucketAdLib | PieceInstancePiece, - partInstance: PlayoutPartInstanceModel, - queue: boolean -): PlayoutPieceInstanceModel { - const span = context.startSpan('convertAdLibToPieceInstance') + isBeingQueued: boolean +): Omit { let duration: number | undefined = undefined if ('expectedDuration' in adLibPiece && adLibPiece['expectedDuration']) { duration = adLibPiece['expectedDuration'] @@ -126,22 +121,16 @@ export function convertAdLibToPieceInstance( } const newPieceId: PieceId = getRandomId() - const newPieceInstance = partInstance.insertAdlibbedPiece( - { - ...(_.omit(adLibPiece, '_rank', 'expectedDuration', 'partId', 'rundownId') as PieceInstancePiece), // TODO - this could be typed stronger - _id: newPieceId, - // startPartId: partInstance.part._id, - pieceType: IBlueprintPieceType.Normal, - enable: { - start: queue ? 0 : 'now', - duration: !queue && adLibPiece.lifespan === PieceLifespan.WithinPart ? duration : undefined, - }, + return { + ...(_.omit(adLibPiece, '_rank', 'expectedDuration', 'partId', 'rundownId') as PieceInstancePiece), // TODO - this could be typed stronger + _id: newPieceId, + // startPartId: partInstance.part._id, + pieceType: IBlueprintPieceType.Normal, + enable: { + start: isBeingQueued ? 0 : 'now', + duration: !isBeingQueued && adLibPiece.lifespan === PieceLifespan.WithinPart ? duration : undefined, }, - adLibPiece._id - ) - - if (span) span.end() - return newPieceInstance + } } /** diff --git a/packages/job-worker/src/playout/scratchpad.ts b/packages/job-worker/src/playout/scratchpad.ts index 4c7eea77f0..cada6016ae 100644 --- a/packages/job-worker/src/playout/scratchpad.ts +++ b/packages/job-worker/src/playout/scratchpad.ts @@ -4,7 +4,7 @@ import { getRandomId } from '@sofie-automation/corelib/dist/lib' import { ActivateScratchpadProps } from '@sofie-automation/corelib/dist/worker/studio' import { getCurrentTime } from '../lib' import { JobContext } from '../jobs' -import { runJobWithPlayoutCache } from './lock' +import { runJobWithPlayoutModel } from './lock' import { performTakeToNextedPart } from './take' import { PartInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PlayoutModel } from './model/PlayoutModel' @@ -12,27 +12,27 @@ import { PlayoutModel } from './model/PlayoutModel' export async function handleActivateScratchpad(context: JobContext, data: ActivateScratchpadProps): Promise { if (!context.studio.settings.allowScratchpad) throw UserError.create(UserErrorMessage.ScratchpadNotAllowed) - return runJobWithPlayoutCache( + return runJobWithPlayoutModel( context, data, - async (cache) => { - const playlist = cache.Playlist + async (playoutModel) => { + const playlist = playoutModel.Playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown) if (playlist.currentPartInfo) throw UserError.create(UserErrorMessage.RundownAlreadyActive) }, - async (cache) => { - const playlist = cache.Playlist + async (playoutModel) => { + const playlist = playoutModel.Playlist if (!playlist.activationId) throw new Error(`Playlist has no activationId!`) - const rundown = cache.getRundown(data.rundownId) + const rundown = playoutModel.getRundown(data.rundownId) if (!rundown) throw new Error(`Rundown "${data.rundownId}" not found!`) // Create the segment rundown.insertScratchpadSegment() // Create the first PartInstance for the segment - const newPartInstance = cache.insertScratchpadPartInstance(rundown, { + const newPartInstance = playoutModel.createScratchpadPartInstance(rundown, { _id: getRandomId(), _rank: 0, externalId: '', @@ -43,10 +43,10 @@ export async function handleActivateScratchpad(context: JobContext, data: Activa }) // Set the part as next - cache.setPartInstanceAsNext(newPartInstance, true, false) + playoutModel.setPartInstanceAsNext(newPartInstance, true, false) // Take into the newly created Part - await performTakeToNextedPart(context, cache, getCurrentTime()) + await performTakeToNextedPart(context, playoutModel, getCurrentTime()) } ) } @@ -57,13 +57,13 @@ export async function handleActivateScratchpad(context: JobContext, data: Activa */ export function validateScratchpartPartInstanceProperties( _context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, partInstanceId: PartInstanceId ): void { - const partInstance = cache.getPartInstance(partInstanceId) + const partInstance = playoutModel.getPartInstance(partInstanceId) if (!partInstance) return - const rundown = cache.getRundown(partInstance.PartInstance.rundownId) + const rundown = playoutModel.getRundown(partInstance.PartInstance.rundownId) if (!rundown) throw new Error( `Failed to find Rundown "${partInstance.PartInstance.rundownId}" for PartInstance "${partInstance.PartInstance._id}"` diff --git a/packages/job-worker/src/playout/setNext.ts b/packages/job-worker/src/playout/setNext.ts index cac6520617..b8ee8f6c97 100644 --- a/packages/job-worker/src/playout/setNext.ts +++ b/packages/job-worker/src/playout/setNext.ts @@ -25,26 +25,27 @@ import { protectString } from '@sofie-automation/corelib/dist/protectedString' /** * Set or clear the nexted part, from a given PartInstance, or SelectNextPartResult * @param context Context for the running job - * @param cache The playout cache of the playlist + * @param playoutModel The playout cache of the playlist * @param rawNextPart The Part to set as next * @param setManually Whether this was manually chosen by the user * @param nextTimeOffset The offset into the Part to start playback */ export async function setNextPart( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, rawNextPart: ReadonlyDeep> | PlayoutPartInstanceModel | null, setManually: boolean, nextTimeOffset?: number | undefined ): Promise { const span = context.startSpan('setNextPart') - const rundownIds = cache.getRundownIds() - const currentPartInstance = cache.CurrentPartInstance - const nextPartInstance = cache.NextPartInstance + const rundownIds = playoutModel.getRundownIds() + const currentPartInstance = playoutModel.CurrentPartInstance + const nextPartInstance = playoutModel.NextPartInstance if (rawNextPart) { - if (!cache.Playlist.activationId) throw new Error(`RundownPlaylist "${cache.Playlist._id}" is not active`) + if (!playoutModel.Playlist.activationId) + throw new Error(`RundownPlaylist "${playoutModel.Playlist._id}" is not active`) // create new instance let newPartInstance: PlayoutPartInstanceModel @@ -57,12 +58,12 @@ export async function setNextPart( if (!rundownIds.includes(inputPartInstance.PartInstance.rundownId)) { throw new Error( - `PartInstance "${inputPartInstance.PartInstance._id}" of rundown "${inputPartInstance.PartInstance.rundownId}" not part of RundownPlaylist "${cache.Playlist._id}"` + `PartInstance "${inputPartInstance.PartInstance._id}" of rundown "${inputPartInstance.PartInstance.rundownId}" not part of RundownPlaylist "${playoutModel.Playlist._id}"` ) } consumesQueuedSegmentId = false - newPartInstance = await prepareExistingPartInstanceForBeingNexted(context, cache, inputPartInstance) + newPartInstance = await prepareExistingPartInstanceForBeingNexted(context, playoutModel, inputPartInstance) } else { const selectedPart: ReadonlyDeep> = rawNextPart if (selectedPart.part.invalid) { @@ -71,7 +72,7 @@ export async function setNextPart( if (!rundownIds.includes(selectedPart.part.rundownId)) { throw new Error( - `Part "${selectedPart.part._id}" of rundown "${selectedPart.part.rundownId}" not part of RundownPlaylist "${cache.Playlist._id}"` + `Part "${selectedPart.part._id}" of rundown "${selectedPart.part.rundownId}" not part of RundownPlaylist "${playoutModel.Playlist._id}"` ) } @@ -80,12 +81,16 @@ export async function setNextPart( if (nextPartInstance && nextPartInstance.PartInstance.part._id === selectedPart.part._id) { // Re-use existing - newPartInstance = await prepareExistingPartInstanceForBeingNexted(context, cache, nextPartInstance) + newPartInstance = await prepareExistingPartInstanceForBeingNexted( + context, + playoutModel, + nextPartInstance + ) } else { // Create new instance newPartInstance = await preparePartInstanceForPartBeingNexted( context, - cache, + playoutModel, currentPartInstance, selectedPart.part ) @@ -94,79 +99,79 @@ export async function setNextPart( const selectedPartInstanceIds = _.compact([ newPartInstance.PartInstance._id, - cache.Playlist.currentPartInfo?.partInstanceId, - cache.Playlist.previousPartInfo?.partInstanceId, + playoutModel.Playlist.currentPartInfo?.partInstanceId, + playoutModel.Playlist.previousPartInfo?.partInstanceId, ]) // reset any previous instances of this part - resetPartInstancesWithPieceInstances(context, cache, { + resetPartInstancesWithPieceInstances(context, playoutModel, { _id: { $nin: selectedPartInstanceIds }, rundownId: newPartInstance.PartInstance.rundownId, 'part._id': newPartInstance.PartInstance.part._id, }) - cache.setPartInstanceAsNext(newPartInstance, setManually, consumesQueuedSegmentId, nextTimeOffset) + playoutModel.setPartInstanceAsNext(newPartInstance, setManually, consumesQueuedSegmentId, nextTimeOffset) } else { // Set to null - cache.setPartInstanceAsNext(null, setManually, false, nextTimeOffset) + playoutModel.setPartInstanceAsNext(null, setManually, false, nextTimeOffset) } - cache.removeUntakenPartInstances() + playoutModel.removeUntakenPartInstances() - resetPartInstancesWhenChangingSegment(context, cache) + resetPartInstancesWhenChangingSegment(context, playoutModel) - await cleanupOrphanedItems(context, cache) + await cleanupOrphanedItems(context, playoutModel) if (span) span.end() } async function prepareExistingPartInstanceForBeingNexted( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, instance: PlayoutPartInstanceModel ): Promise { - await syncPlayheadInfinitesForNextPartInstance(context, cache, cache.CurrentPartInstance, instance) + await syncPlayheadInfinitesForNextPartInstance(context, playoutModel, playoutModel.CurrentPartInstance, instance) return instance } async function preparePartInstanceForPartBeingNexted( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, currentPartInstance: PlayoutPartInstanceModel | null, nextPart: ReadonlyDeep ): Promise { - const rundown = cache.getRundown(nextPart.rundownId) + const rundown = playoutModel.getRundown(nextPart.rundownId) if (!rundown) throw new Error(`Could not find rundown ${nextPart.rundownId}`) - const possiblePieces = await fetchPiecesThatMayBeActiveForPart(context, cache, undefined, nextPart) + const possiblePieces = await fetchPiecesThatMayBeActiveForPart(context, playoutModel, undefined, nextPart) const newPieceInstances = getPieceInstancesForPart( context, - cache, + playoutModel, currentPartInstance, rundown, nextPart, possiblePieces, - protectString('') // Replaced inside cache.createInstanceForPart + protectString('') // Replaced inside playoutModel.createInstanceForPart ) - return cache.createInstanceForPart(nextPart, newPieceInstances) + return playoutModel.createInstanceForPart(nextPart, newPieceInstances) } /** * When entering a segment, or moving backwards in a segment, reset any partInstances in that window * In theory the new segment should already be reset, as we do that upon leaving, but it wont be if jumping to earlier in the same segment or maybe if the rundown wasnt reset */ -function resetPartInstancesWhenChangingSegment(context: JobContext, cache: PlayoutModel) { - const currentPartInstance = cache.CurrentPartInstance?.PartInstance - const nextPartInstance = cache.NextPartInstance?.PartInstance +function resetPartInstancesWhenChangingSegment(context: JobContext, playoutModel: PlayoutModel) { + const currentPartInstance = playoutModel.CurrentPartInstance?.PartInstance + const nextPartInstance = playoutModel.NextPartInstance?.PartInstance if (nextPartInstance) { const resetPartInstanceIds = new Set() if (currentPartInstance) { // Always clean the current segment, anything after the current part (except the next part) - const trailingInOldSegment = cache.LoadedPartInstances.filter( + const trailingInOldSegment = playoutModel.LoadedPartInstances.filter( (p) => !p.PartInstance.reset && p.PartInstance._id !== currentPartInstance._id && @@ -187,7 +192,7 @@ function resetPartInstancesWhenChangingSegment(context: JobContext, cache: Playo nextPartInstance.part._rank < currentPartInstance.part._rank) ) { // clean the whole segment if new, or jumping backwards - const newSegmentParts = cache.LoadedPartInstances.filter( + const newSegmentParts = playoutModel.LoadedPartInstances.filter( (p) => !p.PartInstance.reset && p.PartInstance._id !== nextPartInstance._id && @@ -200,7 +205,7 @@ function resetPartInstancesWhenChangingSegment(context: JobContext, cache: Playo } if (resetPartInstanceIds.size > 0) { - resetPartInstancesWithPieceInstances(context, cache, { + resetPartInstancesWithPieceInstances(context, playoutModel, { _id: { $in: Array.from(resetPartInstanceIds) }, }) } @@ -209,21 +214,21 @@ function resetPartInstancesWhenChangingSegment(context: JobContext, cache: Playo /** * Cleanup any orphaned (deleted) segments and partinstances once they are no longer being played - * @param cache + * @param playoutModel */ -async function cleanupOrphanedItems(context: JobContext, cache: PlayoutModel) { - const playlist = cache.Playlist +async function cleanupOrphanedItems(context: JobContext, playoutModel: PlayoutModel) { + const playlist = playoutModel.Playlist const selectedPartInstancesSegmentIds = new Set() - const currentPartInstance = cache.CurrentPartInstance?.PartInstance - const nextPartInstance = cache.NextPartInstance?.PartInstance + const currentPartInstance = playoutModel.CurrentPartInstance?.PartInstance + const nextPartInstance = playoutModel.NextPartInstance?.PartInstance if (currentPartInstance) selectedPartInstancesSegmentIds.add(currentPartInstance.segmentId) if (nextPartInstance) selectedPartInstancesSegmentIds.add(nextPartInstance.segmentId) // Cleanup any orphaned segments once they are no longer being played. This also cleans up any adlib-parts, that have been marked as deleted as a deferred cleanup operation - const segments = cache.getAllOrderedSegments().filter((s) => !!s.Segment.orphaned) + const segments = playoutModel.getAllOrderedSegments().filter((s) => !!s.Segment.orphaned) const orphanedSegmentIds = new Set(segments.map((s) => s.Segment._id)) const alterSegmentsFromRundowns = new Map() @@ -261,7 +266,7 @@ async function cleanupOrphanedItems(context: JobContext, cache: PlayoutModel) { // We need to run this outside of the current lock, and within an ingest lock, so defer to the work queue for (const [rundownId, candidateSegmentIds] of alterSegmentsFromRundowns) { - const rundown = cache.getRundown(rundownId) + const rundown = playoutModel.getRundown(rundownId) if (rundown?.Rundown?.restoredFromSnapshotId) { // This is not valid as the rundownId won't match the externalId, so ingest will fail // For now do nothing @@ -277,7 +282,7 @@ async function cleanupOrphanedItems(context: JobContext, cache: PlayoutModel) { const removePartInstanceIds: PartInstanceId[] = [] // Cleanup any orphaned partinstances once they are no longer being played (and the segment isnt orphaned) - const orphanedInstances = cache.LoadedPartInstances.filter( + const orphanedInstances = playoutModel.LoadedPartInstances.filter( (p) => p.PartInstance.orphaned === 'deleted' && !p.PartInstance.reset ) for (const partInstance of orphanedInstances) { @@ -296,19 +301,19 @@ async function cleanupOrphanedItems(context: JobContext, cache: PlayoutModel) { // Cleanup any instances from above if (removePartInstanceIds.length > 0) { - resetPartInstancesWithPieceInstances(context, cache, { _id: { $in: removePartInstanceIds } }) + resetPartInstancesWithPieceInstances(context, playoutModel, { _id: { $in: removePartInstanceIds } }) } } /** * Set or clear the queued segment. * @param context Context for the running job - * @param cache The playout cache of the playlist + * @param playoutModel The playout cache of the playlist * @param queuedSegment The segment to queue, or null to clear it */ export async function queueNextSegment( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, queuedSegment: PlayoutSegmentModel | null ): Promise { const span = context.startSpan('queueNextSegment') @@ -319,18 +324,18 @@ export async function queueNextSegment( // Just run so that errors will be thrown if something wrong: const firstPlayablePart = findFirstPlayablePartOrThrow(queuedSegment) - const currentPartInstance = cache.CurrentPartInstance?.PartInstance - const nextPartInstance = cache.NextPartInstance?.PartInstance + const currentPartInstance = playoutModel.CurrentPartInstance?.PartInstance + const nextPartInstance = playoutModel.NextPartInstance?.PartInstance // if there is not currentPartInstance or the nextPartInstance is not in the current segment // behave as if user chose SetNextPart on the first playable part of the segment if (currentPartInstance === undefined || currentPartInstance.segmentId !== nextPartInstance?.segmentId) { // Clear any existing nextSegment, as this call 'replaces' it - cache.setQueuedSegment(null) + playoutModel.setQueuedSegment(null) await setNextPart( context, - cache, + playoutModel, { part: firstPlayablePart, consumesQueuedSegmentId: false, @@ -342,9 +347,9 @@ export async function queueNextSegment( return { nextPartId: firstPlayablePart._id } } - cache.setQueuedSegment(queuedSegment) + playoutModel.setQueuedSegment(queuedSegment) } else { - cache.setQueuedSegment(null) + playoutModel.setQueuedSegment(null) } span?.end() return { queuedSegmentId: queuedSegment?.Segment?._id ?? null } @@ -353,23 +358,23 @@ export async function queueNextSegment( /** * Set the first playable part of a given segment as next. * @param context Context for the running job - * @param cache The playout cache of the playlist + * @param playoutModel The playout cache of the playlist * @param nextSegment The segment, whose first part is to be set as next */ export async function setNextSegment( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, nextSegment: PlayoutSegmentModel ): Promise { const span = context.startSpan('setNextSegment') // Just run so that errors will be thrown if something wrong: const firstPlayablePart = findFirstPlayablePartOrThrow(nextSegment) - cache.setQueuedSegment(null) + playoutModel.setQueuedSegment(null) await setNextPart( context, - cache, + playoutModel, { part: firstPlayablePart, consumesQueuedSegmentId: false, @@ -392,33 +397,33 @@ function findFirstPlayablePartOrThrow(segment: PlayoutSegmentModel): ReadonlyDee /** * Set the nexted part, from a given DBPart * @param context Context for the running job - * @param cache The playout cache of the playlist + * @param playoutModel The playout cache of the playlist * @param nextPart The Part to set as next * @param setManually Whether this was manually chosen by the user * @param nextTimeOffset The offset into the Part to start playback */ export async function setNextPartFromPart( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, nextPart: ReadonlyDeep, setManually: boolean, nextTimeOffset?: number | undefined ): Promise { - const playlist = cache.Playlist + const playlist = playoutModel.Playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown) if (playlist.holdState === RundownHoldState.ACTIVE || playlist.holdState === RundownHoldState.PENDING) { throw UserError.create(UserErrorMessage.DuringHold) } - const consumesQueuedSegmentId = doesPartConsumeQueuedSegmentId(cache, nextPart) + const consumesQueuedSegmentId = doesPartConsumeQueuedSegmentId(playoutModel, nextPart) - await setNextPart(context, cache, { part: nextPart, consumesQueuedSegmentId }, setManually, nextTimeOffset) + await setNextPart(context, playoutModel, { part: nextPart, consumesQueuedSegmentId }, setManually, nextTimeOffset) } -function doesPartConsumeQueuedSegmentId(cache: PlayoutModel, nextPart: ReadonlyDeep) { +function doesPartConsumeQueuedSegmentId(playoutModel: PlayoutModel, nextPart: ReadonlyDeep) { // If we're setting the next point to somewhere other than the current segment, and in the queued segment, clear the queued segment - const playlist = cache.Playlist - const currentPartInstance = cache.CurrentPartInstance?.PartInstance + const playlist = playoutModel.Playlist + const currentPartInstance = playoutModel.CurrentPartInstance?.PartInstance return !!( currentPartInstance && diff --git a/packages/job-worker/src/playout/setNextJobs.ts b/packages/job-worker/src/playout/setNextJobs.ts index b46bc4bc73..9f6f6f7df4 100644 --- a/packages/job-worker/src/playout/setNextJobs.ts +++ b/packages/job-worker/src/playout/setNextJobs.ts @@ -10,7 +10,7 @@ import { QueueNextSegmentResult, } from '@sofie-automation/corelib/dist/worker/studio' import { JobContext } from '../jobs' -import { runJobWithPlayoutCache } from './lock' +import { runJobWithPlayoutModel } from './lock' import { setNextPartFromPart, setNextSegment, queueNextSegment } from './setNext' import { moveNextPart } from './moveNextPart' import { updateTimeline } from './timeline/generate' @@ -21,26 +21,26 @@ import { ReadonlyDeep } from 'type-fest' * Set the next Part to a specified id */ export async function handleSetNextPart(context: JobContext, data: SetNextPartProps): Promise { - return runJobWithPlayoutCache( + return runJobWithPlayoutModel( context, data, - async (cache) => { - const playlist = cache.Playlist + async (playoutModel) => { + const playlist = playoutModel.Playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown, undefined, 412) if (playlist.holdState && playlist.holdState !== RundownHoldState.COMPLETE) { throw UserError.create(UserErrorMessage.DuringHold, undefined, 412) } }, - async (cache) => { + async (playoutModel) => { // Ensure the part is playable and found - const nextPart = cache.findPart(data.nextPartId) + const nextPart = playoutModel.findPart(data.nextPartId) if (!nextPart) throw UserError.create(UserErrorMessage.PartNotFound, undefined, 404) if (!isPartPlayable(nextPart)) throw UserError.create(UserErrorMessage.PartNotPlayable, undefined, 412) - await setNextPartFromPart(context, cache, nextPart, data.setManually ?? false, data.nextTimeOffset) + await setNextPartFromPart(context, playoutModel, nextPart, data.setManually ?? false, data.nextTimeOffset) - await updateTimeline(context, cache) + await updateTimeline(context, playoutModel) } ) } @@ -49,14 +49,14 @@ export async function handleSetNextPart(context: JobContext, data: SetNextPartPr * Move which Part is nexted by a Part(horizontal) or Segment (vertical) delta */ export async function handleMoveNextPart(context: JobContext, data: MoveNextPartProps): Promise { - return runJobWithPlayoutCache( + return runJobWithPlayoutModel( context, data, - async (cache) => { + async (playoutModel) => { if (!data.partDelta && !data.segmentDelta) throw new Error(`rundownMoveNext: invalid delta: (${data.partDelta}, ${data.segmentDelta})`) - const playlist = cache.Playlist + const playlist = playoutModel.Playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown, undefined, 412) if (playlist.holdState === RundownHoldState.ACTIVE || playlist.holdState === RundownHoldState.PENDING) { @@ -67,10 +67,10 @@ export async function handleMoveNextPart(context: JobContext, data: MoveNextPart throw UserError.create(UserErrorMessage.NoCurrentOrNextPart, undefined, 412) } }, - async (cache) => { - const newPartId = await moveNextPart(context, cache, data.partDelta, data.segmentDelta) + async (playoutModel) => { + const newPartId = await moveNextPart(context, playoutModel, data.partDelta, data.segmentDelta) - if (newPartId) await updateTimeline(context, cache) + if (newPartId) await updateTimeline(context, playoutModel) return newPartId } @@ -81,25 +81,25 @@ export async function handleMoveNextPart(context: JobContext, data: MoveNextPart * Set the next part to the first part of a Segment with given id */ export async function handleSetNextSegment(context: JobContext, data: SetNextSegmentProps): Promise { - return runJobWithPlayoutCache( + return runJobWithPlayoutModel( context, data, - async (cache) => { - const playlist = cache.Playlist + async (playoutModel) => { + const playlist = playoutModel.Playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown, undefined, 412) if (playlist.holdState && playlist.holdState !== RundownHoldState.COMPLETE) { throw UserError.create(UserErrorMessage.DuringHold, undefined, 412) } }, - async (cache) => { - const nextSegment = cache.findSegment(data.nextSegmentId) + async (playoutModel) => { + const nextSegment = playoutModel.findSegment(data.nextSegmentId) if (!nextSegment) throw new Error(`Segment "${data.nextSegmentId}" not found!`) - const nextedPartId = await setNextSegment(context, cache, nextSegment) + const nextedPartId = await setNextSegment(context, playoutModel, nextSegment) // Update any future lookaheads - await updateTimeline(context, cache) + await updateTimeline(context, playoutModel) return nextedPartId } @@ -113,28 +113,28 @@ export async function handleQueueNextSegment( context: JobContext, data: QueueNextSegmentProps ): Promise { - return runJobWithPlayoutCache( + return runJobWithPlayoutModel( context, data, - async (cache) => { - const playlist = cache.Playlist + async (playoutModel) => { + const playlist = playoutModel.Playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown, undefined, 412) if (playlist.holdState && playlist.holdState !== RundownHoldState.COMPLETE) { throw UserError.create(UserErrorMessage.DuringHold, undefined, 412) } }, - async (cache) => { + async (playoutModel) => { let queuedSegment: ReadonlyDeep | null = null if (data.queuedSegmentId) { - queuedSegment = cache.findSegment(data.queuedSegmentId) ?? null + queuedSegment = playoutModel.findSegment(data.queuedSegmentId) ?? null if (!queuedSegment) throw new Error(`Segment "${data.queuedSegmentId}" not found!`) } - const result = await queueNextSegment(context, cache, queuedSegment) + const result = await queueNextSegment(context, playoutModel, queuedSegment) // Update any future lookaheads - await updateTimeline(context, cache) + await updateTimeline(context, playoutModel) return result } diff --git a/packages/job-worker/src/playout/take.ts b/packages/job-worker/src/playout/take.ts index 03225b9603..8b956b0cec 100644 --- a/packages/job-worker/src/playout/take.ts +++ b/packages/job-worker/src/playout/take.ts @@ -30,7 +30,7 @@ import { calculatePartTimings } from '@sofie-automation/corelib/dist/playout/tim import { convertPartInstanceToBlueprints, convertResolvedPieceInstanceToBlueprints } from '../blueprints/context/lib' import { processAndPrunePieceInstanceTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' import { TakeNextPartProps } from '@sofie-automation/corelib/dist/worker/studio' -import { runJobWithPlayoutCache } from './lock' +import { runJobWithPlayoutModel } from './lock' import _ = require('underscore') /** @@ -39,11 +39,11 @@ import _ = require('underscore') export async function handleTakeNextPart(context: JobContext, data: TakeNextPartProps): Promise { const now = getCurrentTime() - return runJobWithPlayoutCache( + return runJobWithPlayoutModel( context, data, - async (cache) => { - const playlist = cache.Playlist + async (playoutModel) => { + const playlist = playoutModel.Playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown, undefined, 412) @@ -53,13 +53,13 @@ export async function handleTakeNextPart(context: JobContext, data: TakeNextPart if ((playlist.currentPartInfo?.partInstanceId ?? null) !== data.fromPartInstanceId) throw UserError.create(UserErrorMessage.TakeFromIncorrectPart, undefined, 412) }, - async (cache) => { - const playlist = cache.Playlist + async (playoutModel) => { + const playlist = playoutModel.Playlist let lastTakeTime = playlist.lastTakeTime ?? 0 if (playlist.currentPartInfo) { - const currentPartInstance = cache.CurrentPartInstance?.PartInstance + const currentPartInstance = playoutModel.CurrentPartInstance?.PartInstance if (currentPartInstance?.timings?.plannedStartedPlayback) { lastTakeTime = Math.max(lastTakeTime, currentPartInstance.timings.plannedStartedPlayback) } else { @@ -81,7 +81,7 @@ export async function handleTakeNextPart(context: JobContext, data: TakeNextPart }) } - return performTakeToNextedPart(context, cache, now) + return performTakeToNextedPart(context, playoutModel, now) } ) } @@ -89,37 +89,42 @@ export async function handleTakeNextPart(context: JobContext, data: TakeNextPart /** * Perform a Take into the nexted Part, and prepare a new nexted Part * @param context Context for current job - * @param cache Cache for the active Playlist + * @param playoutModel Cache for the active Playlist * @param now Current timestamp */ -export async function performTakeToNextedPart(context: JobContext, cache: PlayoutModel, now: number): Promise { +export async function performTakeToNextedPart( + context: JobContext, + playoutModel: PlayoutModel, + now: number +): Promise { const span = context.startSpan('takeNextPartInner') - if (!cache.Playlist.activationId) throw new Error(`Rundown Playlist "${cache.Playlist._id}" is not active!`) + if (!playoutModel.Playlist.activationId) + throw new Error(`Rundown Playlist "${playoutModel.Playlist._id}" is not active!`) - const timeOffset: number | null = cache.Playlist.nextTimeOffset || null + const timeOffset: number | null = playoutModel.Playlist.nextTimeOffset || null - const currentPartInstance = cache.CurrentPartInstance - const nextPartInstance = cache.NextPartInstance - const previousPartInstance = cache.PreviousPartInstance + const currentPartInstance = playoutModel.CurrentPartInstance + const nextPartInstance = playoutModel.NextPartInstance + const previousPartInstance = playoutModel.PreviousPartInstance const currentOrNextPartInstance = nextPartInstance ?? currentPartInstance if (!currentOrNextPartInstance) { // Some temporary logging to diagnose some cases where this route is hit - logger.warn(`No partinstance was found during take`, JSON.stringify(cache.Playlist)) + logger.warn(`No partinstance was found during take`, JSON.stringify(playoutModel.Playlist)) logger.warn( 'All PartInstances in cache', - cache.SortedLoadedPartInstances.map((p) => p.PartInstance._id) + playoutModel.SortedLoadedPartInstances.map((p) => p.PartInstance._id) ) - logger.warn('Deleted PartInstances in cache', cache.HackDeletedPartInstanceIds) + logger.warn('Deleted PartInstances in cache', playoutModel.HackDeletedPartInstanceIds) logger.warn( 'Parts in cache', - cache.getAllOrderedParts().map((d) => d._id) + playoutModel.getAllOrderedParts().map((d) => d._id) ) const validIds = _.compact([ - cache.Playlist.currentPartInfo?.partInstanceId, - cache.Playlist.nextPartInfo?.partInstanceId, + playoutModel.Playlist.currentPartInfo?.partInstanceId, + playoutModel.Playlist.nextPartInfo?.partInstanceId, ]) if (validIds.length) { const mongoDocs = await context.directCollections.PartInstances.findFetch({ _id: { $in: validIds } }) @@ -129,7 +134,7 @@ export async function performTakeToNextedPart(context: JobContext, cache: Playou throw new Error(`No partInstance could be found!`) } const currentRundown = currentOrNextPartInstance - ? cache.getRundown(currentOrNextPartInstance.PartInstance.rundownId) + ? playoutModel.getRundown(currentOrNextPartInstance.PartInstance.rundownId) : undefined if (!currentRundown) throw new Error(`Rundown "${currentOrNextPartInstance?.PartInstance?.rundownId ?? ''}" could not be found!`) @@ -167,12 +172,12 @@ export async function performTakeToNextedPart(context: JobContext, cache: Playou } } - if (cache.Playlist.holdState === RundownHoldState.COMPLETE) { - cache.setHoldState(RundownHoldState.NONE) + if (playoutModel.Playlist.holdState === RundownHoldState.COMPLETE) { + playoutModel.setHoldState(RundownHoldState.NONE) // If hold is active, then this take is to clear it - } else if (cache.Playlist.holdState === RundownHoldState.ACTIVE) { - await completeHold(context, cache, await pShowStyle, currentPartInstance) + } else if (playoutModel.Playlist.holdState === RundownHoldState.ACTIVE) { + await completeHold(context, playoutModel, await pShowStyle, currentPartInstance) if (span) span.end() @@ -181,7 +186,7 @@ export async function performTakeToNextedPart(context: JobContext, cache: Playou const takePartInstance = nextPartInstance if (!takePartInstance) throw new Error('takePart not found!') - const takeRundown = cache.getRundown(takePartInstance.PartInstance.rundownId) + const takeRundown = playoutModel.getRundown(takePartInstance.PartInstance.rundownId) if (!takeRundown) throw new Error(`takeRundown: takeRundown not found! ("${takePartInstance.PartInstance.rundownId}")`) @@ -189,17 +194,17 @@ export async function performTakeToNextedPart(context: JobContext, cache: Playou takePartInstance.clearPlannedTimings() // it is only a first take if the Playlist has no startedPlayback and the taken PartInstance is not untimed - const isFirstTake = !cache.Playlist.startedPlayback && !takePartInstance.PartInstance.part.untimed + const isFirstTake = !playoutModel.Playlist.startedPlayback && !takePartInstance.PartInstance.part.untimed - clearQueuedSegmentId(cache, takePartInstance.PartInstance, cache.Playlist.nextPartInfo) + clearQueuedSegmentId(playoutModel, takePartInstance.PartInstance, playoutModel.Playlist.nextPartInfo) const nextPart = selectNextPart( context, - cache.Playlist, + playoutModel.Playlist, takePartInstance.PartInstance, null, - cache.getAllOrderedSegments(), - cache.getAllOrderedParts() + playoutModel.getAllOrderedSegments(), + playoutModel.getAllOrderedParts() ) const showStyle = await pShowStyle @@ -226,7 +231,7 @@ export async function performTakeToNextedPart(context: JobContext, cache: Playou updatePartInstanceOnTake( context, - cache.Playlist, + playoutModel.Playlist, showStyle, blueprint, takeRundown.Rundown, @@ -234,24 +239,27 @@ export async function performTakeToNextedPart(context: JobContext, cache: Playou currentPartInstance ) - cache.cycleSelectedPartInstances() + playoutModel.cycleSelectedPartInstances() takePartInstance.setTaken(now, timeOffset ?? 0) - resetPreviousSegment(cache) + resetPreviousSegment(playoutModel) // Once everything is synced, we can choose the next part - await setNextPart(context, cache, nextPart, false) + await setNextPart(context, playoutModel, nextPart, false) // Setup the parts for the HOLD we are starting - if (cache.Playlist.previousPartInfo && (cache.Playlist.holdState as RundownHoldState) === RundownHoldState.ACTIVE) { + if ( + playoutModel.Playlist.previousPartInfo && + (playoutModel.Playlist.holdState as RundownHoldState) === RundownHoldState.ACTIVE + ) { startHold(context, currentPartInstance, nextPartInstance) } - await afterTake(context, cache, takePartInstance, timeOffset) + await afterTake(context, playoutModel, takePartInstance, timeOffset) // Last: const takeDoneTime = getCurrentTime() - cache.deferBeforeSave(async (cache2) => { + playoutModel.deferBeforeSave(async (cache2) => { await afterTakeUpdateTimingsAndEvents(context, cache2, showStyle, blueprint, isFirstTake, takeDoneTime) }) @@ -260,42 +268,42 @@ export async function performTakeToNextedPart(context: JobContext, cache: Playou /** * Clear the nexted Segment, if taking into a PartInstance that consumes it - * @param cache Cache for the active Playlist + * @param playoutModel Cache for the active Playlist * @param takenPartInstance PartInstance to check */ export function clearQueuedSegmentId( - cache: PlayoutModel, + playoutModel: PlayoutModel, takenPartInstance: ReadonlyDeep | undefined, takenPartInfo: ReadonlyDeep | null ): void { if ( takenPartInfo?.consumesQueuedSegmentId && takenPartInstance && - cache.Playlist.queuedSegmentId === takenPartInstance.segmentId + playoutModel.Playlist.queuedSegmentId === takenPartInstance.segmentId ) { // clear the queuedSegmentId if the newly taken partInstance says it was selected because of it - cache.setQueuedSegment(null) + playoutModel.setQueuedSegment(null) } } /** * Reset the Segment of the previousPartInstance, if playback has left that Segment and the Rundown is looping - * @param cache Cache for the active Playlist + * @param playoutModel Cache for the active Playlist */ -export function resetPreviousSegment(cache: PlayoutModel): void { - const previousPartInstance = cache.PreviousPartInstance - const currentPartInstance = cache.CurrentPartInstance +export function resetPreviousSegment(playoutModel: PlayoutModel): void { + const previousPartInstance = playoutModel.PreviousPartInstance + const currentPartInstance = playoutModel.CurrentPartInstance // If the playlist is looping and // If the previous and current part are not in the same segment, then we have just left a segment if ( - cache.Playlist.loop && + playoutModel.Playlist.loop && previousPartInstance && previousPartInstance.PartInstance.segmentId !== currentPartInstance?.PartInstance?.segmentId ) { // Reset the old segment const segmentId = previousPartInstance.PartInstance.segmentId - for (const partInstance of cache.LoadedPartInstances) { + for (const partInstance of playoutModel.LoadedPartInstances) { if (partInstance.PartInstance.segmentId === segmentId) { partInstance.markAsReset() } @@ -305,25 +313,25 @@ export function resetPreviousSegment(cache: PlayoutModel): void { async function afterTakeUpdateTimingsAndEvents( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, showStyle: ReadonlyDeep, blueprint: ReadonlyDeep, isFirstTake: boolean, takeDoneTime: number ): Promise { - const takePartInstance = cache.CurrentPartInstance - const previousPartInstance = cache.PreviousPartInstance + const takePartInstance = playoutModel.CurrentPartInstance + const previousPartInstance = playoutModel.PreviousPartInstance if (takePartInstance) { // Simulate playout, if no gateway - const playoutDevices = cache.PeripheralDevices.filter((d) => d.type === PeripheralDeviceType.PLAYOUT) + const playoutDevices = playoutModel.PeripheralDevices.filter((d) => d.type === PeripheralDeviceType.PLAYOUT) if (playoutDevices.length === 0) { logger.info( `No Playout gateway attached to studio, reporting PartInstance "${ takePartInstance.PartInstance._id }" to have started playback on timestamp ${new Date(takeDoneTime).toISOString()}` ) - reportPartInstanceHasStarted(context, cache, takePartInstance, takeDoneTime) + reportPartInstanceHasStarted(context, playoutModel, takePartInstance, takeDoneTime) if (previousPartInstance) { logger.info( @@ -331,13 +339,15 @@ async function afterTakeUpdateTimingsAndEvents( previousPartInstance.PartInstance._id }" to have stopped playback on timestamp ${new Date(takeDoneTime).toISOString()}` ) - reportPartInstanceHasStopped(context, cache, previousPartInstance, takeDoneTime) + reportPartInstanceHasStopped(context, playoutModel, previousPartInstance, takeDoneTime) } // Future: is there anything we can do for simulating autoNext? } - const takeRundown = takePartInstance ? cache.getRundown(takePartInstance.PartInstance.rundownId) : undefined + const takeRundown = takePartInstance + ? playoutModel.getRundown(takePartInstance.PartInstance.rundownId) + : undefined if (isFirstTake && takeRundown) { if (blueprint.blueprint.onRundownFirstTake) { @@ -452,7 +462,7 @@ export function updatePartInstanceOnTake( export async function afterTake( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, takePartInstance: PlayoutPartInstanceModel, timeOffsetIntoPart: number | null = null ): Promise { @@ -460,16 +470,16 @@ export async function afterTake( // This function should be called at the end of a "take" event (when the Parts have been updated) // or after a new part has started playing - await updateTimeline(context, cache, timeOffsetIntoPart || undefined) + await updateTimeline(context, playoutModel, timeOffsetIntoPart || undefined) - cache.deferAfterSave(async () => { + playoutModel.deferAfterSave(async () => { // This is low-prio, defer so that it's executed well after publications has been updated, // so that the playout gateway has haf the chance to learn about the timeline changes if (takePartInstance.PartInstance.part.shouldNotifyCurrentPlayingPart) { context .queueEventJob(EventsJobs.NotifyCurrentlyPlayingPart, { rundownId: takePartInstance.PartInstance.rundownId, - isRehearsal: !!cache.Playlist.rehearsal, + isRehearsal: !!playoutModel.Playlist.rehearsal, partExternalId: takePartInstance.PartInstance.part.externalId, }) .catch((e) => { @@ -501,7 +511,7 @@ function startHold( const infiniteInstanceId = instance.prepareForHold() // This gets deleted once the nextpart is activated, so it doesnt linger for long - const extendedPieceInstance = holdToPartInstance.addHoldPieceInstance(instance, infiniteInstanceId) + const extendedPieceInstance = holdToPartInstance.insertHoldPieceInstance(instance, infiniteInstanceId) const content = clone(instance.PieceInstance.piece.content) as VTContent | undefined if (content?.fileName && content.sourceDuration && instance.PieceInstance.plannedStartedPlayback) { @@ -518,19 +528,19 @@ function startHold( async function completeHold( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, showStyleCompound: ReadonlyDeep, currentPartInstance: PlayoutPartInstanceModel | null ): Promise { - cache.setHoldState(RundownHoldState.COMPLETE) + playoutModel.setHoldState(RundownHoldState.COMPLETE) - if (cache.Playlist.currentPartInfo) { + if (playoutModel.Playlist.currentPartInfo) { if (!currentPartInstance) throw new Error('currentPart not found!') // Clear the current extension line innerStopPieces( context, - cache, + playoutModel, showStyleCompound.sourceLayers, currentPartInstance, (p) => !!p.infinite?.fromHold, @@ -538,5 +548,5 @@ async function completeHold( ) } - await updateTimeline(context, cache) + await updateTimeline(context, playoutModel) } diff --git a/packages/job-worker/src/playout/timeline/generate.ts b/packages/job-worker/src/playout/timeline/generate.ts index 8021ae1c3d..9d286b61fa 100644 --- a/packages/job-worker/src/playout/timeline/generate.ts +++ b/packages/job-worker/src/playout/timeline/generate.ts @@ -54,8 +54,8 @@ import { applyAbPlaybackForTimeline } from '../abPlayback' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { PlayoutPartInstanceModel } from '../model/PlayoutPartInstanceModel' -function isCacheForStudio(cache: StudioPlayoutModelBase): cache is StudioPlayoutModel { - const tmp = cache as StudioPlayoutModel +function isModelForStudio(model: StudioPlayoutModelBase): model is StudioPlayoutModel { + const tmp = model as StudioPlayoutModel return !!tmp.isStudio } @@ -74,19 +74,19 @@ function generateTimelineVersions( export async function updateStudioTimeline( context: JobContext, - cache: StudioPlayoutModel | PlayoutModel + playoutModel: StudioPlayoutModel | PlayoutModel ): Promise { const span = context.startSpan('updateStudioTimeline') logger.debug('updateStudioTimeline running...') const studio = context.studio // Ensure there isn't a playlist active, as that should be using a different function call - if (isCacheForStudio(cache)) { - const activePlaylists = cache.getActiveRundownPlaylists() + if (isModelForStudio(playoutModel)) { + const activePlaylists = playoutModel.getActiveRundownPlaylists() if (activePlaylists.length > 0) { throw new Error(`Studio has an active playlist`) } } else { - if (cache.Playlist.activationId) { + if (playoutModel.Playlist.activationId) { throw new Error(`Studio has an active playlist`) } } @@ -127,16 +127,16 @@ export async function updateStudioTimeline( flattenAndProcessTimelineObjects(context, baselineObjects) // Future: We should handle any 'now' objects that are at the root of this timeline - preserveOrReplaceNowTimesInObjects(cache, baselineObjects) + preserveOrReplaceNowTimesInObjects(playoutModel, baselineObjects) - if (cache.isMultiGatewayMode) { + if (playoutModel.isMultiGatewayMode) { logAnyRemainingNowTimes(context, baselineObjects) } - saveTimeline(context, cache, baselineObjects, versions) + saveTimeline(context, playoutModel, baselineObjects, versions) if (studioBaseline) { - updateBaselineExpectedPackagesOnStudio(context, cache, studioBaseline) + updateBaselineExpectedPackagesOnStudio(context, playoutModel, studioBaseline) } logger.debug('updateStudioTimeline done!') @@ -145,37 +145,40 @@ export async function updateStudioTimeline( export async function updateTimeline( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, timeOffsetIntoPart?: Time ): Promise { const span = context.startSpan('updateTimeline') logger.debug('updateTimeline running...') - if (!cache.Playlist.activationId) { - throw new Error(`RundownPlaylist ("${cache.Playlist._id}") is not active")`) + if (!playoutModel.Playlist.activationId) { + throw new Error(`RundownPlaylist ("${playoutModel.Playlist._id}") is not active")`) } - const { versions, objs: timelineObjs, timingContext: timingInfo } = await getTimelineRundown(context, cache) + const { versions, objs: timelineObjs, timingContext: timingInfo } = await getTimelineRundown(context, playoutModel) flattenAndProcessTimelineObjects(context, timelineObjs) - preserveOrReplaceNowTimesInObjects(cache, timelineObjs) + preserveOrReplaceNowTimesInObjects(playoutModel, timelineObjs) - if (cache.isMultiGatewayMode) { - deNowifyMultiGatewayTimeline(context, cache, timelineObjs, timeOffsetIntoPart, timingInfo) + if (playoutModel.isMultiGatewayMode) { + deNowifyMultiGatewayTimeline(context, playoutModel, timelineObjs, timeOffsetIntoPart, timingInfo) logAnyRemainingNowTimes(context, timelineObjs) } - saveTimeline(context, cache, timelineObjs, versions) + saveTimeline(context, playoutModel, timelineObjs, versions) logger.debug('updateTimeline done!') if (span) span.end() } -function preserveOrReplaceNowTimesInObjects(cache: StudioPlayoutModelBase, timelineObjs: Array) { - const timeline = cache.Timeline +function preserveOrReplaceNowTimesInObjects( + studioPlayoutModel: StudioPlayoutModelBase, + timelineObjs: Array +) { + const timeline = studioPlayoutModel.Timeline const oldTimelineObjsMap = normalizeArray( (timeline?.timelineBlob !== undefined && deserializeTimelineBlob(timeline.timelineBlob)) || [], 'id' @@ -235,7 +238,7 @@ function logAnyRemainingNowTimes(_context: JobContext, timelineObjs: Array versions: TimelineCompleteGenerationVersions @@ -308,12 +311,12 @@ async function getTimelineRundown( try { let timelineObjs: Array = [] - const currentPartInstance = cache.CurrentPartInstance - const nextPartInstance = cache.NextPartInstance - const previousPartInstance = cache.PreviousPartInstance + const currentPartInstance = playoutModel.CurrentPartInstance + const nextPartInstance = playoutModel.NextPartInstance + const previousPartInstance = playoutModel.PreviousPartInstance const partForRundown = currentPartInstance || nextPartInstance - const activeRundown = partForRundown && cache.getRundown(partForRundown.PartInstance.rundownId) + const activeRundown = partForRundown && playoutModel.getRundown(partForRundown.PartInstance.rundownId) let timelineVersions: TimelineCompleteGenerationVersions | undefined if (activeRundown) { @@ -337,7 +340,7 @@ async function getTimelineRundown( if (partInstancesInfo.next) { // the nextPartInstance doesn't have accurate cached `calculatedTimings` yet, so calculate a prediction partInstancesInfo.next.calculatedTimings = calculatePartTimings( - cache.Playlist.holdState, + playoutModel.Playlist.holdState, partInstancesInfo.current?.partInstance?.part, partInstancesInfo.current?.pieceInstances?.map?.((p) => p.piece), partInstancesInfo.next.partInstance.part, @@ -348,7 +351,7 @@ async function getTimelineRundown( } // next (on pvw (or on pgm if first)) - const pLookaheadObjs = getLookeaheadObjects(context, cache, partInstancesInfo) + const pLookaheadObjs = getLookeaheadObjects(context, playoutModel, partInstancesInfo) const rawBaselineItems = activeRundown.BaselineObjects if (rawBaselineItems.length > 0) { timelineObjs = timelineObjs.concat(transformBaselineItemsIntoTimeline(rawBaselineItems)) @@ -358,7 +361,7 @@ async function getTimelineRundown( const rundownTimelineResult = buildTimelineObjsForRundown( context, - cache, + playoutModel, activeRundown.Rundown, partInstancesInfo ) @@ -384,7 +387,7 @@ async function getTimelineRundown( context.getStudioBlueprintConfig(), showStyle, context.getShowStyleBlueprintConfig(showStyle), - cache.Playlist, + playoutModel.Playlist, activeRundown.Rundown, previousPartInstance?.PartInstance, currentPartInstance?.PartInstance, @@ -398,7 +401,7 @@ async function getTimelineRundown( abHelper, blueprint, showStyle, - cache.Playlist, + playoutModel.Playlist, resolvedPieces, timelineObjs ) @@ -410,7 +413,7 @@ async function getTimelineRundown( tlGenRes = await blueprint.blueprint.onTimelineGenerate( blueprintContext, timelineObjs, - clone(cache.Playlist.previousPersistentState), + clone(playoutModel.Playlist.previousPersistentState), clone(currentPartInstance?.PartInstance?.previousPartEndState), resolvedPieces.map(convertResolvedPieceInstanceToBlueprints) ) @@ -425,7 +428,7 @@ async function getTimelineRundown( }) } - cache.setOnTimelineGenerateResult( + playoutModel.setOnTimelineGenerateResult( tlGenRes?.persistentState, newAbSessionsResult, blueprintContext.abSessionsHelper.knownSessions diff --git a/packages/job-worker/src/playout/timeline/multi-gateway.ts b/packages/job-worker/src/playout/timeline/multi-gateway.ts index 684fb03faf..31b52a430c 100644 --- a/packages/job-worker/src/playout/timeline/multi-gateway.ts +++ b/packages/job-worker/src/playout/timeline/multi-gateway.ts @@ -23,7 +23,7 @@ import { PlayoutPieceInstanceModel } from '../model/PlayoutPieceInstanceModel' */ export function deNowifyMultiGatewayTimeline( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, timelineObjs: TimelineObjRundown[], timeOffsetIntoPart: Time | undefined, timingContext: RundownTimelineTimingContext | undefined @@ -32,19 +32,19 @@ export function deNowifyMultiGatewayTimeline( const timelineObjsMap = normalizeArray(timelineObjs, 'id') - const nowOffsetLatency = calculateNowOffsetLatency(context, cache, timeOffsetIntoPart) + const nowOffsetLatency = calculateNowOffsetLatency(context, playoutModel, timeOffsetIntoPart) const targetNowTime = getCurrentTime() + (nowOffsetLatency ?? 0) // Replace `start: 'now'` in currentPartInstance on timeline - const currentPartInstance = cache.CurrentPartInstance + const currentPartInstance = playoutModel.CurrentPartInstance if (!currentPartInstance) return const partGroupTimings = updatePartInstancePlannedTimes( - cache, + playoutModel, targetNowTime, timingContext, currentPartInstance, - cache.NextPartInstance + playoutModel.NextPartInstance ) deNowifyCurrentPieces( @@ -55,19 +55,21 @@ export function deNowifyMultiGatewayTimeline( timelineObjsMap ) - updatePlannedTimingsForPieceInstances(cache, currentPartInstance, partGroupTimings, timelineObjsMap) + updatePlannedTimingsForPieceInstances(playoutModel, currentPartInstance, partGroupTimings, timelineObjsMap) } export function calculateNowOffsetLatency( context: JobContext, - cache: StudioPlayoutModelBase, + studioPlayoutModel: StudioPlayoutModelBase, timeOffsetIntoPart: Time | undefined ): Time | undefined { /** The timestamp that "now" was set to */ let nowOffsetLatency: Time | undefined - if (cache.isMultiGatewayMode) { - const playoutDevices = cache.PeripheralDevices.filter((device) => device.type === PeripheralDeviceType.PLAYOUT) + if (studioPlayoutModel.isMultiGatewayMode) { + const playoutDevices = studioPlayoutModel.PeripheralDevices.filter( + (device) => device.type === PeripheralDeviceType.PLAYOUT + ) const worstLatency = Math.max(0, ...playoutDevices.map((device) => getExpectedLatency(device).safe)) /** Add a little more latency, to account for network latency variability */ const ADD_SAFE_LATENCY = context.studio.settings.multiGatewayNowSafeLatency || 30 @@ -89,7 +91,7 @@ interface PartGroupTimings { } function updatePartInstancePlannedTimes( - cache: PlayoutModel, + playoutModel: PlayoutModel, targetNowTime: number, timingContext: RundownTimelineTimingContext, currentPartInstance: PlayoutPartInstanceModel, @@ -108,7 +110,7 @@ function updatePartInstancePlannedTimes( } // Also mark the previous as ended - const previousPartInstance = cache.PreviousPartInstance + const previousPartInstance = playoutModel.PreviousPartInstance if (previousPartInstance) { const previousPartEndTime = currentPartGroupStartTime + (timingContext.previousPartOverlap ?? 0) previousPartInstance.setPlannedStoppedPlayback(previousPartEndTime) @@ -201,13 +203,13 @@ function deNowifyCurrentPieces( } function updatePlannedTimingsForPieceInstances( - cache: PlayoutModel, + playoutModel: PlayoutModel, currentPartInstance: PlayoutPartInstanceModel, partGroupTimings: PartGroupTimings, timelineObjsMap: Record ) { const existingInfiniteTimings = new Map() - const previousPartInstance = cache.PreviousPartInstance + const previousPartInstance = playoutModel.PreviousPartInstance if (previousPartInstance) { const pieceInstances = previousPartInstance.PieceInstances for (const pieceInstance of pieceInstances) { @@ -232,7 +234,7 @@ function updatePlannedTimingsForPieceInstances( preserveOrTrackInfiniteTimings(existingInfiniteTimings, timelineObjsMap, pieceInstance) } - const nextPartInstance = cache.NextPartInstance + const nextPartInstance = playoutModel.NextPartInstance if (nextPartInstance && partGroupTimings.nextStartTime) { const nextPartGroupStartTime0 = partGroupTimings.nextStartTime for (const pieceInstance of nextPartInstance.PieceInstances) { diff --git a/packages/job-worker/src/playout/timeline/rundown.ts b/packages/job-worker/src/playout/timeline/rundown.ts index f1e8fbe6c8..7a5d38a2f3 100644 --- a/packages/job-worker/src/playout/timeline/rundown.ts +++ b/packages/job-worker/src/playout/timeline/rundown.ts @@ -50,14 +50,14 @@ export interface RundownTimelineResult { export function buildTimelineObjsForRundown( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, _activeRundown: ReadonlyDeep, partInstancesInfo: SelectedPartInstancesTimelineInfo ): RundownTimelineResult { const span = context.startSpan('buildTimelineObjsForRundown') const timelineObjs: Array = [] - const activePlaylist = cache.Playlist + const activePlaylist = playoutModel.Playlist const currentTime = getCurrentTime() timelineObjs.push( diff --git a/packages/job-worker/src/playout/timelineJobs.ts b/packages/job-worker/src/playout/timelineJobs.ts index 385f8499fe..8552c10c91 100644 --- a/packages/job-worker/src/playout/timelineJobs.ts +++ b/packages/job-worker/src/playout/timelineJobs.ts @@ -1,9 +1,9 @@ import { UpdateTimelineAfterIngestProps } from '@sofie-automation/corelib/dist/worker/studio' import { JobContext } from '../jobs' -import { runJobWithPlaylistLock, runWithPlaylistCache } from './lock' +import { runJobWithPlaylistLock, runWithPlayoutModel } from './lock' import { updateStudioTimeline, updateTimeline } from './timeline/generate' import { getSystemVersion } from '../lib' -import { runJobWithStudioCache } from '../studio/lock' +import { runJobWithStudioPlayoutModel } from '../studio/lock' import { shouldUpdateStudioBaselineInner as libShouldUpdateStudioBaselineInner } from '@sofie-automation/corelib/dist/studio/baseline' import { StudioPlayoutModel } from '../studio/StudioPlayoutModel' @@ -12,27 +12,27 @@ import { StudioPlayoutModel } from '../studio/StudioPlayoutModel' * Has no effect if a Playlist is active */ export async function handleUpdateStudioBaseline(context: JobContext, _data: void): Promise { - return runJobWithStudioCache(context, async (cache) => { - const activePlaylists = cache.getActiveRundownPlaylists() + return runJobWithStudioPlayoutModel(context, async (studioPlayoutModel) => { + const activePlaylists = studioPlayoutModel.getActiveRundownPlaylists() if (activePlaylists.length === 0) { - await updateStudioTimeline(context, cache) - return shouldUpdateStudioBaselineInner(context, cache) + await updateStudioTimeline(context, studioPlayoutModel) + return shouldUpdateStudioBaselineInner(context, studioPlayoutModel) } else { - return shouldUpdateStudioBaselineInner(context, cache) + return shouldUpdateStudioBaselineInner(context, studioPlayoutModel) } }) } async function shouldUpdateStudioBaselineInner( context: JobContext, - cache: StudioPlayoutModel + playoutModel: StudioPlayoutModel ): Promise { const studio = context.studio - if (cache.getActiveRundownPlaylists().length > 0) return false + if (playoutModel.getActiveRundownPlaylists().length > 0) return false - const timeline = cache.Timeline + const timeline = playoutModel.Timeline const blueprint = studio.blueprintId ? await context.directCollections.Blueprints.findOne(studio.blueprintId) : null if (!blueprint) return 'missingBlueprint' @@ -50,10 +50,10 @@ export async function handleUpdateTimelineAfterIngest( await runJobWithPlaylistLock(context, data, async (playlist, lock) => { if (playlist?.activationId && (playlist.currentPartInfo || playlist.nextPartInfo)) { // TODO - r37 added a retry mechanic to this. should that be kept? - await runWithPlaylistCache(context, playlist, lock, null, async (cache) => { - const currentPartInstance = cache.CurrentPartInstance + await runWithPlayoutModel(context, playlist, lock, null, async (playoutModel) => { + const currentPartInstance = playoutModel.CurrentPartInstance if ( - !cache.isMultiGatewayMode && + !playoutModel.isMultiGatewayMode && currentPartInstance && !currentPartInstance.PartInstance.timings?.reportedStartedPlayback ) { @@ -62,7 +62,7 @@ export async function handleUpdateTimelineAfterIngest( } else { // It is safe enough (except adlibs) to update the timeline directly // If the playlist is active, then updateTimeline as lookahead could have been affected - await updateTimeline(context, cache) + await updateTimeline(context, playoutModel) } }) } diff --git a/packages/job-worker/src/playout/timings/index.ts b/packages/job-worker/src/playout/timings/index.ts index 2adda97635..17bfb8e17b 100644 --- a/packages/job-worker/src/playout/timings/index.ts +++ b/packages/job-worker/src/playout/timings/index.ts @@ -1,7 +1,7 @@ import { OnPlayoutPlaybackChangedProps } from '@sofie-automation/corelib/dist/worker/studio' import { logger } from '../../logging' import { JobContext } from '../../jobs' -import { runJobWithPlayoutCache } from '../lock' +import { runJobWithPlayoutModel } from '../lock' import { assertNever } from '@sofie-automation/corelib/dist/lib' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { PlayoutChangedType } from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI' @@ -17,27 +17,27 @@ export async function handleOnPlayoutPlaybackChanged( context: JobContext, data: OnPlayoutPlaybackChangedProps ): Promise { - return runJobWithPlayoutCache(context, data, null, async (cache) => { + return runJobWithPlayoutModel(context, data, null, async (playoutModel) => { for (const change of data.changes) { try { if (change.type === PlayoutChangedType.PART_PLAYBACK_STARTED) { - await onPartPlaybackStarted(context, cache, { + await onPartPlaybackStarted(context, playoutModel, { partInstanceId: change.data.partInstanceId, startedPlayback: change.data.time, }) } else if (change.type === PlayoutChangedType.PART_PLAYBACK_STOPPED) { - onPartPlaybackStopped(context, cache, { + onPartPlaybackStopped(context, playoutModel, { partInstanceId: change.data.partInstanceId, stoppedPlayback: change.data.time, }) } else if (change.type === PlayoutChangedType.PIECE_PLAYBACK_STARTED) { - onPiecePlaybackStarted(context, cache, { + onPiecePlaybackStarted(context, playoutModel, { partInstanceId: change.data.partInstanceId, pieceInstanceId: change.data.pieceInstanceId, startedPlayback: change.data.time, }) } else if (change.type === PlayoutChangedType.PIECE_PLAYBACK_STOPPED) { - onPiecePlaybackStopped(context, cache, { + onPiecePlaybackStopped(context, playoutModel, { partInstanceId: change.data.partInstanceId, pieceInstanceId: change.data.pieceInstanceId, stoppedPlayback: change.data.time, diff --git a/packages/job-worker/src/playout/timings/partPlayback.ts b/packages/job-worker/src/playout/timings/partPlayback.ts index e45952677c..db2ab543a2 100644 --- a/packages/job-worker/src/playout/timings/partPlayback.ts +++ b/packages/job-worker/src/playout/timings/partPlayback.ts @@ -15,20 +15,22 @@ import { Time } from '@sofie-automation/blueprints-integration' * Set the playback of a part is confirmed to have started * If the part reported to be playing is not the current part, then make it be the current * @param context Context from the job queue - * @param cache DB cache for the current playlist + * @param playoutModel DB cache for the current playlist * @param data Details on the part start event */ export async function onPartPlaybackStarted( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, data: { partInstanceId: PartInstanceId startedPlayback: Time } ): Promise { - const playingPartInstance = cache.getPartInstance(data.partInstanceId) + const playingPartInstance = playoutModel.getPartInstance(data.partInstanceId) if (!playingPartInstance) - throw new Error(`PartInstance "${data.partInstanceId}" in RundownPlayst "${cache.PlaylistId}" not found!`) + throw new Error( + `PartInstance "${data.partInstanceId}" in RundownPlayst "${playoutModel.PlaylistId}" not found!` + ) // make sure we don't run multiple times, even if TSR calls us multiple times const hasStartedPlaying = !!playingPartInstance.PartInstance.timings?.reportedStartedPlayback @@ -39,29 +41,29 @@ export async function onPartPlaybackStarted( ).toISOString()}` ) - const playlist = cache.Playlist + const playlist = playoutModel.Playlist - const rundown = cache.getRundown(playingPartInstance.PartInstance.rundownId) + const rundown = playoutModel.getRundown(playingPartInstance.PartInstance.rundownId) if (!rundown) throw new Error(`Rundown "${playingPartInstance.PartInstance.rundownId}" not found!`) - const currentPartInstance = cache.CurrentPartInstance + const currentPartInstance = playoutModel.CurrentPartInstance if (playlist.currentPartInfo?.partInstanceId === data.partInstanceId) { // this is the current part, it has just started playback - reportPartInstanceHasStarted(context, cache, playingPartInstance, data.startedPlayback) + reportPartInstanceHasStarted(context, playoutModel, playingPartInstance, data.startedPlayback) // complete the take - await afterTake(context, cache, playingPartInstance) + await afterTake(context, playoutModel, playingPartInstance) } else if (playlist.nextPartInfo?.partInstanceId === data.partInstanceId) { // this is the next part, clearly an autoNext has taken place - cache.cycleSelectedPartInstances() + playoutModel.cycleSelectedPartInstances() - reportPartInstanceHasStarted(context, cache, playingPartInstance, data.startedPlayback) + reportPartInstanceHasStarted(context, playoutModel, playingPartInstance, data.startedPlayback) // Update generated properties on the newly playing partInstance const currentRundown = currentPartInstance - ? cache.getRundown(currentPartInstance.PartInstance.rundownId) + ? playoutModel.getRundown(currentPartInstance.PartInstance.rundownId) : undefined const showStyleRundown = currentRundown ?? rundown const showStyle = await context.getShowStyleCompound( @@ -71,7 +73,7 @@ export async function onPartPlaybackStarted( const blueprint = await context.getShowStyleBlueprint(showStyle._id) updatePartInstanceOnTake( context, - cache.Playlist, + playoutModel.Playlist, showStyle, blueprint, rundown.Rundown, @@ -79,8 +81,8 @@ export async function onPartPlaybackStarted( currentPartInstance ) - clearQueuedSegmentId(cache, playingPartInstance.PartInstance, playlist.nextPartInfo) - resetPreviousSegment(cache) + clearQueuedSegmentId(playoutModel, playingPartInstance.PartInstance, playlist.nextPartInfo) + resetPreviousSegment(playoutModel) // Update the next partinstance const nextPart = selectNextPart( @@ -88,13 +90,13 @@ export async function onPartPlaybackStarted( playlist, playingPartInstance.PartInstance, null, - cache.getAllOrderedSegments(), - cache.getAllOrderedParts() + playoutModel.getAllOrderedSegments(), + playoutModel.getAllOrderedParts() ) - await setNextPart(context, cache, nextPart, false) + await setNextPart(context, playoutModel, nextPart, false) // complete the take - await afterTake(context, cache, playingPartInstance) + await afterTake(context, playoutModel, playingPartInstance) } else { // a part is being played that has not been selected for playback by Core @@ -110,7 +112,7 @@ export async function onPartPlaybackStarted( const previousReported = playlist.lastIncorrectPartPlaybackReported if (previousReported && Date.now() - previousReported > INCORRECT_PLAYING_PART_DEBOUNCE) { // first time this has happened for a while, let's make sure it has the correct timeline - await updateTimeline(context, cache) + await updateTimeline(context, playoutModel) } logger.error( @@ -123,20 +125,20 @@ export async function onPartPlaybackStarted( /** * Set the playback of a part is confirmed to have stopped * @param context Context from the job queue - * @param cache DB cache for the current playlist + * @param playoutModel DB cache for the current playlist * @param data Details on the part stop event */ export function onPartPlaybackStopped( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, data: { partInstanceId: PartInstanceId stoppedPlayback: Time } ): void { - const playlist = cache.Playlist + const playlist = playoutModel.Playlist - const partInstance = cache.getPartInstance(data.partInstanceId) + const partInstance = playoutModel.getPartInstance(data.partInstanceId) if (partInstance) { // make sure we don't run multiple times, even if TSR calls us multiple times @@ -150,7 +152,7 @@ export function onPartPlaybackStopped( }" has stopped playback on timestamp ${new Date(data.stoppedPlayback).toISOString()}` ) - reportPartInstanceHasStopped(context, cache, partInstance, data.stoppedPlayback) + reportPartInstanceHasStopped(context, playoutModel, partInstance, data.stoppedPlayback) } } else if (!playlist.activationId) { logger.warn(`onPartPlaybackStopped: Received for inactive RundownPlaylist "${playlist._id}"`) @@ -164,35 +166,35 @@ export function onPartPlaybackStopped( /** * Set the playback of a PartInstance is confirmed to have started * @param context Context from the job queue - * @param cache DB cache for the current playlist + * @param playoutModel DB cache for the current playlist * @param partInstance PartInstance to be updated * @param timestamp timestamp the PieceInstance started */ export function reportPartInstanceHasStarted( _context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, partInstance: PlayoutPartInstanceModel, timestamp: Time ): void { if (partInstance) { const timestampUpdated = partInstance.setReportedStartedPlayback(timestamp) - if (timestamp && !cache.isMultiGatewayMode) { + if (timestamp && !playoutModel.isMultiGatewayMode) { partInstance.setPlannedStartedPlayback(timestamp) } - const previousPartInstance = cache.PreviousPartInstance - if (timestampUpdated && !cache.isMultiGatewayMode && previousPartInstance) { + const previousPartInstance = playoutModel.PreviousPartInstance + if (timestampUpdated && !playoutModel.isMultiGatewayMode && previousPartInstance) { // Ensure the plannedStoppedPlayback is set for the previous partinstance too previousPartInstance.setPlannedStoppedPlayback(timestamp) } // Update the playlist: if (!partInstance.PartInstance.part.untimed) { - cache.setRundownStartedPlayback(partInstance.PartInstance.rundownId, timestamp) + playoutModel.setRundownStartedPlayback(partInstance.PartInstance.rundownId, timestamp) } if (timestampUpdated) { - cache.queuePartInstanceTimingEvent(partInstance.PartInstance._id) + playoutModel.queuePartInstanceTimingEvent(partInstance.PartInstance._id) } } } @@ -200,22 +202,22 @@ export function reportPartInstanceHasStarted( /** * Set the playback of a PartInstance is confirmed to have stopped * @param context Context from the job queue - * @param cache DB cache for the current playlist + * @param playoutModel DB cache for the current playlist * @param partInstance PartInstance to be updated * @param timestamp timestamp the PieceInstance stopped */ export function reportPartInstanceHasStopped( _context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, partInstance: PlayoutPartInstanceModel, timestamp: Time ): void { const timestampUpdated = partInstance.setReportedStoppedPlayback(timestamp) - if (timestampUpdated && !cache.isMultiGatewayMode) { + if (timestampUpdated && !playoutModel.isMultiGatewayMode) { partInstance.setPlannedStoppedPlayback(timestamp) } if (timestampUpdated) { - cache.queuePartInstanceTimingEvent(partInstance.PartInstance._id) + playoutModel.queuePartInstanceTimingEvent(partInstance.PartInstance._id) } } diff --git a/packages/job-worker/src/playout/timings/piecePlayback.ts b/packages/job-worker/src/playout/timings/piecePlayback.ts index 2efa0ecd16..1fe2b1a87c 100644 --- a/packages/job-worker/src/playout/timings/piecePlayback.ts +++ b/packages/job-worker/src/playout/timings/piecePlayback.ts @@ -9,21 +9,21 @@ import { PlayoutPieceInstanceModel } from '../model/PlayoutPieceInstanceModel' /** * Set the playback of a piece is confirmed to have started * @param context Context from the job queue - * @param cache DB cache for the current playlist + * @param playoutModel DB cache for the current playlist * @param data Details on the piece start event */ export function onPiecePlaybackStarted( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, data: { partInstanceId: PartInstanceId pieceInstanceId: PieceInstanceId startedPlayback: Time } ): void { - const playlist = cache.Playlist + const playlist = playoutModel.Playlist - const partInstance = cache.getPartInstance(data.partInstanceId) + const partInstance = playoutModel.getPartInstance(data.partInstanceId) if (!partInstance) { if (!playlist.activationId) { logger.warn(`onPiecePlaybackStarted: Received for inactive RundownPlaylist "${playlist._id}"`) @@ -52,7 +52,7 @@ export function onPiecePlaybackStarted( data.pieceInstanceId }" has started playback on timestamp ${new Date(data.startedPlayback).toISOString()}` ) - reportPieceHasStarted(context, cache, partInstance, pieceInstance, data.startedPlayback) + reportPieceHasStarted(context, playoutModel, partInstance, pieceInstance, data.startedPlayback) // We don't need to bother with an updateTimeline(), as this hasn't changed anything, but lets us accurately add started items when reevaluating } @@ -61,21 +61,21 @@ export function onPiecePlaybackStarted( /** * Set the playback of a piece is confirmed to have stopped * @param context Context from the job queue - * @param cache DB cache for the current playlist + * @param playoutModel DB cache for the current playlist * @param data Details on the piece stop event */ export function onPiecePlaybackStopped( context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, data: { partInstanceId: PartInstanceId pieceInstanceId: PieceInstanceId stoppedPlayback: Time } ): void { - const playlist = cache.Playlist + const playlist = playoutModel.Playlist - const partInstance = cache.getPartInstance(data.partInstanceId) + const partInstance = playoutModel.getPartInstance(data.partInstanceId) if (!partInstance) { // PartInstance not found, so we can rely on the onPartPlaybackStopped callback erroring return @@ -101,32 +101,32 @@ export function onPiecePlaybackStopped( }" has stopped playback on timestamp ${new Date(data.stoppedPlayback).toISOString()}` ) - reportPieceHasStopped(context, cache, pieceInstance, data.stoppedPlayback) + reportPieceHasStopped(context, playoutModel, pieceInstance, data.stoppedPlayback) } } /** * Set the playback of a PieceInstance is confirmed to have started * @param context Context from the job queue - * @param cache DB cache for the current playlist + * @param playoutModel DB cache for the current playlist * @param pieceInstance PieceInstance to be updated * @param timestamp timestamp the PieceInstance started */ function reportPieceHasStarted( _context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, partInstance: PlayoutPartInstanceModel, pieceInstance: PlayoutPieceInstanceModel, timestamp: Time ): void { const timestampChanged = pieceInstance.setReportedStartedPlayback(timestamp) if (timestampChanged) { - if (!cache.isMultiGatewayMode) { + if (!playoutModel.isMultiGatewayMode) { pieceInstance.setPlannedStartedPlayback(timestamp) } // Update the copy in the next-part if there is one, so that the infinite has the same start after a take - const nextPartInstance = cache.NextPartInstance + const nextPartInstance = playoutModel.NextPartInstance if ( pieceInstance.PieceInstance.infinite && nextPartInstance && @@ -140,37 +140,37 @@ function reportPieceHasStarted( ) { nextPieceInstance.setReportedStartedPlayback(timestamp) - if (!cache.isMultiGatewayMode) { + if (!playoutModel.isMultiGatewayMode) { nextPieceInstance.setPlannedStartedPlayback(timestamp) } } } } - cache.queuePartInstanceTimingEvent(partInstance.PartInstance._id) + playoutModel.queuePartInstanceTimingEvent(partInstance.PartInstance._id) } } /** * Set the playback of a PieceInstance is confirmed to have stopped * @param context Context from the job queue - * @param cache DB cache for the current playlist + * @param playoutModel DB cache for the current playlist * @param pieceInstance PieceInstance to be updated * @param timestamp timestamp the PieceInstance stopped */ function reportPieceHasStopped( _context: JobContext, - cache: PlayoutModel, + playoutModel: PlayoutModel, pieceInstance: PlayoutPieceInstanceModel, timestamp: Time ): void { const timestampChanged = pieceInstance.setReportedStoppedPlayback(timestamp) if (timestampChanged) { - if (!cache.isMultiGatewayMode) { + if (!playoutModel.isMultiGatewayMode) { pieceInstance.setPlannedStoppedPlayback(timestamp) } - cache.queuePartInstanceTimingEvent(pieceInstance.PieceInstance.partInstanceId) + playoutModel.queuePartInstanceTimingEvent(pieceInstance.PieceInstance.partInstanceId) } } diff --git a/packages/job-worker/src/playout/timings/timelineTriggerTime.ts b/packages/job-worker/src/playout/timings/timelineTriggerTime.ts index f221913dc5..a6cebc6e4b 100644 --- a/packages/job-worker/src/playout/timings/timelineTriggerTime.ts +++ b/packages/job-worker/src/playout/timings/timelineTriggerTime.ts @@ -8,7 +8,7 @@ import { runJobWithPlaylistLock } from '../lock' import { saveTimeline } from '../timeline/generate' import { applyToArray } from '@sofie-automation/corelib/dist/lib' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { runJobWithStudioCache } from '../../studio/lock' +import { runJobWithStudioPlayoutModel } from '../../studio/lock' import { StudioPlayoutModel } from '../../studio/StudioPlayoutModel' import { DbCacheWriteCollection } from '../../cache/CacheCollection' import { PieceTimelineMetadata } from '../timeline/pieceGroup' @@ -21,7 +21,7 @@ import { ReadonlyDeep } from 'type-fest' */ export async function handleTimelineTriggerTime(context: JobContext, data: OnTimelineTriggerTimeProps): Promise { if (data.results.length > 0) { - await runJobWithStudioCache(context, async (studioCache) => { + await runJobWithStudioPlayoutModel(context, async (studioCache) => { const activePlaylists = studioCache.getActiveRundownPlaylists() if (studioCache.isMultiGatewayMode) { @@ -68,7 +68,7 @@ export async function handleTimelineTriggerTime(context: JobContext, data: OnTim function timelineTriggerTimeInner( context: JobContext, - cache: StudioPlayoutModel, + studioPlayoutModel: StudioPlayoutModel, results: OnTimelineTriggerTimeProps['results'], pieceInstanceCache: DbCacheWriteCollection | undefined, activePlaylist: ReadonlyDeep | undefined @@ -76,7 +76,7 @@ function timelineTriggerTimeInner( let lastTakeTime: number | undefined // ------------------------------ - const timeline = cache.Timeline + const timeline = studioPlayoutModel.Timeline if (timeline) { const timelineObjs = deserializeTimelineBlob(timeline.timelineBlob) let tlChanged = false @@ -141,7 +141,7 @@ function timelineTriggerTimeInner( } } if (tlChanged) { - saveTimeline(context, cache, timelineObjs, timeline.generationVersions) + saveTimeline(context, studioPlayoutModel, timelineObjs, timeline.generationVersions) } } } diff --git a/packages/job-worker/src/rundown.ts b/packages/job-worker/src/rundown.ts index 5f298719c0..b30fbaf72f 100644 --- a/packages/job-worker/src/rundown.ts +++ b/packages/job-worker/src/rundown.ts @@ -115,11 +115,11 @@ export async function updatePartInstanceRanks( * Update the ranks of all PartInstances in the given segments. * Syncs the ranks from matching Parts to PartInstances. */ -export function updatePartInstanceRanksAfterAdlib(cache: PlayoutModel, segmentId: SegmentId): void { - const newParts = cache.findSegment(segmentId)?.Parts ?? [] +export function updatePartInstanceRanksAfterAdlib(playoutModel: PlayoutModel, segmentId: SegmentId): void { + const newParts = playoutModel.findSegment(segmentId)?.Parts ?? [] const segmentPartInstances = _.sortBy( - cache.LoadedPartInstances.filter((p) => p.PartInstance.segmentId === segmentId).map((p) => + playoutModel.LoadedPartInstances.filter((p) => p.PartInstance.segmentId === segmentId).map((p) => clone(p.PartInstance) ), (p) => p.part._rank @@ -127,7 +127,7 @@ export function updatePartInstanceRanksAfterAdlib(cache: PlayoutModel, segmentId const newRanks = calculateNewRanksForParts(segmentId, null, newParts, segmentPartInstances) for (const [instanceId, info] of newRanks.entries()) { - const partInstance = cache.getPartInstance(instanceId) + const partInstance = playoutModel.getPartInstance(instanceId) if (!partInstance) continue // TODO - should this throw? if (info.deleted) { diff --git a/packages/job-worker/src/rundownPlaylists.ts b/packages/job-worker/src/rundownPlaylists.ts index 0bc6b8a186..d05a4877e5 100644 --- a/packages/job-worker/src/rundownPlaylists.ts +++ b/packages/job-worker/src/rundownPlaylists.ts @@ -16,7 +16,7 @@ import { BlueprintResultRundownPlaylist, IBlueprintRundown } from '@sofie-automa import { JobContext } from './jobs' import { logger } from './logging' import { resetRundownPlaylist } from './playout/lib' -import { runJobWithPlaylistLock, runWithPlaylistCache } from './playout/lock' +import { runJobWithPlaylistLock, runWithPlayoutModel } from './playout/lock' import { updateTimeline } from './playout/timeline/generate' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { WrappedStudioBlueprint } from './blueprints/cache' @@ -77,7 +77,7 @@ export async function handleRegenerateRundownPlaylist( ) if (rundowns.length === 0) return [] - await runWithPlaylistCache(context, playlist, playlistLock, null, async (cache) => { + await runWithPlayoutModel(context, playlist, playlistLock, null, async (cache) => { await resetRundownPlaylist(context, cache) if (cache.Playlist.activationId) { diff --git a/packages/job-worker/src/studio/StudioPlayoutModelImpl.ts b/packages/job-worker/src/studio/StudioPlayoutModelImpl.ts index 4db8b3c2ab..9843410f02 100644 --- a/packages/job-worker/src/studio/StudioPlayoutModelImpl.ts +++ b/packages/job-worker/src/studio/StudioPlayoutModelImpl.ts @@ -16,7 +16,7 @@ import { logger } from '../logging' import { StudioPlayoutModel, DeferredAfterSaveFunction } from './StudioPlayoutModel' /** - * This is a cache used for studio operations. + * This is a model used for studio operations. */ export class StudioPlayoutModelImpl implements StudioPlayoutModel { diff --git a/packages/job-worker/src/studio/cleanup.ts b/packages/job-worker/src/studio/cleanup.ts index d61806dcfb..a1d052773f 100644 --- a/packages/job-worker/src/studio/cleanup.ts +++ b/packages/job-worker/src/studio/cleanup.ts @@ -1,15 +1,15 @@ import { runJobWithPlaylistLock } from '../playout/lock' import { JobContext } from '../jobs' -import { runJobWithStudioCache } from './lock' +import { runJobWithStudioPlayoutModel } from './lock' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' /** * Cleanup any RundownPlaylists that contain no Rundowns */ export async function handleRemoveEmptyPlaylists(context: JobContext, _data: void): Promise { - await runJobWithStudioCache(context, async (cache) => { + await runJobWithStudioPlayoutModel(context, async (studioPlayoutModel) => { // Skip any playlists which are active - const tmpPlaylists = cache.RundownPlaylists.filter((p) => !p.activationId, { fields: { _id: 1 } }) + const tmpPlaylists = studioPlayoutModel.RundownPlaylists.filter((p) => !p.activationId, { fields: { _id: 1 } }) // We want to run them all in parallel await Promise.allSettled( diff --git a/packages/job-worker/src/studio/lock.ts b/packages/job-worker/src/studio/lock.ts index 22ec8d01d2..bfdccddd9e 100644 --- a/packages/job-worker/src/studio/lock.ts +++ b/packages/job-worker/src/studio/lock.ts @@ -4,22 +4,22 @@ import { loadStudioPlayoutModel } from './StudioPlayoutModelImpl' /** * Run a typical studio job - * This means loading the studio cache, doing some calculations and saving the result + * This means loading the studioPlayoutModel, doing some calculations and saving the result */ -export async function runJobWithStudioCache( +export async function runJobWithStudioPlayoutModel( context: JobContext, - fcn: (cache: StudioPlayoutModel) => Promise + fcn: (studioPlayoutModel: StudioPlayoutModel) => Promise ): Promise { - const cache = await loadStudioPlayoutModel(context) + const studioPlayoutModel = await loadStudioPlayoutModel(context) try { - const res = await fcn(cache) + const res = await fcn(studioPlayoutModel) - await cache.saveAllToDatabase() + await studioPlayoutModel.saveAllToDatabase() return res } catch (err) { - cache.dispose() + studioPlayoutModel.dispose() throw err } } From d9dee473068c4d96d15a6ef799ace830cf1fe1c0 Mon Sep 17 00:00:00 2001 From: Baud <107637688+sbaudlr@users.noreply.github.com> Date: Wed, 18 Oct 2023 17:00:22 +0100 Subject: [PATCH 122/479] feat: A/B player Ids as strings (#1054) --- .../blueprints-integration/src/abPlayback.ts | 6 +- .../corelib/src/dataModel/RundownPlaylist.ts | 2 +- .../abPlayback/__tests__/abPlayback.spec.ts | 74 ++++++++++++++++++- .../playout/abPlayback/abPlaybackResolver.ts | 25 +++++-- .../playout/abPlayback/applyAssignments.ts | 26 +++---- .../shared-lib/src/core/model/Timeline.ts | 2 +- 6 files changed, 105 insertions(+), 30 deletions(-) diff --git a/packages/blueprints-integration/src/abPlayback.ts b/packages/blueprints-integration/src/abPlayback.ts index ffcac1484a..8a7c782d4b 100644 --- a/packages/blueprints-integration/src/abPlayback.ts +++ b/packages/blueprints-integration/src/abPlayback.ts @@ -20,7 +20,7 @@ export const AB_MEDIA_PLAYER_AUTO = '__auto__' * Description of a player in an AB pool */ export interface ABPlayerDefinition { - playerId: number + playerId: number | string } /** @@ -30,7 +30,7 @@ export interface ABTimelineLayerChangeRule { /** What AB pools can this rule be used for */ acceptedPoolNames: string[] /** A function to generate the new layer name for a chosen playerId */ - newLayerName: (playerId: number) => string + newLayerName: (playerId: number | string) => string /** Whether this rule can be used for lookaheadObjects */ allowsLookahead: boolean } @@ -60,7 +60,7 @@ export interface ABResolverConfiguration { customApplyToObject?: ( context: ICommonContext, poolName: string, - playerId: number, + playerId: number | string, timelineObject: OnGenerateTimelineObj ) => boolean } diff --git a/packages/corelib/src/dataModel/RundownPlaylist.ts b/packages/corelib/src/dataModel/RundownPlaylist.ts index e01aa38a06..dc662cc7dc 100644 --- a/packages/corelib/src/dataModel/RundownPlaylist.ts +++ b/packages/corelib/src/dataModel/RundownPlaylist.ts @@ -27,7 +27,7 @@ export interface ABSessionInfo { export interface ABSessionAssignment { sessionId: string - playerId: number + playerId: number | string lookahead: boolean // purely informational for debugging } diff --git a/packages/job-worker/src/playout/abPlayback/__tests__/abPlayback.spec.ts b/packages/job-worker/src/playout/abPlayback/__tests__/abPlayback.spec.ts index 33bff6c0e0..9e8542f10a 100644 --- a/packages/job-worker/src/playout/abPlayback/__tests__/abPlayback.spec.ts +++ b/packages/job-worker/src/playout/abPlayback/__tests__/abPlayback.spec.ts @@ -72,7 +72,7 @@ function resolveAbSessions( timelineObjs: OnGenerateTimelineObjExt[], previousAssignmentMap: ABSessionAssignments, sessionPool: string, - playerIds: number[], + playerIds: Array, now: number ): AssignmentResult { const sessionRequests = calculateSessionTimeRanges( @@ -148,6 +148,78 @@ describe('resolveMediaPlayers', () => { expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, 'clip_ghi') }) + test('basic pieces - players with string Ids', () => { + const previousAssignments = {} + const pieces = [ + createBasicResolvedPieceInstance('0', 400, 5000, 'abc'), + createBasicResolvedPieceInstance('1', 400, 5000, 'def'), + createBasicResolvedPieceInstance('2', 800, 4000, 'ghi'), + ] + + mockGetPieceSessionId.mockImplementation((piece, name) => `${piece._id}_${name}`) + + const assignments = resolveAbSessions( + abSessionHelper, + resolverOptions, + pieces, + [], + previousAssignments, + POOL_NAME, + ['player1', 'player2'], + 4500 + ) + expect(assignments.failedRequired).toEqual(['inst_2_clip_ghi']) + expect(assignments.failedOptional).toHaveLength(0) + expect(assignments.requests).toHaveLength(3) + expect(assignments.requests).toEqual([ + { end: 5400, id: 'inst_0_clip_abc', playerId: 'player1', start: 400, optional: false }, + { end: 5400, id: 'inst_1_clip_def', playerId: 'player2', start: 400, optional: false }, + { end: 4800, id: 'inst_2_clip_ghi', playerId: undefined, start: 800, optional: false }, // Massive overlap + ]) + + expect(mockGetPieceSessionId).toHaveBeenCalledTimes(3) + expect(mockGetObjectSessionId).toHaveBeenCalledTimes(0) + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, 'clip_abc') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, 'clip_def') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, 'clip_ghi') + }) + + test('basic pieces - players with number and string Ids', () => { + const previousAssignments = {} + const pieces = [ + createBasicResolvedPieceInstance('0', 400, 5000, 'abc'), + createBasicResolvedPieceInstance('1', 400, 5000, 'def'), + createBasicResolvedPieceInstance('2', 800, 4000, 'ghi'), + ] + + mockGetPieceSessionId.mockImplementation((piece, name) => `${piece._id}_${name}`) + + const assignments = resolveAbSessions( + abSessionHelper, + resolverOptions, + pieces, + [], + previousAssignments, + POOL_NAME, + [1, 'player2'], + 4500 + ) + expect(assignments.failedRequired).toEqual(['inst_2_clip_ghi']) + expect(assignments.failedOptional).toHaveLength(0) + expect(assignments.requests).toHaveLength(3) + expect(assignments.requests).toEqual([ + { end: 5400, id: 'inst_0_clip_abc', playerId: 1, start: 400, optional: false }, + { end: 5400, id: 'inst_1_clip_def', playerId: 'player2', start: 400, optional: false }, + { end: 4800, id: 'inst_2_clip_ghi', playerId: undefined, start: 800, optional: false }, // Massive overlap + ]) + + expect(mockGetPieceSessionId).toHaveBeenCalledTimes(3) + expect(mockGetObjectSessionId).toHaveBeenCalledTimes(0) + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, 'clip_abc') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, 'clip_def') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, 'clip_ghi') + }) + test('Multiple pieces same id', () => { const previousAssignments = {} const pieces = [ diff --git a/packages/job-worker/src/playout/abPlayback/abPlaybackResolver.ts b/packages/job-worker/src/playout/abPlayback/abPlaybackResolver.ts index 468dc7cf35..7d8cc16c26 100644 --- a/packages/job-worker/src/playout/abPlayback/abPlaybackResolver.ts +++ b/packages/job-worker/src/playout/abPlayback/abPlaybackResolver.ts @@ -8,7 +8,7 @@ export interface SessionRequest { readonly end: number | undefined readonly optional?: boolean readonly lookaheadRank?: number - playerId?: number + playerId?: number | string } export interface AssignmentResult { @@ -21,7 +21,7 @@ export interface AssignmentResult { } interface SlotAvailability { - id: number + id: number | string before: (SessionRequest & { end: number }) | null after: SessionRequest | null clashes: SessionRequest[] @@ -55,7 +55,7 @@ function safeMin(arr: T[], func: (val: T) => number): T | undefined { */ export function resolveAbAssignmentsFromRequests( resolverOptions: ABResolverOptions, - playerIds: number[], + playerIds: Array, rawRequests: SessionRequest[], now: number // Current time ): AssignmentResult { @@ -80,7 +80,7 @@ export function resolveAbAssignmentsFromRequests( return res } - const originalLookaheadAssignments: Record = {} + const originalLookaheadAssignments: Record = {} for (const req of rawRequests) { if (req.lookaheadRank !== undefined && req.playerId !== undefined) { originalLookaheadAssignments[req.id] = req.playerId @@ -104,7 +104,7 @@ export function resolveAbAssignmentsFromRequests( pendingRequests = grouped[undefined as any] // build map of slots and what they already have assigned - const slots: Map = new Map() + const slots: Map = new Map() _.each(playerIds, (id) => slots.set(id, grouped[id] || [])) const beforeHasGap = (p: SlotAvailability, req: SessionRequest): boolean => @@ -313,7 +313,7 @@ export function resolveAbAssignmentsFromRequests( // Ensure lookahead gets assigned based on priority not some randomness // Includes slots which have either no sessions, or the last has a known end time - const lastSessionPerSlot: Record = {} // playerId, end + const lastSessionPerSlot: Record = {} // playerId, end for (const [playerId, sessions] of slots) { const last = _.last(sessions.filter((s) => s.lookaheadRank === undefined)) if (!last) { @@ -359,7 +359,12 @@ export function resolveAbAssignmentsFromRequests( const req = remainingLookaheads[i] if (slot) { - req.playerId = Number(slot[0]) + // Check if we were originally given a player index rather than a string Id + if (playerIds.find((id) => typeof id === 'number' && id === Number(slot[0]))) { + req.playerId = Number(slot[0]) + } else { + req.playerId = slot[0] + } } else { delete req.playerId } @@ -368,7 +373,11 @@ export function resolveAbAssignmentsFromRequests( return res } -function getAvailability(id: number, thisReq: SessionRequest, orderedRequests: SessionRequest[]): SlotAvailability { +function getAvailability( + id: number | string, + thisReq: SessionRequest, + orderedRequests: SessionRequest[] +): SlotAvailability { const res: SlotAvailability = { id, before: null, diff --git a/packages/job-worker/src/playout/abPlayback/applyAssignments.ts b/packages/job-worker/src/playout/abPlayback/applyAssignments.ts index c2b37eeddd..03e240df9a 100644 --- a/packages/job-worker/src/playout/abPlayback/applyAssignments.ts +++ b/packages/job-worker/src/playout/abPlayback/applyAssignments.ts @@ -33,7 +33,7 @@ export function applyAbPlayerObjectAssignments( poolName: string ): ABSessionAssignments { const newAssignments: ABSessionAssignments = {} - const persistAssignment = (sessionId: string, playerId: number, lookahead: boolean): void => { + const persistAssignment = (sessionId: string, playerId: number | string, lookahead: boolean): void => { // Track the assignment, so that the next onTimelineGenerate can try to reuse the same session if (newAssignments[sessionId]) { // TODO - warn? @@ -117,24 +117,18 @@ function updateObjectsToAbPlayer( context: ICommonContext, abConfiguration: Pick, poolName: string, - poolIndex: number, + playerId: number | string, objs: OnGenerateTimelineObj[] ): OnGenerateTimelineObj[] { const failedObjects: OnGenerateTimelineObj[] = [] for (const obj of objs) { - const updatedKeyframes = applyUpdateToKeyframes(poolName, poolIndex, obj) + const updatedKeyframes = applyUpdateToKeyframes(poolName, playerId, obj) - const updatedLayer = applylayerMoveRule( - abConfiguration.timelineObjectLayerChangeRules, - poolName, - poolIndex, - obj - ) + const updatedLayer = applylayerMoveRule(abConfiguration.timelineObjectLayerChangeRules, poolName, playerId, obj) const updatedCustom = - abConfiguration.customApplyToObject && - abConfiguration.customApplyToObject(context, poolName, poolIndex, obj) + abConfiguration.customApplyToObject && abConfiguration.customApplyToObject(context, poolName, playerId, obj) if (!updatedKeyframes && !updatedLayer && !updatedCustom) { failedObjects.push(obj) @@ -146,7 +140,7 @@ function updateObjectsToAbPlayer( function applyUpdateToKeyframes( poolName: string, - poolIndex: number, + playerId: number | string, obj: OnGenerateTimelineObj ): boolean { if (!obj.keyframes) return false @@ -160,7 +154,7 @@ function applyUpdateToKeyframes( // Preserve from other ab pools if (kf.abSession.poolName !== poolName) return kf - if (kf.abSession.playerIndex === poolIndex) { + if (kf.abSession.playerId === playerId) { // Make sure any ab keyframe is active kf.disabled = false updated = true @@ -178,7 +172,7 @@ function applyUpdateToKeyframes( function applylayerMoveRule( timelineObjectLayerChangeRules: ABTimelineLayerChangeRules | undefined, poolName: string, - poolIndex: number, + playerId: number | string, obj: OnGenerateTimelineObj ): boolean { const ruleId = obj.isLookahead ? obj.lookaheadForLayer || obj.layer : obj.layer @@ -190,13 +184,13 @@ function applylayerMoveRule( if (obj.isLookahead && moveRule.allowsLookahead && obj.lookaheadForLayer) { // This works on the assumption that layer will contain lookaheadForLayer, but not the exact syntax. // Hopefully this will be durable to any potential future core changes - const newLayer = moveRule.newLayerName(poolIndex) + const newLayer = moveRule.newLayerName(playerId) obj.layer = (obj.layer + '').replace(obj.lookaheadForLayer + '', newLayer) obj.lookaheadForLayer = newLayer return true } else if (!obj.isLookahead || (obj.isLookahead && !obj.lookaheadForLayer)) { - obj.layer = moveRule.newLayerName(poolIndex) + obj.layer = moveRule.newLayerName(playerId) return true } diff --git a/packages/shared-lib/src/core/model/Timeline.ts b/packages/shared-lib/src/core/model/Timeline.ts index 82382715b8..2eeb041172 100644 --- a/packages/shared-lib/src/core/model/Timeline.ts +++ b/packages/shared-lib/src/core/model/Timeline.ts @@ -58,7 +58,7 @@ export interface TimelineKeyframeCoreExt Date: Thu, 19 Oct 2023 09:56:09 +0100 Subject: [PATCH 123/479] chore: remove unimplemented `executeOnUserDataChanged` adlib-action property --- packages/blueprints-integration/src/action.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/blueprints-integration/src/action.ts b/packages/blueprints-integration/src/action.ts index 7bd0121117..6eda749774 100644 --- a/packages/blueprints-integration/src/action.ts +++ b/packages/blueprints-integration/src/action.ts @@ -9,14 +9,14 @@ export interface ActionUserData { [key: string]: any } -export enum ActionExecuteAfterChanged { - /** Do not execute the action after userData has changed, unless specifically triggered by the user */ - none = 'none', - /** Execute the action immediately after userData has changed */ - immediately = 'immediately', - /** Execute the action after userData has changed and there was an identifiable period of calm in the changes */ - debounce = 'debounce', -} +// export enum ActionExecuteAfterChanged { +// /** Do not execute the action after userData has changed, unless specifically triggered by the user */ +// none = 'none', +// /** Execute the action immediately after userData has changed */ +// immediately = 'immediately', +// /** Execute the action after userData has changed and there was an identifiable period of calm in the changes */ +// debounce = 'debounce', +// } export interface IBlueprintActionManifestDisplay { /** A label to be displayed to the user */ @@ -94,9 +94,9 @@ export interface IBlueprintActionManifest { userDataManifest: { /** List of editable fields in userData, to allow for customising */ editableFields?: JSONBlob - /** Execute the action after userData is changed. If not present ActionExecuteAfterChanged.none is assumed. */ - executeOnUserDataChanged?: ActionExecuteAfterChanged // Potential future properties: + // /** Execute the action after userData is changed. If not present ActionExecuteAfterChanged.none is assumed. */ + // executeOnUserDataChanged?: ActionExecuteAfterChanged // asloDisplayACtionButton: boolean } From 9ce324216c1fba8de71d0ef014e184aa661ee5a4 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 17 Oct 2023 16:03:05 +0100 Subject: [PATCH 124/479] chore: add method docs and simplify some implementations SOFIE-2513 --- packages/job-worker/src/__mocks__/context.ts | 4 +- .../__tests__/context-adlibActions.test.ts | 7 +- packages/job-worker/src/cache/CacheBase.ts | 16 +- .../cache/__tests__/DatabaseCaches.test.ts | 21 +- .../job-worker/src/ingest/expectedPackages.ts | 30 +-- .../src/ingest/expectedPlayoutItems.ts | 37 ++-- .../src/ingest/syncChangesToPartInstance.ts | 13 +- packages/job-worker/src/jobs/index.ts | 4 +- packages/job-worker/src/modelBase.ts | 29 +++ .../src/playout/activePlaylistActions.ts | 13 +- .../job-worker/src/playout/adlibAction.ts | 17 +- packages/job-worker/src/playout/lib.ts | 15 -- .../src/playout/model/PlayoutModel.ts | 201 +++++++++++++++++- .../playout/model/PlayoutPartInstanceModel.ts | 122 +++++++++-- .../model/PlayoutPieceInstanceModel.ts | 39 ++++ .../src/playout/model/PlayoutRundownModel.ts | 49 ++++- .../src/playout/model/PlayoutSegmentModel.ts | 16 +- .../model/implementation/LoadPlayoutModel.ts | 37 +++- .../model/implementation/PlayoutModelImpl.ts | 43 +++- .../PlayoutPartInstanceModelImpl.ts | 40 ++-- .../PlayoutPieceInstanceModelImpl.ts | 25 +++ .../implementation/PlayoutRundownModelImpl.ts | 16 +- .../implementation/PlayoutSegmentModelImpl.ts | 6 +- .../model/implementation/SavePlayoutModel.ts | 10 + packages/job-worker/src/playout/scratchpad.ts | 7 +- packages/job-worker/src/playout/take.ts | 24 +-- .../src/playout/timeline/generate.ts | 2 +- .../src/playout/timeline/multi-gateway.ts | 2 +- .../job-worker/src/playout/timelineJobs.ts | 2 +- .../playout/timings/timelineTriggerTime.ts | 2 +- .../src/studio/StudioPlayoutModel.ts | 35 --- packages/job-worker/src/studio/lock.ts | 4 +- .../src/studio/model/StudioBaselineHelper.ts | 57 +++++ .../src/studio/model/StudioPlayoutModel.ts | 68 ++++++ .../{ => model}/StudioPlayoutModelImpl.ts | 55 ++--- packages/job-worker/src/workers/context.ts | 6 +- 36 files changed, 799 insertions(+), 275 deletions(-) create mode 100644 packages/job-worker/src/modelBase.ts delete mode 100644 packages/job-worker/src/studio/StudioPlayoutModel.ts create mode 100644 packages/job-worker/src/studio/model/StudioBaselineHelper.ts create mode 100644 packages/job-worker/src/studio/model/StudioPlayoutModel.ts rename packages/job-worker/src/studio/{ => model}/StudioPlayoutModelImpl.ts (74%) diff --git a/packages/job-worker/src/__mocks__/context.ts b/packages/job-worker/src/__mocks__/context.ts index 6a91595925..5ca56aeb2e 100644 --- a/packages/job-worker/src/__mocks__/context.ts +++ b/packages/job-worker/src/__mocks__/context.ts @@ -16,7 +16,7 @@ import { preprocessStudioConfig, preprocessShowStyleConfig, } from '../blueprints/config' -import { ICacheBase2 } from '../cache/CacheBase' +import { BaseModel } from '../modelBase' import { PlaylistLock, RundownLock } from '../jobs/lock' import { ReadonlyDeep } from 'type-fest' import { @@ -114,7 +114,7 @@ export class MockJobContext implements JobContext { } } - trackCache(_cache: ICacheBase2): void { + trackCache(_cache: BaseModel): void { // TODO // throw new Error('Method not implemented.') } diff --git a/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts b/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts index b4533808e9..499d34ee92 100644 --- a/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts +++ b/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts @@ -37,6 +37,7 @@ import { TimelineObjRundown, TimelineObjType } from '@sofie-automation/corelib/d import { PlayoutPartInstanceModelImpl } from '../../playout/model/implementation/PlayoutPartInstanceModelImpl' import { writePartInstancesAndPieceInstances } from '../../playout/model/implementation/SavePlayoutModel' import { PlayoutPieceInstanceModel } from '../../playout/model/PlayoutPieceInstanceModel' +import { DatabasePersistedModel } from '../../modelBase' import * as PlayoutAdlib from '../../playout/adlibUtils' type TinnerStopPieces = jest.MockedFunction @@ -186,9 +187,9 @@ describe('Test blueprint api context', () => { async function wrapWithPlayoutModel( context: JobContext, playlistId: RundownPlaylistId, - fcn: (playoutModel: PlayoutModel) => Promise + fcn: (playoutModel: PlayoutModel & DatabasePersistedModel) => Promise ): Promise { - return runJobWithPlayoutModel(context, { playlistId }, null, fcn) + return runJobWithPlayoutModel(context, { playlistId }, null, fcn as any) } async function setupMyDefaultRundown(): Promise<{ @@ -228,7 +229,7 @@ describe('Test blueprint api context', () => { async function saveAllToDatabase( context: JobContext, - playoutModel: PlayoutModel, + playoutModel: PlayoutModel & DatabasePersistedModel, allPartInstances: PlayoutPartInstanceModel[] ) { // We need to push changes back to 'mongo' for these tests diff --git a/packages/job-worker/src/cache/CacheBase.ts b/packages/job-worker/src/cache/CacheBase.ts index e6cf57e8c8..575eacfb67 100644 --- a/packages/job-worker/src/cache/CacheBase.ts +++ b/packages/job-worker/src/cache/CacheBase.ts @@ -7,6 +7,7 @@ import { IS_PRODUCTION } from '../environment' import { logger } from '../logging' import { sleep } from '@sofie-automation/corelib/dist/lib' import { JobContext } from '../jobs' +import { BaseModel } from '../modelBase' type DeferredFunction = (cache: Cache) => void | Promise type DeferredAfterSaveFunction> = (cache: ReadOnlyCache) => void | Promise @@ -29,21 +30,8 @@ export type ReadOnlyCache> = Omit< 'defer' | 'deferAfterSave' | 'saveAllToDatabase' > -export interface ICacheBase2 { - readonly DisplayName: string - - dispose(): void - - /** - * Assert that no changes should have been made to the cache, will throw an Error otherwise. This can be used in - * place of `saveAllToDatabase()`, when the code controlling the cache expects no changes to have been made and any - * changes made are an error and will cause issues. - */ - assertNoChanges(): void -} - /** This cache contains data relevant in a studio */ -export abstract class ReadOnlyCacheBase> implements ICacheBase2 { +export abstract class ReadOnlyCacheBase> implements BaseModel { protected _deferredBeforeSaveFunctions: DeferredFunction[] = [] protected _deferredAfterSaveFunctions: DeferredAfterSaveFunction[] = [] diff --git a/packages/job-worker/src/cache/__tests__/DatabaseCaches.test.ts b/packages/job-worker/src/cache/__tests__/DatabaseCaches.test.ts index bcaa17afd6..e0f0eba6bc 100644 --- a/packages/job-worker/src/cache/__tests__/DatabaseCaches.test.ts +++ b/packages/job-worker/src/cache/__tests__/DatabaseCaches.test.ts @@ -4,7 +4,6 @@ import { sleep } from '@sofie-automation/corelib/dist/lib' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { getRundownId } from '../../ingest/lib' import { CacheForIngest } from '../../ingest/cache' -import { loadStudioPlayoutModel } from '../../studio/StudioPlayoutModelImpl' import { MockJobContext, setupDefaultJobEnvironment } from '../../__mocks__/context' describe('DatabaseCaches', () => { @@ -305,18 +304,18 @@ describe('DatabaseCaches', () => { // }).toThrow(/failed .+ assertion,.+ deferred/gi) // } - { - const cache = await loadStudioPlayoutModel(context) + // { + // const cache = await loadStudioPlayoutModel(context) - // Insert a document: - cache.deferAfterSave(() => { - // - }) + // // Insert a document: + // cache.deferAfterSave(() => { + // // + // }) - expect(() => { - cache.assertNoChanges() - }).toThrow(/failed .+ assertion,.+ after-save deferred/gi) - } + // expect(() => { + // cache.assertNoChanges() + // }).toThrow(/failed .+ assertion,.+ after-save deferred/gi) + // } } finally { await lock.release() } diff --git a/packages/job-worker/src/ingest/expectedPackages.ts b/packages/job-worker/src/ingest/expectedPackages.ts index d52df74be9..c7ae67594e 100644 --- a/packages/job-worker/src/ingest/expectedPackages.ts +++ b/packages/job-worker/src/ingest/expectedPackages.ts @@ -34,7 +34,7 @@ import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataMod import { saveIntoCache } from '../cache/lib' import { saveIntoDb } from '../db/changes' import { PlayoutModel } from '../playout/model/PlayoutModel' -import { StudioPlayoutModel } from '../studio/StudioPlayoutModel' +import { StudioPlayoutModel } from '../studio/model/StudioPlayoutModel' import { ReadonlyDeep } from 'type-fest' import { ExpectedPackage, BlueprintResultBaseline } from '@sofie-automation/blueprints-integration' import { updateExpectedMediaItemsOnRundown } from './expectedMediaItems' @@ -369,7 +369,7 @@ export function updateBaselineExpectedPackagesOnRundown( baseline: BlueprintResultBaseline ): void { // @todo: this call is for backwards compatibility and soon to be removed - updateBaselineExpectedPlayoutItemsOnRundown(context, cache, baseline.expectedPlayoutItems) + updateBaselineExpectedPlayoutItemsOnRundown(context, cache, baseline.expectedPlayoutItems ?? []) // Fill in ids of unnamed expectedPackages setDefaultIdOnExpectedPackages(baseline.expectedPackages) @@ -402,29 +402,21 @@ export function updateBaselineExpectedPackagesOnStudio( baseline: BlueprintResultBaseline ): void { // @todo: this call is for backwards compatibility and soon to be removed - updateBaselineExpectedPlayoutItemsOnStudio(context, cache, baseline.expectedPlayoutItems) + updateBaselineExpectedPlayoutItemsOnStudio(context, cache, baseline.expectedPlayoutItems ?? []) // Fill in ids of unnamed expectedPackages setDefaultIdOnExpectedPackages(baseline.expectedPackages) const bases = generateExpectedPackageBases(context.studio, context.studio._id, baseline.expectedPackages ?? []) - cache.deferAfterSave(async () => { - await saveIntoDb( - context, - context.directCollections.ExpectedPackages, - { - studioId: context.studio._id, + cache.setExpectedPackagesForStudioBaseline( + bases.map((item): ExpectedPackageDBFromStudioBaselineObjects => { + return { + ...item, fromPieceType: ExpectedPackageDBType.STUDIO_BASELINE_OBJECTS, - }, - bases.map((item): ExpectedPackageDBFromStudioBaselineObjects => { - return { - ...item, - fromPieceType: ExpectedPackageDBType.STUDIO_BASELINE_OBJECTS, - pieceId: null, - } - }) - ) - }) + pieceId: null, + } + }) + ) } export function setDefaultIdOnExpectedPackages(expectedPackages: ExpectedPackage.Any[] | undefined): void { diff --git a/packages/job-worker/src/ingest/expectedPlayoutItems.ts b/packages/job-worker/src/ingest/expectedPlayoutItems.ts index b059aab6fd..0feb7ed809 100644 --- a/packages/job-worker/src/ingest/expectedPlayoutItems.ts +++ b/packages/job-worker/src/ingest/expectedPlayoutItems.ts @@ -11,10 +11,8 @@ import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataM import { getRandomId } from '@sofie-automation/corelib/dist/lib' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { saveIntoCache } from '../cache/lib' -import { saveIntoDb } from '../db/changes' import { PlayoutModel } from '../playout/model/PlayoutModel' -import { StudioPlayoutModel } from '../studio/StudioPlayoutModel' -import _ = require('underscore') +import { StudioPlayoutModel } from '../studio/model/StudioPlayoutModel' import { ExpectedPlayoutItemGeneric } from '@sofie-automation/blueprints-integration' import { JobContext } from '../jobs' import { CacheForIngest } from './cache' @@ -28,7 +26,7 @@ function extractExpectedPlayoutItems( const expectedPlayoutItemsGeneric: ExpectedPlayoutItem[] = [] if (piece.expectedPlayoutItems) { - _.each(piece.expectedPlayoutItems, (pieceItem, i) => { + piece.expectedPlayoutItems.forEach((pieceItem, i) => { expectedPlayoutItemsGeneric.push({ ...pieceItem, _id: protectString(piece._id + '_' + i), @@ -77,13 +75,13 @@ export async function updateExpectedPlayoutItemsOnRundown(context: JobContext, c export function updateBaselineExpectedPlayoutItemsOnRundown( context: JobContext, cache: CacheForIngest, - items?: ExpectedPlayoutItemGeneric[] + items: ExpectedPlayoutItemGeneric[] ): void { saveIntoCache( context, cache.ExpectedPlayoutItems, (p) => p.baseline === 'rundown', - (items || []).map((item): ExpectedPlayoutItemRundown => { + items.map((item): ExpectedPlayoutItemRundown => { return { ...item, _id: getRandomId(), @@ -97,21 +95,16 @@ export function updateBaselineExpectedPlayoutItemsOnRundown( export function updateBaselineExpectedPlayoutItemsOnStudio( context: JobContext, cache: StudioPlayoutModel | PlayoutModel, - items?: ExpectedPlayoutItemGeneric[] + items: ExpectedPlayoutItemGeneric[] ): void { - cache.deferAfterSave(async () => { - await saveIntoDb( - context, - context.directCollections.ExpectedPlayoutItems, - { studioId: context.studio._id, baseline: 'studio' }, - (items || []).map((item): ExpectedPlayoutItemStudio => { - return { - ...item, - _id: getRandomId(), - studioId: context.studio._id, - baseline: 'studio', - } - }) - ) - }) + cache.setExpectedPlayoutItemsForStudioBaseline( + items.map((item): ExpectedPlayoutItemStudio => { + return { + ...item, + _id: getRandomId(), + studioId: context.studio._id, + baseline: 'studio', + } + }) + ) } diff --git a/packages/job-worker/src/ingest/syncChangesToPartInstance.ts b/packages/job-worker/src/ingest/syncChangesToPartInstance.ts index 253c4443a8..f8d5f37aa8 100644 --- a/packages/job-worker/src/ingest/syncChangesToPartInstance.ts +++ b/packages/job-worker/src/ingest/syncChangesToPartInstance.ts @@ -16,7 +16,7 @@ import { getPieceInstancesForPart, syncPlayheadInfinitesForNextPartInstance, } from '../playout/infinites' -import { isTooCloseToAutonext, updateExpectedDurationWithPrerollForPartInstance } from '../playout/lib' +import { isTooCloseToAutonext } from '../playout/lib' import _ = require('underscore') import { SyncIngestUpdateToPartInstanceContext } from '../blueprints/context' import { @@ -194,10 +194,7 @@ export async function syncChangesToPartInstances( } if (playStatus === 'next') { - updateExpectedDurationWithPrerollForPartInstance( - playoutModel, - existingPartInstance.PartInstance._id - ) + existingPartInstance.recalculateExpectedDurationWithPreroll() } // Save notes: @@ -218,11 +215,7 @@ export async function syncChangesToPartInstances( // TODO - old notes from the sync may need to be pruned, or we will end up with duplicates and 'stuck' notes?+ existingPartInstance.appendNotes(newNotes) - validateScratchpartPartInstanceProperties( - context, - playoutModel, - existingPartInstance.PartInstance._id - ) + validateScratchpartPartInstanceProperties(context, playoutModel, existingPartInstance) } if (existingPartInstance.PartInstance._id === playoutModel.Playlist.currentPartInfo?.partInstanceId) { diff --git a/packages/job-worker/src/jobs/index.ts b/packages/job-worker/src/jobs/index.ts index 5cf834641e..6b4a8fad49 100644 --- a/packages/job-worker/src/jobs/index.ts +++ b/packages/job-worker/src/jobs/index.ts @@ -15,7 +15,7 @@ import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { ProcessedShowStyleConfig, ProcessedStudioConfig } from '../blueprints/config' import { StudioJobFunc } from '@sofie-automation/corelib/dist/worker/studio' import { PlaylistLock, RundownLock } from './lock' -import { ICacheBase2 } from '../cache/CacheBase' +import { BaseModel } from '../modelBase' import { TimelineComplete } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { ProcessedShowStyleBase, ProcessedShowStyleVariant, ProcessedShowStyleCompound } from './showStyle' @@ -27,7 +27,7 @@ export { ProcessedShowStyleVariant, ProcessedShowStyleBase, ProcessedShowStyleCo */ export interface JobContext extends StudioCacheContext { /** Internal: Track a cache, to check it was saved at the end of the job */ - trackCache(cache: ICacheBase2): void + trackCache(cache: BaseModel): void /** Aquire the CacheForPlayout/write lock for a Playlist */ lockPlaylist(playlistId: RundownPlaylistId): Promise diff --git a/packages/job-worker/src/modelBase.ts b/packages/job-worker/src/modelBase.ts new file mode 100644 index 0000000000..537666fb46 --- /dev/null +++ b/packages/job-worker/src/modelBase.ts @@ -0,0 +1,29 @@ +/** + * A base type for loaded Models + */ +export interface BaseModel { + /** + * Name to display in debug logs about this Model + */ + readonly DisplayName: string + + /** + * Mark the model as disposed + * After this call, the model should discard any pending changes, and reject any requests to persist the changes + */ + dispose(): void + + /** + * Assert that no changes should have been made to the cache, will throw an Error otherwise. This can be used in + * place of `saveAllToDatabase()`, when the code controlling the cache expects no changes to have been made and any + * changes made are an error and will cause issues. + */ + assertNoChanges(): void +} + +export interface DatabasePersistedModel { + /** + * Issue a save of the contents of this model to the database + */ + saveAllToDatabase(): Promise +} diff --git a/packages/job-worker/src/playout/activePlaylistActions.ts b/packages/job-worker/src/playout/activePlaylistActions.ts index 7aa469b7a4..67a56abe41 100644 --- a/packages/job-worker/src/playout/activePlaylistActions.ts +++ b/packages/job-worker/src/playout/activePlaylistActions.ts @@ -9,7 +9,6 @@ import { selectNextPart } from './selectNextPart' import { setNextPart } from './setNext' import { updateStudioTimeline, updateTimeline } from './timeline/generate' import { getCurrentTime } from '../lib' -import { EventsJobs } from '@sofie-automation/corelib/dist/worker/events' import { cleanTimelineDatastore } from './datastore' import { RundownActivationContext } from '../blueprints/context/RundownActivationContext' import { ReadonlyDeep } from 'type-fest' @@ -133,17 +132,7 @@ export async function deactivateRundownPlaylistInner( if (currentPartInstance) { rundown = playoutModel.getRundown(currentPartInstance.PartInstance.rundownId)?.Rundown - playoutModel.deferAfterSave(async () => { - context - .queueEventJob(EventsJobs.NotifyCurrentlyPlayingPart, { - rundownId: currentPartInstance.PartInstance.rundownId, - isRehearsal: !!playoutModel.Playlist.rehearsal, - partExternalId: null, - }) - .catch((e) => { - logger.warn(`Failed to queue NotifyCurrentlyPlayingPart job: ${e}`) - }) - }) + playoutModel.queueNotifyCurrentlyPlayingPartEvent(currentPartInstance.PartInstance.rundownId, null) } else if (nextPartInstance) { rundown = playoutModel.getRundown(nextPartInstance.PartInstance.rundownId)?.Rundown } diff --git a/packages/job-worker/src/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index ca0cf7cb11..8e47729ba2 100644 --- a/packages/job-worker/src/playout/adlibAction.ts +++ b/packages/job-worker/src/playout/adlibAction.ts @@ -12,7 +12,6 @@ import { getCurrentTime } from '../lib' import { ReadonlyDeep } from 'type-fest' import { PlayoutModel } from './model/PlayoutModel' import { syncPlayheadInfinitesForNextPartInstance } from './infinites' -import { updateExpectedDurationWithPrerollForPartInstance } from './lib' import { runJobWithPlaylistLock } from './lock' import { updateTimeline } from './timeline/generate' import { performTakeToNextedPart } from './take' @@ -79,7 +78,7 @@ export async function handleExecuteAdlibAction( if (blueprint.blueprint.executeAction) { // load a full cache for the regular actions & executet the handler - const playoutModel: PlayoutModel = await createPlayoutModelfromInitModel(context, initCache) + const playoutModel = await createPlayoutModelfromInitModel(context, initCache) const fullRundown = playoutModel.getRundown(rundown._id) if (!fullRundown) throw new Error(`Rundown "${rundown._id}" missing between caches`) @@ -198,18 +197,18 @@ async function applyAnyExecutionSideEffects( } if (actionContext.nextPartState !== ActionPartChange.NONE) { - const nextPartInstanceId = playoutModel.Playlist.nextPartInfo?.partInstanceId - if (nextPartInstanceId) { - updateExpectedDurationWithPrerollForPartInstance(playoutModel, nextPartInstanceId) + const nextPartInstance = playoutModel.NextPartInstance + if (nextPartInstance) { + nextPartInstance.recalculateExpectedDurationWithPreroll() - validateScratchpartPartInstanceProperties(context, playoutModel, nextPartInstanceId) + validateScratchpartPartInstanceProperties(context, playoutModel, nextPartInstance) } } if (actionContext.currentPartState !== ActionPartChange.NONE) { - const currentPartInstanceId = playoutModel.Playlist.currentPartInfo?.partInstanceId - if (currentPartInstanceId) { - validateScratchpartPartInstanceProperties(context, playoutModel, currentPartInstanceId) + const currentPartInstance = playoutModel.CurrentPartInstance + if (currentPartInstance) { + validateScratchpartPartInstanceProperties(context, playoutModel, currentPartInstance) } } diff --git a/packages/job-worker/src/playout/lib.ts b/packages/job-worker/src/playout/lib.ts index caf1d6ec2b..d0f20c543e 100644 --- a/packages/job-worker/src/playout/lib.ts +++ b/packages/job-worker/src/playout/lib.ts @@ -219,18 +219,3 @@ export function isTooCloseToAutonext( return false } - -/** - * Update the expectedDurationWithPreroll on the specified PartInstance. - * The value is used by the UI to approximate the duration of a PartInstance as it will be played out - */ -export function updateExpectedDurationWithPrerollForPartInstance( - playoutModel: PlayoutModel, - partInstanceId: PartInstanceId -): void { - const nextPartInstance = playoutModel.getPartInstance(partInstanceId) - if (nextPartInstance) { - // Update expectedDurationWithPreroll of the next part instance, as it may have changed and is used by the ui until it is taken - nextPartInstance.recalculateExpectedDurationWithPreroll() - } -} diff --git a/packages/job-worker/src/playout/model/PlayoutModel.ts b/packages/job-worker/src/playout/model/PlayoutModel.ts index a87c722c82..79d6d088b5 100644 --- a/packages/job-worker/src/playout/model/PlayoutModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutModel.ts @@ -8,7 +8,7 @@ import { RundownPlaylistId, SegmentId, } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { ICacheBase2 } from '../../cache/CacheBase' +import { BaseModel } from '../../modelBase' import { ABSessionAssignments, ABSessionInfo, @@ -16,7 +16,7 @@ import { RundownHoldState, } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { ReadonlyDeep } from 'type-fest' -import { StudioPlayoutModelBase, StudioPlayoutModelBaseReadonly } from '../../studio/StudioPlayoutModel' +import { StudioPlayoutModelBase, StudioPlayoutModelBaseReadonly } from '../../studio/model/StudioPlayoutModel' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { PieceInstance, PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { PlaylistLock } from '../../jobs/lock' @@ -30,64 +30,177 @@ import { PlayoutPieceInstanceModel } from './PlayoutPieceInstanceModel' export type DeferredFunction = (playoutModel: PlayoutModel) => void | Promise export type DeferredAfterSaveFunction = (playoutModel: PlayoutModelReadonly) => void | Promise +/** + * A lightweight version of the `PlayoutModel`, used to perform some pre-checks before loading the full model + * + * This represents a `RundownPlaylist` in a `Studio`, in a minimal readonly fashion + */ export interface PlayoutModelPreInit { + /** + * The Id of the RundownPlaylist this PlayoutModel operates for + */ readonly PlaylistId: RundownPlaylistId + /** + * Reference to the lock for the RundownPlaylist + */ readonly PlaylistLock: PlaylistLock + /** + * All of the PeripheralDevices that belong to the Studio of this RundownPlaylist + */ readonly PeripheralDevices: ReadonlyDeep + /** + * The RundownPlaylist this PlayoutModel operates for + */ readonly Playlist: ReadonlyDeep + /** + * The unwrapped Rundowns in this RundownPlaylist + */ readonly Rundowns: ReadonlyDeep + /** + * Get a Rundown which belongs to this RundownPlaylist + * @param id Id of the Rundown + */ getRundown(id: RundownId): DBRundown | undefined } +/** + * A readonly version of the `PlayoutModel` + * + * This represents a `RundownPlaylist` and its content in a `Studio`, in a readonly fashion + */ export interface PlayoutModelReadonly extends StudioPlayoutModelBaseReadonly { + /** + * The Id of the RundownPlaylist this PlayoutModel operates for + */ readonly PlaylistId: RundownPlaylistId - + /** + * Reference to the lock for the RundownPlaylist + */ readonly PlaylistLock: PlaylistLock + /** + * The RundownPlaylist this PlayoutModel operates for + */ get Playlist(): ReadonlyDeep + /** + * The Rundowns in this RundownPlaylist + */ get Rundowns(): readonly PlayoutRundownModel[] + /** + * All of the loaded PartInstances which are not one of the Previous, Current or Next + * This may or may not contain all PartInstances from the RundownPlaylist, depending on implementation. + * At a minimum it will contain all PartInstances from the Segments of the previous, current and next PartInstances + */ get OlderPartInstances(): PlayoutPartInstanceModel[] + /** + * The PartInstance previously played, if any + */ get PreviousPartInstance(): PlayoutPartInstanceModel | null + /** + * The PartInstance currently being played, if any + */ get CurrentPartInstance(): PlayoutPartInstanceModel | null + /** + * The PartInstance which is next to be played, if any + */ get NextPartInstance(): PlayoutPartInstanceModel | null + /** + * Ids of the previous, current and next PartInstances + */ get SelectedPartInstanceIds(): PartInstanceId[] + /** + * The previous, current and next PartInstances + */ get SelectedPartInstances(): PlayoutPartInstanceModel[] + /** + * All of the loaded PartInstances + * This may or may not contain all PartInstances from the RundownPlaylist, depending on implementation. + * At a minimum it will contain all PartInstances from the Segments of the previous, current and next PartInstances + */ get LoadedPartInstances(): PlayoutPartInstanceModel[] + /** + * All of the loaded PartInstances, sorted by order of playback + */ get SortedLoadedPartInstances(): PlayoutPartInstanceModel[] + /** + * Get a PartInstance which belongs to this RundownPlaylist + * @param id Id of the PartInstance + */ getPartInstance(partInstanceId: PartInstanceId): PlayoutPartInstanceModel | undefined /** - * Search for a Part through the whole Playlist - * @param id + * Search for a Part through the whole RundownPlaylist + * @param id Id of the Part */ findPart(id: PartId): ReadonlyDeep | undefined + /** + * Collect all Parts in the RundownPlaylist, and return them sorted by the Segment and Part ranks + */ getAllOrderedParts(): ReadonlyDeep[] + /** + * Search for a Segment through the whole RundownPlaylist + * @param id Id of the Segment + */ findSegment(id: SegmentId): ReadonlyDeep | undefined + /** + * Collect all Segments in the RundownPlaylist, and return them sorted by their ranks + */ getAllOrderedSegments(): ReadonlyDeep[] + /** + * Get a Rundown which belongs to this RundownPlaylist + * @param id Id of the Rundown + */ getRundown(id: RundownId): PlayoutRundownModel | undefined + /** + * Get the Ids of the Rundowns in this RundownPlaylist + */ getRundownIds(): RundownId[] + /** + * Search for a PieceInstance in the RundownPlaylist + * @param id Id of the PieceInstance + * @returns The found PieceInstance and its parent PartInstance + */ findPieceInstance( id: PieceInstanceId ): { partInstance: PlayoutPartInstanceModel; pieceInstance: PlayoutPieceInstanceModel } | undefined } -export interface PlayoutModel extends PlayoutModelReadonly, StudioPlayoutModelBase, ICacheBase2 { +/** + * A view of a `RundownPlaylist` and its content in a `Studio` + */ +export interface PlayoutModel extends PlayoutModelReadonly, StudioPlayoutModelBase, BaseModel { /** * Temporary hack for debug logging */ get HackDeletedPartInstanceIds(): PartInstanceId[] + /** + * Set the RundownPlaylist as activated (or reactivate) + * @param rehearsal Whether to activate in rehearsal mode + * @returns Id of this activation + */ activatePlaylist(rehearsal: boolean): RundownPlaylistActivationId + /** + * Clear the currently selected PartInstances, so that nothing is selected for playback + */ clearSelectedPartInstances(): void + /** + * Insert an adlibbed PartInstance into the RundownPlaylist + * @param part Part to insert + * @param pieces Planned Pieces to insert into Part + * @param fromAdlibId Id of the source Adlib, if any + * @param infinitePieceInstances Infinite PieceInstances to be continued + * @returns The inserted PlayoutPartInstanceModel + */ createAdlibbedPartInstance( part: Omit, pieces: Omit[], @@ -95,21 +208,59 @@ export interface PlayoutModel extends PlayoutModelReadonly, StudioPlayoutModelBa infinitePieceInstances: PieceInstance[] ): PlayoutPartInstanceModel + /** + * Insert a planned PartInstance into the RundownPlaylist + * Future: This needs refactoring to take Pieces not PieceInstances + * @param nextPart Part to insert + * @param pieceInstances All the PieceInstances to insert + * @returns The inserted PlayoutPartInstanceModel + */ createInstanceForPart(nextPart: ReadonlyDeep, pieceInstances: PieceInstance[]): PlayoutPartInstanceModel + /** + * Insert an adlibbed PartInstance into the Scratchpad Segment of a Rundown in this RundownPlaylist + * @param rundown Rundown to insert for + * @param part Part to insert + * @returns The inserted PlayoutPartInstanceModel + */ createScratchpadPartInstance( rundown: PlayoutRundownModel, part: Omit ): PlayoutPartInstanceModel + /** + * Cycle the selected PartInstances + * The current will become the previous, the next will become the current, and there will be no next PartInstance. + */ cycleSelectedPartInstances(): void + /** + * Set the RundownPlaylist as deactivated + */ deactivatePlaylist(): void + /** + * Queue a `PartInstanceTimingEvent` to be performed upon completion of this Playout operation + * @param partInstanceId Id of the PartInstance the event is in relation to + */ queuePartInstanceTimingEvent(partInstanceId: PartInstanceId): void + /** + * Queue a `NotifyCurrentlyPlayingPart` operation to be performed upon completion of this Playout operation + * @param rundownId The Rundown to report the notification to + * @param partInstance The PartInstance the event is in relation to + */ + queueNotifyCurrentlyPlayingPartEvent(rundownId: RundownId, partInstance: PlayoutPartInstanceModel | null): void + + /** + * Remove all loaded PartInstances marked as `rehearsal` from this RundownPlaylist + */ removeAllRehearsalPartInstances(): void + /** + * Remove any untaken PartInstances from this RundownPlaylist + * This ignores any which are a selected PartInstance + */ removeUntakenPartInstances(): void /** @@ -117,14 +268,31 @@ export interface PlayoutModel extends PlayoutModelReadonly, StudioPlayoutModelBa */ resetPlaylist(regenerateActivationId: boolean): void + /** + * Update the HOLD state of the RundownPlaylist + * @param newState New HOLD state + */ setHoldState(newState: RundownHoldState): void + /** + * Store the persistent results of the AB playback resolving and onTimelineGenerate + * @param persistentState Blueprint owned state from onTimelineGenerate + * @param assignedAbSessions The applied AB sessions + * @param trackedAbSessions The known AB sessions + */ setOnTimelineGenerateResult( persistentState: unknown | undefined, assignedAbSessions: Record, trackedAbSessions: ABSessionInfo[] ): void + /** + * Set a PartInstance as the nexted PartInstance + * @param partInstance PartInstance to be set as next, or none + * @param setManually Whether this was specified by the user + * @param consumesQueuedSegmentId Whether this consumes the `queuedSegment` property of the RundownPlaylist + * @param nextTimeOffset The time offset of the next line + */ setPartInstanceAsNext( partInstance: PlayoutPartInstanceModel | null, setManually: boolean, @@ -132,16 +300,29 @@ export interface PlayoutModel extends PlayoutModelReadonly, StudioPlayoutModelBa nextTimeOffset?: number ): void + /** + * Set a Segment as queued, indicating it should be played after the current Segment + * @param segment Segment to set as queued, or none + */ setQueuedSegment(segment: PlayoutSegmentModel | null): void + /** + * Track a Rundown as having started playback + * @param rundownId If of the Rundown + * @param timestamp Timestamp playback started + */ setRundownStartedPlayback(rundownId: RundownId, timestamp: number): void /** Lifecycle */ - /** @deprecated */ + /** + * @deprecated + * Defer some code to be run before the data is saved + */ deferBeforeSave(fcn: DeferredFunction): void - /** @deprecated */ + /** + * @deprecated + * Defer some code to be run after the data is saved + */ deferAfterSave(fcn: DeferredAfterSaveFunction): void - - saveAllToDatabase(): Promise } diff --git a/packages/job-worker/src/playout/model/PlayoutPartInstanceModel.ts b/packages/job-worker/src/playout/model/PlayoutPartInstanceModel.ts index e39b324cfb..1fb3b38b92 100644 --- a/packages/job-worker/src/playout/model/PlayoutPartInstanceModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutPartInstanceModel.ts @@ -1,9 +1,4 @@ -import { - PieceId, - PieceInstanceId, - PieceInstanceInfiniteId, - RundownPlaylistActivationId, -} from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PieceId, PieceInstanceId, RundownPlaylistActivationId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ReadonlyDeep } from 'type-fest' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { PieceInstance, PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' @@ -21,7 +16,14 @@ export interface PlayoutPartInstanceModelSnapshot { } export interface PlayoutPartInstanceModel { + /** + * The PartInstance properties + */ readonly PartInstance: ReadonlyDeep + + /** + * All the PieceInstances in the PartInstance + */ readonly PieceInstances: PlayoutPieceInstanceModel[] /** @@ -38,23 +40,44 @@ export interface PlayoutPartInstanceModel { */ snapshotRestore(snapshot: PlayoutPartInstanceModelSnapshot): void + /** + * Add some user notes for this PartInstance + * Future: it is only possible to add these, there is no way to 'replace' or remove them + * @param notes New notes to add + */ appendNotes(notes: PartNote[]): void + /** + * Block a take out of this PartInstance from happening until the specified timestamp + * This can be necessary when an uninteruptable Piece is being played out + * @param timestamp Timestampt to block until + */ blockTakeUntil(timestamp: Time | null): void - clearPlannedTimings(): void - + /** + * Get a PieceInstance which belongs to this PartInstance + * @param id Id of the PieceInstance + */ getPieceInstance(id: PieceInstanceId): PlayoutPieceInstanceModel | undefined + /** + * Insert a Piece into this PartInstance as an adlibbed PieceInstance + * @param piece Piece to insert + * @param fromAdlibId Id of the source Adlib, if any + * @returns The inserted PlayoutPieceInstanceModel + */ insertAdlibbedPiece( piece: Omit, fromAdlibId: PieceId | undefined ): PlayoutPieceInstanceModel - insertHoldPieceInstance( - extendPieceInstance: PlayoutPieceInstanceModel, - infiniteInstanceId: PieceInstanceInfiniteId - ): PlayoutPieceInstanceModel + /** + * Extend a PieceInstance into this PartInstance as a Piece extended by HOLD + * The PieceInstance being extended must have been prepared with `prepareForHold` before calling this + * @param extendPieceInstance Piece to extend + * @returns The inserted PlayoutPieceInstanceModel + */ + insertHoldPieceInstance(extendPieceInstance: PlayoutPieceInstanceModel): PlayoutPieceInstanceModel /** * Insert a Piece as if it were originally planned at the time of ingest @@ -64,6 +87,15 @@ export interface PlayoutPartInstanceModel { */ insertPlannedPiece(piece: Omit): PlayoutPieceInstanceModel + /** + * Insert a virtual adlib Piece into this PartInstance + * This will stop another piece following the infinite rules, but has no content and will not be visible in the UI + * @param start Start time of the Piece, relative to the start of the PartInstance + * @param lifespan Infinite lifespan to use + * @param sourceLayerId Id of the SourceLayer the Piece should play on + * @param outputLayerId Id of the OutputLayer the Piece should play on + * @returns The inserted PlayoutPieceInstanceModel + */ insertVirtualPiece( start: number, lifespan: PieceLifespan, @@ -71,8 +103,17 @@ export interface PlayoutPartInstanceModel { outputLayerId: string ): PlayoutPieceInstanceModel + /** + * Mark this PartInstance as 'reset' + * This will unload it from memory at the end of the operation from both the backend and UI. + * Any UI's will ignore this PartInstance and will use the original Part instead + */ markAsReset(): void + /** + * Recalculate the `expectedDurationWithPreroll` property for this PartInstance + * Future: is this needed? should this be handled internally? + */ recalculateExpectedDurationWithPreroll(): void /** @@ -83,6 +124,12 @@ export interface PlayoutPartInstanceModel { */ removePieceInstance(id: PieceInstanceId): boolean + /** + * Replace the infinite PieceInstances inherited from the previous playhead + * These PieceInstances are not supposed to be modified directly, as they are 'extensions'. + * This allows them to be replaced without embedding the infinite logic inside the model + * @param pieceInstances New infinite pieces from previous playhead + */ replaceInfinitesFromPreviousPlayhead(pieceInstances: PieceInstance[]): void /** @@ -94,30 +141,75 @@ export interface PlayoutPartInstanceModel { */ mergeOrInsertPieceInstance(pieceInstance: ReadonlyDeep): PlayoutPieceInstanceModel + /** + * Mark this PartInstance as being orphaned + * @param orphaned New orphaned state + */ setOrphaned(orphaned: 'adlib-part' | 'deleted' | undefined): void + /** + * Update the activation id of this PartInstance + * This can be done to move this PartInstance when resetting the Playlist, if some previous PartInstances want to be kept + * @param id New activation id + */ setPlaylistActivationId(id: RundownPlaylistActivationId): void + /** + * Set the Planned started playback time + * This will clear the Planned stopped playback time + * @param time Planned started time + */ setPlannedStartedPlayback(time: Time | undefined): void - setPlannedStoppedPlayback(time: Time): void + /** + * Set the Planned stopped playback time + * @param time Planned stopped time + */ + setPlannedStoppedPlayback(time: Time | undefined): void + /** + * Set the Reported (from playout-gateway) started playback time + * This will clear the Reported stopped playback time + * @param time Reported started time + */ setReportedStartedPlayback(time: Time): boolean + /** + * Set the Reported (from playout-gateway) stopped playback time + * @param time Reported stopped time + */ setReportedStoppedPlayback(time: Time): boolean + /** + * Set the rank of this PartInstance, to update it's position in the Segment + * @param rank New rank + */ setRank(rank: number): void + /** + * Set the PartInstance as having been taken + * @param takeTime The timestamp to record as when it was taken + * @param playOffset The offset into the PartInstance to start playback from + */ setTaken(takeTime: number, playOffset: number): void + /** + * Define some cached values, to be done when taking the PartInstance + * @param partPlayoutTimings Timings used for Playout, these depend on the previous PartInstance and should not change once playback is started + * @param previousPartEndState A state compiled by the Blueprints + */ storePlayoutTimingsAndPreviousEndState( partPlayoutTimings: PartCalculatedTimings, previousPartEndState: unknown ): void /** - * - * @param props + * Update some properties for the wrapped Part + * Note: This is missing a lot of validation, and will become stricter later + * @param props New properties for the Part being wrapped * @returns True if any valid properties were provided */ updatePartProps(props: Partial): boolean + /** + * Ensure that this PartInstance is setup correctly for being in the Scratchpad Segment + */ validateScratchpadSegmentProperties(): void } diff --git a/packages/job-worker/src/playout/model/PlayoutPieceInstanceModel.ts b/packages/job-worker/src/playout/model/PlayoutPieceInstanceModel.ts index 224050d533..a1323b15b9 100644 --- a/packages/job-worker/src/playout/model/PlayoutPieceInstanceModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutPieceInstanceModel.ts @@ -4,18 +4,57 @@ import { PieceInstance, PieceInstancePiece } from '@sofie-automation/corelib/dis import { Time } from '@sofie-automation/blueprints-integration' export interface PlayoutPieceInstanceModel { + /** + * The PieceInstance properties + */ readonly PieceInstance: ReadonlyDeep + /** + * Prepare this PieceInstance to be continued during HOLD + * This sets the PieceInstance up as an infinite, to allow the Timeline to be generated correctly + */ prepareForHold(): PieceInstanceInfiniteId + /** + * Set the PieceInstance as disabled/enabled + * If disabled, it will be ignored by the Timeline and infinites logic + * @param disabled Whether the PieceInstance should be disabled + */ setDisabled(disabled: boolean): void + /** + * Give the PieceInstance a new end point/duration which has been decided by Playout operations + * @param duration New duration/end point + */ setDuration(duration: Required['userDuration']): void + /** + * Set the Planned started playback time + * This will clear the Planned stopped playback time + * @param time Planned started time + */ setPlannedStartedPlayback(time: Time): boolean + /** + * Set the Planned stopped playback time + * @param time Planned stopped time + */ setPlannedStoppedPlayback(time: Time | undefined): boolean + /** + * Set the Reported (from playout-gateway) started playback time + * This will clear the Reported stopped playback time + * @param time Reported started time + */ setReportedStartedPlayback(time: Time): boolean + /** + * Set the Reported (from playout-gateway) stopped playback time + * @param time Reported stopped time + */ setReportedStoppedPlayback(time: Time): boolean + /** + * Update some properties for the wrapped Piece + * Note: This is missing a lot of validation, and will become stricter later + * @param props New properties for the Piece being wrapped + */ updatePieceProps(props: Partial): void } diff --git a/packages/job-worker/src/playout/model/PlayoutRundownModel.ts b/packages/job-worker/src/playout/model/PlayoutRundownModel.ts index 1e8b0ca24a..6339df9043 100644 --- a/packages/job-worker/src/playout/model/PlayoutRundownModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutRundownModel.ts @@ -5,22 +5,67 @@ import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { RundownBaselineObj } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineObj' import { PlayoutSegmentModel } from './PlayoutSegmentModel' +/** + * Wrap a Rundown and its Segments in a readonly and simplified view for Playout operations + */ export interface PlayoutRundownModel { + /** + * The Rundown properties + */ readonly Rundown: ReadonlyDeep + /** + * All the Segments in the Rundown + * Sorted by their rank + */ readonly Segments: readonly PlayoutSegmentModel[] + /** + * The RundownBaselineObjs for this Rundown + */ readonly BaselineObjects: ReadonlyDeep - getSegmentIds(): SegmentId[] - + /** + * Get a Segment which belongs to this Rundown + * @param id Id of the Segment + */ getSegment(id: SegmentId): PlayoutSegmentModel | undefined + /** + * Get all the SegmentIds in this Rundown + * Sorted by the Segment ranks + */ + getSegmentIds(): SegmentId[] + + /** + * Get all the PartIds in this Rundown + * Sorted by the Segment and Part ranks + */ getAllPartIds(): PartId[] + /** + * All the Parts in the Rundown + * Sorted by the Segment and Part ranks + */ getAllOrderedParts(): ReadonlyDeep[] + /** + * Insert the Scratchpad Segment for this Rundown + * Throws if the segment already exists + */ insertScratchpadSegment(): SegmentId + /** + * Remove the Scratchpad Segment for this Rundown + * @returns true if the Segment was found + */ removeScratchpadSegment(): boolean + /** + * Get the Scratchpad Segment for this Rundown, if it exists + */ getScratchpadSegment(): PlayoutSegmentModel | undefined + /** + * Set the rank of the Scratchpad Segment in this Rundown + * Throws if the segment does not exists + * @param rank New rank + */ setScratchpadSegmentRank(rank: number): void } diff --git a/packages/job-worker/src/playout/model/PlayoutSegmentModel.ts b/packages/job-worker/src/playout/model/PlayoutSegmentModel.ts index 880c234282..1063993471 100644 --- a/packages/job-worker/src/playout/model/PlayoutSegmentModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutSegmentModel.ts @@ -3,16 +3,30 @@ import { ReadonlyDeep } from 'type-fest' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +/** + * Wrap a Segment and its Parts in a readonly and simplified view for Playout operations + */ export interface PlayoutSegmentModel { + /** + * The Segment properties + */ readonly Segment: ReadonlyDeep /** - * All the Parts in the segment + * All the Parts in the Segment * Sorted by their rank */ readonly Parts: ReadonlyDeep + /** + * Get a Part which belongs to this Segment + * @param id Id of the Part + */ getPart(id: PartId): ReadonlyDeep | undefined + /** + * Get all the PartIds in this Segment + * Sorted by the Part ranks + */ getPartIds(): PartId[] } diff --git a/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts b/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts index 2e6b3cb3e5..4628791d75 100644 --- a/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts +++ b/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts @@ -1,6 +1,7 @@ import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { ReadOnlyCache } from '../../../cache/CacheBase' +import { DatabasePersistedModel } from '../../../modelBase' import { CacheForIngest } from '../../../ingest/cache' import { PlaylistLock } from '../../../jobs/lock' import { ReadonlyDeep } from 'type-fest' @@ -21,6 +22,14 @@ import { PlayoutPartInstanceModelImpl } from './PlayoutPartInstanceModelImpl' import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { PlayoutModel, PlayoutModelPreInit } from '../PlayoutModel' +/** + * Load a PlayoutModelPreInit for the given RundownPlaylist + * @param context Context from the job queue + * @param playlistLock Lock for the RundownPlaylist to load + * @param tmpPlaylist Temporary copy of the RundownPlaylist to load + * @param reloadPlaylist Whether to reload the RundownPlaylist, or use the temporary copy + * @returns Loaded PlayoutModelPreInit + */ export async function loadPlayoutModelPreInit( context: JobContext, playlistLock: PlaylistLock, @@ -57,18 +66,28 @@ export async function loadPlayoutModelPreInit( return res } +/** + * Load a PlayoutModel partially from the database, partially from an IngestModel. + * Anything belonging to the Rundown of the IngestModel will be taken from there, as it is assumed to be the most up to date copy of the data + * @param context Context from the job queue + * @param playlistLock Lock for the RundownPlaylist to load + * @param loadedPlaylist Preloaded copy of the RundownPlaylist + * @param newRundowns Preloaded copy of the Rundowns belonging to the RundownPlaylist + * @param ingestCache IngestModel to take data from + * @returns Loaded PlayoutModel + */ export async function createPlayoutCachefromIngestCache( context: JobContext, playlistLock: PlaylistLock, - newPlaylist: ReadonlyDeep, + loadedPlaylist: ReadonlyDeep, newRundowns: ReadonlyDeep>, ingestCache: ReadOnlyCache -): Promise { - const [peripheralDevices, playlist, rundowns] = await loadInitData(context, newPlaylist, false, newRundowns) +): Promise { + const [peripheralDevices, playlist, rundowns] = await loadInitData(context, loadedPlaylist, false, newRundowns) const rundownIds = rundowns.map((r) => r._id) const [partInstances, rundownsWithContent, timeline] = await Promise.all([ - loadPartInstances(context, newPlaylist, rundownIds), + loadPartInstances(context, loadedPlaylist, rundownIds), loadRundowns(context, ingestCache, rundowns), loadTimeline(context), ]) @@ -76,7 +95,7 @@ export async function createPlayoutCachefromIngestCache( const res = new PlayoutModelImpl( context, playlistLock, - newPlaylist._id, + loadedPlaylist._id, peripheralDevices, playlist, partInstances, @@ -111,10 +130,16 @@ async function loadInitData( return [peripheralDevices, reloadedPlaylist, rundowns] } +/** + * Load a PlayoutModel from a PlayoutModelPreInit + * @param context Context from the job queue + * @param initModel Preloaded PlayoutModelPreInit describing the RundownPlaylist to load + * @returns Loaded PlayoutModel + */ export async function createPlayoutModelfromInitModel( context: JobContext, initModel: PlayoutModelPreInit -): Promise { +): Promise { const span = context.startSpan('CacheForPlayout.fromInit') if (span) span.setLabel('playlistId', unprotectString(initModel.PlaylistId)) diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts index 176c12c3f9..89b8a8f7a3 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts @@ -50,6 +50,11 @@ import { IS_PRODUCTION } from '../../../environment' import { DeferredAfterSaveFunction, DeferredFunction, PlayoutModel, PlayoutModelReadonly } from '../PlayoutModel' import { writePartInstancesAndPieceInstances, writeScratchpadSegments } from './SavePlayoutModel' import { PlayoutPieceInstanceModel } from '../PlayoutPieceInstanceModel' +import { DatabasePersistedModel } from '../../../modelBase' +import { ExpectedPackageDBFromStudioBaselineObjects } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' +import { ExpectedPlayoutItemStudio } from '@sofie-automation/corelib/dist/dataModel/ExpectedPlayoutItem' +import { StudioBaselineHelper } from '../../../studio/model/StudioBaselineHelper' +import { EventsJobs } from '@sofie-automation/corelib/dist/worker/events' export class PlayoutModelReadonlyImpl implements PlayoutModelReadonly { public readonly PlaylistId: RundownPlaylistId @@ -223,7 +228,9 @@ export class PlayoutModelReadonlyImpl implements PlayoutModelReadonly { * It contains everything that is needed to generate the timeline, and everything except for pieces needed to update the partinstances. * Anything not in this cache should not be needed often, and only for specific operations (eg, AdlibActions needed to run one). */ -export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements PlayoutModel { +export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements PlayoutModel, DatabasePersistedModel { + readonly #baselineHelper: StudioBaselineHelper + #deferredBeforeSaveFunctions: DeferredFunction[] = [] #deferredAfterSaveFunctions: DeferredAfterSaveFunction[] = [] #disposed = false @@ -232,6 +239,7 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou #TimelineHasChanged = false #PendingPartInstanceTimingEvents = new Set() + #PendingNotifyCurrentlyPlayingPartEvent = new Map() get HackDeletedPartInstanceIds(): PartInstanceId[] { const result: PartInstanceId[] = [] @@ -253,6 +261,8 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou ) { super(context, playlistLock, playlistId, peripheralDevices, playlist, partInstances, rundowns, timeline) context.trackCache(this) + + this.#baselineHelper = new StudioBaselineHelper(context) } public get DisplayName(): string { @@ -430,6 +440,14 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou this.#PendingPartInstanceTimingEvents.add(partInstanceId) } + queueNotifyCurrentlyPlayingPartEvent(rundownId: RundownId, partInstance: PlayoutPartInstanceModel | null): void { + if (partInstance && partInstance.PartInstance.part.shouldNotifyCurrentPlayingPart) { + this.#PendingNotifyCurrentlyPlayingPartEvent.set(rundownId, partInstance.PartInstance.part.externalId) + } else if (!partInstance) { + this.#PendingNotifyCurrentlyPlayingPartEvent.set(rundownId, null) + } + } + removeAllRehearsalPartInstances(): void { const partInstancesToRemove: PartInstanceId[] = [] @@ -542,6 +560,7 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou : undefined, ...writePartInstancesAndPieceInstances(this.context, this.AllPartInstances), writeScratchpadSegments(this.context, this.RundownsImpl), + this.#baselineHelper.saveAllToDatabase(), ]) this.#PlaylistHasChanged = false @@ -558,6 +577,21 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou } this.#PendingPartInstanceTimingEvents.clear() + for (const [rundownId, partExternalId] of this.#PendingNotifyCurrentlyPlayingPartEvent) { + // This is low-prio, defer so that it's executed well after publications has been updated, + // so that the playout gateway has had the chance to learn about the timeline changes + this.context + .queueEventJob(EventsJobs.NotifyCurrentlyPlayingPart, { + rundownId: rundownId, + isRehearsal: !!this.Playlist.rehearsal, + partExternalId: partExternalId, + }) + .catch((e) => { + logger.warn(`Failed to queue NotifyCurrentlyPlayingPart job: ${e}`) + }) + } + this.#PendingNotifyCurrentlyPlayingPartEvent.clear() + if (span) span.end() } @@ -643,6 +677,13 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou this.#TimelineHasChanged = true } + setExpectedPackagesForStudioBaseline(packages: ExpectedPackageDBFromStudioBaselineObjects[]): void { + this.#baselineHelper.setExpectedPackages(packages) + } + setExpectedPlayoutItemsForStudioBaseline(playoutItems: ExpectedPlayoutItemStudio[]): void { + this.#baselineHelper.setExpectedPlayoutItems(playoutItems) + } + /** Lifecycle */ /** @deprecated */ diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts index f049c8e45d..b3d729259b 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts @@ -1,9 +1,4 @@ -import { - PieceId, - PieceInstanceId, - PieceInstanceInfiniteId, - RundownPlaylistActivationId, -} from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PieceId, PieceInstanceId, RundownPlaylistActivationId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ReadonlyDeep } from 'type-fest' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { @@ -206,16 +201,6 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { this.#compareAndSetPartInstanceValue('blockTakeUntil', timestamp ?? undefined) } - clearPlannedTimings(): void { - const timings = { ...this.PartInstanceImpl.timings } - if (timings.plannedStartedPlayback) { - delete timings.plannedStartedPlayback - delete timings.plannedStoppedPlayback - - this.#compareAndSetPartInstanceValue('timings', timings, true) - } - } - getPieceInstance(id: PieceInstanceId): PlayoutPieceInstanceModel | undefined { return this.PieceInstancesImpl.get(id) ?? undefined } @@ -252,10 +237,14 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { return pieceInstanceModel } - insertHoldPieceInstance( - extendPieceInstance: PlayoutPieceInstanceModel, - infiniteInstanceId: PieceInstanceInfiniteId - ): PlayoutPieceInstanceModel { + insertHoldPieceInstance(extendPieceInstance: PlayoutPieceInstanceModel): PlayoutPieceInstanceModel { + const extendPieceInfinite = extendPieceInstance.PieceInstance.infinite + if (!extendPieceInfinite) throw new Error('Piece being extended is not infinite!') + if (extendPieceInfinite.infiniteInstanceIndex !== 0 || extendPieceInfinite.fromPreviousPart) + throw new Error('Piece being extended is not infinite due to HOLD!') + + const infiniteInstanceId = extendPieceInfinite.infiniteInstanceId + // make the extension const newInstance: PieceInstance = { _id: protectString(extendPieceInstance.PieceInstance._id + '_hold'), @@ -444,11 +433,16 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { this.#compareAndSetPartInstanceValue('timings', timings, true) } - setPlannedStoppedPlayback(time: Time): void { + setPlannedStoppedPlayback(time: Time | undefined): void { const timings = { ...this.PartInstanceImpl.timings } if (timings?.plannedStartedPlayback && !timings.plannedStoppedPlayback) { - timings.plannedStoppedPlayback = time - timings.duration = time - timings.plannedStartedPlayback + if (time) { + timings.plannedStoppedPlayback = time + timings.duration = time - timings.plannedStartedPlayback + } else { + delete timings.plannedStoppedPlayback + delete timings.duration + } this.#compareAndSetPartInstanceValue('timings', timings, true) } diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutPieceInstanceModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutPieceInstanceModelImpl.ts index 45bc20a7e4..d41ecfe42a 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutPieceInstanceModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutPieceInstanceModelImpl.ts @@ -7,8 +7,17 @@ import { PlayoutPieceInstanceModel } from '../PlayoutPieceInstanceModel' import _ = require('underscore') export class PlayoutPieceInstanceModelImpl implements PlayoutPieceInstanceModel { + /** + * The raw mutable PieceInstance + * Danger: This should not be modified externally, this is exposed for cloning and saving purposes + */ PieceInstanceImpl: PieceInstance + /** + * Set/delete a value for this PieceInstance, and track that there are changes + * @param key Property key + * @param newValue Property value + */ setPieceInstanceValue(key: T, newValue: PieceInstance[T]): void { if (newValue === undefined) { delete this.PieceInstanceImpl[key] @@ -19,6 +28,12 @@ export class PlayoutPieceInstanceModelImpl implements PlayoutPieceInstanceModel this.#HasChanges = true } + /** + * Set/delete a value for this PieceInstance if the value has cahnged, and track that there are changes + * @param key Property key + * @param newValue Property value + * @param deepEqual Perform a deep equality check + */ compareAndSetPieceInstanceValue( key: T, newValue: PieceInstance[T], @@ -38,10 +53,16 @@ export class PlayoutPieceInstanceModelImpl implements PlayoutPieceInstanceModel } #HasChanges = false + /** + * Whether this PieceInstance has unsaved changes + */ get HasChanges(): boolean { return this.#HasChanges } + /** + * Clear the `HasChanges` flag + */ clearChangedFlag(): void { this.#HasChanges = false } @@ -55,6 +76,10 @@ export class PlayoutPieceInstanceModelImpl implements PlayoutPieceInstanceModel this.#HasChanges = hasChanges } + /** + * Merge properties from another PieceInstance onto this one + * @param pieceInstance PieceInstance to merge properties from + */ mergeProperties(pieceInstance: ReadonlyDeep): void { this.PieceInstanceImpl = { ...this.PieceInstanceImpl, diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutRundownModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutRundownModelImpl.ts index 22f810064b..2406fdecb9 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutRundownModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutRundownModelImpl.ts @@ -18,9 +18,15 @@ export class PlayoutRundownModelImpl implements PlayoutRundownModel { readonly BaselineObjects: ReadonlyDeep #scratchPadSegmentHasChanged = false + /** + * Check if the Scratchpad Segment has unsaved changes + */ get ScratchPadSegmentHasChanged(): boolean { return this.#scratchPadSegmentHasChanged } + /** + * Clear the `ScratchPadSegmentHasChanged` flag + */ clearScratchPadSegmentChangedFlag(): void { this.#scratchPadSegmentHasChanged = false } @@ -41,14 +47,14 @@ export class PlayoutRundownModelImpl implements PlayoutRundownModel { return this.#segments } - getSegmentIds(): SegmentId[] { - return this.Segments.map((segment) => segment.Segment._id) - } - getSegment(id: SegmentId): PlayoutSegmentModel | undefined { return this.Segments.find((segment) => segment.Segment._id === id) } + getSegmentIds(): SegmentId[] { + return this.Segments.map((segment) => segment.Segment._id) + } + getAllPartIds(): PartId[] { return this.getAllOrderedParts().map((p) => p._id) } @@ -104,6 +110,8 @@ export class PlayoutRundownModelImpl implements PlayoutRundownModel { if (!segment) throw new Error('Scratchpad segment does not exist!') segment.setScratchpadRank(rank) + this.#segments.sort((a, b) => a.Segment._rank - b.Segment._rank) + this.#scratchPadSegmentHasChanged = true } } diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutSegmentModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutSegmentModelImpl.ts index f2d5fc0bef..8482015137 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutSegmentModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutSegmentModelImpl.ts @@ -27,7 +27,11 @@ export class PlayoutSegmentModelImpl implements PlayoutSegmentModel { return this.Parts.map((part) => part._id) } - // Internal mutation hack + /** + * Internal mutation 'hack' to modify the rank of the ScratchPad segment + * This segment belongs to Playout, so is allowed to be modified in this way + * @param rank New rank for the segment + */ setScratchpadRank(rank: number): void { if (this.#Segment.orphaned !== SegmentOrphanedReason.SCRATCHPAD) throw new Error('setScratchpadRank can only be used on a SCRATCHPAD segment') diff --git a/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts b/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts index dc1badd30b..2e7f9ad791 100644 --- a/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts +++ b/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts @@ -8,6 +8,11 @@ import { JobContext } from '../../../jobs' import { PlayoutPartInstanceModelImpl } from './PlayoutPartInstanceModelImpl' import { PlayoutRundownModelImpl } from './PlayoutRundownModelImpl' +/** + * Save any changed Scratchpad Segments + * @param context Context from the job queue + * @param rundowns Rundowns whose Scratchpad Segment may need saving + */ export async function writeScratchpadSegments( context: JobContext, rundowns: readonly PlayoutRundownModelImpl[] @@ -47,6 +52,11 @@ export async function writeScratchpadSegments( } } +/** + * Save any changed or deleted PartInstances and their PieceInstances + * @param context Context from the job queue + * @param partInstances Map of PartInstances to check for changes or deletion + */ export function writePartInstancesAndPieceInstances( context: JobContext, partInstances: Map diff --git a/packages/job-worker/src/playout/scratchpad.ts b/packages/job-worker/src/playout/scratchpad.ts index cada6016ae..7e37f0b6f5 100644 --- a/packages/job-worker/src/playout/scratchpad.ts +++ b/packages/job-worker/src/playout/scratchpad.ts @@ -6,8 +6,8 @@ import { getCurrentTime } from '../lib' import { JobContext } from '../jobs' import { runJobWithPlayoutModel } from './lock' import { performTakeToNextedPart } from './take' -import { PartInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PlayoutModel } from './model/PlayoutModel' +import { PlayoutPartInstanceModel } from './model/PlayoutPartInstanceModel' export async function handleActivateScratchpad(context: JobContext, data: ActivateScratchpadProps): Promise { if (!context.studio.settings.allowScratchpad) throw UserError.create(UserErrorMessage.ScratchpadNotAllowed) @@ -58,11 +58,8 @@ export async function handleActivateScratchpad(context: JobContext, data: Activa export function validateScratchpartPartInstanceProperties( _context: JobContext, playoutModel: PlayoutModel, - partInstanceId: PartInstanceId + partInstance: PlayoutPartInstanceModel ): void { - const partInstance = playoutModel.getPartInstance(partInstanceId) - if (!partInstance) return - const rundown = playoutModel.getRundown(partInstance.PartInstance.rundownId) if (!rundown) throw new Error( diff --git a/packages/job-worker/src/playout/take.ts b/packages/job-worker/src/playout/take.ts index 8b956b0cec..9504a26bc8 100644 --- a/packages/job-worker/src/playout/take.ts +++ b/packages/job-worker/src/playout/take.ts @@ -25,7 +25,6 @@ import { PartEventContext, RundownContext } from '../blueprints/context' import { WrappedShowStyleBlueprint } from '../blueprints/cache' import { innerStopPieces } from './adlibUtils' import { reportPartInstanceHasStarted, reportPartInstanceHasStopped } from './timings/partPlayback' -import { EventsJobs } from '@sofie-automation/corelib/dist/worker/events' import { calculatePartTimings } from '@sofie-automation/corelib/dist/playout/timings' import { convertPartInstanceToBlueprints, convertResolvedPieceInstanceToBlueprints } from '../blueprints/context/lib' import { processAndPrunePieceInstanceTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' @@ -191,7 +190,8 @@ export async function performTakeToNextedPart( throw new Error(`takeRundown: takeRundown not found! ("${takePartInstance.PartInstance.rundownId}")`) // Autonext may have setup the plannedStartedPlayback. Clear it so that a new value is generated - takePartInstance.clearPlannedTimings() + takePartInstance.setPlannedStartedPlayback(undefined) + takePartInstance.setPlannedStoppedPlayback(undefined) // it is only a first take if the Playlist has no startedPlayback and the taken PartInstance is not untimed const isFirstTake = !playoutModel.Playlist.startedPlayback && !takePartInstance.PartInstance.part.untimed @@ -472,21 +472,7 @@ export async function afterTake( await updateTimeline(context, playoutModel, timeOffsetIntoPart || undefined) - playoutModel.deferAfterSave(async () => { - // This is low-prio, defer so that it's executed well after publications has been updated, - // so that the playout gateway has haf the chance to learn about the timeline changes - if (takePartInstance.PartInstance.part.shouldNotifyCurrentPlayingPart) { - context - .queueEventJob(EventsJobs.NotifyCurrentlyPlayingPart, { - rundownId: takePartInstance.PartInstance.rundownId, - isRehearsal: !!playoutModel.Playlist.rehearsal, - partExternalId: takePartInstance.PartInstance.part.externalId, - }) - .catch((e) => { - logger.warn(`Failed to queue NotifyCurrentlyPlayingPart job: ${e}`) - }) - } - }) + playoutModel.queueNotifyCurrentlyPlayingPartEvent(takePartInstance.PartInstance.rundownId, takePartInstance) if (span) span.end() } @@ -508,10 +494,10 @@ function startHold( pieceInstancesToCopy.forEach((instance) => { if (!instance.PieceInstance.infinite) { // mark current one as infinite - const infiniteInstanceId = instance.prepareForHold() + instance.prepareForHold() // This gets deleted once the nextpart is activated, so it doesnt linger for long - const extendedPieceInstance = holdToPartInstance.insertHoldPieceInstance(instance, infiniteInstanceId) + const extendedPieceInstance = holdToPartInstance.insertHoldPieceInstance(instance) const content = clone(instance.PieceInstance.piece.content) as VTContent | undefined if (content?.fileName && content.sourceDuration && instance.PieceInstance.plannedStartedPlayback) { diff --git a/packages/job-worker/src/playout/timeline/generate.ts b/packages/job-worker/src/playout/timeline/generate.ts index 9d286b61fa..0bd5c86568 100644 --- a/packages/job-worker/src/playout/timeline/generate.ts +++ b/packages/job-worker/src/playout/timeline/generate.ts @@ -30,7 +30,7 @@ import { processAndPrunePieceInstanceTimings, PieceInstanceWithTimings, } from '@sofie-automation/corelib/dist/playout/processAndPrune' -import { StudioPlayoutModel, StudioPlayoutModelBase } from '../../studio/StudioPlayoutModel' +import { StudioPlayoutModel, StudioPlayoutModelBase } from '../../studio/model/StudioPlayoutModel' import { getLookeaheadObjects } from '../lookahead' import { StudioBaselineContext, OnTimelineGenerateContext } from '../../blueprints/context' import { ExpectedPackageDBType } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' diff --git a/packages/job-worker/src/playout/timeline/multi-gateway.ts b/packages/job-worker/src/playout/timeline/multi-gateway.ts index 31b52a430c..a4cb38eb75 100644 --- a/packages/job-worker/src/playout/timeline/multi-gateway.ts +++ b/packages/job-worker/src/playout/timeline/multi-gateway.ts @@ -4,7 +4,7 @@ import { PeripheralDeviceType } from '@sofie-automation/corelib/dist/dataModel/P import { TimelineObjRundown } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { normalizeArray } from '@sofie-automation/corelib/dist/lib' import { PieceTimelineMetadata } from './pieceGroup' -import { StudioPlayoutModelBase } from '../../studio/StudioPlayoutModel' +import { StudioPlayoutModelBase } from '../../studio/model/StudioPlayoutModel' import { JobContext } from '../../jobs' import { getCurrentTime } from '../../lib' import { PlayoutModel } from '../model/PlayoutModel' diff --git a/packages/job-worker/src/playout/timelineJobs.ts b/packages/job-worker/src/playout/timelineJobs.ts index 8552c10c91..d23f85fb24 100644 --- a/packages/job-worker/src/playout/timelineJobs.ts +++ b/packages/job-worker/src/playout/timelineJobs.ts @@ -5,7 +5,7 @@ import { updateStudioTimeline, updateTimeline } from './timeline/generate' import { getSystemVersion } from '../lib' import { runJobWithStudioPlayoutModel } from '../studio/lock' import { shouldUpdateStudioBaselineInner as libShouldUpdateStudioBaselineInner } from '@sofie-automation/corelib/dist/studio/baseline' -import { StudioPlayoutModel } from '../studio/StudioPlayoutModel' +import { StudioPlayoutModel } from '../studio/model/StudioPlayoutModel' /** * Update the timeline with a regenerated Studio Baseline diff --git a/packages/job-worker/src/playout/timings/timelineTriggerTime.ts b/packages/job-worker/src/playout/timings/timelineTriggerTime.ts index a6cebc6e4b..81fc11fbba 100644 --- a/packages/job-worker/src/playout/timings/timelineTriggerTime.ts +++ b/packages/job-worker/src/playout/timings/timelineTriggerTime.ts @@ -9,7 +9,7 @@ import { saveTimeline } from '../timeline/generate' import { applyToArray } from '@sofie-automation/corelib/dist/lib' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { runJobWithStudioPlayoutModel } from '../../studio/lock' -import { StudioPlayoutModel } from '../../studio/StudioPlayoutModel' +import { StudioPlayoutModel } from '../../studio/model/StudioPlayoutModel' import { DbCacheWriteCollection } from '../../cache/CacheCollection' import { PieceTimelineMetadata } from '../timeline/pieceGroup' import { deserializeTimelineBlob } from '@sofie-automation/corelib/dist/dataModel/Timeline' diff --git a/packages/job-worker/src/studio/StudioPlayoutModel.ts b/packages/job-worker/src/studio/StudioPlayoutModel.ts deleted file mode 100644 index c796020937..0000000000 --- a/packages/job-worker/src/studio/StudioPlayoutModel.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { - TimelineComplete, - TimelineCompleteGenerationVersions, - TimelineObjGeneric, -} from '@sofie-automation/corelib/dist/dataModel/Timeline' -import { ICacheBase2 } from '../cache/CacheBase' -import { ReadonlyDeep } from 'type-fest' - -export interface StudioPlayoutModelBaseReadonly { - readonly PeripheralDevices: ReadonlyDeep - - get Timeline(): TimelineComplete | null - - readonly isMultiGatewayMode: boolean -} - -export interface StudioPlayoutModelBase extends StudioPlayoutModelBaseReadonly { - setTimeline(timelineObjs: TimelineObjGeneric[], generationVersions: TimelineCompleteGenerationVersions): void -} - -export type DeferredAfterSaveFunction = (cache: StudioPlayoutModelBaseReadonly) => void | Promise - -export interface StudioPlayoutModel extends StudioPlayoutModelBase, ICacheBase2 { - readonly isStudio: true - - readonly RundownPlaylists: ReadonlyDeep - - getActiveRundownPlaylists(excludeRundownPlaylistId?: RundownPlaylistId): ReadonlyDeep - - /** @deprecated */ - deferAfterSave(fcn: DeferredAfterSaveFunction): void -} diff --git a/packages/job-worker/src/studio/lock.ts b/packages/job-worker/src/studio/lock.ts index bfdccddd9e..b0aa672ca7 100644 --- a/packages/job-worker/src/studio/lock.ts +++ b/packages/job-worker/src/studio/lock.ts @@ -1,6 +1,6 @@ import { JobContext } from '../jobs' -import { StudioPlayoutModel } from './StudioPlayoutModel' -import { loadStudioPlayoutModel } from './StudioPlayoutModelImpl' +import { StudioPlayoutModel } from './model/StudioPlayoutModel' +import { loadStudioPlayoutModel } from './model/StudioPlayoutModelImpl' /** * Run a typical studio job diff --git a/packages/job-worker/src/studio/model/StudioBaselineHelper.ts b/packages/job-worker/src/studio/model/StudioBaselineHelper.ts new file mode 100644 index 0000000000..5b41352248 --- /dev/null +++ b/packages/job-worker/src/studio/model/StudioBaselineHelper.ts @@ -0,0 +1,57 @@ +import { JobContext } from '../../jobs' +import { + ExpectedPackageDB, + ExpectedPackageDBFromStudioBaselineObjects, + ExpectedPackageDBType, +} from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' +import { ExpectedPlayoutItemStudio } from '@sofie-automation/corelib/dist/dataModel/ExpectedPlayoutItem' +import { saveIntoDb } from '../../db/changes' + +export class StudioBaselineHelper { + readonly #context: JobContext + + #pendingExpectedPackages: ExpectedPackageDBFromStudioBaselineObjects[] | undefined + #pendingExpectedPlayoutItems: ExpectedPlayoutItemStudio[] | undefined + + constructor(context: JobContext) { + this.#context = context + } + + hasChanges(): boolean { + return !!this.#pendingExpectedPackages || !!this.#pendingExpectedPlayoutItems + } + + setExpectedPackages(packages: ExpectedPackageDBFromStudioBaselineObjects[]): void { + this.#pendingExpectedPackages = packages + } + setExpectedPlayoutItems(playoutItems: ExpectedPlayoutItemStudio[]): void { + this.#pendingExpectedPlayoutItems = playoutItems + } + + async saveAllToDatabase(): Promise { + await Promise.all([ + this.#pendingExpectedPlayoutItems + ? saveIntoDb( + this.#context, + this.#context.directCollections.ExpectedPlayoutItems, + { studioId: this.#context.studioId, baseline: 'studio' }, + this.#pendingExpectedPlayoutItems + ) + : undefined, + this.#pendingExpectedPackages + ? saveIntoDb( + this.#context, + this.#context.directCollections.ExpectedPackages, + { + studioId: this.#context.studioId, + fromPieceType: ExpectedPackageDBType.STUDIO_BASELINE_OBJECTS, + }, + this.#pendingExpectedPackages + ) + : undefined, + ]) + + this.#pendingExpectedPlayoutItems = undefined + this.#pendingExpectedPackages = undefined + } +} diff --git a/packages/job-worker/src/studio/model/StudioPlayoutModel.ts b/packages/job-worker/src/studio/model/StudioPlayoutModel.ts new file mode 100644 index 0000000000..66359f7ff2 --- /dev/null +++ b/packages/job-worker/src/studio/model/StudioPlayoutModel.ts @@ -0,0 +1,68 @@ +import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { + TimelineComplete, + TimelineCompleteGenerationVersions, + TimelineObjGeneric, +} from '@sofie-automation/corelib/dist/dataModel/Timeline' +import { BaseModel } from '../../modelBase' +import { ReadonlyDeep } from 'type-fest' +import { ExpectedPackageDBFromStudioBaselineObjects } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' +import { ExpectedPlayoutItemStudio } from '@sofie-automation/corelib/dist/dataModel/ExpectedPlayoutItem' + +export interface StudioPlayoutModelBaseReadonly { + /** + * All of the PeripheralDevices that belong to the Studio of this RundownPlaylist + */ + readonly PeripheralDevices: ReadonlyDeep + + /** + * Get the Timeline for the current Studio + */ + get Timeline(): TimelineComplete | null + + /** + * Whether this Studio is operating in multi-gateway mode + */ + readonly isMultiGatewayMode: boolean +} + +export interface StudioPlayoutModelBase extends StudioPlayoutModelBaseReadonly { + /** + * Update the ExpectedPackages for the StudioBaseline of the current Studio + * @param packages ExpectedPackages to store + */ + setExpectedPackagesForStudioBaseline(packages: ExpectedPackageDBFromStudioBaselineObjects[]): void + /** + * Update the ExpectedPlayoutItems for the StudioBaseline of the current Studio + * @param playoutItems ExpectedPlayoutItems to store + */ + setExpectedPlayoutItemsForStudioBaseline(playoutItems: ExpectedPlayoutItemStudio[]): void + + /** + * Update the Timeline for the current Studio + * @param timelineObjs Timeline objects to be run in the Studio + * @param generationVersions Details about the versions where these objects were generated + */ + setTimeline(timelineObjs: TimelineObjGeneric[], generationVersions: TimelineCompleteGenerationVersions): void +} + +/** + * A view of a `Studio` and its RundownPlaylists for playout when a RundownPlaylist is not activated + */ +export interface StudioPlayoutModel extends StudioPlayoutModelBase, BaseModel { + readonly isStudio: true + + /** + * The unwrapped RundownPlaylists in this Studio + */ + readonly RundownPlaylists: ReadonlyDeep + + /** + * Get any activated RundownPlaylists in this Studio + * Note: This should return one or none, but could return more if in a bad state + * @param excludeRundownPlaylistId Ignore a given RundownPlaylist, useful to see if any other RundownPlaylists are active + */ + getActiveRundownPlaylists(excludeRundownPlaylistId?: RundownPlaylistId): ReadonlyDeep +} diff --git a/packages/job-worker/src/studio/StudioPlayoutModelImpl.ts b/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts similarity index 74% rename from packages/job-worker/src/studio/StudioPlayoutModelImpl.ts rename to packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts index 9843410f02..39adf47aec 100644 --- a/packages/job-worker/src/studio/StudioPlayoutModelImpl.ts +++ b/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts @@ -7,20 +7,23 @@ import { TimelineCompleteGenerationVersions, TimelineObjGeneric, } from '@sofie-automation/corelib/dist/dataModel/Timeline' -import { JobContext } from '../jobs' +import { JobContext } from '../../jobs' import { ReadonlyDeep } from 'type-fest' import { getRandomId } from '@sofie-automation/corelib/dist/lib' -import { getCurrentTime } from '../lib' -import { IS_PRODUCTION } from '../environment' -import { logger } from '../logging' -import { StudioPlayoutModel, DeferredAfterSaveFunction } from './StudioPlayoutModel' +import { getCurrentTime } from '../../lib' +import { IS_PRODUCTION } from '../../environment' +import { logger } from '../../logging' +import { StudioPlayoutModel } from './StudioPlayoutModel' +import { DatabasePersistedModel } from '../../modelBase' +import { ExpectedPackageDBFromStudioBaselineObjects } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' +import { ExpectedPlayoutItemStudio } from '@sofie-automation/corelib/dist/dataModel/ExpectedPlayoutItem' +import { StudioBaselineHelper } from './StudioBaselineHelper' /** * This is a model used for studio operations. */ - export class StudioPlayoutModelImpl implements StudioPlayoutModel { - #deferredAfterSaveFunctions: DeferredAfterSaveFunction[] = [] + readonly #baselineHelper: StudioBaselineHelper #disposed = false public readonly isStudio = true @@ -43,6 +46,8 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel { ) { context.trackCache(this) + this.#baselineHelper = new StudioBaselineHelper(context) + this.PeripheralDevices = peripheralDevices this.RundownPlaylists = rundownPlaylists @@ -72,6 +77,13 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel { return this.#isMultiGatewayMode } + setExpectedPackagesForStudioBaseline(packages: ExpectedPackageDBFromStudioBaselineObjects[]): void { + this.#baselineHelper.setExpectedPackages(packages) + } + setExpectedPlayoutItemsForStudioBaseline(playoutItems: ExpectedPlayoutItemStudio[]): void { + this.#baselineHelper.setExpectedPlayoutItems(playoutItems) + } + setTimeline(timelineObjs: TimelineObjGeneric[], generationVersions: TimelineCompleteGenerationVersions): void { this.#Timeline = { _id: this.context.studioId, @@ -88,9 +100,6 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel { */ dispose(): void { this.#disposed = true - - // Discard any hooks too - this.#deferredAfterSaveFunctions.length = 0 } async saveAllToDatabase(): Promise { @@ -106,11 +115,7 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel { } this.#TimelineHasChanged = false - // Execute cache.deferAfterSave()'s - for (const fn of this.#deferredAfterSaveFunctions) { - await fn(this as any) - } - this.#deferredAfterSaveFunctions.length = 0 // clear the array + await this.#baselineHelper.saveAllToDatabase() if (span) span.end() } @@ -131,12 +136,10 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel { } } - if (this.#deferredAfterSaveFunctions.length > 0) + if (this.#baselineHelper.hasChanges()) logOrThrowError( new Error( - `Failed no changes in cache assertion, there were ${ - this.#deferredAfterSaveFunctions.length - } after-save deferred functions` + `Failed no changes in cache assertion, baseline ExpectedPackages or ExpectedPlayoutItems has changes` ) ) @@ -145,14 +148,16 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel { if (span) span.end() } - - /** @deprecated */ - deferAfterSave(fcn: DeferredAfterSaveFunction): void { - this.#deferredAfterSaveFunctions.push(fcn) - } } -export async function loadStudioPlayoutModel(context: JobContext): Promise { +/** + * Load a StudioPlayoutModel for the current Studio + * @param context Context from the job queue + * @returns Loaded StudioPlayoutModel + */ +export async function loadStudioPlayoutModel( + context: JobContext +): Promise { const span = context.startSpan('loadStudioPlayoutModel') const studioId = context.studioId diff --git a/packages/job-worker/src/workers/context.ts b/packages/job-worker/src/workers/context.ts index 7efea976a0..ba5f8e8dd2 100644 --- a/packages/job-worker/src/workers/context.ts +++ b/packages/job-worker/src/workers/context.ts @@ -34,7 +34,7 @@ import { import { getStudioQueueName, StudioJobFunc } from '@sofie-automation/corelib/dist/worker/studio' import { LockBase, PlaylistLock, RundownLock } from '../jobs/lock' import { logger } from '../logging' -import { ICacheBase2 } from '../cache/CacheBase' +import { BaseModel } from '../modelBase' import { LocksManager } from './locks' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { EventsJobFunc, getEventsQueueName } from '@sofie-automation/corelib/dist/worker/events' @@ -278,7 +278,7 @@ export class StudioCacheContextImpl implements StudioCacheContext { export class JobContextImpl extends StudioCacheContextImpl implements JobContext { private readonly locks: Array = [] - private readonly caches: Array = [] + private readonly caches: Array = [] constructor( directCollections: Readonly, @@ -291,7 +291,7 @@ export class JobContextImpl extends StudioCacheContextImpl implements JobContext super(directCollections, cacheData) } - trackCache(cache: ICacheBase2): void { + trackCache(cache: BaseModel): void { this.caches.push(cache) } From 792325479486cb59f522841a013209e4abb49eca Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Fri, 27 Oct 2023 12:39:38 +0200 Subject: [PATCH 125/479] chore: homogenize structured cache property naming --- .../__tests__/context-adlibActions.test.ts | 176 ++++----- .../SyncIngestUpdateToPartInstanceContext.ts | 32 +- .../src/blueprints/context/adlibActions.ts | 58 +-- packages/job-worker/src/cache/CacheBase.ts | 2 +- .../src/ingest/__tests__/ingest.test.ts | 10 +- packages/job-worker/src/ingest/cache.ts | 2 +- packages/job-worker/src/ingest/commit.ts | 30 +- .../src/ingest/syncChangesToPartInstance.ts | 70 ++-- packages/job-worker/src/ingest/updateNext.ts | 26 +- packages/job-worker/src/modelBase.ts | 2 +- packages/job-worker/src/peripheralDevice.ts | 2 +- .../src/playout/__tests__/infinites.test.ts | 2 +- .../src/playout/__tests__/timeline.test.ts | 58 +-- .../src/playout/activePlaylistActions.ts | 36 +- .../src/playout/activePlaylistJobs.ts | 8 +- .../job-worker/src/playout/adlibAction.ts | 14 +- packages/job-worker/src/playout/adlibJobs.ts | 104 +++--- packages/job-worker/src/playout/adlibUtils.ts | 28 +- packages/job-worker/src/playout/datastore.ts | 2 +- packages/job-worker/src/playout/debug.ts | 10 +- packages/job-worker/src/playout/holdJobs.ts | 24 +- packages/job-worker/src/playout/infinites.ts | 108 +++--- packages/job-worker/src/playout/lib.ts | 18 +- .../job-worker/src/playout/lookahead/index.ts | 2 +- .../job-worker/src/playout/lookahead/util.ts | 6 +- .../src/playout/model/PlayoutModel.ts | 36 +- .../playout/model/PlayoutPartInstanceModel.ts | 4 +- .../model/PlayoutPieceInstanceModel.ts | 2 +- .../src/playout/model/PlayoutRundownModel.ts | 6 +- .../src/playout/model/PlayoutSegmentModel.ts | 4 +- .../model/implementation/LoadPlayoutModel.ts | 30 +- .../model/implementation/PlayoutModelImpl.ts | 344 +++++++++--------- .../PlayoutPartInstanceModelImpl.ts | 196 +++++----- .../PlayoutPieceInstanceModelImpl.ts | 2 +- .../implementation/PlayoutRundownModelImpl.ts | 32 +- .../implementation/PlayoutSegmentModelImpl.ts | 10 +- .../model/implementation/SavePlayoutModel.ts | 10 +- .../job-worker/src/playout/moveNextPart.ts | 18 +- .../job-worker/src/playout/resolvedPieces.ts | 4 +- packages/job-worker/src/playout/scratchpad.ts | 14 +- .../job-worker/src/playout/selectNextPart.ts | 4 +- packages/job-worker/src/playout/setNext.ts | 120 +++--- .../job-worker/src/playout/setNextJobs.ts | 8 +- packages/job-worker/src/playout/take.ts | 142 ++++---- .../src/playout/timeline/generate.ts | 54 +-- .../src/playout/timeline/multi-gateway.ts | 76 ++-- .../src/playout/timeline/rundown.ts | 2 +- .../job-worker/src/playout/timelineJobs.ts | 6 +- .../src/playout/timings/partPlayback.ts | 44 +-- .../src/playout/timings/piecePlayback.ts | 26 +- .../playout/timings/timelineTriggerTime.ts | 2 +- packages/job-worker/src/rundown.ts | 8 +- packages/job-worker/src/rundownPlaylists.ts | 2 +- packages/job-worker/src/studio/cleanup.ts | 2 +- .../src/studio/model/StudioPlayoutModel.ts | 6 +- .../studio/model/StudioPlayoutModelImpl.ts | 36 +- packages/job-worker/src/workers/context.ts | 2 +- 57 files changed, 1043 insertions(+), 1039 deletions(-) diff --git a/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts b/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts index 499d34ee92..9cb3c7da0a 100644 --- a/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts +++ b/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts @@ -146,17 +146,17 @@ describe('Test blueprint api context', () => { } async function getActionExecutionContext(jobContext: JobContext, playoutModel: PlayoutModel) { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist expect(playlist).toBeTruthy() - const rundown = playoutModel.Rundowns[0] + const rundown = playoutModel.rundowns[0] expect(rundown).toBeTruthy() const activationId = playlist.activationId as RundownPlaylistActivationId expect(activationId).toBeTruthy() const showStyle = await jobContext.getShowStyleCompound( - rundown.Rundown.showStyleVariantId, - rundown.Rundown.showStyleBaseId + rundown.rundown.showStyleVariantId, + rundown.rundown.showStyleBaseId ) const showStyleConfig = jobContext.getShowStyleBlueprintConfig(showStyle) @@ -236,7 +236,7 @@ describe('Test blueprint api context', () => { await Promise.all( writePartInstancesAndPieceInstances( context, - normalizeArrayToMapFunc(allPartInstances as PlayoutPartInstanceModelImpl[], (p) => p.PartInstance._id) + normalizeArrayToMapFunc(allPartInstances as PlayoutPartInstanceModelImpl[], (p) => p.partInstance._id) ) ) await playoutModel.saveAllToDatabase() @@ -261,8 +261,8 @@ describe('Test blueprint api context', () => { } } else if ('PartInstance' in info) { return { - partInstanceId: info.PartInstance._id, - rundownId: info.PartInstance.rundownId, + partInstanceId: info.partInstance._id, + rundownId: info.partInstance.rundownId, manuallySelected: false, consumesQueuedSegmentId: false, } @@ -326,7 +326,7 @@ describe('Test blueprint api context', () => { await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { const { context } = await getActionExecutionContext(jobContext, playoutModel) - expect(playoutModel.LoadedPartInstances).toHaveLength(0) + expect(playoutModel.loadedPartInstances).toHaveLength(0) await expect(context.getPartInstance('next')).resolves.toBeUndefined() await expect(context.getPartInstance('current')).resolves.toBeUndefined() @@ -336,12 +336,12 @@ describe('Test blueprint api context', () => { await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { const { context } = await getActionExecutionContext(jobContext, playoutModel) - expect(playoutModel.LoadedPartInstances).toHaveLength(2) + expect(playoutModel.loadedPartInstances).toHaveLength(2) // Check the current part await expect(context.getPartInstance('next')).resolves.toBeUndefined() await expect(context.getPartInstance('current')).resolves.toMatchObject({ - _id: allPartInstances[1].PartInstance._id, + _id: allPartInstances[1].partInstance._id, }) }) @@ -349,11 +349,11 @@ describe('Test blueprint api context', () => { await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { const { context } = await getActionExecutionContext(jobContext, playoutModel) - expect(playoutModel.LoadedPartInstances).toHaveLength(3) + expect(playoutModel.loadedPartInstances).toHaveLength(3) // Now the next part await expect(context.getPartInstance('next')).resolves.toMatchObject({ - _id: allPartInstances[2].PartInstance._id, + _id: allPartInstances[2].partInstance._id, }) await expect(context.getPartInstance('current')).resolves.toBeUndefined() }) @@ -383,7 +383,7 @@ describe('Test blueprint api context', () => { await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { const { context } = await getActionExecutionContext(jobContext, playoutModel) - expect(playoutModel.LoadedPartInstances).toHaveLength(0) + expect(playoutModel.loadedPartInstances).toHaveLength(0) await expect(context.getPieceInstances('next')).resolves.toHaveLength(0) await expect(context.getPieceInstances('current')).resolves.toHaveLength(0) @@ -393,7 +393,7 @@ describe('Test blueprint api context', () => { await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { const { context } = await getActionExecutionContext(jobContext, playoutModel) - expect(playoutModel.LoadedPartInstances).toHaveLength(2) + expect(playoutModel.loadedPartInstances).toHaveLength(2) // Check the current part await expect(context.getPieceInstances('next')).resolves.toHaveLength(0) @@ -404,7 +404,7 @@ describe('Test blueprint api context', () => { await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { const { context } = await getActionExecutionContext(jobContext, playoutModel) - expect(playoutModel.LoadedPartInstances).toHaveLength(3) + expect(playoutModel.loadedPartInstances).toHaveLength(3) // Now the next part await expect(context.getPieceInstances('next')).resolves.toHaveLength(1) @@ -438,7 +438,7 @@ describe('Test blueprint api context', () => { await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { const { context } = await getActionExecutionContext(jobContext, playoutModel) - expect(playoutModel.LoadedPartInstances).toHaveLength(0) + expect(playoutModel.loadedPartInstances).toHaveLength(0) expect(getResolvedPiecesForCurrentPartInstanceMock).toHaveBeenCalledTimes(0) @@ -458,7 +458,7 @@ describe('Test blueprint api context', () => { expect(context2).toBe(jobContext) expect(sourceLayers).toBeTruthy() expect(now).toBeFalsy() - mockCalledIds.push(partInstance.PartInstance._id) + mockCalledIds.push(partInstance.partInstance._id) return [ { instance: { @@ -478,14 +478,14 @@ describe('Test blueprint api context', () => { await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { const { context } = await getActionExecutionContext(jobContext, playoutModel) - expect(playoutModel.LoadedPartInstances).toHaveLength(2) + expect(playoutModel.loadedPartInstances).toHaveLength(2) // Check the current part await expect(context.getResolvedPieceInstances('next')).resolves.toHaveLength(0) await expect( context.getResolvedPieceInstances('current').then((res) => res.map((p) => p._id)) ).resolves.toEqual(['abc']) expect(getResolvedPiecesForCurrentPartInstanceMock).toHaveBeenCalledTimes(1) - expect(mockCalledIds).toEqual([allPartInstances[1].PartInstance._id]) + expect(mockCalledIds).toEqual([allPartInstances[1].partInstance._id]) }) mockCalledIds = [] @@ -495,7 +495,7 @@ describe('Test blueprint api context', () => { await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { const { context } = await getActionExecutionContext(jobContext, playoutModel) - expect(playoutModel.LoadedPartInstances).toHaveLength(3) + expect(playoutModel.loadedPartInstances).toHaveLength(3) // Now the next part await expect( @@ -503,7 +503,7 @@ describe('Test blueprint api context', () => { ).resolves.toEqual(['abc']) await expect(context.getResolvedPieceInstances('current')).resolves.toHaveLength(0) expect(getResolvedPiecesForCurrentPartInstanceMock).toHaveBeenCalledTimes(1) - expect(mockCalledIds).toEqual([allPartInstances[2].PartInstance._id]) + expect(mockCalledIds).toEqual([allPartInstances[2].partInstance._id]) }) }) }) @@ -567,7 +567,7 @@ describe('Test blueprint api context', () => { await saveAllToDatabase(jobContext, playoutModel, allPartInstances) await expect(context.findLastPieceOnLayer(sourceLayerIds[0])).resolves.toMatchObject({ - _id: insertedPieceInstance.PieceInstance._id, + _id: insertedPieceInstance.pieceInstance._id, }) await expect( context.findLastPieceOnLayer(sourceLayerIds[0], { originalOnly: true }) @@ -596,7 +596,7 @@ describe('Test blueprint api context', () => { await saveAllToDatabase(jobContext, playoutModel, allPartInstances) await expect(context.findLastPieceOnLayer(sourceLayerIds[0])).resolves.toMatchObject({ - _id: insertedPieceInstance2.PieceInstance._id, + _id: insertedPieceInstance2.pieceInstance._id, }) await expect( context.findLastPieceOnLayer(sourceLayerIds[0], { originalOnly: true }) @@ -666,12 +666,12 @@ describe('Test blueprint api context', () => { // Check it await expect(context.findLastPieceOnLayer(sourceLayerIds[0])).resolves.toMatchObject({ - _id: insertedPieceInstance2.PieceInstance._id, + _id: insertedPieceInstance2.pieceInstance._id, }) await expect( context.findLastPieceOnLayer(sourceLayerIds[0], { excludeCurrentPart: true }) ).resolves.toMatchObject({ - _id: insertedPieceInstance.PieceInstance._id, + _id: insertedPieceInstance.pieceInstance._id, }) }) }) @@ -742,21 +742,21 @@ describe('Test blueprint api context', () => { // Check it await expect(context.findLastPieceOnLayer(sourceLayerIds[0])).resolves.toMatchObject({ - _id: insertedPieceInstance2.PieceInstance._id, + _id: insertedPieceInstance2.pieceInstance._id, }) await expect( context.findLastPieceOnLayer(sourceLayerIds[0], { pieceMetaDataFilter: {} }) ).resolves.toMatchObject({ - _id: insertedPieceInstance2.PieceInstance._id, + _id: insertedPieceInstance2.pieceInstance._id, }) await expect( context.findLastPieceOnLayer(sourceLayerIds[0], { pieceMetaDataFilter: { prop1: 'hello' } }) - ).resolves.toMatchObject({ _id: insertedPieceInstance2.PieceInstance._id }) + ).resolves.toMatchObject({ _id: insertedPieceInstance2.pieceInstance._id }) await expect( context.findLastPieceOnLayer(sourceLayerIds[0], { pieceMetaDataFilter: { prop1: { $ne: 'hello' } }, }) - ).resolves.toMatchObject({ _id: insertedPieceInstance.PieceInstance._id }) + ).resolves.toMatchObject({ _id: insertedPieceInstance.pieceInstance._id }) }) }) }) @@ -808,7 +808,7 @@ describe('Test blueprint api context', () => { const expectedPieceInstanceSourceLayer0 = pieceInstances.find( (p) => - p.partInstanceId === playoutModel.Playlist.currentPartInfo?.partInstanceId && + p.partInstanceId === playoutModel.playlist.currentPartInfo?.partInstanceId && p.piece.sourceLayerId === sourceLayerIds[0] ) expect(expectedPieceInstanceSourceLayer0).not.toBeUndefined() @@ -822,7 +822,7 @@ describe('Test blueprint api context', () => { const expectedPieceInstanceSourceLayer1 = pieceInstances.find( (p) => - p.partInstanceId === playoutModel.Playlist.currentPartInfo?.partInstanceId && + p.partInstanceId === playoutModel.playlist.currentPartInfo?.partInstanceId && p.piece.sourceLayerId === sourceLayerIds[1] ) expect(expectedPieceInstanceSourceLayer1).not.toBeUndefined() @@ -891,7 +891,7 @@ describe('Test blueprint api context', () => { const expectedPieceInstanceSourceLayer0 = pieceInstances.find( (p) => - p.partInstanceId === playoutModel.Playlist.currentPartInfo?.partInstanceId && + p.partInstanceId === playoutModel.playlist.currentPartInfo?.partInstanceId && p.piece.sourceLayerId === sourceLayerIds[0] ) expect(expectedPieceInstanceSourceLayer0).not.toBeUndefined() @@ -960,22 +960,22 @@ describe('Test blueprint api context', () => { await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { const { context } = await getActionExecutionContext(jobContext, playoutModel) - expect(playoutModel.LoadedPartInstances).toHaveLength(0) + expect(playoutModel.loadedPartInstances).toHaveLength(0) await expect( context.getPartInstanceForPreviousPiece({ - partInstanceId: allPartInstances[1].PartInstance._id, + partInstanceId: allPartInstances[1].partInstance._id, } as any) ).resolves.toMatchObject({ - _id: allPartInstances[1].PartInstance._id, + _id: allPartInstances[1].partInstance._id, }) await expect( context.getPartInstanceForPreviousPiece({ - partInstanceId: allPartInstances[4].PartInstance._id, + partInstanceId: allPartInstances[4].partInstance._id, } as any) ).resolves.toMatchObject({ - _id: allPartInstances[4].PartInstance._id, + _id: allPartInstances[4].partInstance._id, }) }) @@ -985,22 +985,22 @@ describe('Test blueprint api context', () => { await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { const { context } = await getActionExecutionContext(jobContext, playoutModel) - expect(playoutModel.LoadedPartInstances).toHaveLength(2) + expect(playoutModel.loadedPartInstances).toHaveLength(2) await expect( context.getPartInstanceForPreviousPiece({ - partInstanceId: allPartInstances[1].PartInstance._id, + partInstanceId: allPartInstances[1].partInstance._id, } as any) ).resolves.toMatchObject({ - _id: allPartInstances[1].PartInstance._id, + _id: allPartInstances[1].partInstance._id, }) await expect( context.getPartInstanceForPreviousPiece({ - partInstanceId: allPartInstances[4].PartInstance._id, + partInstanceId: allPartInstances[4].partInstance._id, } as any) ).resolves.toMatchObject({ - _id: allPartInstances[4].PartInstance._id, + _id: allPartInstances[4].partInstance._id, }) }) }) @@ -1047,28 +1047,28 @@ describe('Test blueprint api context', () => { await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { const { context } = await getActionExecutionContext(jobContext, playoutModel) - expect(playoutModel.LoadedPartInstances).toHaveLength(0) + expect(playoutModel.loadedPartInstances).toHaveLength(0) const pieceInstance0 = (await jobContext.mockCollections.PieceInstances.findOne({ - partInstanceId: allPartInstances[0].PartInstance._id, + partInstanceId: allPartInstances[0].partInstance._id, })) as PieceInstance expect(pieceInstance0).not.toBeUndefined() await expect( context.getPartForPreviousPiece({ _id: unprotectString(pieceInstance0.piece._id) }) ).resolves.toMatchObject({ - _id: allPartInstances[0].PartInstance.part._id, + _id: allPartInstances[0].partInstance.part._id, }) const pieceInstance1 = (await jobContext.mockCollections.PieceInstances.findOne({ - partInstanceId: allPartInstances[1].PartInstance._id, + partInstanceId: allPartInstances[1].partInstance._id, })) as PieceInstance expect(pieceInstance1).not.toBeUndefined() await expect( context.getPartForPreviousPiece({ _id: unprotectString(pieceInstance1.piece._id) }) ).resolves.toMatchObject({ - _id: allPartInstances[1].PartInstance.part._id, + _id: allPartInstances[1].partInstance.part._id, }) }) }) @@ -1087,7 +1087,7 @@ describe('Test blueprint api context', () => { await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { const { context } = await getActionExecutionContext(jobContext, playoutModel) - const currentPartInstance = playoutModel.CurrentPartInstance! + const currentPartInstance = playoutModel.currentPartInstance! expect(currentPartInstance).toBeTruthy() currentPartInstance.setTaken(getCurrentTime(), 0) @@ -1129,7 +1129,7 @@ describe('Test blueprint api context', () => { } as any, ]) - const currentPartInstance = playoutModel.CurrentPartInstance! + const currentPartInstance = playoutModel.currentPartInstance! expect(currentPartInstance).toBeTruthy() currentPartInstance.setTaken(getCurrentTime(), 0) @@ -1143,9 +1143,9 @@ describe('Test blueprint api context', () => { expect.anything(), [{ externalId: 'input1' }], 'blueprint0', - partInstance.PartInstance.rundownId, - partInstance.PartInstance.segmentId, - partInstance.PartInstance.part._id, + partInstance.partInstance.rundownId, + partInstance.partInstance.segmentId, + partInstance.partInstance.part._id, true ) expect(insertSpy).toHaveBeenCalledTimes(1) @@ -1154,8 +1154,8 @@ describe('Test blueprint api context', () => { const newPieceInstance = playoutModel.findPieceInstance(protectString(newPieceInstanceId)) ?.pieceInstance as PlayoutPieceInstanceModel expect(newPieceInstance).toBeTruthy() - expect(newPieceInstance.PieceInstance.dynamicallyInserted).toBeTruthy() - expect(newPieceInstance.PieceInstance.partInstanceId).toEqual(partInstance.PartInstance._id) + expect(newPieceInstance.pieceInstance.dynamicallyInserted).toBeTruthy() + expect(newPieceInstance.pieceInstance.partInstanceId).toEqual(partInstance.partInstance._id) }) }) }) @@ -1165,11 +1165,11 @@ describe('Test blueprint api context', () => { const { jobContext, playlistId, allPartInstances } = await setupMyDefaultRundown() const pieceInstance = (await jobContext.mockCollections.PieceInstances.findOne({ - partInstanceId: allPartInstances[0].PartInstance._id, + partInstanceId: allPartInstances[0].partInstance._id, })) as PieceInstance expect(pieceInstance).toBeTruthy() const pieceInstanceOther = (await jobContext.mockCollections.PieceInstances.findOne({ - partInstanceId: allPartInstances[1].PartInstance._id, + partInstanceId: allPartInstances[1].partInstance._id, })) as PieceInstance expect(pieceInstanceOther).toBeTruthy() @@ -1241,8 +1241,8 @@ describe('Test blueprint api context', () => { const { context } = await getActionExecutionContext(jobContext, playoutModel) // Ensure there are no pending updates already - for (const partInstance of playoutModel.LoadedPartInstances) { - expect((partInstance as PlayoutPartInstanceModelImpl).HasAnyChanges()).toBeFalsy() + for (const partInstance of playoutModel.loadedPartInstances) { + expect((partInstance as PlayoutPartInstanceModelImpl).hasAnyChanges()).toBeFalsy() } // Update it and expect it to match @@ -1273,7 +1273,7 @@ describe('Test blueprint api context', () => { playoutModel.findPieceInstance(pieceInstance0._id)! expect(pieceInstance1).toBeTruthy() - expect(resultPiece).toEqual(convertPieceInstanceToBlueprints(pieceInstance1.PieceInstance)) + expect(resultPiece).toEqual(convertPieceInstanceToBlueprints(pieceInstance1.pieceInstance)) const pieceInstance0After = { ...pieceInstance0Before, piece: { @@ -1287,10 +1287,10 @@ describe('Test blueprint api context', () => { ), }, } - expect(pieceInstance1.PieceInstance).toEqual(pieceInstance0After) - expect((partInstance1 as PlayoutPartInstanceModelImpl).PartInstanceHasChanges).toBeFalsy() - expect((partInstance1 as PlayoutPartInstanceModelImpl).ChangedPieceInstanceIds()).toEqual([ - pieceInstance1.PieceInstance._id, + expect(pieceInstance1.pieceInstance).toEqual(pieceInstance0After) + expect((partInstance1 as PlayoutPartInstanceModelImpl).partInstanceHasChanges).toBeFalsy() + expect((partInstance1 as PlayoutPartInstanceModelImpl).changedPieceInstanceIds()).toEqual([ + pieceInstance1.pieceInstance._id, ]) expect(context.nextPartState).toEqual(ActionPartChange.NONE) @@ -1356,7 +1356,7 @@ describe('Test blueprint api context', () => { }) partInstanceModel.setPlannedStartedPlayback(getCurrentTime()) - expect(isTooCloseToAutonext(partInstanceModel.PartInstance, true)).toBeTruthy() + expect(isTooCloseToAutonext(partInstanceModel.partInstance, true)).toBeTruthy() await expect(context.queuePart({} as any, [{}] as any)).rejects.toThrow( 'Too close to an autonext to queue a part' ) @@ -1401,7 +1401,7 @@ describe('Test blueprint api context', () => { postProcessPiecesMock.mockImplementationOnce(postProcessPiecesOrig) insertQueuedPartWithPiecesMock.mockImplementationOnce(insertQueuedPartWithPiecesOrig) expect((await context.queuePart(newPart, [newPiece]))._id).toEqual( - playoutModel.Playlist.nextPartInfo?.partInstanceId + playoutModel.playlist.nextPartInfo?.partInstanceId ) expect(postProcessPiecesMock).toHaveBeenCalledTimes(1) @@ -1409,17 +1409,17 @@ describe('Test blueprint api context', () => { // Verify some properties not exposed to the blueprints const newPartInstance = playoutModel.getPartInstance( - playoutModel.Playlist.nextPartInfo!.partInstanceId + playoutModel.playlist.nextPartInfo!.partInstanceId )! expect(newPartInstance).toBeTruthy() - expect(newPartInstance.PartInstance.part._rank).toBeLessThan(9000) - expect(newPartInstance.PartInstance.part._rank).toBeGreaterThan(partInstance.part._rank) - expect(newPartInstance.PartInstance.orphaned).toEqual('adlib-part') + expect(newPartInstance.partInstance.part._rank).toBeLessThan(9000) + expect(newPartInstance.partInstance.part._rank).toBeGreaterThan(partInstance.part._rank) + expect(newPartInstance.partInstance.orphaned).toEqual('adlib-part') const newNextPartInstances = await context.getPieceInstances('next') expect(newNextPartInstances).toHaveLength(1) expect(newNextPartInstances[0].partInstanceId).toEqual( - unprotectString(newPartInstance.PartInstance._id) + unprotectString(newPartInstance.partInstance._id) ) expect(context.nextPartState).toEqual(ActionPartChange.SAFE_CHANGE) @@ -1463,7 +1463,7 @@ describe('Test blueprint api context', () => { expect(context2).toBe(jobContext) expect(playoutModel2).toBe(playoutModel) expect(showStyleBase).toBeTruthy() - expect(partInstance.PartInstance).toStrictEqual(currentPartInstance) + expect(partInstance.partInstance).toStrictEqual(currentPartInstance) expect(offset).toEqual(34) filter = filter2 @@ -1522,7 +1522,7 @@ describe('Test blueprint api context', () => { expect(context2).toBe(jobContext) expect(playoutModel2).toBe(playoutModel) expect(showStyleBase).toBeTruthy() - expect(partInstance.PartInstance).toStrictEqual(currentPartInstance) + expect(partInstance.partInstance).toStrictEqual(currentPartInstance) expect(offset).toEqual(34) filter = filter2 @@ -1554,15 +1554,15 @@ describe('Test blueprint api context', () => { } function getPieceInstanceCounts(playoutModel: PlayoutModel): PieceInstanceCounts { let other = 0 - for (const partInstance of playoutModel.OlderPartInstances) { - other += partInstance.PieceInstances.length + for (const partInstance of playoutModel.olderPartInstances) { + other += partInstance.pieceInstances.length } return { other, - previous: playoutModel.PreviousPartInstance?.PieceInstances?.length ?? 0, - current: playoutModel.CurrentPartInstance?.PieceInstances?.length ?? 0, - next: playoutModel.NextPartInstance?.PieceInstances?.length ?? 0, + previous: playoutModel.previousPartInstance?.pieceInstances?.length ?? 0, + current: playoutModel.currentPartInstance?.pieceInstances?.length ?? 0, + next: playoutModel.nextPartInstance?.pieceInstances?.length ?? 0, } } @@ -1601,7 +1601,7 @@ describe('Test blueprint api context', () => { const pieceInstanceFromOther = (await jobContext.mockCollections.PieceInstances.findOne({ rundownId, - partInstanceId: { $ne: partInstance.PartInstance._id }, + partInstanceId: { $ne: partInstance.partInstance._id }, })) as PieceInstance expect(pieceInstanceFromOther).toBeTruthy() @@ -1630,15 +1630,15 @@ describe('Test blueprint api context', () => { expect(beforePieceInstancesCounts.other).toEqual(0) // Find the instance, and create its backing piece - const targetPieceInstance = playoutModel.NextPartInstance!.PieceInstances[0] + const targetPieceInstance = playoutModel.nextPartInstance!.pieceInstances[0] expect(targetPieceInstance).toBeTruthy() await expect( - context.removePieceInstances('next', [unprotectString(targetPieceInstance.PieceInstance._id)]) - ).resolves.toEqual([unprotectString(targetPieceInstance.PieceInstance._id)]) + context.removePieceInstances('next', [unprotectString(targetPieceInstance.pieceInstance._id)]) + ).resolves.toEqual([unprotectString(targetPieceInstance.pieceInstance._id)]) // Ensure it was all removed - expect(playoutModel.findPieceInstance(targetPieceInstance.PieceInstance._id)).toBeFalsy() + expect(playoutModel.findPieceInstance(targetPieceInstance.pieceInstance._id)).toBeFalsy() expect(context.nextPartState).toEqual(ActionPartChange.SAFE_CHANGE) }) }) @@ -1697,7 +1697,7 @@ describe('Test blueprint api context', () => { const { context } = await getActionExecutionContext(jobContext, playoutModel) // Ensure there are no pending updates already - expect((playoutModel.NextPartInstance! as PlayoutPartInstanceModelImpl).HasAnyChanges()).toBeFalsy() + expect((playoutModel.nextPartInstance! as PlayoutPartInstanceModelImpl).hasAnyChanges()).toBeFalsy() // Update it and expect it to match const partInstance0Before = clone(partInstance0) @@ -1709,10 +1709,10 @@ describe('Test blueprint api context', () => { badProperty: 9, // This will be dropped } const resultPart = await context.updatePartInstance('next', partInstance0Delta) - const partInstance1 = playoutModel.NextPartInstance! as PlayoutPartInstanceModelImpl + const partInstance1 = playoutModel.nextPartInstance! as PlayoutPartInstanceModelImpl expect(partInstance1).toBeTruthy() - expect(resultPart).toEqual(convertPartInstanceToBlueprints(partInstance1.PartInstance)) + expect(resultPart).toEqual(convertPartInstanceToBlueprints(partInstance1.partInstance)) const pieceInstance0After = { ...partInstance0Before, @@ -1721,9 +1721,9 @@ describe('Test blueprint api context', () => { ..._.omit(partInstance0Delta, 'badProperty', '_id'), }, } - expect(partInstance1.PartInstance).toEqual(pieceInstance0After) - expect(partInstance1.PartInstanceHasChanges).toBeTruthy() - expect(partInstance1.ChangedPieceInstanceIds()).toHaveLength(0) + expect(partInstance1.partInstance).toEqual(pieceInstance0After) + expect(partInstance1.partInstanceHasChanges).toBeTruthy() + expect(partInstance1.changedPieceInstanceIds()).toHaveLength(0) expect(context.nextPartState).toEqual(ActionPartChange.SAFE_CHANGE) expect(context.currentPartState).toEqual(ActionPartChange.NONE) diff --git a/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts b/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts index b5c712951d..7d28375728 100644 --- a/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts +++ b/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts @@ -87,9 +87,9 @@ export class SyncIngestUpdateToPartInstanceContext }, ], this.showStyleCompound.blueprintId, - this.partInstance.PartInstance.rundownId, - this.partInstance.PartInstance.segmentId, - this.partInstance.PartInstance.part._id, + this.partInstance.partInstance.rundownId, + this.partInstance.partInstance.segmentId, + this.partInstance.partInstance.part._id, this.playStatus === 'current' )[0] : proposedPieceInstance.piece @@ -112,15 +112,15 @@ export class SyncIngestUpdateToPartInstanceContext this._context, [trimmedPiece], this.showStyleCompound.blueprintId, - this.partInstance.PartInstance.rundownId, - this.partInstance.PartInstance.segmentId, - this.partInstance.PartInstance.part._id, + this.partInstance.partInstance.rundownId, + this.partInstance.partInstance.segmentId, + this.partInstance.partInstance.part._id, this.playStatus === 'current' )[0] const newPieceInstance = this.partInstance.insertPlannedPiece(piece) - return convertPieceInstanceToBlueprints(newPieceInstance.PieceInstance) + return convertPieceInstanceToBlueprints(newPieceInstance.pieceInstance) } updatePieceInstance(pieceInstanceId: string, updatedPiece: Partial): IBlueprintPieceInstance { // filter the submission to the allowed ones @@ -135,7 +135,7 @@ export class SyncIngestUpdateToPartInstanceContext if (!pieceInstance) { throw new Error(`PieceInstance "${pieceInstanceId}" could not be found`) } - if (pieceInstance.PieceInstance.partInstanceId !== this.partInstance.PartInstance._id) { + if (pieceInstance.pieceInstance.partInstanceId !== this.partInstance.partInstance._id) { throw new Error(`PieceInstance "${pieceInstanceId}" does not belong to the current PartInstance`) } @@ -143,7 +143,7 @@ export class SyncIngestUpdateToPartInstanceContext if (trimmedPiece.content?.timelineObjects) { timelineObjectsString = serializePieceTimelineObjectsBlob( postProcessTimelineObjects( - pieceInstance.PieceInstance.piece._id, + pieceInstance.pieceInstance.piece._id, this.showStyleCompound.blueprintId, trimmedPiece.content.timelineObjects ) @@ -159,14 +159,14 @@ export class SyncIngestUpdateToPartInstanceContext }) } - return convertPieceInstanceToBlueprints(pieceInstance.PieceInstance) + return convertPieceInstanceToBlueprints(pieceInstance.pieceInstance) } updatePartInstance(updatePart: Partial): IBlueprintPartInstance { if (!this.partInstance) throw new Error(`PartInstance has been removed`) // for autoNext, the new expectedDuration cannot be shorter than the time a part has been on-air for - if (updatePart.expectedDuration && (updatePart.autoNext ?? this.partInstance.PartInstance.part.autoNext)) { - const onAir = this.partInstance.PartInstance.timings?.reportedStartedPlayback + if (updatePart.expectedDuration && (updatePart.autoNext ?? this.partInstance.partInstance.part.autoNext)) { + const onAir = this.partInstance.partInstance.timings?.reportedStartedPlayback const minTime = Date.now() - (onAir ?? 0) + EXPECTED_INGEST_TO_PLAYOUT_TIME if (onAir && minTime > updatePart.expectedDuration) { updatePart.expectedDuration = minTime @@ -177,7 +177,7 @@ export class SyncIngestUpdateToPartInstanceContext throw new Error(`Cannot update PartInstance. Some valid properties must be defined`) } - return convertPartInstanceToBlueprints(this.partInstance.PartInstance) + return convertPartInstanceToBlueprints(this.partInstance.partInstance) } removePartInstance(): void { @@ -190,11 +190,11 @@ export class SyncIngestUpdateToPartInstanceContext if (!this.partInstance) throw new Error(`PartInstance has been removed`) const rawPieceInstanceIdSet = new Set(protectStringArray(pieceInstanceIds)) - const pieceInstances = this.partInstance.PieceInstances.filter((p) => - rawPieceInstanceIdSet.has(p.PieceInstance._id) + const pieceInstances = this.partInstance.pieceInstances.filter((p) => + rawPieceInstanceIdSet.has(p.pieceInstance._id) ) - const pieceInstanceIdsToRemove = pieceInstances.map((p) => p.PieceInstance._id) + const pieceInstanceIdsToRemove = pieceInstances.map((p) => p.pieceInstance._id) for (const id of pieceInstanceIdsToRemove) { this.partInstance.removePieceInstance(id) diff --git a/packages/job-worker/src/blueprints/context/adlibActions.ts b/packages/job-worker/src/blueprints/context/adlibActions.ts index 6aa94a37b1..8300b5f20b 100644 --- a/packages/job-worker/src/blueprints/context/adlibActions.ts +++ b/packages/job-worker/src/blueprints/context/adlibActions.ts @@ -150,9 +150,9 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct private _getPartInstance(part: 'current' | 'next'): PlayoutPartInstanceModel | null { switch (part) { case 'current': - return this._playoutModel.CurrentPartInstance + return this._playoutModel.currentPartInstance case 'next': - return this._playoutModel.NextPartInstance + return this._playoutModel.nextPartInstance default: assertNever(part) logger.warn(`Blueprint action requested unknown PartInstance "${part}"`) @@ -163,11 +163,11 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct async getPartInstance(part: 'current' | 'next'): Promise { const partInstance = this._getPartInstance(part) - return partInstance ? convertPartInstanceToBlueprints(partInstance.PartInstance) : undefined + return partInstance ? convertPartInstanceToBlueprints(partInstance.partInstance) : undefined } async getPieceInstances(part: 'current' | 'next'): Promise { const partInstance = this._getPartInstance(part) - return partInstance?.PieceInstances?.map((p) => convertPieceInstanceToBlueprints(p.PieceInstance)) ?? [] + return partInstance?.pieceInstances?.map((p) => convertPieceInstanceToBlueprints(p.pieceInstance)) ?? [] } async getResolvedPieceInstances(part: 'current' | 'next'): Promise { const partInstance = this._getPartInstance(part) @@ -200,8 +200,8 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct } } - if (options?.excludeCurrentPart && this._playoutModel.Playlist.currentPartInfo) { - query['partInstanceId'] = { $ne: this._playoutModel.Playlist.currentPartInfo.partInstanceId } + if (options?.excludeCurrentPart && this._playoutModel.playlist.currentPartInfo) { + query['partInstanceId'] = { $ne: this._playoutModel.playlist.currentPartInfo.partInstanceId } } const sourceLayerId = Array.isArray(sourceLayerId0) ? sourceLayerId0 : [sourceLayerId0] @@ -233,8 +233,8 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct } } - if (options?.excludeCurrentPart && this._playoutModel.CurrentPartInstance) { - query['startPartId'] = { $ne: this._playoutModel.CurrentPartInstance.PartInstance.part._id } + if (options?.excludeCurrentPart && this._playoutModel.currentPartInstance) { + query['startPartId'] = { $ne: this._playoutModel.currentPartInstance.partInstance.part._id } } const sourceLayerId = Array.isArray(sourceLayerId0) ? sourceLayerId0 : [sourceLayerId0] @@ -258,7 +258,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct const loadedPartInstanceModel = this._playoutModel.getPartInstance(partInstanceId) if (loadedPartInstanceModel) { - return convertPartInstanceToBlueprints(loadedPartInstanceModel.PartInstance) + return convertPartInstanceToBlueprints(loadedPartInstanceModel.partInstance) } // It might be reset and so not in the loaded model @@ -297,7 +297,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct throw new Error('Cannot insert piece when no active part') } - const rundown = this._playoutModel.getRundown(partInstance.PartInstance.rundownId) + const rundown = this._playoutModel.getRundown(partInstance.partInstance.rundownId) if (!rundown) { throw new Error('Failed to find rundown of partInstance') } @@ -308,9 +308,9 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct this._context, [trimmedPiece], this.showStyleCompound.blueprintId, - partInstance.PartInstance.rundownId, - partInstance.PartInstance.segmentId, - partInstance.PartInstance.part._id, + partInstance.partInstance.rundownId, + partInstance.partInstance.segmentId, + partInstance.partInstance.part._id, part === 'current' )[0] piece._id = getRandomId() // Make id random, as postProcessPieces is too predictable (for ingest) @@ -324,7 +324,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct this.nextPartState = Math.max(this.nextPartState, ActionPartChange.SAFE_CHANGE) } - return convertPieceInstanceToBlueprints(newPieceInstance.PieceInstance) + return convertPieceInstanceToBlueprints(newPieceInstance.pieceInstance) } async updatePieceInstance( pieceInstanceId: string, @@ -343,16 +343,16 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct const { pieceInstance } = foundPieceInstance - if (pieceInstance.PieceInstance.infinite?.fromPreviousPart) { + if (pieceInstance.pieceInstance.infinite?.fromPreviousPart) { throw new Error('Cannot update an infinite piece that is continued from a previous part') } const updatesCurrentPart: ActionPartChange = - pieceInstance.PieceInstance.partInstanceId === this._playoutModel.Playlist.currentPartInfo?.partInstanceId + pieceInstance.pieceInstance.partInstanceId === this._playoutModel.playlist.currentPartInfo?.partInstanceId ? ActionPartChange.SAFE_CHANGE : ActionPartChange.NONE const updatesNextPart: ActionPartChange = - pieceInstance.PieceInstance.partInstanceId === this._playoutModel.Playlist.nextPartInfo?.partInstanceId + pieceInstance.pieceInstance.partInstanceId === this._playoutModel.playlist.nextPartInfo?.partInstanceId ? ActionPartChange.SAFE_CHANGE : ActionPartChange.NONE if (!updatesCurrentPart && !updatesNextPart) { @@ -363,7 +363,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct if (trimmedPiece.content?.timelineObjects) { timelineObjectsString = serializePieceTimelineObjectsBlob( postProcessTimelineObjects( - pieceInstance.PieceInstance.piece._id, + pieceInstance.pieceInstance.piece._id, this.showStyleCompound.blueprintId, trimmedPiece.content.timelineObjects ) @@ -380,10 +380,10 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct this.nextPartState = Math.max(this.nextPartState, updatesNextPart) this.currentPartState = Math.max(this.currentPartState, updatesCurrentPart) - return convertPieceInstanceToBlueprints(pieceInstance.PieceInstance) + return convertPieceInstanceToBlueprints(pieceInstance.pieceInstance) } async queuePart(rawPart: IBlueprintPart, rawPieces: IBlueprintPiece[]): Promise { - const currentPartInstance = this._playoutModel.CurrentPartInstance + const currentPartInstance = this._playoutModel.currentPartInstance if (!currentPartInstance) { throw new Error('Cannot queue part when no current partInstance') } @@ -395,7 +395,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct throw new Error('Cannot queue part when next part has already been modified') } - if (isTooCloseToAutonext(currentPartInstance.PartInstance, true)) { + if (isTooCloseToAutonext(currentPartInstance.partInstance, true)) { throw new Error('Too close to an autonext to queue a part') } @@ -418,8 +418,8 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct this._context, rawPieces, this.showStyleCompound.blueprintId, - currentPartInstance.PartInstance.rundownId, - currentPartInstance.PartInstance.segmentId, + currentPartInstance.partInstance.rundownId, + currentPartInstance.partInstance.segmentId, newPart._id, false ) @@ -440,9 +440,9 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct ) this.nextPartState = ActionPartChange.SAFE_CHANGE - this.queuedPartInstanceId = newPartInstance.PartInstance._id + this.queuedPartInstanceId = newPartInstance.partInstance._id - return convertPartInstanceToBlueprints(newPartInstance.PartInstance) + return convertPartInstanceToBlueprints(newPartInstance.partInstance) } async moveNextPart(partDelta: number, segmentDelta: number): Promise { await moveNextPart(this._context, this._playoutModel, partDelta, segmentDelta) @@ -469,7 +469,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct part === 'current' ? ActionPartChange.SAFE_CHANGE : ActionPartChange.NONE ) - return convertPartInstanceToBlueprints(partInstance.PartInstance) + return convertPartInstanceToBlueprints(partInstance.partInstance) } async stopPiecesOnLayers(sourceLayerIds: string[], timeOffset?: number | undefined): Promise { @@ -523,7 +523,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct if (time !== null && (time < getCurrentTime() || typeof time !== 'number')) throw new Error('Cannot block taking out of the current part, to a time in the past') - const partInstance = this._playoutModel.CurrentPartInstance + const partInstance = this._playoutModel.currentPartInstance if (!partInstance) { throw new Error('Cannot block take when there is no part playing') } @@ -535,10 +535,10 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct filter: (pieceInstance: ReadonlyDeep) => boolean, timeOffset: number | undefined ) { - if (!this._playoutModel.Playlist.currentPartInfo) { + if (!this._playoutModel.playlist.currentPartInfo) { return [] } - const partInstance = this._playoutModel.CurrentPartInstance + const partInstance = this._playoutModel.currentPartInstance if (!partInstance) { throw new Error('Cannot stop pieceInstances when no current partInstance') } diff --git a/packages/job-worker/src/cache/CacheBase.ts b/packages/job-worker/src/cache/CacheBase.ts index 575eacfb67..a8bcc8d6cf 100644 --- a/packages/job-worker/src/cache/CacheBase.ts +++ b/packages/job-worker/src/cache/CacheBase.ts @@ -39,7 +39,7 @@ export abstract class ReadOnlyCacheBase> impl context.trackCache(this) } - abstract DisplayName: string + abstract displayName: string private getAllCollections() { const highPrioDBs: DbCacheWritable[] = [] diff --git a/packages/job-worker/src/ingest/__tests__/ingest.test.ts b/packages/job-worker/src/ingest/__tests__/ingest.test.ts index aac8492c3e..ed49bf6fad 100644 --- a/packages/job-worker/src/ingest/__tests__/ingest.test.ts +++ b/packages/job-worker/src/ingest/__tests__/ingest.test.ts @@ -2006,10 +2006,10 @@ describe('Test ingest actions for rundowns and segments', () => { }, null, async (cache) => { - const rundown0 = cache.Rundowns[0] + const rundown0 = cache.rundowns[0] expect(rundown0).toBeTruthy() - const currentPartInstance = cache.CurrentPartInstance as PlayoutPartInstanceModel + const currentPartInstance = cache.currentPartInstance as PlayoutPartInstanceModel expect(currentPartInstance).toBeTruthy() // Simulate a queued part @@ -2019,9 +2019,9 @@ describe('Test ingest actions for rundowns and segments', () => { rundown0, currentPartInstance, { - _id: protectString(`after_${currentPartInstance.PartInstance._id}_part`), + _id: protectString(`after_${currentPartInstance.partInstance._id}_part`), _rank: 0, - externalId: `after_${currentPartInstance.PartInstance._id}_externalId`, + externalId: `after_${currentPartInstance.partInstance._id}_externalId`, title: 'New part', expectedDurationWithPreroll: undefined, }, @@ -2029,7 +2029,7 @@ describe('Test ingest actions for rundowns and segments', () => { undefined ) - return newPartInstance.PartInstance._id + return newPartInstance.partInstance._id } ) diff --git a/packages/job-worker/src/ingest/cache.ts b/packages/job-worker/src/ingest/cache.ts index fe6b6adce2..a9ed2d06a0 100644 --- a/packages/job-worker/src/ingest/cache.ts +++ b/packages/job-worker/src/ingest/cache.ts @@ -51,7 +51,7 @@ export class CacheForIngest extends CacheBase { return this.Rundown.doc?._id ?? getRundownId(this.context.studioId, this.RundownExternalId) } - public get DisplayName(): string { + public get displayName(): string { return `CacheForIngest "${this.RundownExternalId}"` } diff --git a/packages/job-worker/src/ingest/commit.ts b/packages/job-worker/src/ingest/commit.ts index b9410f533d..664f33ee8e 100644 --- a/packages/job-worker/src/ingest/commit.ts +++ b/packages/job-worker/src/ingest/commit.ts @@ -230,14 +230,14 @@ export async function CommitIngestOperation( // Run in the background, we don't want to hold onto the lock to do this context .queueEventJob(EventsJobs.RundownDataChanged, { - playlistId: playoutCache.PlaylistId, + playlistId: playoutCache.playlistId, rundownId: ingestCache.RundownId, }) .catch((e) => { logger.error(`Queue RundownDataChanged failed: ${e}`) }) - triggerUpdateTimelineAfterIngestData(context, playoutCache.PlaylistId) + triggerUpdateTimelineAfterIngestData(context, playoutCache.playlistId) }) // wait for the ingest changes to save @@ -354,7 +354,7 @@ export function hackConvertIngestCacheToRundownWithSegments(cache: ReadOnlyCache const segmentsWithParts = cache.Segments.findAll(null).map( (segment) => new PlayoutSegmentModelImpl(segment, groupedParts.get(segment._id) ?? []) ) - const groupedSegmentsWithParts = groupByToMapFunc(segmentsWithParts, (s) => s.Segment.rundownId) + const groupedSegmentsWithParts = groupByToMapFunc(segmentsWithParts, (s) => s.segment.rundownId) return new PlayoutRundownModelImpl(rundown, groupedSegmentsWithParts.get(rundown._id) ?? [], []) } @@ -375,7 +375,7 @@ async function updatePartInstancesBasicProperties( await context.directCollections.PartInstances.findFetch( { reset: { $ne: true }, - rundownId: rundownModel.Rundown._id, + rundownId: rundownModel.rundown._id, orphaned: { $exists: false }, 'part._id': { $nin: knownPartIds }, }, @@ -494,12 +494,12 @@ export async function updatePlayoutAfterChangingRundownInPlaylist( ): Promise { // ensure the 'old' playout is updated to remove any references to the rundown await runWithPlayoutModel(context, playlist, playlistLock, null, async (playoutCache) => { - if (playoutCache.Rundowns.length === 0) { - if (playoutCache.Playlist.activationId) - throw new Error(`RundownPlaylist "${playoutCache.PlaylistId}" has no contents but is active...`) + if (playoutCache.rundowns.length === 0) { + if (playoutCache.playlist.activationId) + throw new Error(`RundownPlaylist "${playoutCache.playlistId}" has no contents but is active...`) // Remove an empty playlist - await context.directCollections.RundownPlaylists.remove({ _id: playoutCache.PlaylistId }) + await context.directCollections.RundownPlaylists.remove({ _id: playoutCache.playlistId }) playoutCache.assertNoChanges() return @@ -511,14 +511,14 @@ export async function updatePlayoutAfterChangingRundownInPlaylist( const rundownModel = playoutCache.getRundown(insertedRundown._id) if (rundownModel) { // If a rundown has changes, ensure instances are updated - await updatePartInstancesBasicProperties(context, rundownModel, playoutCache.Playlist) + await updatePartInstancesBasicProperties(context, rundownModel, playoutCache.playlist) } } await ensureNextPartIsValid(context, playoutCache) - if (playoutCache.Playlist.activationId) { - triggerUpdateTimelineAfterIngestData(context, playoutCache.PlaylistId) + if (playoutCache.playlist.activationId) { + triggerUpdateTimelineAfterIngestData(context, playoutCache.playlistId) } }) } @@ -836,15 +836,15 @@ async function preserveUnsyncedPlayingSegmentContents( } async function validateScratchpad(_context: JobContext, playoutModel: PlayoutModel) { - for (const rundown of playoutModel.Rundowns) { + for (const rundown of playoutModel.rundowns) { const scratchpadSegment = rundown.getScratchpadSegment() if (scratchpadSegment) { // Ensure the _rank is just before the real content - const otherSegmentsInRundown = rundown.Segments.filter( - (s) => s.Segment.orphaned !== SegmentOrphanedReason.SCRATCHPAD + const otherSegmentsInRundown = rundown.segments.filter( + (s) => s.segment.orphaned !== SegmentOrphanedReason.SCRATCHPAD ) - const minNormalRank = Math.min(0, ...otherSegmentsInRundown.map((s) => s.Segment._rank)) + const minNormalRank = Math.min(0, ...otherSegmentsInRundown.map((s) => s.segment._rank)) rundown.setScratchpadSegmentRank(minNormalRank - 1) } diff --git a/packages/job-worker/src/ingest/syncChangesToPartInstance.ts b/packages/job-worker/src/ingest/syncChangesToPartInstance.ts index f8d5f37aa8..b306d6fee5 100644 --- a/packages/job-worker/src/ingest/syncChangesToPartInstance.ts +++ b/packages/job-worker/src/ingest/syncChangesToPartInstance.ts @@ -51,20 +51,20 @@ export async function syncChangesToPartInstances( playoutModel: PlayoutModel, ingestCache: ReadOnlyCache ): Promise { - if (playoutModel.Playlist.activationId) { + if (playoutModel.playlist.activationId) { // Get the final copy of the rundown const rundownWrapped = hackConvertIngestCacheToRundownWithSegments(ingestCache) const showStyle = await context.getShowStyleCompound( - rundownWrapped.Rundown.showStyleVariantId, - rundownWrapped.Rundown.showStyleBaseId + rundownWrapped.rundown.showStyleVariantId, + rundownWrapped.rundown.showStyleBaseId ) const blueprint = await context.getShowStyleBlueprint(showStyle._id) if (blueprint.blueprint.syncIngestUpdateToPartInstance) { - const currentPartInstance = playoutModel.CurrentPartInstance - const nextPartInstance = playoutModel.NextPartInstance - const previousPartInstance = playoutModel.PreviousPartInstance + const currentPartInstance = playoutModel.currentPartInstance + const nextPartInstance = playoutModel.nextPartInstance + const previousPartInstance = playoutModel.previousPartInstance const instances: SyncedInstance[] = [] if (currentPartInstance) { @@ -73,10 +73,10 @@ export async function syncChangesToPartInstances( // changed (like timing) that will affect what's going on. // The previous "planned" Part Instance needs to be inserted into the `instances` first, so that // it's ran first through the blueprints. - if (currentPartInstance.PartInstance.orphaned === 'adlib-part') { + if (currentPartInstance.partInstance.orphaned === 'adlib-part') { const partAndPartInstance = findLastUnorphanedPartInstanceInSegment( playoutModel, - currentPartInstance.PartInstance + currentPartInstance.partInstance ) if (partAndPartInstance) { insertToSyncedInstanceCandidates( @@ -110,7 +110,7 @@ export async function syncChangesToPartInstances( ingestCache, nextPartInstance, currentPartInstance, - isTooCloseToAutonext(currentPartInstance?.PartInstance, false) ? 'current' : 'next' + isTooCloseToAutonext(currentPartInstance?.partInstance, false) ? 'current' : 'next' ) } @@ -121,12 +121,12 @@ export async function syncChangesToPartInstances( newPart, piecesThatMayBeActive, } of instances) { - const pieceInstancesInPart = existingPartInstance.PieceInstances + const pieceInstancesInPart = existingPartInstance.pieceInstances - const partId = existingPartInstance.PartInstance.part._id + const partId = existingPartInstance.partInstance.part._id const existingResultPartInstance: BlueprintSyncIngestPartInstance = { - partInstance: convertPartInstanceToBlueprints(existingPartInstance.PartInstance), - pieceInstances: pieceInstancesInPart.map((p) => convertPieceInstanceToBlueprints(p.PieceInstance)), + partInstance: convertPartInstanceToBlueprints(existingPartInstance.partInstance), + pieceInstances: pieceInstancesInPart.map((p) => convertPieceInstanceToBlueprints(p.pieceInstance)), } const proposedPieceInstances = getPieceInstancesForPart( @@ -134,15 +134,15 @@ export async function syncChangesToPartInstances( playoutModel, previousPartInstance, rundownWrapped, - newPart ?? existingPartInstance.PartInstance.part, + newPart ?? existingPartInstance.partInstance.part, await piecesThatMayBeActive, - existingPartInstance.PartInstance._id + existingPartInstance.partInstance._id ) logger.info(`Syncing ingest changes for part: ${partId} (orphaned: ${!!newPart})`) const referencedAdlibIds = new Set( - _.compact(pieceInstancesInPart.map((p) => p.PieceInstance.adLibSourceId)) + _.compact(pieceInstancesInPart.map((p) => p.pieceInstance.adLibSourceId)) ) const newResultData: BlueprintSyncIngestNewData = { part: newPart ? convertPartToBlueprints(newPart) : undefined, @@ -167,12 +167,12 @@ export async function syncChangesToPartInstances( const syncContext = new SyncIngestUpdateToPartInstanceContext( context, { - name: `Update to ${existingPartInstance.PartInstance.part.externalId}`, - identifier: `rundownId=${existingPartInstance.PartInstance.part.rundownId},segmentId=${existingPartInstance.PartInstance.part.segmentId}`, + name: `Update to ${existingPartInstance.partInstance.part.externalId}`, + identifier: `rundownId=${existingPartInstance.partInstance.part.rundownId},segmentId=${existingPartInstance.partInstance.part.segmentId}`, }, context.studio, showStyle, - rundownWrapped.Rundown, + rundownWrapped.rundown, existingPartInstance, proposedPieceInstances, playStatus @@ -218,13 +218,13 @@ export async function syncChangesToPartInstances( validateScratchpartPartInstanceProperties(context, playoutModel, existingPartInstance) } - if (existingPartInstance.PartInstance._id === playoutModel.Playlist.currentPartInfo?.partInstanceId) { + if (existingPartInstance.partInstance._id === playoutModel.playlist.currentPartInfo?.partInstanceId) { // This should be run after 'current', before 'next': await syncPlayheadInfinitesForNextPartInstance( context, playoutModel, - playoutModel.CurrentPartInstance, - playoutModel.NextPartInstance + playoutModel.currentPartInstance, + playoutModel.nextPartInstance ) } } @@ -256,7 +256,7 @@ function insertToSyncedInstanceCandidates( context, playoutModel, ingestCache, - part ?? thisPartInstance.PartInstance.part + part ?? thisPartInstance.partInstance.part ), }) } @@ -274,7 +274,7 @@ function findPartAndInsertToSyncedInstanceCandidates( previousPartInstance: PlayoutPartInstanceModel | null, playStatus: PlayStatus ): void { - const newPart = playoutModel.findPart(thisPartInstance.PartInstance.part._id) + const newPart = playoutModel.findPart(thisPartInstance.partInstance.part._id) insertToSyncedInstanceCandidates( context, @@ -300,19 +300,21 @@ function findLastUnorphanedPartInstanceInSegment( part: ReadonlyDeep } | null { // Find the "latest" (last played), non-orphaned PartInstance in this Segment, in this play-through - const previousPartInstance = playoutModel.SortedLoadedPartInstances.reverse().find( - (p) => - p.PartInstance.playlistActivationId === currentPartInstance.playlistActivationId && - p.PartInstance.segmentId === currentPartInstance.segmentId && - p.PartInstance.segmentPlayoutId === currentPartInstance.segmentPlayoutId && - p.PartInstance.takeCount < currentPartInstance.takeCount && - !!p.PartInstance.orphaned && - !p.PartInstance.reset - ) + const previousPartInstance = playoutModel.sortedLoadedPartInstances + .reverse() + .find( + (p) => + p.partInstance.playlistActivationId === currentPartInstance.playlistActivationId && + p.partInstance.segmentId === currentPartInstance.segmentId && + p.partInstance.segmentPlayoutId === currentPartInstance.segmentPlayoutId && + p.partInstance.takeCount < currentPartInstance.takeCount && + !!p.partInstance.orphaned && + !p.partInstance.reset + ) if (!previousPartInstance) return null - const previousPart = playoutModel.findPart(previousPartInstance.PartInstance.part._id) + const previousPart = playoutModel.findPart(previousPartInstance.partInstance.part._id) if (!previousPart) return null return { diff --git a/packages/job-worker/src/ingest/updateNext.ts b/packages/job-worker/src/ingest/updateNext.ts index 19c00b0425..8fe4157ea2 100644 --- a/packages/job-worker/src/ingest/updateNext.ts +++ b/packages/job-worker/src/ingest/updateNext.ts @@ -16,16 +16,16 @@ export async function ensureNextPartIsValid(context: JobContext, playoutModel: P const span = context.startSpan('api.ingest.ensureNextPartIsValid') // Ensure the next-id is still valid - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist if (playlist?.activationId) { - const currentPartInstance = playoutModel.CurrentPartInstance - const nextPartInstance = playoutModel.NextPartInstance + const currentPartInstance = playoutModel.currentPartInstance + const nextPartInstance = playoutModel.nextPartInstance if ( playlist.nextPartInfo?.manuallySelected && nextPartInstance && - isPartPlayable(nextPartInstance.PartInstance.part) && - nextPartInstance.PartInstance.orphaned !== 'deleted' + isPartPlayable(nextPartInstance.partInstance.part) && + nextPartInstance.partInstance.orphaned !== 'deleted' ) { // Manual next part is almost always valid. This includes orphaned (adlib-part) partinstances span?.end() @@ -33,7 +33,7 @@ export async function ensureNextPartIsValid(context: JobContext, playoutModel: P } // If we are close to an autonext, then leave it to avoid glitches - if (isTooCloseToAutonext(currentPartInstance?.PartInstance) && nextPartInstance) { + if (isTooCloseToAutonext(currentPartInstance?.partInstance) && nextPartInstance) { span?.end() return } @@ -46,8 +46,8 @@ export async function ensureNextPartIsValid(context: JobContext, playoutModel: P const newNextPart = selectNextPart( context, playlist, - currentPartInstance.PartInstance, - nextPartInstance.PartInstance, + currentPartInstance.partInstance, + nextPartInstance.partInstance, orderedSegments, orderedParts ) @@ -56,22 +56,22 @@ export async function ensureNextPartIsValid(context: JobContext, playoutModel: P // Nothing should be nexted !newNextPart || // The nexted-part should be different to what is selected - newNextPart.part._id !== nextPartInstance.PartInstance.part._id || + newNextPart.part._id !== nextPartInstance.partInstance.part._id || // The nexted-part Instance is no longer playable - !isPartPlayable(nextPartInstance.PartInstance.part) + !isPartPlayable(nextPartInstance.partInstance.part) ) { // The 'new' next part is before the current next, so move the next point await setNextPart(context, playoutModel, newNextPart ?? null, false) await updateTimeline(context, playoutModel) } - } else if (!nextPartInstance || nextPartInstance.PartInstance.orphaned === 'deleted') { + } else if (!nextPartInstance || nextPartInstance.partInstance.orphaned === 'deleted') { // Don't have a nextPart or it has been deleted, so autoselect something const newNextPart = selectNextPart( context, playlist, - currentPartInstance?.PartInstance ?? null, - nextPartInstance?.PartInstance ?? null, + currentPartInstance?.partInstance ?? null, + nextPartInstance?.partInstance ?? null, orderedSegments, orderedParts ) diff --git a/packages/job-worker/src/modelBase.ts b/packages/job-worker/src/modelBase.ts index 537666fb46..2ad23599de 100644 --- a/packages/job-worker/src/modelBase.ts +++ b/packages/job-worker/src/modelBase.ts @@ -5,7 +5,7 @@ export interface BaseModel { /** * Name to display in debug logs about this Model */ - readonly DisplayName: string + readonly displayName: string /** * Mark the model as disposed diff --git a/packages/job-worker/src/peripheralDevice.ts b/packages/job-worker/src/peripheralDevice.ts index 69aac9581f..41bf718a44 100644 --- a/packages/job-worker/src/peripheralDevice.ts +++ b/packages/job-worker/src/peripheralDevice.ts @@ -193,7 +193,7 @@ export async function listPlayoutDevices( playoutModel: PlayoutModel ): Promise { const parentDevicesMap = normalizeArrayToMap( - playoutModel.PeripheralDevices.filter( + playoutModel.peripheralDevices.filter( (doc) => doc.studioId === context.studioId && doc.type === PeripheralDeviceType.PLAYOUT ), '_id' diff --git a/packages/job-worker/src/playout/__tests__/infinites.test.ts b/packages/job-worker/src/playout/__tests__/infinites.test.ts index 7a9b226fd9..ad14b2c2e0 100644 --- a/packages/job-worker/src/playout/__tests__/infinites.test.ts +++ b/packages/job-worker/src/playout/__tests__/infinites.test.ts @@ -43,7 +43,7 @@ describe('canContinueAdlibOnEndInfinites', () => { expect(rundown).toBeTruthy() return runJobWithPlayoutModel(context, { playlistId: tmpPlaylist._id }, null, async (playoutModel) => { - const playlist = playoutModel.Playlist as SetRequired, 'activationId'> + const playlist = playoutModel.playlist as SetRequired, 'activationId'> if (!playlist.activationId) throw new Error('Missing activationId') return fcn(playoutModel, playlist) }) diff --git a/packages/job-worker/src/playout/__tests__/timeline.test.ts b/packages/job-worker/src/playout/__tests__/timeline.test.ts index 71112c6ef4..22826631b4 100644 --- a/packages/job-worker/src/playout/__tests__/timeline.test.ts +++ b/packages/job-worker/src/playout/__tests__/timeline.test.ts @@ -188,7 +188,7 @@ function checkTimingsRaw( const objs = normalizeArrayToMap(timelineObjs, 'id') // previous part group - const prevPartTlObj = previousPartInstance ? objs.get(getPartGroupId(previousPartInstance.PartInstance)) : undefined + const prevPartTlObj = previousPartInstance ? objs.get(getPartGroupId(previousPartInstance.partInstance)) : undefined if (expectedTimings.previousPart) { expect(prevPartTlObj).toBeTruthy() expect(prevPartTlObj?.enable).toMatchObject(expectedTimings.previousPart) @@ -199,17 +199,17 @@ function checkTimingsRaw( // current part group is assumed to start at now // Current pieces - const currentPieces = currentPartInstance.PieceInstances + const currentPieces = currentPartInstance.pieceInstances const targetCurrentPieces: PartTimelineTimings['currentPieces'] = {} const targetCurrentInfinitePieces: PartTimelineTimings['currentInfinitePieces'] = {} for (const piece of currentPieces) { - let entryId = unprotectString(piece.PieceInstance.piece._id) + let entryId = unprotectString(piece.pieceInstance.piece._id) if (entryId.startsWith(unprotectString(rundownId))) entryId = entryId.substring(unprotectString(rundownId).length + 1) - if (piece.PieceInstance.piece.lifespan === PieceLifespan.WithinPart) { - const pieceObj = objs.get(getPieceGroupId(piece.PieceInstance)) - const controlObj = objs.get(getPieceControlObjectId(piece.PieceInstance)) + if (piece.pieceInstance.piece.lifespan === PieceLifespan.WithinPart) { + const pieceObj = objs.get(getPieceGroupId(piece.pieceInstance)) + const controlObj = objs.get(getPieceControlObjectId(piece.pieceInstance)) targetCurrentPieces[entryId] = controlObj ? { @@ -219,13 +219,13 @@ function checkTimingsRaw( : null } else { const partGroupId = - getPartGroupId(protectString(unprotectString(piece.PieceInstance._id))) + '_infinite' + getPartGroupId(protectString(unprotectString(piece.pieceInstance._id))) + '_infinite' const partObj = objs.get(partGroupId) if (!partObj) { targetCurrentInfinitePieces[entryId] = null } else { - const pieceObj = objs.get(getPieceGroupId(piece.PieceInstance)) - const controlObj = objs.get(getPieceControlObjectId(piece.PieceInstance)) + const pieceObj = objs.get(getPieceGroupId(piece.pieceInstance)) + const controlObj = objs.get(getPieceControlObjectId(piece.pieceInstance)) targetCurrentInfinitePieces[entryId] = { partGroup: partObj.enable, @@ -242,14 +242,14 @@ function checkTimingsRaw( if (previousPartInstance) { // Previous pieces - const previousPieces = previousPartInstance.PieceInstances + const previousPieces = previousPartInstance.pieceInstances let previousOutTransition: PartTimelineTimings['previousOutTransition'] for (const piece of previousPieces) { - if (piece.PieceInstance.piece.pieceType === IBlueprintPieceType.OutTransition) { + if (piece.pieceInstance.piece.pieceType === IBlueprintPieceType.OutTransition) { if (previousOutTransition !== undefined) throw new Error('Too many out transition pieces were found') - const pieceObj = objs.get(getPieceGroupId(piece.PieceInstance)) - const controlObj = objs.get(getPieceControlObjectId(piece.PieceInstance)) + const pieceObj = objs.get(getPieceGroupId(piece.pieceInstance)) + const controlObj = objs.get(getPieceControlObjectId(piece.pieceInstance)) previousOutTransition = controlObj ? { childGroup: parsePieceGroupPrerollAndPostroll(pieceObj?.enable ?? []), @@ -955,7 +955,7 @@ describe('Timeline', () => { const { currentPartInstance } = await getPartInstances() await checkTimings({ // old part ends immediately - previousPart: { end: `#${getPartGroupId(currentPartInstance!.PartInstance)}.start + 0` }, + previousPart: { end: `#${getPartGroupId(currentPartInstance!.partInstance)}.start + 0` }, currentPieces: { // pieces are not delayed piece010: { @@ -1115,7 +1115,7 @@ describe('Timeline', () => { const { currentPartInstance } = await getPartInstances() await checkTimings({ - previousPart: { end: `#${getPartGroupId(currentPartInstance!.PartInstance)}.start + 500` }, // note: this seems odd, but the pieces are delayed to compensate + previousPart: { end: `#${getPartGroupId(currentPartInstance!.partInstance)}.start + 500` }, // note: this seems odd, but the pieces are delayed to compensate currentPieces: { piece010: { controlObj: { start: 500 }, // note: Offset matches extension of previous partGroup @@ -1132,11 +1132,11 @@ describe('Timeline', () => { describe('Adlib pieces', () => { async function doStartAdlibPiece(playlistId: RundownPlaylistId, adlibSource: AdLibPiece) { await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => { - const currentPartInstance = playoutModel.CurrentPartInstance as PlayoutPartInstanceModel + const currentPartInstance = playoutModel.currentPartInstance as PlayoutPartInstanceModel expect(currentPartInstance).toBeTruthy() const rundown = playoutModel.getRundown( - currentPartInstance.PartInstance.rundownId + currentPartInstance.partInstance.rundownId ) as PlayoutRundownModel expect(rundown).toBeTruthy() @@ -1228,7 +1228,7 @@ describe('Timeline', () => { playlistId, literal({ _id: protectString('adlib1'), - rundownId: currentPartInstance!.PartInstance.rundownId, + rundownId: currentPartInstance!.partInstance.rundownId, externalId: 'fake', name: 'Adlibbed piece', lifespan: PieceLifespan.WithinPart, @@ -1250,7 +1250,7 @@ describe('Timeline', () => { controlObj: { start: 500, // This one gave the preroll end: `#piece_group_control_${ - currentPartInstance!.PartInstance._id + currentPartInstance!.partInstance._id }_${rundownId}_piece000_cap_now.start + 0`, }, childGroup: { @@ -1393,7 +1393,7 @@ describe('Timeline', () => { playlistId, literal({ _id: protectString('adlib1'), - rundownId: currentPartInstance!.PartInstance.rundownId, + rundownId: currentPartInstance!.partInstance.rundownId, externalId: 'fake', name: 'Adlibbed piece', lifespan: PieceLifespan.WithinPart, @@ -1416,7 +1416,7 @@ describe('Timeline', () => { controlObj: { start: 500, // This one gave the preroll end: `#piece_group_control_${ - currentPartInstance!.PartInstance._id + currentPartInstance!.partInstance._id }_${_rundownId}_piece000_cap_now.start + 0`, }, childGroup: { @@ -1437,7 +1437,7 @@ describe('Timeline', () => { // Our adlibbed piece controlObj: { start: `#piece_group_control_${ - currentPartInstance!.PartInstance._id + currentPartInstance!.partInstance._id }_${adlibbedPieceId}_start_now + 340`, }, childGroup: { @@ -1559,31 +1559,31 @@ describe('Timeline', () => { }, }, partGroup: { - start: `#part_group_${currentPartInstance.PartInstance._id}.start`, + start: `#part_group_${currentPartInstance.partInstance._id}.start`, }, }, }, previousOutTransition: undefined, }) - const currentPieceInstances = currentPartInstance.PieceInstances + const currentPieceInstances = currentPartInstance.pieceInstances const pieceInstance0 = currentPieceInstances.find( - (instance) => instance.PieceInstance.piece._id === protectString(`${rundownId}_piece000`) + (instance) => instance.pieceInstance.piece._id === protectString(`${rundownId}_piece000`) ) if (!pieceInstance0) throw new Error('pieceInstance0 must be defined') const pieceInstance1 = currentPieceInstances.find( - (instance) => instance.PieceInstance.piece._id === protectString(`${rundownId}_piece001`) + (instance) => instance.pieceInstance.piece._id === protectString(`${rundownId}_piece001`) ) if (!pieceInstance1) throw new Error('pieceInstance1 must be defined') const currentTime = 12300 await doOnPlayoutPlaybackChanged(context, playlistId, { baseTime: currentTime, - partId: currentPartInstance.PartInstance._id, + partId: currentPartInstance.partInstance._id, includePart: true, pieceOffsets: { - [unprotectString(pieceInstance0.PieceInstance._id)]: 500, - [unprotectString(pieceInstance1.PieceInstance._id)]: 500, + [unprotectString(pieceInstance0.pieceInstance._id)]: 500, + [unprotectString(pieceInstance1.pieceInstance._id)]: 500, }, }) diff --git a/packages/job-worker/src/playout/activePlaylistActions.ts b/packages/job-worker/src/playout/activePlaylistActions.ts index 67a56abe41..79e709c534 100644 --- a/packages/job-worker/src/playout/activePlaylistActions.ts +++ b/packages/job-worker/src/playout/activePlaylistActions.ts @@ -18,15 +18,15 @@ export async function activateRundownPlaylist( playoutModel: PlayoutModel, rehearsal: boolean ): Promise { - logger.info('Activating rundown ' + playoutModel.Playlist._id + (rehearsal ? ' (Rehearsal)' : '')) + logger.info('Activating rundown ' + playoutModel.playlist._id + (rehearsal ? ' (Rehearsal)' : '')) rehearsal = !!rehearsal - const wasActive = !!playoutModel.Playlist.activationId + const wasActive = !!playoutModel.playlist.activationId const anyOtherActiveRundowns = await getActiveRundownPlaylistsInStudioFromDb( context, context.studio._id, - playoutModel.Playlist._id + playoutModel.playlist._id ) if (anyOtherActiveRundowns.length) { // logger.warn('Only one rundown can be active at the same time. Active rundowns: ' + _.map(anyOtherActiveRundowns, rundown => rundown._id)) @@ -37,7 +37,7 @@ export async function activateRundownPlaylist( ) } - if (!playoutModel.Playlist.activationId) { + if (!playoutModel.playlist.activationId) { // Reset the playlist if it wasnt already active await resetRundownPlaylist(context, playoutModel) } @@ -46,14 +46,14 @@ export async function activateRundownPlaylist( let rundown: ReadonlyDeep | undefined - const currentPartInstance = playoutModel.CurrentPartInstance - if (!currentPartInstance || currentPartInstance.PartInstance.reset) { + const currentPartInstance = playoutModel.currentPartInstance + if (!currentPartInstance || currentPartInstance.partInstance.reset) { playoutModel.clearSelectedPartInstances() // If we are not playing anything, then regenerate the next part const firstPart = selectNextPart( context, - playoutModel.Playlist, + playoutModel.playlist, null, null, playoutModel.getAllOrderedSegments(), @@ -62,18 +62,18 @@ export async function activateRundownPlaylist( await setNextPart(context, playoutModel, firstPart, false) if (firstPart) { - rundown = playoutModel.getRundown(firstPart.part.rundownId)?.Rundown + rundown = playoutModel.getRundown(firstPart.part.rundownId)?.rundown } } else { // Otherwise preserve the active partInstances - for (const partInstance of playoutModel.SelectedPartInstances) { + for (const partInstance of playoutModel.selectedPartInstances) { partInstance.setPlaylistActivationId(newActivationId) } - const nextPartInstance = playoutModel.NextPartInstance + const nextPartInstance = playoutModel.nextPartInstance if (nextPartInstance) { - rundown = playoutModel.getRundown(nextPartInstance.PartInstance.rundownId)?.Rundown - if (!rundown) throw new Error(`Could not find rundown "${nextPartInstance.PartInstance.rundownId}"`) + rundown = playoutModel.getRundown(nextPartInstance.partInstance.rundownId)?.rundown + if (!rundown) throw new Error(`Could not find rundown "${nextPartInstance.partInstance.rundownId}"`) } } @@ -123,18 +123,18 @@ export async function deactivateRundownPlaylistInner( playoutModel: PlayoutModel ): Promise | undefined> { const span = context.startSpan('deactivateRundownPlaylistInner') - logger.info(`Deactivating rundown playlist "${playoutModel.Playlist._id}"`) + logger.info(`Deactivating rundown playlist "${playoutModel.playlist._id}"`) - const currentPartInstance = playoutModel.CurrentPartInstance - const nextPartInstance = playoutModel.NextPartInstance + const currentPartInstance = playoutModel.currentPartInstance + const nextPartInstance = playoutModel.nextPartInstance let rundown: ReadonlyDeep | undefined if (currentPartInstance) { - rundown = playoutModel.getRundown(currentPartInstance.PartInstance.rundownId)?.Rundown + rundown = playoutModel.getRundown(currentPartInstance.partInstance.rundownId)?.rundown - playoutModel.queueNotifyCurrentlyPlayingPartEvent(currentPartInstance.PartInstance.rundownId, null) + playoutModel.queueNotifyCurrentlyPlayingPartEvent(currentPartInstance.partInstance.rundownId, null) } else if (nextPartInstance) { - rundown = playoutModel.getRundown(nextPartInstance.PartInstance.rundownId)?.Rundown + rundown = playoutModel.getRundown(nextPartInstance.partInstance.rundownId)?.rundown } playoutModel.deactivatePlaylist() diff --git a/packages/job-worker/src/playout/activePlaylistJobs.ts b/packages/job-worker/src/playout/activePlaylistJobs.ts index a3bd7be4a4..9f125264f1 100644 --- a/packages/job-worker/src/playout/activePlaylistJobs.ts +++ b/packages/job-worker/src/playout/activePlaylistJobs.ts @@ -47,7 +47,7 @@ export async function handlePrepareRundownPlaylistForBroadcast( context, data, async (playoutModel) => { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist if (playlist.activationId) throw UserError.create(UserErrorMessage.RundownAlreadyActive) await checkNoOtherPlaylistsActive(context, playlist) @@ -70,7 +70,7 @@ export async function handleResetRundownPlaylist(context: JobContext, data: Rese context, data, async (playoutModel) => { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist if (playlist.activationId && !playlist.rehearsal && !context.studio.settings.allowRundownResetOnAir) { throw UserError.create(UserErrorMessage.RundownResetWhileActive) } @@ -115,7 +115,7 @@ export async function handleResetRundownPlaylist(context: JobContext, data: Rese if (data.activate) { // Do the activation await activateRundownPlaylist(context, playoutModel, data.activate !== 'active') // Activate rundown - } else if (playoutModel.Playlist.activationId) { + } else if (playoutModel.playlist.activationId) { // Only update the timeline if this is the active playlist await updateTimeline(context, playoutModel) } @@ -135,7 +135,7 @@ export async function handleActivateRundownPlaylist( // 'activateRundownPlaylist', data, async (playoutModel) => { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist await checkNoOtherPlaylistsActive(context, playlist) }, async (playoutModel) => { diff --git a/packages/job-worker/src/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index 26d114abf6..0a76630079 100644 --- a/packages/job-worker/src/playout/adlibAction.ts +++ b/packages/job-worker/src/playout/adlibAction.ts @@ -135,12 +135,12 @@ export async function executeActionInner( ): Promise { const now = getCurrentTime() - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist const actionContext = new ActionExecutionContext( { - name: `${rundown.Rundown.name}(${playlist.name})`, - identifier: `playlist=${playlist._id},rundown=${rundown.Rundown._id},currentPartInstance=${ + name: `${rundown.rundown.name}(${playlist.name})`, + identifier: `playlist=${playlist._id},rundown=${rundown.rundown._id},currentPartInstance=${ playlist.currentPartInfo?.partInstanceId },execution=${getRandomId()}`, tempSendUserNotesIntoBlackHole: true, // TODO-CONTEXT store these notes @@ -195,13 +195,13 @@ async function applyAnyExecutionSideEffects( await syncPlayheadInfinitesForNextPartInstance( context, playoutModel, - playoutModel.CurrentPartInstance, - playoutModel.NextPartInstance + playoutModel.currentPartInstance, + playoutModel.nextPartInstance ) } if (actionContext.nextPartState !== ActionPartChange.NONE) { - const nextPartInstance = playoutModel.NextPartInstance + const nextPartInstance = playoutModel.nextPartInstance if (nextPartInstance) { nextPartInstance.recalculateExpectedDurationWithPreroll() @@ -210,7 +210,7 @@ async function applyAnyExecutionSideEffects( } if (actionContext.currentPartState !== ActionPartChange.NONE) { - const currentPartInstance = playoutModel.CurrentPartInstance + const currentPartInstance = playoutModel.currentPartInstance if (currentPartInstance) { validateScratchpartPartInstanceProperties(context, playoutModel, currentPartInstance) } diff --git a/packages/job-worker/src/playout/adlibJobs.ts b/packages/job-worker/src/playout/adlibJobs.ts index 679a2e38e0..e23a6e0f1c 100644 --- a/packages/job-worker/src/playout/adlibJobs.ts +++ b/packages/job-worker/src/playout/adlibJobs.ts @@ -39,7 +39,7 @@ export async function handleTakePieceAsAdlibNow(context: JobContext, data: TakeP context, data, async (playoutModel) => { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown) if (playlist.holdState === RundownHoldState.ACTIVE || playlist.holdState === RundownHoldState.PENDING) { throw UserError.create(UserErrorMessage.DuringHold) @@ -49,11 +49,11 @@ export async function handleTakePieceAsAdlibNow(context: JobContext, data: TakeP throw UserError.create(UserErrorMessage.AdlibCurrentPart) }, async (playoutModel) => { - const currentPartInstance = playoutModel.CurrentPartInstance + const currentPartInstance = playoutModel.currentPartInstance if (!currentPartInstance) throw UserError.create(UserErrorMessage.InactiveRundown) - const currentRundown = playoutModel.getRundown(currentPartInstance.PartInstance.rundownId) + const currentRundown = playoutModel.getRundown(currentPartInstance.partInstance.rundownId) if (!currentRundown) - throw new Error(`Missing Rundown for PartInstance: ${currentPartInstance.PartInstance._id}`) + throw new Error(`Missing Rundown for PartInstance: ${currentPartInstance.partInstance._id}`) const rundownIds = playoutModel.getRundownIds() @@ -62,7 +62,7 @@ export async function handleTakePieceAsAdlibNow(context: JobContext, data: TakeP ) const pieceToCopy = pieceInstanceToCopy - ? clone(pieceInstanceToCopy.pieceInstance.PieceInstance.piece) + ? clone(pieceInstanceToCopy.pieceInstance.pieceInstance.piece) : ((await context.directCollections.Pieces.findOne({ _id: data.pieceInstanceIdOrPieceIdToCopy as PieceId, startRundownId: { $in: rundownIds }, @@ -75,8 +75,8 @@ export async function handleTakePieceAsAdlibNow(context: JobContext, data: TakeP } const showStyleCompound = await context.getShowStyleCompound( - currentRundown.Rundown.showStyleVariantId, - currentRundown.Rundown.showStyleBaseId + currentRundown.rundown.showStyleVariantId, + currentRundown.rundown.showStyleBaseId ) if (!pieceToCopy.allowDirectPlay) { throw UserError.from( @@ -146,12 +146,12 @@ async function pieceTakeNowAsAdlib( // Disable the original piece if from the same Part if ( pieceInstanceToCopy && - pieceInstanceToCopy.pieceInstance.PieceInstance.partInstanceId === currentPartInstance.PartInstance._id + pieceInstanceToCopy.pieceInstance.pieceInstance.partInstanceId === currentPartInstance.partInstance._id ) { // Ensure the piece being copied isnt currently live if ( - pieceInstanceToCopy.pieceInstance.PieceInstance.plannedStartedPlayback && - pieceInstanceToCopy.pieceInstance.PieceInstance.plannedStartedPlayback <= getCurrentTime() + pieceInstanceToCopy.pieceInstance.pieceInstance.plannedStartedPlayback && + pieceInstanceToCopy.pieceInstance.pieceInstance.plannedStartedPlayback <= getCurrentTime() ) { const resolvedPieces = getResolvedPiecesForCurrentPartInstance( context, @@ -159,7 +159,7 @@ async function pieceTakeNowAsAdlib( currentPartInstance ) const resolvedPieceBeingCopied = resolvedPieces.find( - (p) => p.instance._id === pieceInstanceToCopy.pieceInstance.PieceInstance._id + (p) => p.instance._id === pieceInstanceToCopy.pieceInstance.pieceInstance._id ) if ( @@ -171,7 +171,7 @@ async function pieceTakeNowAsAdlib( // logger.debug(`Piece "${piece._id}" is currently live and cannot be used as an ad-lib`) throw UserError.from( new Error( - `PieceInstance "${pieceInstanceToCopy.pieceInstance.PieceInstance._id}" is currently live and cannot be used as an ad-lib` + `PieceInstance "${pieceInstanceToCopy.pieceInstance.pieceInstance._id}" is currently live and cannot be used as an ad-lib` ), UserErrorMessage.PieceAsAdlibCurrentlyLive ) @@ -185,8 +185,8 @@ async function pieceTakeNowAsAdlib( await syncPlayheadInfinitesForNextPartInstance( context, playoutModel, - playoutModel.CurrentPartInstance, - playoutModel.NextPartInstance + playoutModel.currentPartInstance, + playoutModel.nextPartInstance ) await updateTimeline(context, playoutModel) @@ -200,7 +200,7 @@ export async function handleAdLibPieceStart(context: JobContext, data: AdlibPiec context, data, async (playoutModel) => { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown) if (playlist.holdState === RundownHoldState.ACTIVE || playlist.holdState === RundownHoldState.PENDING) { throw UserError.create(UserErrorMessage.DuringHold) @@ -212,13 +212,13 @@ export async function handleAdLibPieceStart(context: JobContext, data: AdlibPiec async (playoutModel) => { const partInstance = playoutModel.getPartInstance(data.partInstanceId) if (!partInstance) throw new Error(`PartInstance "${data.partInstanceId}" not found!`) - const rundown = playoutModel.getRundown(partInstance.PartInstance.rundownId) - if (!rundown) throw new Error(`Rundown "${partInstance.PartInstance.rundownId}" not found!`) + const rundown = playoutModel.getRundown(partInstance.partInstance.rundownId) + if (!rundown) throw new Error(`Rundown "${partInstance.partInstance.rundownId}" not found!`) // Rundows that share the same showstyle variant as the current rundown, so adlibs from these rundowns are safe to play - const safeRundownIds = playoutModel.Rundowns.filter( - (rd) => rd.Rundown.showStyleVariantId === rundown.Rundown.showStyleVariantId - ).map((r) => r.Rundown._id) + const safeRundownIds = playoutModel.rundowns + .filter((rd) => rd.rundown.showStyleVariantId === rundown.rundown.showStyleVariantId) + .map((r) => r.rundown._id) let adLibPiece: AdLibPiece | BucketAdLib | undefined if (data.pieceType === 'baseline') { @@ -237,10 +237,10 @@ export async function handleAdLibPieceStart(context: JobContext, data: AdlibPiec studioId: context.studioId, }) - if (bucketAdlib && bucketAdlib.showStyleVariantId !== rundown.Rundown.showStyleVariantId) { + if (bucketAdlib && bucketAdlib.showStyleVariantId !== rundown.rundown.showStyleVariantId) { throw UserError.from( new Error( - `Bucket AdLib "${data.adLibPieceId}" is not compatible with rundown "${rundown.Rundown._id}"!` + `Bucket AdLib "${data.adLibPieceId}" is not compatible with rundown "${rundown.rundown._id}"!` ), UserErrorMessage.BucketAdlibIncompatible ) @@ -281,7 +281,7 @@ export async function handleStartStickyPieceOnSourceLayer( context, data, async (playoutModel) => { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown) if (playlist.holdState === RundownHoldState.ACTIVE || playlist.holdState === RundownHoldState.PENDING) { throw UserError.create(UserErrorMessage.DuringHold) @@ -289,13 +289,13 @@ export async function handleStartStickyPieceOnSourceLayer( if (!playlist.currentPartInfo) throw UserError.create(UserErrorMessage.NoCurrentPart) }, async (playoutModel) => { - const currentPartInstance = playoutModel.CurrentPartInstance + const currentPartInstance = playoutModel.currentPartInstance if (!currentPartInstance) throw UserError.create(UserErrorMessage.NoCurrentPart) - const rundown = playoutModel.getRundown(currentPartInstance.PartInstance.rundownId) - if (!rundown) throw new Error(`Rundown "${currentPartInstance.PartInstance.rundownId}" not found!`) + const rundown = playoutModel.getRundown(currentPartInstance.partInstance.rundownId) + if (!rundown) throw new Error(`Rundown "${currentPartInstance.partInstance.rundownId}" not found!`) - const showStyleBase = await context.getShowStyleBase(rundown.Rundown.showStyleBaseId) + const showStyleBase = await context.getShowStyleBase(rundown.rundown.showStyleBaseId) const sourceLayer = showStyleBase.sourceLayers[data.sourceLayerId] if (!sourceLayer) throw new Error(`Source layer "${data.sourceLayerId}" not found!`) @@ -333,7 +333,7 @@ export async function handleStopPiecesOnSourceLayers( context, data, async (playoutModel) => { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown) if (playlist.holdState === RundownHoldState.ACTIVE || playlist.holdState === RundownHoldState.PENDING) { throw UserError.create(UserErrorMessage.DuringHold) @@ -343,13 +343,13 @@ export async function handleStopPiecesOnSourceLayers( async (playoutModel) => { const partInstance = playoutModel.getPartInstance(data.partInstanceId) if (!partInstance) throw new Error(`PartInstance "${data.partInstanceId}" not found!`) - const lastStartedPlayback = partInstance.PartInstance.timings?.plannedStartedPlayback + const lastStartedPlayback = partInstance.partInstance.timings?.plannedStartedPlayback if (!lastStartedPlayback) throw new Error(`Part "${data.partInstanceId}" has yet to start playback!`) - const rundown = playoutModel.getRundown(partInstance.PartInstance.rundownId) - if (!rundown) throw new Error(`Rundown "${partInstance.PartInstance.rundownId}" not found!`) + const rundown = playoutModel.getRundown(partInstance.partInstance.rundownId) + if (!rundown) throw new Error(`Rundown "${partInstance.partInstance.rundownId}" not found!`) - const showStyleBase = await context.getShowStyleBase(rundown.Rundown.showStyleBaseId) + const showStyleBase = await context.getShowStyleBase(rundown.rundown.showStyleBaseId) const sourceLayerIds = new Set(data.sourceLayerIds) const changedIds = innerStopPieces( context, @@ -364,8 +364,8 @@ export async function handleStopPiecesOnSourceLayers( await syncPlayheadInfinitesForNextPartInstance( context, playoutModel, - playoutModel.CurrentPartInstance, - playoutModel.NextPartInstance + playoutModel.currentPartInstance, + playoutModel.nextPartInstance ) await updateTimeline(context, playoutModel) @@ -382,22 +382,22 @@ export async function handleDisableNextPiece(context: JobContext, data: DisableN context, data, async (playoutModel) => { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown) if (!playlist.currentPartInfo) throw UserError.create(UserErrorMessage.NoCurrentPart) }, async (playoutModel) => { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist - const currentPartInstance = playoutModel.CurrentPartInstance - const nextPartInstance = playoutModel.NextPartInstance + const currentPartInstance = playoutModel.currentPartInstance + const nextPartInstance = playoutModel.nextPartInstance if (!currentPartInstance) throw new Error(`PartInstance "${playlist.currentPartInfo?.partInstanceId}" not found!`) - const rundown = playoutModel.getRundown(currentPartInstance.PartInstance.rundownId) - if (!rundown) throw new Error(`Rundown "${currentPartInstance.PartInstance.rundownId}" not found!`) - const showStyleBase = await context.getShowStyleBase(rundown.Rundown.showStyleBaseId) + const rundown = playoutModel.getRundown(currentPartInstance.partInstance.rundownId) + if (!rundown) throw new Error(`Rundown "${currentPartInstance.partInstance.rundownId}" not found!`) + const showStyleBase = await context.getShowStyleBase(rundown.rundown.showStyleBaseId) // logger.info(o) // logger.info(JSON.stringify(o, '', 2)) @@ -408,28 +408,28 @@ export async function handleDisableNextPiece(context: JobContext, data: DisableN // Find next piece to disable let nowInPart = 0 - if (!ignoreStartedPlayback && partInstance.PartInstance.timings?.plannedStartedPlayback) { - nowInPart = getCurrentTime() - partInstance.PartInstance.timings?.plannedStartedPlayback + if (!ignoreStartedPlayback && partInstance.partInstance.timings?.plannedStartedPlayback) { + nowInPart = getCurrentTime() - partInstance.partInstance.timings?.plannedStartedPlayback } - const filteredPieces = partInstance.PieceInstances.filter((piece) => { - const sourceLayer = allowedSourceLayers[piece.PieceInstance.piece.sourceLayerId] + const filteredPieces = partInstance.pieceInstances.filter((piece) => { + const sourceLayer = allowedSourceLayers[piece.pieceInstance.piece.sourceLayerId] if ( sourceLayer?.allowDisable && - !piece.PieceInstance.piece.virtual && - piece.PieceInstance.piece.pieceType === IBlueprintPieceType.Normal + !piece.pieceInstance.piece.virtual && + piece.pieceInstance.piece.pieceType === IBlueprintPieceType.Normal ) return true return false }) const sortedByLayer = _.sortBy(filteredPieces, (piece) => { - const sourceLayer = allowedSourceLayers[piece.PieceInstance.piece.sourceLayerId] + const sourceLayer = allowedSourceLayers[piece.pieceInstance.piece.sourceLayerId] return sourceLayer?._rank || -9999 }) const sortedPieces = [...sortedByLayer] - sortedPieces.sort((a, b) => comparePieceStart(a.PieceInstance.piece, b.PieceInstance.piece, nowInPart)) + sortedPieces.sort((a, b) => comparePieceStart(a.pieceInstance.piece, b.pieceInstance.piece, nowInPart)) const findLast = !!data.undo @@ -437,8 +437,8 @@ export async function handleDisableNextPiece(context: JobContext, data: DisableN return sortedPieces.find((piece) => { return ( - piece.PieceInstance.piece.enable.start >= nowInPart && - ((!data.undo && !piece.PieceInstance.disabled) || (data.undo && piece.PieceInstance.disabled)) + piece.pieceInstance.piece.enable.start >= nowInPart && + ((!data.undo && !piece.pieceInstance.disabled) || (data.undo && piece.pieceInstance.disabled)) ) }) } @@ -458,7 +458,7 @@ export async function handleDisableNextPiece(context: JobContext, data: DisableN logger.debug( (data.undo ? 'Disabling' : 'Enabling') + ' next PieceInstance ' + - candidatePieceInstance.PieceInstance._id + candidatePieceInstance.pieceInstance._id ) candidatePieceInstance.setDisabled(!data.undo) disabledPiece = true diff --git a/packages/job-worker/src/playout/adlibUtils.ts b/packages/job-worker/src/playout/adlibUtils.ts index 02351eb57e..a55b9c40ea 100644 --- a/packages/job-worker/src/playout/adlibUtils.ts +++ b/packages/job-worker/src/playout/adlibUtils.ts @@ -59,7 +59,7 @@ export async function innerStartOrQueueAdLibPiece( [genericAdlibPiece], adLibPiece._id ) - queuedPartInstanceId = newPartInstance.PartInstance._id + queuedPartInstanceId = newPartInstance.partInstance._id // syncPlayheadInfinitesForNextPartInstance is handled by setNextPart } else { @@ -70,7 +70,7 @@ export async function innerStartOrQueueAdLibPiece( context, playoutModel, currentPartInstance, - playoutModel.NextPartInstance + playoutModel.nextPartInstance ) } @@ -92,7 +92,7 @@ export async function innerFindLastPieceOnLayer( const query: MongoQuery = { ...customQuery, - playlistActivationId: playoutModel.Playlist.activationId, + playlistActivationId: playoutModel.playlist.activationId, rundownId: { $in: rundownIds }, 'piece.sourceLayerId': { $in: sourceLayerId }, plannedStartedPlayback: { @@ -126,7 +126,7 @@ export async function innerFindLastScriptedPieceOnLayer( ): Promise { const span = context.startSpan('innerFindLastScriptedPieceOnLayer') - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist const rundownIds = playoutModel.getRundownIds() // TODO - this should throw instead of return more? @@ -135,7 +135,7 @@ export async function innerFindLastScriptedPieceOnLayer( return } - const currentPartInstance = playoutModel.CurrentPartInstance?.PartInstance + const currentPartInstance = playoutModel.currentPartInstance?.partInstance if (!currentPartInstance) { return @@ -192,14 +192,14 @@ function updateRankForAdlibbedPartInstance( playoutModel: PlayoutModel, newPartInstance: PlayoutPartInstanceModel ) { - const currentPartInstance = playoutModel.CurrentPartInstance + const currentPartInstance = playoutModel.currentPartInstance if (!currentPartInstance) throw new Error('CurrentPartInstance not found') // Find the following part, so we can pick a good rank const followingPart = selectNextPart( context, - playoutModel.Playlist, - currentPartInstance.PartInstance, + playoutModel.playlist, + currentPartInstance.partInstance, null, playoutModel.getAllOrderedSegments(), playoutModel.getAllOrderedParts(), @@ -207,12 +207,12 @@ function updateRankForAdlibbedPartInstance( ) newPartInstance.setRank( getRank( - currentPartInstance.PartInstance.part, - followingPart?.part?.segmentId === newPartInstance.PartInstance.segmentId ? followingPart?.part : undefined + currentPartInstance.partInstance.part, + followingPart?.part?.segmentId === newPartInstance.partInstance.segmentId ? followingPart?.part : undefined ) ) - updatePartInstanceRanksAfterAdlib(playoutModel, newPartInstance.PartInstance.segmentId) + updatePartInstanceRanksAfterAdlib(playoutModel, newPartInstance.partInstance.segmentId) } export async function insertQueuedPartWithPieces( @@ -228,8 +228,8 @@ export async function insertQueuedPartWithPieces( const newPartFull: DBPart = { ...newPart, - segmentId: currentPartInstance.PartInstance.segmentId, - rundownId: currentPartInstance.PartInstance.rundownId, + segmentId: currentPartInstance.partInstance.segmentId, + rundownId: currentPartInstance.partInstance.rundownId, } // Find any rundown defined infinites that we should inherit @@ -271,7 +271,7 @@ export function innerStopPieces( const span = context.startSpan('innerStopPieces') const stoppedInstances: PieceInstanceId[] = [] - const lastStartedPlayback = currentPartInstance.PartInstance.timings?.plannedStartedPlayback + const lastStartedPlayback = currentPartInstance.partInstance.timings?.plannedStartedPlayback if (lastStartedPlayback === undefined) { throw new Error('Cannot stop pieceInstances when partInstance hasnt started playback') } diff --git a/packages/job-worker/src/playout/datastore.ts b/packages/job-worker/src/playout/datastore.ts index 28206349a5..9e7d6c13fd 100644 --- a/packages/job-worker/src/playout/datastore.ts +++ b/packages/job-worker/src/playout/datastore.ts @@ -11,7 +11,7 @@ export function getDatastoreId(studioId: StudioId, key: string): TimelineDatasto /** Remove documents in the TimelineDatastore collection where mode is temporary and has no references from the timeline */ export async function cleanTimelineDatastore(context: JobContext, playoutModel: PlayoutModel): Promise { - const timeline = playoutModel.Timeline + const timeline = playoutModel.timeline if (!timeline) { return diff --git a/packages/job-worker/src/playout/debug.ts b/packages/job-worker/src/playout/debug.ts index 294bbfe315..4b5fe747f5 100644 --- a/packages/job-worker/src/playout/debug.ts +++ b/packages/job-worker/src/playout/debug.ts @@ -24,8 +24,8 @@ export async function handleDebugSyncPlayheadInfinitesForNextPartInstance( await syncPlayheadInfinitesForNextPartInstance( context, playoutModel, - playoutModel.CurrentPartInstance, - playoutModel.NextPartInstance + playoutModel.currentPartInstance, + playoutModel.nextPartInstance ) }) } @@ -41,11 +41,11 @@ export async function handleDebugRegenerateNextPartInstance( logger.info('regenerateNextPartInstance') await runJobWithPlayoutModel(context, data, null, async (playoutModel) => { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist const originalNextPartInfo = playlist.nextPartInfo if (originalNextPartInfo && playlist.activationId) { - const nextPartInstance = playoutModel.NextPartInstance - const part = nextPartInstance ? playoutModel.findPart(nextPartInstance.PartInstance.part._id) : undefined + const nextPartInstance = playoutModel.nextPartInstance + const part = nextPartInstance ? playoutModel.findPart(nextPartInstance.partInstance.part._id) : undefined if (part) { await setNextPart(context, playoutModel, null, false) await setNextPart( diff --git a/packages/job-worker/src/playout/holdJobs.ts b/packages/job-worker/src/playout/holdJobs.ts index 08333c01ce..ab00c41738 100644 --- a/packages/job-worker/src/playout/holdJobs.ts +++ b/packages/job-worker/src/playout/holdJobs.ts @@ -14,7 +14,7 @@ export async function handleActivateHold(context: JobContext, data: ActivateHold context, data, async (playoutModel) => { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown) @@ -24,27 +24,27 @@ export async function handleActivateHold(context: JobContext, data: ActivateHold if (playlist.holdState) throw UserError.create(UserErrorMessage.HoldAlreadyActive) }, async (playoutModel) => { - const playlist = playoutModel.Playlist - const currentPartInstance = playoutModel.CurrentPartInstance + const playlist = playoutModel.playlist + const currentPartInstance = playoutModel.currentPartInstance if (!currentPartInstance) throw new Error(`PartInstance "${playlist.currentPartInfo?.partInstanceId}" not found!`) - const nextPartInstance = playoutModel.NextPartInstance + const nextPartInstance = playoutModel.nextPartInstance if (!nextPartInstance) throw new Error(`PartInstance "${playlist.nextPartInfo?.partInstanceId}" not found!`) if ( - currentPartInstance.PartInstance.part.holdMode !== PartHoldMode.FROM || - nextPartInstance.PartInstance.part.holdMode !== PartHoldMode.TO || - currentPartInstance.PartInstance.part.segmentId !== nextPartInstance.PartInstance.part.segmentId + currentPartInstance.partInstance.part.holdMode !== PartHoldMode.FROM || + nextPartInstance.partInstance.part.holdMode !== PartHoldMode.TO || + currentPartInstance.partInstance.part.segmentId !== nextPartInstance.partInstance.part.segmentId ) { throw UserError.create(UserErrorMessage.HoldIncompatibleParts) } - const hasDynamicallyInserted = currentPartInstance.PieceInstances.find( + const hasDynamicallyInserted = currentPartInstance.pieceInstances.find( (p) => - !!p.PieceInstance.dynamicallyInserted && + !!p.pieceInstance.dynamicallyInserted && // If its a continuation of an infinite adlib it is probably a graphic, so is 'fine' - !p.PieceInstance.infinite?.fromPreviousPart && - !p.PieceInstance.infinite?.fromPreviousPlayhead + !p.pieceInstance.infinite?.fromPreviousPart && + !p.pieceInstance.infinite?.fromPreviousPlayhead ) if (hasDynamicallyInserted) throw UserError.create(UserErrorMessage.HoldAfterAdlib) @@ -63,7 +63,7 @@ export async function handleDeactivateHold(context: JobContext, data: Deactivate context, data, async (playoutModel) => { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown) diff --git a/packages/job-worker/src/playout/infinites.ts b/packages/job-worker/src/playout/infinites.ts index 05d8d98087..caf7935672 100644 --- a/packages/job-worker/src/playout/infinites.ts +++ b/packages/job-worker/src/playout/infinites.ts @@ -32,7 +32,7 @@ function getShowStyleIdsRundownMapping(rundowns: readonly PlayoutRundownModel[]) const ret = new Map() for (const rundown of rundowns) { - ret.set(rundown.Rundown._id, rundown.Rundown.showStyleBaseId) + ret.set(rundown.rundown._id, rundown.rundown.showStyleBaseId) } return ret @@ -54,10 +54,10 @@ export function candidatePartIsAfterPreviewPartInstance( } else { // Check if the segment is after the other const previousSegmentIndex = orderedSegments.findIndex( - (s) => s.Segment._id === previousPartInstance.segmentId + (s) => s.segment._id === previousPartInstance.segmentId ) const candidateSegmentIndex = orderedSegments.findIndex( - (s) => s.Segment._id === candidateInstance.segmentId + (s) => s.segment._id === candidateInstance.segmentId ) if (previousSegmentIndex === -1 || candidateSegmentIndex === -1) { @@ -84,21 +84,21 @@ function getIdsBeforeThisPart(context: JobContext, playoutModel: PlayoutModel, n const currentSegment = currentRundown?.getSegment(nextPart.segmentId) // Get the normal parts - const partsBeforeThisInSegment = currentSegment?.Parts?.filter((p) => p._rank < nextPart._rank) ?? [] + const partsBeforeThisInSegment = currentSegment?.parts?.filter((p) => p._rank < nextPart._rank) ?? [] // Find any orphaned parts - const partInstancesBeforeThisInSegment = playoutModel.LoadedPartInstances.filter( + const partInstancesBeforeThisInSegment = playoutModel.loadedPartInstances.filter( (p) => - p.PartInstance.segmentId === nextPart.segmentId && - !!p.PartInstance.orphaned && - p.PartInstance.part._rank < nextPart._rank + p.partInstance.segmentId === nextPart.segmentId && + !!p.partInstance.orphaned && + p.partInstance.part._rank < nextPart._rank ) - partsBeforeThisInSegment.push(...partInstancesBeforeThisInSegment.map((p) => p.PartInstance.part)) + partsBeforeThisInSegment.push(...partInstancesBeforeThisInSegment.map((p) => p.partInstance.part)) const partsBeforeThisInSegmentSorted = _.sortBy(partsBeforeThisInSegment, (p) => p._rank).map((p) => p._id) const nextPartSegment = currentRundown?.getSegment(nextPart.segmentId) - if (nextPartSegment?.Segment?.orphaned === SegmentOrphanedReason.SCRATCHPAD) { + if (nextPartSegment?.segment?.orphaned === SegmentOrphanedReason.SCRATCHPAD) { if (span) span.end() return { partsToReceiveOnSegmentEndFrom: partsBeforeThisInSegmentSorted, @@ -111,17 +111,19 @@ function getIdsBeforeThisPart(context: JobContext, playoutModel: PlayoutModel, n const currentSegment = currentRundown?.getSegment(nextPart.segmentId) const segmentsToReceiveOnRundownEndFrom = currentRundown && currentSegment - ? currentRundown.Segments.filter( - (s) => - s.Segment.rundownId === nextPart.rundownId && - s.Segment._rank < currentSegment.Segment._rank && - s.Segment.orphaned !== SegmentOrphanedReason.SCRATCHPAD - ).map((p) => p.Segment._id) + ? currentRundown.segments + .filter( + (s) => + s.segment.rundownId === nextPart.rundownId && + s.segment._rank < currentSegment.segment._rank && + s.segment.orphaned !== SegmentOrphanedReason.SCRATCHPAD + ) + .map((p) => p.segment._id) : [] const sortedRundownIds = sortRundownIDsInPlaylist( - playoutModel.Playlist.rundownIdsInOrder, - playoutModel.Rundowns.map((rd) => rd.Rundown._id) + playoutModel.playlist.rundownIdsInOrder, + playoutModel.rundowns.map((rd) => rd.rundown._id) ) const currentRundownIndex = sortedRundownIds.indexOf(nextPart.rundownId) const rundownsToReceiveOnShowStyleEndFrom = @@ -221,46 +223,46 @@ export async function syncPlayheadInfinitesForNextPartInstance( const span = context.startSpan('syncPlayheadInfinitesForNextPartInstance') if (toPartInstance && fromPartInstance) { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist if (!playlist.activationId) throw new Error(`RundownPlaylist "${playlist._id}" is not active`) const { partsToReceiveOnSegmentEndFrom, segmentsToReceiveOnRundownEndFrom, rundownsToReceiveOnShowStyleEndFrom, - } = getIdsBeforeThisPart(context, playoutModel, toPartInstance.PartInstance.part) + } = getIdsBeforeThisPart(context, playoutModel, toPartInstance.partInstance.part) - const currentRundown = playoutModel.getRundown(fromPartInstance.PartInstance.rundownId) - if (!currentRundown) throw new Error(`Rundown "${fromPartInstance.PartInstance.rundownId}" not found!`) + const currentRundown = playoutModel.getRundown(fromPartInstance.partInstance.rundownId) + if (!currentRundown) throw new Error(`Rundown "${fromPartInstance.partInstance.rundownId}" not found!`) - const currentSegment = currentRundown.getSegment(fromPartInstance.PartInstance.segmentId) - if (!currentSegment) throw new Error(`Segment "${fromPartInstance.PartInstance.segmentId}" not found!`) + const currentSegment = currentRundown.getSegment(fromPartInstance.partInstance.segmentId) + if (!currentSegment) throw new Error(`Segment "${fromPartInstance.partInstance.segmentId}" not found!`) - const nextRundown = playoutModel.getRundown(toPartInstance.PartInstance.rundownId) - if (!nextRundown) throw new Error(`Rundown "${toPartInstance.PartInstance.rundownId}" not found!`) + const nextRundown = playoutModel.getRundown(toPartInstance.partInstance.rundownId) + if (!nextRundown) throw new Error(`Rundown "${toPartInstance.partInstance.rundownId}" not found!`) - const nextSegment = nextRundown.getSegment(toPartInstance.PartInstance.segmentId) - if (!nextSegment) throw new Error(`Segment "${toPartInstance.PartInstance.segmentId}" not found!`) + const nextSegment = nextRundown.getSegment(toPartInstance.partInstance.segmentId) + if (!nextSegment) throw new Error(`Segment "${toPartInstance.partInstance.segmentId}" not found!`) - const showStyleBase = await context.getShowStyleBase(nextRundown.Rundown.showStyleBaseId) + const showStyleBase = await context.getShowStyleBase(nextRundown.rundown.showStyleBaseId) const nextPartIsAfterCurrentPart = candidatePartIsAfterPreviewPartInstance( context, playoutModel.getAllOrderedSegments(), - fromPartInstance.PartInstance, - toPartInstance.PartInstance.part + fromPartInstance.partInstance, + toPartInstance.partInstance.part ) - const nowInPart = getCurrentTime() - (fromPartInstance.PartInstance.timings?.plannedStartedPlayback ?? 0) + const nowInPart = getCurrentTime() - (fromPartInstance.partInstance.timings?.plannedStartedPlayback ?? 0) const prunedPieceInstances = processAndPrunePieceInstanceTimings( showStyleBase.sourceLayers, - fromPartInstance.PieceInstances.map((p) => p.PieceInstance), + fromPartInstance.pieceInstances.map((p) => p.pieceInstance), nowInPart, undefined, true ) - const rundownIdsToShowstyleIds = getShowStyleIdsRundownMapping(playoutModel.Rundowns) + const rundownIdsToShowstyleIds = getShowStyleIdsRundownMapping(playoutModel.rundowns) const infinites = libgetPlayheadTrackingInfinitesForPart( playlist.activationId, @@ -268,13 +270,13 @@ export async function syncPlayheadInfinitesForNextPartInstance( new Set(segmentsToReceiveOnRundownEndFrom), rundownsToReceiveOnShowStyleEndFrom, rundownIdsToShowstyleIds, - fromPartInstance.PartInstance, - currentSegment?.Segment, + fromPartInstance.partInstance, + currentSegment?.segment, prunedPieceInstances, - nextRundown.Rundown, - toPartInstance.PartInstance.part, - nextSegment?.Segment, - toPartInstance.PartInstance._id, + nextRundown.rundown, + toPartInstance.partInstance.part, + nextSegment?.segment, + toPartInstance.partInstance._id, nextPartIsAfterCurrentPart, false ) @@ -308,28 +310,28 @@ export function getPieceInstancesForPart( const { partsToReceiveOnSegmentEndFrom, segmentsToReceiveOnRundownEndFrom, rundownsToReceiveOnShowStyleEndFrom } = getIdsBeforeThisPart(context, playoutModel, part) - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist if (!playlist.activationId) throw new Error(`RundownPlaylist "${playlist._id}" is not active`) - const playingPieceInstances = playingPartInstance?.PieceInstances ?? [] + const playingPieceInstances = playingPartInstance?.pieceInstances ?? [] const nextPartIsAfterCurrentPart = candidatePartIsAfterPreviewPartInstance( context, playoutModel.getAllOrderedSegments(), - playingPartInstance?.PartInstance, + playingPartInstance?.partInstance, part ) - const rundownIdsToShowstyleIds = getShowStyleIdsRundownMapping(playoutModel.Rundowns) + const rundownIdsToShowstyleIds = getShowStyleIdsRundownMapping(playoutModel.rundowns) let playingRundown: PlayoutRundownModel | undefined let playingSegment: PlayoutSegmentModel | undefined if (playingPartInstance) { - playingRundown = playoutModel.getRundown(playingPartInstance.PartInstance.rundownId) - if (!playingRundown) throw new Error(`Rundown "${playingPartInstance.PartInstance.rundownId}" not found!`) + playingRundown = playoutModel.getRundown(playingPartInstance.partInstance.rundownId) + if (!playingRundown) throw new Error(`Rundown "${playingPartInstance.partInstance.rundownId}" not found!`) - playingSegment = playingRundown.getSegment(playingPartInstance.PartInstance.segmentId) - if (!playingSegment) throw new Error(`Segment "${playingPartInstance.PartInstance.segmentId}" not found!`) + playingSegment = playingRundown.getSegment(playingPartInstance.partInstance.segmentId) + if (!playingSegment) throw new Error(`Segment "${playingPartInstance.partInstance.segmentId}" not found!`) } const segment = rundown.getSegment(part.segmentId) @@ -337,11 +339,11 @@ export function getPieceInstancesForPart( const res = libgetPieceInstancesForPart( playlist.activationId, - playingPartInstance?.PartInstance, - playingSegment?.Segment, - playingPieceInstances.map((p) => p.PieceInstance), - rundown.Rundown, - segment.Segment, + playingPartInstance?.partInstance, + playingSegment?.segment, + playingPieceInstances.map((p) => p.pieceInstance), + rundown.rundown, + segment.segment, part, new Set(partsToReceiveOnSegmentEndFrom), new Set(segmentsToReceiveOnRundownEndFrom), diff --git a/packages/job-worker/src/playout/lib.ts b/packages/job-worker/src/playout/lib.ts index d0f20c543e..3214165b2b 100644 --- a/packages/job-worker/src/playout/lib.ts +++ b/packages/job-worker/src/playout/lib.ts @@ -19,25 +19,25 @@ import { selectNextPart } from './selectNextPart' * Remove all dynamically inserted/updated pieces, parts, timings etc.. */ export async function resetRundownPlaylist(context: JobContext, playoutModel: PlayoutModel): Promise { - logger.info('resetRundownPlaylist ' + playoutModel.Playlist._id) + logger.info('resetRundownPlaylist ' + playoutModel.playlist._id) // Remove all dunamically inserted pieces (adlibs etc) // const rundownIds = new Set((cache.getRundownIds())) - playoutModel.resetPlaylist(!!playoutModel.Playlist.activationId) + playoutModel.resetPlaylist(!!playoutModel.playlist.activationId) playoutModel.removeAllRehearsalPartInstances() resetPartInstancesWithPieceInstances(context, playoutModel) // Remove the scratchpad - for (const rundown of playoutModel.Rundowns) { + for (const rundown of playoutModel.rundowns) { rundown.removeScratchpadSegment() } - if (playoutModel.Playlist.activationId) { + if (playoutModel.playlist.activationId) { // put the first on queue: const firstPart = selectNextPart( context, - playoutModel.Playlist, + playoutModel.playlist, null, null, playoutModel.getAllOrderedSegments(), @@ -60,17 +60,17 @@ export function resetPartInstancesWithPieceInstances( selector?: MongoQuery ): void { const partInstanceIdsToReset: PartInstanceId[] = [] - for (const partInstance of playoutModel.LoadedPartInstances) { - if (!partInstance.PartInstance.reset && (!selector || mongoWhere(partInstance.PartInstance, selector))) { + for (const partInstance of playoutModel.loadedPartInstances) { + if (!partInstance.partInstance.reset && (!selector || mongoWhere(partInstance.partInstance, selector))) { partInstance.markAsReset() - partInstanceIdsToReset.push(partInstance.PartInstance._id) + partInstanceIdsToReset.push(partInstance.partInstance._id) } } // Defer ones which arent loaded playoutModel.deferAfterSave(async (playoutModel) => { const rundownIds = playoutModel.getRundownIds() - const partInstanceIdsInCache = playoutModel.LoadedPartInstances.map((p) => p.PartInstance._id) + const partInstanceIdsInCache = playoutModel.loadedPartInstances.map((p) => p.partInstance._id) // Find all the partInstances which are not loaded, but should be reset const resetInDb = await context.directCollections.PartInstances.findFetch( diff --git a/packages/job-worker/src/playout/lookahead/index.ts b/packages/job-worker/src/playout/lookahead/index.ts index 5cf9761caf..ca2a88e62b 100644 --- a/packages/job-worker/src/playout/lookahead/index.ts +++ b/packages/job-worker/src/playout/lookahead/index.ts @@ -165,7 +165,7 @@ export async function getLookeaheadObjects( const lookaheadObjs = findLookaheadForLayer( context, - playoutModel.Playlist.currentPartInfo?.partInstanceId ?? null, + playoutModel.playlist.currentPartInfo?.partInstanceId ?? null, partInstancesInfo, previousPartInfo, orderedPartInfos, diff --git a/packages/job-worker/src/playout/lookahead/util.ts b/packages/job-worker/src/playout/lookahead/util.ts index 5cf54c020e..3e3de1cbce 100644 --- a/packages/job-worker/src/playout/lookahead/util.ts +++ b/packages/job-worker/src/playout/lookahead/util.ts @@ -45,11 +45,11 @@ export function getOrderedPartsAfterPlayhead( } const span = context.startSpan('getOrderedPartsAfterPlayhead') - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist const orderedSegments = playoutModel.getAllOrderedSegments() const orderedParts = playoutModel.getAllOrderedParts() - const currentPartInstance = playoutModel.CurrentPartInstance?.PartInstance - const nextPartInstance = playoutModel.NextPartInstance?.PartInstance + const currentPartInstance = playoutModel.currentPartInstance?.partInstance + const nextPartInstance = playoutModel.nextPartInstance?.partInstance // If the nextPartInstance consumes the const alreadyConsumedQueuedSegmentId = diff --git a/packages/job-worker/src/playout/model/PlayoutModel.ts b/packages/job-worker/src/playout/model/PlayoutModel.ts index 79d6d088b5..b82a99384d 100644 --- a/packages/job-worker/src/playout/model/PlayoutModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutModel.ts @@ -39,25 +39,25 @@ export interface PlayoutModelPreInit { /** * The Id of the RundownPlaylist this PlayoutModel operates for */ - readonly PlaylistId: RundownPlaylistId + readonly playlistId: RundownPlaylistId /** * Reference to the lock for the RundownPlaylist */ - readonly PlaylistLock: PlaylistLock + readonly playlistLock: PlaylistLock /** * All of the PeripheralDevices that belong to the Studio of this RundownPlaylist */ - readonly PeripheralDevices: ReadonlyDeep + readonly peripheralDevices: ReadonlyDeep /** * The RundownPlaylist this PlayoutModel operates for */ - readonly Playlist: ReadonlyDeep + readonly playlist: ReadonlyDeep /** * The unwrapped Rundowns in this RundownPlaylist */ - readonly Rundowns: ReadonlyDeep + readonly rundowns: ReadonlyDeep /** * Get a Rundown which belongs to this RundownPlaylist @@ -75,57 +75,57 @@ export interface PlayoutModelReadonly extends StudioPlayoutModelBaseReadonly { /** * The Id of the RundownPlaylist this PlayoutModel operates for */ - readonly PlaylistId: RundownPlaylistId + readonly playlistId: RundownPlaylistId /** * Reference to the lock for the RundownPlaylist */ - readonly PlaylistLock: PlaylistLock + readonly playlistLock: PlaylistLock /** * The RundownPlaylist this PlayoutModel operates for */ - get Playlist(): ReadonlyDeep + get playlist(): ReadonlyDeep /** * The Rundowns in this RundownPlaylist */ - get Rundowns(): readonly PlayoutRundownModel[] + get rundowns(): readonly PlayoutRundownModel[] /** * All of the loaded PartInstances which are not one of the Previous, Current or Next * This may or may not contain all PartInstances from the RundownPlaylist, depending on implementation. * At a minimum it will contain all PartInstances from the Segments of the previous, current and next PartInstances */ - get OlderPartInstances(): PlayoutPartInstanceModel[] + get olderPartInstances(): PlayoutPartInstanceModel[] /** * The PartInstance previously played, if any */ - get PreviousPartInstance(): PlayoutPartInstanceModel | null + get previousPartInstance(): PlayoutPartInstanceModel | null /** * The PartInstance currently being played, if any */ - get CurrentPartInstance(): PlayoutPartInstanceModel | null + get currentPartInstance(): PlayoutPartInstanceModel | null /** * The PartInstance which is next to be played, if any */ - get NextPartInstance(): PlayoutPartInstanceModel | null + get nextPartInstance(): PlayoutPartInstanceModel | null /** * Ids of the previous, current and next PartInstances */ - get SelectedPartInstanceIds(): PartInstanceId[] + get selectedPartInstanceIds(): PartInstanceId[] /** * The previous, current and next PartInstances */ - get SelectedPartInstances(): PlayoutPartInstanceModel[] + get selectedPartInstances(): PlayoutPartInstanceModel[] /** * All of the loaded PartInstances * This may or may not contain all PartInstances from the RundownPlaylist, depending on implementation. * At a minimum it will contain all PartInstances from the Segments of the previous, current and next PartInstances */ - get LoadedPartInstances(): PlayoutPartInstanceModel[] + get loadedPartInstances(): PlayoutPartInstanceModel[] /** * All of the loaded PartInstances, sorted by order of playback */ - get SortedLoadedPartInstances(): PlayoutPartInstanceModel[] + get sortedLoadedPartInstances(): PlayoutPartInstanceModel[] /** * Get a PartInstance which belongs to this RundownPlaylist * @param id Id of the PartInstance @@ -179,7 +179,7 @@ export interface PlayoutModel extends PlayoutModelReadonly, StudioPlayoutModelBa /** * Temporary hack for debug logging */ - get HackDeletedPartInstanceIds(): PartInstanceId[] + get hackDeletedPartInstanceIds(): PartInstanceId[] /** * Set the RundownPlaylist as activated (or reactivate) diff --git a/packages/job-worker/src/playout/model/PlayoutPartInstanceModel.ts b/packages/job-worker/src/playout/model/PlayoutPartInstanceModel.ts index 1fb3b38b92..37ca45c981 100644 --- a/packages/job-worker/src/playout/model/PlayoutPartInstanceModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutPartInstanceModel.ts @@ -19,12 +19,12 @@ export interface PlayoutPartInstanceModel { /** * The PartInstance properties */ - readonly PartInstance: ReadonlyDeep + readonly partInstance: ReadonlyDeep /** * All the PieceInstances in the PartInstance */ - readonly PieceInstances: PlayoutPieceInstanceModel[] + readonly pieceInstances: PlayoutPieceInstanceModel[] /** * Take a snapshot of the current state of this PlayoutPartInstanceModel diff --git a/packages/job-worker/src/playout/model/PlayoutPieceInstanceModel.ts b/packages/job-worker/src/playout/model/PlayoutPieceInstanceModel.ts index a1323b15b9..0f233afe45 100644 --- a/packages/job-worker/src/playout/model/PlayoutPieceInstanceModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutPieceInstanceModel.ts @@ -7,7 +7,7 @@ export interface PlayoutPieceInstanceModel { /** * The PieceInstance properties */ - readonly PieceInstance: ReadonlyDeep + readonly pieceInstance: ReadonlyDeep /** * Prepare this PieceInstance to be continued during HOLD diff --git a/packages/job-worker/src/playout/model/PlayoutRundownModel.ts b/packages/job-worker/src/playout/model/PlayoutRundownModel.ts index 6339df9043..4ed16cb94c 100644 --- a/packages/job-worker/src/playout/model/PlayoutRundownModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutRundownModel.ts @@ -12,17 +12,17 @@ export interface PlayoutRundownModel { /** * The Rundown properties */ - readonly Rundown: ReadonlyDeep + readonly rundown: ReadonlyDeep /** * All the Segments in the Rundown * Sorted by their rank */ - readonly Segments: readonly PlayoutSegmentModel[] + readonly segments: readonly PlayoutSegmentModel[] /** * The RundownBaselineObjs for this Rundown */ - readonly BaselineObjects: ReadonlyDeep + readonly baselineObjects: ReadonlyDeep /** * Get a Segment which belongs to this Rundown diff --git a/packages/job-worker/src/playout/model/PlayoutSegmentModel.ts b/packages/job-worker/src/playout/model/PlayoutSegmentModel.ts index 1063993471..2da8ab724f 100644 --- a/packages/job-worker/src/playout/model/PlayoutSegmentModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutSegmentModel.ts @@ -10,13 +10,13 @@ export interface PlayoutSegmentModel { /** * The Segment properties */ - readonly Segment: ReadonlyDeep + readonly segment: ReadonlyDeep /** * All the Parts in the Segment * Sorted by their rank */ - readonly Parts: ReadonlyDeep + readonly parts: ReadonlyDeep /** * Get a Part which belongs to this Segment diff --git a/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts b/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts index 4628791d75..cc82067dff 100644 --- a/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts +++ b/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts @@ -52,13 +52,13 @@ export async function loadPlayoutModelPreInit( if (!Playlist) throw new Error(`Playlist "${tmpPlaylist._id}" not found!`) const res: PlayoutModelPreInit = { - PlaylistId: playlistLock.playlistId, - PlaylistLock: playlistLock, + playlistId: playlistLock.playlistId, + playlistLock: playlistLock, - PeripheralDevices, + peripheralDevices: PeripheralDevices, - Playlist, - Rundowns, + playlist: Playlist, + rundowns: Rundowns, getRundown: (id: RundownId) => Rundowns.find((rd) => rd._id === id), } @@ -141,26 +141,26 @@ export async function createPlayoutModelfromInitModel( initModel: PlayoutModelPreInit ): Promise { const span = context.startSpan('CacheForPlayout.fromInit') - if (span) span.setLabel('playlistId', unprotectString(initModel.PlaylistId)) + if (span) span.setLabel('playlistId', unprotectString(initModel.playlistId)) - if (!initModel.PlaylistLock.isLocked) { + if (!initModel.playlistLock.isLocked) { throw new Error('Cannot create cache with released playlist lock') } - const rundownIds = initModel.Rundowns.map((r) => r._id) + const rundownIds = initModel.rundowns.map((r) => r._id) const [partInstances, rundownsWithContent, timeline] = await Promise.all([ - loadPartInstances(context, initModel.Playlist, rundownIds), - loadRundowns(context, null, initModel.Rundowns), + loadPartInstances(context, initModel.playlist, rundownIds), + loadRundowns(context, null, initModel.rundowns), loadTimeline(context), ]) const res = new PlayoutModelImpl( context, - initModel.PlaylistLock, - initModel.PlaylistId, - initModel.PeripheralDevices, - clone(initModel.Playlist), + initModel.playlistLock, + initModel.playlistId, + initModel.peripheralDevices, + clone(initModel.playlist), partInstances, rundownsWithContent, timeline @@ -213,7 +213,7 @@ async function loadRundowns( const segmentsWithParts = segments.map( (segment) => new PlayoutSegmentModelImpl(segment, groupedParts.get(segment._id) ?? []) ) - const groupedSegmentsWithParts = groupByToMapFunc(segmentsWithParts, (s) => s.Segment.rundownId) + const groupedSegmentsWithParts = groupByToMapFunc(segmentsWithParts, (s) => s.segment.rundownId) const groupedBaselineObjects = groupByToMap(baselineObjects, 'rundownId') diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts index 89b8a8f7a3..aeefcebf9a 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts @@ -57,28 +57,28 @@ import { StudioBaselineHelper } from '../../../studio/model/StudioBaselineHelper import { EventsJobs } from '@sofie-automation/corelib/dist/worker/events' export class PlayoutModelReadonlyImpl implements PlayoutModelReadonly { - public readonly PlaylistId: RundownPlaylistId + public readonly playlistId: RundownPlaylistId - public readonly PlaylistLock: PlaylistLock + public readonly playlistLock: PlaylistLock - public readonly PeripheralDevices: ReadonlyDeep + public readonly peripheralDevices: ReadonlyDeep - protected readonly PlaylistImpl: DBRundownPlaylist - public get Playlist(): ReadonlyDeep { - return this.PlaylistImpl + protected readonly playlistImpl: DBRundownPlaylist + public get playlist(): ReadonlyDeep { + return this.playlistImpl } - protected readonly RundownsImpl: readonly PlayoutRundownModelImpl[] - public get Rundowns(): readonly PlayoutRundownModel[] { - return this.RundownsImpl + protected readonly rundownsImpl: readonly PlayoutRundownModelImpl[] + public get rundowns(): readonly PlayoutRundownModel[] { + return this.rundownsImpl } - protected TimelineImpl: TimelineComplete | null - public get Timeline(): TimelineComplete | null { - return this.TimelineImpl + protected timelineImpl: TimelineComplete | null + public get timeline(): TimelineComplete | null { + return this.timelineImpl } - protected AllPartInstances: Map + protected allPartInstances: Map public constructor( protected readonly context: JobContext, @@ -90,70 +90,70 @@ export class PlayoutModelReadonlyImpl implements PlayoutModelReadonly { rundowns: PlayoutRundownModelImpl[], timeline: TimelineComplete | undefined ) { - this.PlaylistId = playlistId - this.PlaylistLock = playlistLock + this.playlistId = playlistId + this.playlistLock = playlistLock - this.PeripheralDevices = peripheralDevices - this.PlaylistImpl = playlist + this.peripheralDevices = peripheralDevices + this.playlistImpl = playlist - this.RundownsImpl = rundowns + this.rundownsImpl = rundowns - this.TimelineImpl = timeline ?? null + this.timelineImpl = timeline ?? null - this.AllPartInstances = normalizeArrayToMapFunc(partInstances, (p) => p.PartInstance._id) + this.allPartInstances = normalizeArrayToMapFunc(partInstances, (p) => p.partInstance._id) } - public get OlderPartInstances(): PlayoutPartInstanceModel[] { - const allPartInstances = this.LoadedPartInstances + public get olderPartInstances(): PlayoutPartInstanceModel[] { + const allPartInstances = this.loadedPartInstances - const ignoreIds = new Set(this.SelectedPartInstanceIds) + const ignoreIds = new Set(this.selectedPartInstanceIds) - return allPartInstances.filter((partInstance) => !ignoreIds.has(partInstance.PartInstance._id)) + return allPartInstances.filter((partInstance) => !ignoreIds.has(partInstance.partInstance._id)) } - public get PreviousPartInstance(): PlayoutPartInstanceModel | null { - if (!this.Playlist.previousPartInfo?.partInstanceId) return null - const partInstance = this.AllPartInstances.get(this.Playlist.previousPartInfo.partInstanceId) + public get previousPartInstance(): PlayoutPartInstanceModel | null { + if (!this.playlist.previousPartInfo?.partInstanceId) return null + const partInstance = this.allPartInstances.get(this.playlist.previousPartInfo.partInstanceId) if (!partInstance) return null // throw new Error('PreviousPartInstance is missing') return partInstance } - public get CurrentPartInstance(): PlayoutPartInstanceModel | null { - if (!this.Playlist.currentPartInfo?.partInstanceId) return null - const partInstance = this.AllPartInstances.get(this.Playlist.currentPartInfo.partInstanceId) + public get currentPartInstance(): PlayoutPartInstanceModel | null { + if (!this.playlist.currentPartInfo?.partInstanceId) return null + const partInstance = this.allPartInstances.get(this.playlist.currentPartInfo.partInstanceId) if (!partInstance) return null // throw new Error('CurrentPartInstance is missing') return partInstance } - public get NextPartInstance(): PlayoutPartInstanceModel | null { - if (!this.Playlist.nextPartInfo?.partInstanceId) return null - const partInstance = this.AllPartInstances.get(this.Playlist.nextPartInfo.partInstanceId) + public get nextPartInstance(): PlayoutPartInstanceModel | null { + if (!this.playlist.nextPartInfo?.partInstanceId) return null + const partInstance = this.allPartInstances.get(this.playlist.nextPartInfo.partInstanceId) if (!partInstance) return null // throw new Error('NextPartInstance is missing') return partInstance } - public get SelectedPartInstanceIds(): PartInstanceId[] { + public get selectedPartInstanceIds(): PartInstanceId[] { return _.compact([ - this.Playlist.previousPartInfo?.partInstanceId, - this.Playlist.currentPartInfo?.partInstanceId, - this.Playlist.nextPartInfo?.partInstanceId, + this.playlist.previousPartInfo?.partInstanceId, + this.playlist.currentPartInfo?.partInstanceId, + this.playlist.nextPartInfo?.partInstanceId, ]) } - public get SelectedPartInstances(): PlayoutPartInstanceModel[] { - return _.compact([this.CurrentPartInstance, this.PreviousPartInstance, this.NextPartInstance]) + public get selectedPartInstances(): PlayoutPartInstanceModel[] { + return _.compact([this.currentPartInstance, this.previousPartInstance, this.nextPartInstance]) } - public get LoadedPartInstances(): PlayoutPartInstanceModel[] { - return Array.from(this.AllPartInstances.values()).filter((v): v is PlayoutPartInstanceModelImpl => v !== null) + public get loadedPartInstances(): PlayoutPartInstanceModel[] { + return Array.from(this.allPartInstances.values()).filter((v): v is PlayoutPartInstanceModelImpl => v !== null) } - public get SortedLoadedPartInstances(): PlayoutPartInstanceModel[] { - const allInstances = this.LoadedPartInstances - allInstances.sort((a, b) => a.PartInstance.takeCount - b.PartInstance.takeCount) + public get sortedLoadedPartInstances(): PlayoutPartInstanceModel[] { + const allInstances = this.loadedPartInstances + allInstances.sort((a, b) => a.partInstance.takeCount - b.partInstance.takeCount) return allInstances } public getPartInstance(partInstanceId: PartInstanceId): PlayoutPartInstanceModel | undefined { - return this.AllPartInstances.get(partInstanceId) ?? undefined + return this.allPartInstances.get(partInstanceId) ?? undefined } /** @@ -161,8 +161,8 @@ export class PlayoutModelReadonlyImpl implements PlayoutModelReadonly { * @param id */ findPart(id: PartId): ReadonlyDeep | undefined { - for (const rundown of this.Rundowns) { - for (const segment of rundown.Segments) { + for (const rundown of this.rundowns) { + for (const segment of rundown.segments) { const part = segment.getPart(id) if (part) return part } @@ -171,11 +171,11 @@ export class PlayoutModelReadonlyImpl implements PlayoutModelReadonly { return undefined } getAllOrderedParts(): ReadonlyDeep[] { - return this.Rundowns.flatMap((rundown) => rundown.getAllOrderedParts()) + return this.rundowns.flatMap((rundown) => rundown.getAllOrderedParts()) } findSegment(id: SegmentId): ReadonlyDeep | undefined { - for (const rundown of this.Rundowns) { + for (const rundown of this.rundowns) { const segment = rundown.getSegment(id) if (segment) return segment } @@ -183,23 +183,23 @@ export class PlayoutModelReadonlyImpl implements PlayoutModelReadonly { return undefined } getAllOrderedSegments(): ReadonlyDeep[] { - return this.Rundowns.flatMap((rundown) => rundown.Segments) + return this.rundowns.flatMap((rundown) => rundown.segments) } getRundown(id: RundownId): PlayoutRundownModel | undefined { - return this.Rundowns.find((rundown) => rundown.Rundown._id === id) + return this.rundowns.find((rundown) => rundown.rundown._id === id) } getRundownIds(): RundownId[] { return sortRundownIDsInPlaylist( - this.Playlist.rundownIdsInOrder, - this.Rundowns.map((rd) => rd.Rundown._id) + this.playlist.rundownIdsInOrder, + this.rundowns.map((rd) => rd.rundown._id) ) } findPieceInstance( id: PieceInstanceId ): { partInstance: PlayoutPartInstanceModel; pieceInstance: PlayoutPieceInstanceModel } | undefined { - for (const partInstance of this.LoadedPartInstances) { + for (const partInstance of this.loadedPartInstances) { const pieceInstance = partInstance.getPieceInstance(id) if (pieceInstance) return { partInstance, pieceInstance } } @@ -213,7 +213,7 @@ export class PlayoutModelReadonlyImpl implements PlayoutModelReadonly { if (this.context.studio.settings.forceMultiGatewayMode) { this.#isMultiGatewayMode = true } else { - const playoutDevices = this.PeripheralDevices.filter( + const playoutDevices = this.peripheralDevices.filter( (device) => device.type === PeripheralDeviceType.PLAYOUT ) this.#isMultiGatewayMode = playoutDevices.length > 1 @@ -235,15 +235,15 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou #deferredAfterSaveFunctions: DeferredAfterSaveFunction[] = [] #disposed = false - #PlaylistHasChanged = false - #TimelineHasChanged = false + #playlistHasChanged = false + #timelineHasChanged = false #PendingPartInstanceTimingEvents = new Set() #PendingNotifyCurrentlyPlayingPartEvent = new Map() - get HackDeletedPartInstanceIds(): PartInstanceId[] { + get hackDeletedPartInstanceIds(): PartInstanceId[] { const result: PartInstanceId[] = [] - for (const [id, doc] of this.AllPartInstances) { + for (const [id, doc] of this.allPartInstances) { if (!doc) result.push(id) } return result @@ -265,29 +265,29 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou this.#baselineHelper = new StudioBaselineHelper(context) } - public get DisplayName(): string { - return `PlayoutModel "${this.PlaylistId}"` + public get displayName(): string { + return `PlayoutModel "${this.playlistId}"` } activatePlaylist(rehearsal: boolean): RundownPlaylistActivationId { - this.PlaylistImpl.activationId = getRandomId() - this.PlaylistImpl.rehearsal = rehearsal + this.playlistImpl.activationId = getRandomId() + this.playlistImpl.rehearsal = rehearsal - this.#PlaylistHasChanged = true + this.#playlistHasChanged = true - return this.PlaylistImpl.activationId + return this.playlistImpl.activationId } clearSelectedPartInstances(): void { - this.PlaylistImpl.currentPartInfo = null - this.PlaylistImpl.nextPartInfo = null - this.PlaylistImpl.previousPartInfo = null - this.PlaylistImpl.holdState = RundownHoldState.NONE + this.playlistImpl.currentPartInfo = null + this.playlistImpl.nextPartInfo = null + this.playlistImpl.previousPartInfo = null + this.playlistImpl.holdState = RundownHoldState.NONE - delete this.PlaylistImpl.lastTakeTime - delete this.PlaylistImpl.queuedSegmentId + delete this.playlistImpl.lastTakeTime + delete this.playlistImpl.queuedSegmentId - this.#PlaylistHasChanged = true + this.#playlistHasChanged = true } #fixupPieceInstancesForPartInstance(partInstance: DBPartInstance, pieceInstances: PieceInstance[]): void { @@ -304,22 +304,22 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou fromAdlibId: PieceId | undefined, infinitePieceInstances: PieceInstance[] ): PlayoutPartInstanceModel { - const currentPartInstance = this.CurrentPartInstance + const currentPartInstance = this.currentPartInstance if (!currentPartInstance) throw new Error('No currentPartInstance') const newPartInstance: DBPartInstance = { _id: getRandomId(), - rundownId: currentPartInstance.PartInstance.rundownId, - segmentId: currentPartInstance.PartInstance.segmentId, - playlistActivationId: currentPartInstance.PartInstance.playlistActivationId, - segmentPlayoutId: currentPartInstance.PartInstance.segmentPlayoutId, - takeCount: currentPartInstance.PartInstance.takeCount + 1, - rehearsal: currentPartInstance.PartInstance.rehearsal, + rundownId: currentPartInstance.partInstance.rundownId, + segmentId: currentPartInstance.partInstance.segmentId, + playlistActivationId: currentPartInstance.partInstance.playlistActivationId, + segmentPlayoutId: currentPartInstance.partInstance.segmentPlayoutId, + takeCount: currentPartInstance.partInstance.takeCount + 1, + rehearsal: currentPartInstance.partInstance.rehearsal, orphaned: 'adlib-part', part: { ...part, - rundownId: currentPartInstance.PartInstance.rundownId, - segmentId: currentPartInstance.PartInstance.segmentId, + rundownId: currentPartInstance.partInstance.rundownId, + segmentId: currentPartInstance.partInstance.segmentId, }, } @@ -333,21 +333,21 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou partInstance.recalculateExpectedDurationWithPreroll() - this.AllPartInstances.set(newPartInstance._id, partInstance) + this.allPartInstances.set(newPartInstance._id, partInstance) return partInstance } createInstanceForPart(nextPart: ReadonlyDeep, pieceInstances: PieceInstance[]): PlayoutPartInstanceModel { - const playlistActivationId = this.Playlist.activationId + const playlistActivationId = this.playlist.activationId if (!playlistActivationId) throw new Error(`Playlist is not active`) - const currentPartInstance = this.CurrentPartInstance + const currentPartInstance = this.currentPartInstance - const newTakeCount = currentPartInstance ? currentPartInstance.PartInstance.takeCount + 1 : 0 // Increment + const newTakeCount = currentPartInstance ? currentPartInstance.partInstance.takeCount + 1 : 0 // Increment const segmentPlayoutId: SegmentPlayoutId = - currentPartInstance && nextPart.segmentId === currentPartInstance.PartInstance.segmentId - ? currentPartInstance.PartInstance.segmentPlayoutId + currentPartInstance && nextPart.segmentId === currentPartInstance.partInstance.segmentId + ? currentPartInstance.partInstance.segmentPlayoutId : getRandomId() const newPartInstance: DBPartInstance = { @@ -357,7 +357,7 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou playlistActivationId: playlistActivationId, segmentPlayoutId, takeCount: newTakeCount, - rehearsal: !!this.Playlist.rehearsal, + rehearsal: !!this.playlist.rehearsal, part: clone(nextPart), timings: { setAsNext: getCurrentTime(), @@ -369,7 +369,7 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou const partInstance = new PlayoutPartInstanceModelImpl(newPartInstance, pieceInstances, true) partInstance.recalculateExpectedDurationWithPreroll() - this.AllPartInstances.set(newPartInstance._id, partInstance) + this.allPartInstances.set(newPartInstance._id, partInstance) return partInstance } @@ -378,62 +378,62 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou rundown: PlayoutRundownModel, part: Omit ): PlayoutPartInstanceModel { - const currentPartInstance = this.CurrentPartInstance + const currentPartInstance = this.currentPartInstance if (!currentPartInstance) throw new Error('No currentPartInstance') const scratchpadSegment = rundown.getScratchpadSegment() if (!scratchpadSegment) throw new Error('No scratchpad segment') - if (this.LoadedPartInstances.find((p) => p.PartInstance.segmentId === scratchpadSegment.Segment._id)) + if (this.loadedPartInstances.find((p) => p.partInstance.segmentId === scratchpadSegment.segment._id)) throw new Error('Scratchpad segment already has content') - const activationId = this.Playlist.activationId + const activationId = this.playlist.activationId if (!activationId) throw new Error('Playlist is not active') const newPartInstance: DBPartInstance = { _id: getRandomId(), - rundownId: rundown.Rundown._id, - segmentId: scratchpadSegment.Segment._id, + rundownId: rundown.rundown._id, + segmentId: scratchpadSegment.segment._id, playlistActivationId: activationId, segmentPlayoutId: getRandomId(), takeCount: 1, - rehearsal: !!this.Playlist.rehearsal, + rehearsal: !!this.playlist.rehearsal, orphaned: 'adlib-part', part: { ...part, - rundownId: rundown.Rundown._id, - segmentId: scratchpadSegment.Segment._id, + rundownId: rundown.rundown._id, + segmentId: scratchpadSegment.segment._id, }, } const partInstance = new PlayoutPartInstanceModelImpl(newPartInstance, [], true) partInstance.recalculateExpectedDurationWithPreroll() - this.AllPartInstances.set(newPartInstance._id, partInstance) + this.allPartInstances.set(newPartInstance._id, partInstance) return partInstance } cycleSelectedPartInstances(): void { - this.PlaylistImpl.previousPartInfo = this.PlaylistImpl.currentPartInfo - this.PlaylistImpl.currentPartInfo = this.PlaylistImpl.nextPartInfo - this.PlaylistImpl.nextPartInfo = null - this.PlaylistImpl.lastTakeTime = getCurrentTime() + this.playlistImpl.previousPartInfo = this.playlistImpl.currentPartInfo + this.playlistImpl.currentPartInfo = this.playlistImpl.nextPartInfo + this.playlistImpl.nextPartInfo = null + this.playlistImpl.lastTakeTime = getCurrentTime() - if (!this.PlaylistImpl.holdState || this.PlaylistImpl.holdState === RundownHoldState.COMPLETE) { - this.PlaylistImpl.holdState = RundownHoldState.NONE + if (!this.playlistImpl.holdState || this.playlistImpl.holdState === RundownHoldState.COMPLETE) { + this.playlistImpl.holdState = RundownHoldState.NONE } else { - this.PlaylistImpl.holdState = this.PlaylistImpl.holdState + 1 + this.playlistImpl.holdState = this.playlistImpl.holdState + 1 } - this.#PlaylistHasChanged = true + this.#playlistHasChanged = true } deactivatePlaylist(): void { - delete this.PlaylistImpl.activationId + delete this.playlistImpl.activationId this.clearSelectedPartInstances() - this.#PlaylistHasChanged = true + this.#playlistHasChanged = true } queuePartInstanceTimingEvent(partInstanceId: PartInstanceId): void { @@ -441,8 +441,8 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou } queueNotifyCurrentlyPlayingPartEvent(rundownId: RundownId, partInstance: PlayoutPartInstanceModel | null): void { - if (partInstance && partInstance.PartInstance.part.shouldNotifyCurrentPlayingPart) { - this.#PendingNotifyCurrentlyPlayingPartEvent.set(rundownId, partInstance.PartInstance.part.externalId) + if (partInstance && partInstance.partInstance.part.shouldNotifyCurrentPlayingPart) { + this.#PendingNotifyCurrentlyPlayingPartEvent.set(rundownId, partInstance.partInstance.part.externalId) } else if (!partInstance) { this.#PendingNotifyCurrentlyPlayingPartEvent.set(rundownId, null) } @@ -451,9 +451,9 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou removeAllRehearsalPartInstances(): void { const partInstancesToRemove: PartInstanceId[] = [] - for (const [id, partInstance] of this.AllPartInstances.entries()) { - if (partInstance?.PartInstance.rehearsal) { - this.AllPartInstances.set(id, null) + for (const [id, partInstance] of this.allPartInstances.entries()) { + if (partInstance?.partInstance.rehearsal) { + this.allPartInstances.set(id, null) partInstancesToRemove.push(id) } } @@ -462,7 +462,7 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou this.deferAfterSave(async (playoutModel) => { const rundownIds = playoutModel.getRundownIds() // We need to keep any for PartInstances which are still existent in the cache (as they werent removed) - const partInstanceIdsInCache = playoutModel.LoadedPartInstances.map((p) => p.PartInstance._id) + const partInstanceIdsInCache = playoutModel.loadedPartInstances.map((p) => p.partInstance._id) // Find all the partInstances which are not loaded, but should be removed const removeFromDb = await this.context.directCollections.PartInstances.findFetch( @@ -495,9 +495,9 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou } removeUntakenPartInstances(): void { - for (const partInstance of this.OlderPartInstances) { - if (!partInstance.PartInstance.isTaken) { - this.AllPartInstances.set(partInstance.PartInstance._id, null) + for (const partInstance of this.olderPartInstances) { + if (!partInstance.partInstance.isTaken) { + this.allPartInstances.set(partInstance.partInstance._id, null) } } } @@ -506,22 +506,22 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou * Reset the playlist for playout */ resetPlaylist(regenerateActivationId: boolean): void { - this.PlaylistImpl.previousPartInfo = null - this.PlaylistImpl.currentPartInfo = null - this.PlaylistImpl.nextPartInfo = null - this.PlaylistImpl.holdState = RundownHoldState.NONE - this.PlaylistImpl.resetTime = getCurrentTime() + this.playlistImpl.previousPartInfo = null + this.playlistImpl.currentPartInfo = null + this.playlistImpl.nextPartInfo = null + this.playlistImpl.holdState = RundownHoldState.NONE + this.playlistImpl.resetTime = getCurrentTime() - delete this.PlaylistImpl.lastTakeTime - delete this.PlaylistImpl.startedPlayback - delete this.PlaylistImpl.rundownsStartedPlayback - delete this.PlaylistImpl.previousPersistentState - delete this.PlaylistImpl.trackedAbSessions - delete this.PlaylistImpl.queuedSegmentId + delete this.playlistImpl.lastTakeTime + delete this.playlistImpl.startedPlayback + delete this.playlistImpl.rundownsStartedPlayback + delete this.playlistImpl.previousPersistentState + delete this.playlistImpl.trackedAbSessions + delete this.playlistImpl.queuedSegmentId - if (regenerateActivationId) this.PlaylistImpl.activationId = getRandomId() + if (regenerateActivationId) this.playlistImpl.activationId = getRandomId() - this.#PlaylistHasChanged = true + this.#playlistHasChanged = true } async saveAllToDatabase(): Promise { @@ -530,7 +530,7 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou } // TODO - ideally we should make sure to preserve the lock during this operation - if (!this.PlaylistLock.isLocked) { + if (!this.playlistLock.isLocked) { throw new Error('Cannot save changes with released playlist lock') } @@ -543,8 +543,8 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou this.#deferredBeforeSaveFunctions.length = 0 // clear the array // Prioritise the timeline for publication reasons - if (this.#TimelineHasChanged && this.TimelineImpl) { - await this.context.directCollections.Timelines.replace(this.TimelineImpl) + if (this.#timelineHasChanged && this.timelineImpl) { + await this.context.directCollections.Timelines.replace(this.timelineImpl) if (!process.env.JEST_WORKER_ID) { // Wait a little bit before saving the rest. // The idea is that this allows for the high priority publications to update (such as the Timeline), @@ -552,18 +552,18 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou await sleep(2) } } - this.#TimelineHasChanged = false + this.#timelineHasChanged = false await Promise.all([ - this.#PlaylistHasChanged - ? this.context.directCollections.RundownPlaylists.replace(this.PlaylistImpl) + this.#playlistHasChanged + ? this.context.directCollections.RundownPlaylists.replace(this.playlistImpl) : undefined, - ...writePartInstancesAndPieceInstances(this.context, this.AllPartInstances), - writeScratchpadSegments(this.context, this.RundownsImpl), + ...writePartInstancesAndPieceInstances(this.context, this.allPartInstances), + writeScratchpadSegments(this.context, this.rundownsImpl), this.#baselineHelper.saveAllToDatabase(), ]) - this.#PlaylistHasChanged = false + this.#playlistHasChanged = false // Execute cache.deferAfterSave()'s for (const fn of this.#deferredAfterSaveFunctions) { @@ -573,7 +573,7 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou for (const partInstanceId of this.#PendingPartInstanceTimingEvents) { // Run in the background, we don't want to hold onto the lock to do this - queuePartInstanceTimingEvent(this.context, this.PlaylistId, partInstanceId) + queuePartInstanceTimingEvent(this.context, this.playlistId, partInstanceId) } this.#PendingPartInstanceTimingEvents.clear() @@ -583,7 +583,7 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou this.context .queueEventJob(EventsJobs.NotifyCurrentlyPlayingPart, { rundownId: rundownId, - isRehearsal: !!this.Playlist.rehearsal, + isRehearsal: !!this.playlist.rehearsal, partExternalId: partExternalId, }) .catch((e) => { @@ -596,9 +596,9 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou } setHoldState(newState: RundownHoldState): void { - this.PlaylistImpl.holdState = newState + this.playlistImpl.holdState = newState - this.#PlaylistHasChanged = true + this.#playlistHasChanged = true } setOnTimelineGenerateResult( @@ -606,11 +606,11 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou assignedAbSessions: Record, trackedAbSessions: ABSessionInfo[] ): void { - this.PlaylistImpl.previousPersistentState = persistentState - this.PlaylistImpl.assignedAbSessions = assignedAbSessions - this.PlaylistImpl.trackedAbSessions = trackedAbSessions + this.playlistImpl.previousPersistentState = persistentState + this.playlistImpl.assignedAbSessions = assignedAbSessions + this.playlistImpl.trackedAbSessions = trackedAbSessions - this.#PlaylistHasChanged = true + this.#playlistHasChanged = true } setPartInstanceAsNext( @@ -620,61 +620,61 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou nextTimeOffset?: number ): void { if (partInstance) { - const storedPartInstance = this.AllPartInstances.get(partInstance.PartInstance._id) + const storedPartInstance = this.allPartInstances.get(partInstance.partInstance._id) if (!storedPartInstance) throw new Error(`PartInstance being set as next was not constructed correctly`) // Make sure we were given the exact same object if (storedPartInstance !== partInstance) throw new Error(`PartInstance being set as next is not current`) } if (partInstance) { - this.PlaylistImpl.nextPartInfo = literal({ - partInstanceId: partInstance.PartInstance._id, - rundownId: partInstance.PartInstance.rundownId, - manuallySelected: !!(setManually || partInstance.PartInstance.orphaned), + this.playlistImpl.nextPartInfo = literal({ + partInstanceId: partInstance.partInstance._id, + rundownId: partInstance.partInstance.rundownId, + manuallySelected: !!(setManually || partInstance.partInstance.orphaned), consumesQueuedSegmentId, }) - this.PlaylistImpl.nextTimeOffset = nextTimeOffset || null + this.playlistImpl.nextTimeOffset = nextTimeOffset || null } else { - this.PlaylistImpl.nextPartInfo = null - this.PlaylistImpl.nextTimeOffset = null + this.playlistImpl.nextPartInfo = null + this.playlistImpl.nextTimeOffset = null } - this.#PlaylistHasChanged = true + this.#playlistHasChanged = true } setQueuedSegment(segment: PlayoutSegmentModel | null): void { - this.PlaylistImpl.queuedSegmentId = segment?.Segment?._id ?? undefined + this.playlistImpl.queuedSegmentId = segment?.segment?._id ?? undefined - this.#PlaylistHasChanged = true + this.#playlistHasChanged = true } setRundownStartedPlayback(rundownId: RundownId, timestamp: number): void { - if (!this.PlaylistImpl.rundownsStartedPlayback) { - this.PlaylistImpl.rundownsStartedPlayback = {} + if (!this.playlistImpl.rundownsStartedPlayback) { + this.playlistImpl.rundownsStartedPlayback = {} } // If the partInstance is "untimed", it will not update the playlist's startedPlayback and will not count time in the GUI: const rundownIdStr = unprotectString(rundownId) - if (!this.PlaylistImpl.rundownsStartedPlayback[rundownIdStr]) { - this.PlaylistImpl.rundownsStartedPlayback[rundownIdStr] = timestamp + if (!this.playlistImpl.rundownsStartedPlayback[rundownIdStr]) { + this.playlistImpl.rundownsStartedPlayback[rundownIdStr] = timestamp } - if (!this.PlaylistImpl.startedPlayback) { - this.PlaylistImpl.startedPlayback = timestamp + if (!this.playlistImpl.startedPlayback) { + this.playlistImpl.startedPlayback = timestamp } - this.#PlaylistHasChanged = true + this.#playlistHasChanged = true } setTimeline(timelineObjs: TimelineObjGeneric[], generationVersions: TimelineCompleteGenerationVersions): void { - this.TimelineImpl = { + this.timelineImpl = { _id: this.context.studioId, timelineHash: getRandomId(), // randomized on every timeline change generated: getCurrentTime(), timelineBlob: serializeTimelineBlob(timelineObjs), generationVersions: generationVersions, } - this.#TimelineHasChanged = true + this.#timelineHasChanged = true } setExpectedPackagesForStudioBaseline(packages: ExpectedPackageDBFromStudioBaselineObjects[]): void { @@ -731,18 +731,18 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou ) ) - if (this.#TimelineHasChanged) + if (this.#timelineHasChanged) logOrThrowError(new Error(`Failed no changes in cache assertion, Timeline has been changed`)) - if (this.#PlaylistHasChanged) + if (this.#playlistHasChanged) logOrThrowError(new Error(`Failed no changes in cache assertion, Playlist has been changed`)) - if (this.RundownsImpl.find((rd) => rd.ScratchPadSegmentHasChanged)) + if (this.rundownsImpl.find((rd) => rd.ScratchPadSegmentHasChanged)) logOrThrowError(new Error(`Failed no changes in cache assertion, a scratchpad Segment has been changed`)) if ( - Array.from(this.AllPartInstances.values()).find( - (part) => !part || part.PartInstanceHasChanges || part.ChangedPieceInstanceIds().length > 0 + Array.from(this.allPartInstances.values()).find( + (part) => !part || part.partInstanceHasChanges || part.changedPieceInstanceIds().length > 0 ) ) logOrThrowError(new Error(`Failed no changes in cache assertion, a PartInstance has been changed`)) diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts index b3d729259b..e2ac11271f 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts @@ -39,16 +39,16 @@ class PlayoutPartInstanceModelSnapshotImpl implements PlayoutPartInstanceModelSn isRestored = false - readonly PartInstance: DBPartInstance - readonly PartInstanceHasChanges: boolean - readonly PieceInstances: ReadonlyMap + readonly partInstance: DBPartInstance + readonly partInstanceHasChanges: boolean + readonly pieceInstances: ReadonlyMap constructor(copyFrom: PlayoutPartInstanceModelImpl) { - this.PartInstance = clone(copyFrom.PartInstanceImpl) - this.PartInstanceHasChanges = copyFrom.PartInstanceHasChanges + this.partInstance = clone(copyFrom.partInstanceImpl) + this.partInstanceHasChanges = copyFrom.partInstanceHasChanges const pieceInstances = new Map() - for (const [pieceInstanceId, pieceInstance] of copyFrom.PieceInstancesImpl) { + for (const [pieceInstanceId, pieceInstance] of copyFrom.pieceInstancesImpl) { if (pieceInstance) { pieceInstances.set(pieceInstanceId, { PieceInstance: clone(pieceInstance.PieceInstanceImpl), @@ -58,28 +58,28 @@ class PlayoutPartInstanceModelSnapshotImpl implements PlayoutPartInstanceModelSn pieceInstances.set(pieceInstanceId, null) } } - this.PieceInstances = pieceInstances + this.pieceInstances = pieceInstances } } export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { - PartInstanceImpl: DBPartInstance - PieceInstancesImpl: Map + partInstanceImpl: DBPartInstance + pieceInstancesImpl: Map #setPartInstanceValue(key: T, newValue: DBPartInstance[T]): void { if (newValue === undefined) { - delete this.PartInstanceImpl[key] + delete this.partInstanceImpl[key] } else { - this.PartInstanceImpl[key] = newValue + this.partInstanceImpl[key] = newValue } - this.#PartInstanceHasChanges = true + this.#partInstanceHasChanges = true } #compareAndSetPartInstanceValue( key: T, newValue: DBPartInstance[T], deepEqual = false ): boolean { - const oldValue = this.PartInstanceImpl[key] + const oldValue = this.partInstanceImpl[key] const areEqual = deepEqual ? _.isEqual(oldValue, newValue) : oldValue === newValue @@ -94,15 +94,15 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { #setPartValue(key: T, newValue: DBPart[T]): void { if (newValue === undefined) { - delete this.PartInstanceImpl.part[key] + delete this.partInstanceImpl.part[key] } else { - this.PartInstanceImpl.part[key] = newValue + this.partInstanceImpl.part[key] = newValue } - this.#PartInstanceHasChanges = true + this.#partInstanceHasChanges = true } #compareAndSetPartValue(key: T, newValue: DBPart[T], deepEqual = false): boolean { - const oldValue = this.PartInstanceImpl.part[key] + const oldValue = this.partInstanceImpl.part[key] const areEqual = deepEqual ? _.isEqual(oldValue, newValue) : oldValue === newValue @@ -115,39 +115,39 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { } } - #PartInstanceHasChanges = false - get PartInstanceHasChanges(): boolean { - return this.#PartInstanceHasChanges + #partInstanceHasChanges = false + get partInstanceHasChanges(): boolean { + return this.#partInstanceHasChanges } - ChangedPieceInstanceIds(): PieceInstanceId[] { + changedPieceInstanceIds(): PieceInstanceId[] { const result: PieceInstanceId[] = [] - for (const [id, pieceInstance] of this.PieceInstancesImpl.entries()) { + for (const [id, pieceInstance] of this.pieceInstancesImpl.entries()) { if (!pieceInstance || pieceInstance.HasChanges) result.push(id) } return result } - HasAnyChanges(): boolean { - return this.#PartInstanceHasChanges || this.ChangedPieceInstanceIds().length > 0 + hasAnyChanges(): boolean { + return this.#partInstanceHasChanges || this.changedPieceInstanceIds().length > 0 } clearChangedFlags(): void { - this.#PartInstanceHasChanges = false + this.#partInstanceHasChanges = false - for (const [id, value] of this.PieceInstancesImpl) { + for (const [id, value] of this.pieceInstancesImpl) { if (!value) { - this.PieceInstancesImpl.delete(id) + this.pieceInstancesImpl.delete(id) } else if (value.HasChanges) { value.clearChangedFlag() } } } - get PartInstance(): ReadonlyDeep { - return this.PartInstanceImpl + get partInstance(): ReadonlyDeep { + return this.partInstanceImpl } - get PieceInstances(): PlayoutPieceInstanceModel[] { + get pieceInstances(): PlayoutPieceInstanceModel[] { const result: PlayoutPieceInstanceModel[] = [] - for (const pieceWrapped of this.PieceInstancesImpl.values()) { + for (const pieceWrapped of this.pieceInstancesImpl.values()) { if (pieceWrapped) result.push(pieceWrapped) } @@ -155,12 +155,12 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { } constructor(partInstance: DBPartInstance, pieceInstances: PieceInstance[], hasChanges: boolean) { - this.PartInstanceImpl = partInstance - this.#PartInstanceHasChanges = hasChanges + this.partInstanceImpl = partInstance + this.#partInstanceHasChanges = hasChanges - this.PieceInstancesImpl = new Map() + this.pieceInstancesImpl = new Map() for (const pieceInstance of pieceInstances) { - this.PieceInstancesImpl.set(pieceInstance._id, new PlayoutPieceInstanceModelImpl(pieceInstance, hasChanges)) + this.pieceInstancesImpl.set(pieceInstance._id, new PlayoutPieceInstanceModelImpl(pieceInstance, hasChanges)) } } @@ -172,29 +172,29 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { if (!(snapshot instanceof PlayoutPartInstanceModelSnapshotImpl)) throw new Error(`Cannot restore a Snapshot from an different Model`) - if (snapshot.PartInstance._id !== this.PartInstance._id) + if (snapshot.partInstance._id !== this.partInstance._id) throw new Error(`Cannot restore a Snapshot from an different PartInstance`) if (snapshot.isRestored) throw new Error(`Cannot restore a Snapshot which has already been restored`) snapshot.isRestored = true - this.PartInstanceImpl = snapshot.PartInstance - this.#PartInstanceHasChanges = snapshot.PartInstanceHasChanges - this.PieceInstancesImpl.clear() - for (const [pieceInstanceId, pieceInstance] of snapshot.PieceInstances) { + this.partInstanceImpl = snapshot.partInstance + this.#partInstanceHasChanges = snapshot.partInstanceHasChanges + this.pieceInstancesImpl.clear() + for (const [pieceInstanceId, pieceInstance] of snapshot.pieceInstances) { if (pieceInstance) { - this.PieceInstancesImpl.set( + this.pieceInstancesImpl.set( pieceInstanceId, new PlayoutPieceInstanceModelImpl(pieceInstance.PieceInstance, pieceInstance.HasChanges) ) } else { - this.PieceInstancesImpl.set(pieceInstanceId, null) + this.pieceInstancesImpl.set(pieceInstanceId, null) } } } appendNotes(notes: PartNote[]): void { - this.#setPartValue('notes', [...(this.PartInstanceImpl.part.notes ?? []), ...clone(notes)]) + this.#setPartValue('notes', [...(this.partInstanceImpl.part.notes ?? []), ...clone(notes)]) } blockTakeUntil(timestamp: Time | null): void { @@ -202,7 +202,7 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { } getPieceInstance(id: PieceInstanceId): PlayoutPieceInstanceModel | undefined { - return this.PieceInstancesImpl.get(id) ?? undefined + return this.pieceInstancesImpl.get(id) ?? undefined } insertAdlibbedPiece( @@ -210,35 +210,35 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { fromAdlibId: PieceId | undefined ): PlayoutPieceInstanceModel { const pieceInstance: PieceInstance = { - _id: protectString(`${this.PartInstance._id}_${piece._id}`), - rundownId: this.PartInstance.rundownId, - playlistActivationId: this.PartInstance.playlistActivationId, - partInstanceId: this.PartInstance._id, + _id: protectString(`${this.partInstance._id}_${piece._id}`), + rundownId: this.partInstance.rundownId, + playlistActivationId: this.partInstance.playlistActivationId, + partInstanceId: this.partInstance._id, piece: clone( omitPiecePropertiesForInstance({ ...piece, - startPartId: this.PartInstanceImpl.part._id, + startPartId: this.partInstanceImpl.part._id, }) ), } // Ensure it is labelled as dynamic - pieceInstance.partInstanceId = this.PartInstance._id - pieceInstance.piece.startPartId = this.PartInstance.part._id + pieceInstance.partInstanceId = this.partInstance._id + pieceInstance.piece.startPartId = this.partInstance.part._id pieceInstance.adLibSourceId = fromAdlibId - if (this.PartInstance.isTaken) pieceInstance.dynamicallyInserted = getCurrentTime() + if (this.partInstance.isTaken) pieceInstance.dynamicallyInserted = getCurrentTime() setupPieceInstanceInfiniteProperties(pieceInstance) const pieceInstanceModel = new PlayoutPieceInstanceModelImpl(pieceInstance, true) - this.PieceInstancesImpl.set(pieceInstance._id, pieceInstanceModel) + this.pieceInstancesImpl.set(pieceInstance._id, pieceInstanceModel) return pieceInstanceModel } insertHoldPieceInstance(extendPieceInstance: PlayoutPieceInstanceModel): PlayoutPieceInstanceModel { - const extendPieceInfinite = extendPieceInstance.PieceInstance.infinite + const extendPieceInfinite = extendPieceInstance.pieceInstance.infinite if (!extendPieceInfinite) throw new Error('Piece being extended is not infinite!') if (extendPieceInfinite.infiniteInstanceIndex !== 0 || extendPieceInfinite.fromPreviousPart) throw new Error('Piece being extended is not infinite due to HOLD!') @@ -247,49 +247,49 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { // make the extension const newInstance: PieceInstance = { - _id: protectString(extendPieceInstance.PieceInstance._id + '_hold'), - playlistActivationId: extendPieceInstance.PieceInstance.playlistActivationId, - rundownId: extendPieceInstance.PieceInstance.rundownId, - partInstanceId: this.PartInstance._id, + _id: protectString(extendPieceInstance.pieceInstance._id + '_hold'), + playlistActivationId: extendPieceInstance.pieceInstance.playlistActivationId, + rundownId: extendPieceInstance.pieceInstance.rundownId, + partInstanceId: this.partInstance._id, dynamicallyInserted: getCurrentTime(), piece: { - ...clone(extendPieceInstance.PieceInstance.piece), + ...clone(extendPieceInstance.pieceInstance.piece), enable: { start: 0 }, extendOnHold: false, }, infinite: { infiniteInstanceId: infiniteInstanceId, infiniteInstanceIndex: 1, - infinitePieceId: extendPieceInstance.PieceInstance.piece._id, + infinitePieceId: extendPieceInstance.pieceInstance.piece._id, fromPreviousPart: true, fromHold: true, }, // Preserve the timings from the playing instance - reportedStartedPlayback: extendPieceInstance.PieceInstance.reportedStartedPlayback, - reportedStoppedPlayback: extendPieceInstance.PieceInstance.reportedStoppedPlayback, - plannedStartedPlayback: extendPieceInstance.PieceInstance.plannedStartedPlayback, - plannedStoppedPlayback: extendPieceInstance.PieceInstance.plannedStoppedPlayback, + reportedStartedPlayback: extendPieceInstance.pieceInstance.reportedStartedPlayback, + reportedStoppedPlayback: extendPieceInstance.pieceInstance.reportedStoppedPlayback, + plannedStartedPlayback: extendPieceInstance.pieceInstance.plannedStartedPlayback, + plannedStoppedPlayback: extendPieceInstance.pieceInstance.plannedStoppedPlayback, } const pieceInstanceModel = new PlayoutPieceInstanceModelImpl(newInstance, true) - this.PieceInstancesImpl.set(newInstance._id, pieceInstanceModel) + this.pieceInstancesImpl.set(newInstance._id, pieceInstanceModel) return pieceInstanceModel } insertPlannedPiece(piece: Omit): PlayoutPieceInstanceModel { - const pieceInstanceId = getPieceInstanceIdForPiece(this.PartInstance._id, piece._id) - if (this.PieceInstancesImpl.has(pieceInstanceId)) + const pieceInstanceId = getPieceInstanceIdForPiece(this.partInstance._id, piece._id) + if (this.pieceInstancesImpl.has(pieceInstanceId)) throw new Error(`PieceInstance "${pieceInstanceId}" already exists`) const newPieceInstance: PieceInstance = { - _id: getPieceInstanceIdForPiece(this.PartInstance._id, piece._id), - rundownId: this.PartInstance.rundownId, - playlistActivationId: this.PartInstance.playlistActivationId, - partInstanceId: this.PartInstance._id, + _id: getPieceInstanceIdForPiece(this.partInstance._id, piece._id), + rundownId: this.partInstance.rundownId, + playlistActivationId: this.partInstance.playlistActivationId, + partInstanceId: this.partInstance._id, piece: { ...piece, - startPartId: this.PartInstance.part._id, + startPartId: this.partInstance.part._id, }, } @@ -297,7 +297,7 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { setupPieceInstanceInfiniteProperties(newPieceInstance) const pieceInstanceModel = new PlayoutPieceInstanceModelImpl(newPieceInstance, true) - this.PieceInstancesImpl.set(pieceInstanceId, pieceInstanceModel) + this.pieceInstancesImpl.set(pieceInstanceId, pieceInstanceModel) return pieceInstanceModel } @@ -310,10 +310,10 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { ): PlayoutPieceInstanceModel { const pieceId: PieceId = getRandomId() const newPieceInstance: PieceInstance = { - _id: protectString(`${this.PartInstance._id}_${pieceId}`), - rundownId: this.PartInstance.rundownId, - playlistActivationId: this.PartInstance.playlistActivationId, - partInstanceId: this.PartInstance._id, + _id: protectString(`${this.partInstance._id}_${pieceId}`), + rundownId: this.partInstance.rundownId, + playlistActivationId: this.partInstance.playlistActivationId, + partInstanceId: this.partInstance._id, piece: { _id: pieceId, externalId: '-', @@ -323,7 +323,7 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { outputLayerId: outputLayerId, invalid: false, name: '', - startPartId: this.PartInstance.part._id, + startPartId: this.partInstance.part._id, pieceType: IBlueprintPieceType.Normal, virtual: true, content: {}, @@ -335,7 +335,7 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { setupPieceInstanceInfiniteProperties(newPieceInstance) const pieceInstanceModel = new PlayoutPieceInstanceModelImpl(newPieceInstance, true) - this.PieceInstancesImpl.set(newPieceInstance._id, pieceInstanceModel) + this.pieceInstancesImpl.set(newPieceInstance._id, pieceInstanceModel) return pieceInstanceModel } @@ -343,7 +343,7 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { markAsReset(): void { this.#compareAndSetPartInstanceValue('reset', true) - for (const pieceInstance of this.PieceInstancesImpl.values()) { + for (const pieceInstance of this.pieceInstancesImpl.values()) { if (!pieceInstance) continue pieceInstance.compareAndSetPieceInstanceValue('reset', true) @@ -352,8 +352,8 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { recalculateExpectedDurationWithPreroll(): void { const newDuration = calculatePartExpectedDurationWithPreroll( - this.PartInstanceImpl.part, - this.PieceInstances.map((p) => p.PieceInstance.piece) + this.partInstanceImpl.part, + this.pieceInstances.map((p) => p.pieceInstance.piece) ) this.#compareAndSetPartValue('expectedDurationWithPreroll', newDuration) @@ -362,9 +362,9 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { removePieceInstance(id: PieceInstanceId): boolean { // Future: should this limit what can be removed based on type/infinite - const pieceInstanceWrapped = this.PieceInstancesImpl.get(id) + const pieceInstanceWrapped = this.pieceInstancesImpl.get(id) if (pieceInstanceWrapped) { - this.PieceInstancesImpl.set(id, null) + this.pieceInstancesImpl.set(id, null) return true } @@ -375,16 +375,16 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { // Future: this should do some of the wrapping from a Piece into a PieceInstance // Remove old infinite pieces - for (const [id, piece] of this.PieceInstancesImpl.entries()) { + for (const [id, piece] of this.pieceInstancesImpl.entries()) { if (!piece) continue - if (piece.PieceInstance.infinite?.fromPreviousPlayhead) { - this.PieceInstancesImpl.set(id, null) + if (piece.pieceInstance.infinite?.fromPreviousPlayhead) { + this.pieceInstancesImpl.set(id, null) } } for (const pieceInstance of pieceInstances) { - if (this.PieceInstancesImpl.has(pieceInstance._id)) + if (this.pieceInstancesImpl.has(pieceInstance._id)) throw new Error( `Cannot replace infinite PieceInstance "${pieceInstance._id}" as it replaces a non-infinite` ) @@ -394,21 +394,21 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { // Future: should this do any deeper validation of the PieceInstances? - this.PieceInstancesImpl.set(pieceInstance._id, new PlayoutPieceInstanceModelImpl(pieceInstance, true)) + this.pieceInstancesImpl.set(pieceInstance._id, new PlayoutPieceInstanceModelImpl(pieceInstance, true)) } } mergeOrInsertPieceInstance(doc: ReadonlyDeep): PlayoutPieceInstanceModel { // Future: this should do some validation of the new PieceInstance - const existingPieceInstance = this.PieceInstancesImpl.get(doc._id) + const existingPieceInstance = this.pieceInstancesImpl.get(doc._id) if (existingPieceInstance) { existingPieceInstance.mergeProperties(doc) return existingPieceInstance } else { const newPieceInstance = new PlayoutPieceInstanceModelImpl(clone(doc), true) - this.PieceInstancesImpl.set(newPieceInstance.PieceInstance._id, newPieceInstance) + this.pieceInstancesImpl.set(newPieceInstance.pieceInstance._id, newPieceInstance) return newPieceInstance } } @@ -420,21 +420,21 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { setPlaylistActivationId(id: RundownPlaylistActivationId): void { this.#compareAndSetPartInstanceValue('playlistActivationId', id) - for (const pieceInstance of this.PieceInstancesImpl.values()) { + for (const pieceInstance of this.pieceInstancesImpl.values()) { if (!pieceInstance) continue pieceInstance.compareAndSetPieceInstanceValue('playlistActivationId', id) } } setPlannedStartedPlayback(time: Time | undefined): void { - const timings = { ...this.PartInstanceImpl.timings } + const timings = { ...this.partInstanceImpl.timings } timings.plannedStartedPlayback = time delete timings.plannedStoppedPlayback this.#compareAndSetPartInstanceValue('timings', timings, true) } setPlannedStoppedPlayback(time: Time | undefined): void { - const timings = { ...this.PartInstanceImpl.timings } + const timings = { ...this.partInstanceImpl.timings } if (timings?.plannedStartedPlayback && !timings.plannedStoppedPlayback) { if (time) { timings.plannedStoppedPlayback = time @@ -448,7 +448,7 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { } } setReportedStartedPlayback(time: Time): boolean { - const timings = { ...this.PartInstanceImpl.timings } + const timings = { ...this.partInstanceImpl.timings } if (!timings.reportedStartedPlayback) { timings.reportedStartedPlayback = time @@ -461,7 +461,7 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { return false } setReportedStoppedPlayback(time: number): boolean { - const timings = { ...this.PartInstanceImpl.timings } + const timings = { ...this.partInstanceImpl.timings } if (!timings.reportedStoppedPlayback) { timings.reportedStoppedPlayback = time @@ -479,7 +479,7 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { setTaken(takeTime: number, playOffset: number): void { this.#compareAndSetPartInstanceValue('isTaken', true) - const timings = { ...this.PartInstanceImpl.timings } + const timings = { ...this.partInstanceImpl.timings } timings.take = takeTime timings.playOffset = playOffset @@ -507,7 +507,7 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { this.#compareAndSetPartInstanceValue( 'part', { - ...this.PartInstanceImpl.part, + ...this.partInstanceImpl.part, ...trimmedProps, }, true diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutPieceInstanceModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutPieceInstanceModelImpl.ts index d41ecfe42a..e011552abf 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutPieceInstanceModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutPieceInstanceModelImpl.ts @@ -67,7 +67,7 @@ export class PlayoutPieceInstanceModelImpl implements PlayoutPieceInstanceModel this.#HasChanges = false } - get PieceInstance(): ReadonlyDeep { + get pieceInstance(): ReadonlyDeep { return this.PieceInstanceImpl } diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutRundownModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutRundownModelImpl.ts index 2406fdecb9..ae41598f2e 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutRundownModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutRundownModelImpl.ts @@ -12,10 +12,10 @@ import { getCurrentTime } from '../../../lib' import { PlayoutSegmentModelImpl } from './PlayoutSegmentModelImpl' export class PlayoutRundownModelImpl implements PlayoutRundownModel { - readonly Rundown: ReadonlyDeep + readonly rundown: ReadonlyDeep readonly #segments: PlayoutSegmentModelImpl[] - readonly BaselineObjects: ReadonlyDeep + readonly baselineObjects: ReadonlyDeep #scratchPadSegmentHasChanged = false /** @@ -36,23 +36,23 @@ export class PlayoutRundownModelImpl implements PlayoutRundownModel { segments: PlayoutSegmentModelImpl[], baselineObjects: ReadonlyDeep ) { - segments.sort((a, b) => a.Segment._rank - b.Segment._rank) + segments.sort((a, b) => a.segment._rank - b.segment._rank) - this.Rundown = rundown + this.rundown = rundown this.#segments = segments - this.BaselineObjects = baselineObjects + this.baselineObjects = baselineObjects } - get Segments(): readonly PlayoutSegmentModel[] { + get segments(): readonly PlayoutSegmentModel[] { return this.#segments } getSegment(id: SegmentId): PlayoutSegmentModel | undefined { - return this.Segments.find((segment) => segment.Segment._id === id) + return this.segments.find((segment) => segment.segment._id === id) } getSegmentIds(): SegmentId[] { - return this.Segments.map((segment) => segment.Segment._id) + return this.segments.map((segment) => segment.segment._id) } getAllPartIds(): PartId[] { @@ -60,14 +60,14 @@ export class PlayoutRundownModelImpl implements PlayoutRundownModel { } getAllOrderedParts(): ReadonlyDeep[] { - return this.Segments.flatMap((segment) => segment.Parts) + return this.segments.flatMap((segment) => segment.parts) } insertScratchpadSegment(): SegmentId { - const existingSegment = this.Segments.find((s) => s.Segment.orphaned === SegmentOrphanedReason.SCRATCHPAD) + const existingSegment = this.segments.find((s) => s.segment.orphaned === SegmentOrphanedReason.SCRATCHPAD) if (existingSegment) throw UserError.create(UserErrorMessage.ScratchpadAlreadyActive) - const minSegmentRank = Math.min(0, ...this.Segments.map((s) => s.Segment._rank)) + const minSegmentRank = Math.min(0, ...this.segments.map((s) => s.segment._rank)) const segmentId: SegmentId = getRandomId() this.#segments.unshift( @@ -77,7 +77,7 @@ export class PlayoutRundownModelImpl implements PlayoutRundownModel { _rank: minSegmentRank - 1, externalId: '__scratchpad__', externalModified: getCurrentTime(), - rundownId: this.Rundown._id, + rundownId: this.rundown._id, orphaned: SegmentOrphanedReason.SCRATCHPAD, name: '', }, @@ -91,7 +91,7 @@ export class PlayoutRundownModelImpl implements PlayoutRundownModel { } removeScratchpadSegment(): boolean { - const index = this.#segments.findIndex((s) => s.Segment.orphaned === SegmentOrphanedReason.SCRATCHPAD) + const index = this.#segments.findIndex((s) => s.segment.orphaned === SegmentOrphanedReason.SCRATCHPAD) if (index === -1) return false this.#segments.splice(index, 1) @@ -102,15 +102,15 @@ export class PlayoutRundownModelImpl implements PlayoutRundownModel { getScratchpadSegment(): PlayoutSegmentModel | undefined { // Note: this assumes there will be up to one per rundown - return this.#segments.find((s) => s.Segment.orphaned === SegmentOrphanedReason.SCRATCHPAD) + return this.#segments.find((s) => s.segment.orphaned === SegmentOrphanedReason.SCRATCHPAD) } setScratchpadSegmentRank(rank: number): void { - const segment = this.#segments.find((s) => s.Segment.orphaned === SegmentOrphanedReason.SCRATCHPAD) + const segment = this.#segments.find((s) => s.segment.orphaned === SegmentOrphanedReason.SCRATCHPAD) if (!segment) throw new Error('Scratchpad segment does not exist!') segment.setScratchpadRank(rank) - this.#segments.sort((a, b) => a.Segment._rank - b.Segment._rank) + this.#segments.sort((a, b) => a.segment._rank - b.segment._rank) this.#scratchPadSegmentHasChanged = true } diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutSegmentModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutSegmentModelImpl.ts index 8482015137..e5b073d083 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutSegmentModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutSegmentModelImpl.ts @@ -6,9 +6,9 @@ import { PlayoutSegmentModel } from '../PlayoutSegmentModel' export class PlayoutSegmentModelImpl implements PlayoutSegmentModel { readonly #Segment: DBSegment - readonly Parts: ReadonlyDeep + readonly parts: ReadonlyDeep - get Segment(): ReadonlyDeep { + get segment(): ReadonlyDeep { return this.#Segment } @@ -16,15 +16,15 @@ export class PlayoutSegmentModelImpl implements PlayoutSegmentModel { parts.sort((a, b) => a._rank - b._rank) this.#Segment = segment - this.Parts = parts + this.parts = parts } getPart(id: PartId): ReadonlyDeep | undefined { - return this.Parts.find((part) => part._id === id) + return this.parts.find((part) => part._id === id) } getPartIds(): PartId[] { - return this.Parts.map((part) => part._id) + return this.parts.map((part) => part._id) } /** diff --git a/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts b/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts index 2e7f9ad791..f430ba6b14 100644 --- a/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts +++ b/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts @@ -22,13 +22,13 @@ export async function writeScratchpadSegments( for (const rundown of rundowns) { if (rundown.ScratchPadSegmentHasChanged) { rundown.clearScratchPadSegmentChangedFlag() - const scratchpadSegment = rundown.getScratchpadSegment()?.Segment + const scratchpadSegment = rundown.getScratchpadSegment()?.segment // Delete a removed scratchpad, and any with the non-current id (just in case) writeOps.push({ deleteMany: { filter: { - rundownId: rundown.Rundown._id, + rundownId: rundown.rundown._id, _id: { $ne: scratchpadSegment?._id ?? protectString('') }, }, }, @@ -71,17 +71,17 @@ export function writePartInstancesAndPieceInstances( if (!partInstance) { deletedPartInstanceIds.push(partInstanceId) } else { - if (partInstance.PartInstanceHasChanges) { + if (partInstance.partInstanceHasChanges) { partInstanceOps.push({ replaceOne: { filter: { _id: partInstanceId }, - replacement: partInstance.PartInstanceImpl, + replacement: partInstance.partInstanceImpl, upsert: true, }, }) } - for (const [pieceInstanceId, pieceInstance] of partInstance.PieceInstancesImpl.entries()) { + for (const [pieceInstanceId, pieceInstance] of partInstance.pieceInstancesImpl.entries()) { if (!pieceInstance) { deletedPieceInstanceIds.push(pieceInstanceId) } else if (pieceInstance.HasChanges) { diff --git a/packages/job-worker/src/playout/moveNextPart.ts b/packages/job-worker/src/playout/moveNextPart.ts index 7b66e5a154..fe324a1c72 100644 --- a/packages/job-worker/src/playout/moveNextPart.ts +++ b/packages/job-worker/src/playout/moveNextPart.ts @@ -15,10 +15,10 @@ export async function moveNextPart( partDelta: number, segmentDelta: number ): Promise { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist - const currentPartInstance = playoutModel.CurrentPartInstance?.PartInstance - const nextPartInstance = playoutModel.NextPartInstance?.PartInstance + const currentPartInstance = playoutModel.currentPartInstance?.partInstance + const nextPartInstance = playoutModel.nextPartInstance?.partInstance const refPartInstance = nextPartInstance ?? currentPartInstance const refPart = refPartInstance?.part @@ -32,11 +32,11 @@ export async function moveNextPart( // Ignores horizontalDelta const considerSegments = rawSegments.filter( (s) => - s.Segment._id === refPart.segmentId || - !s.Segment.isHidden || - s.Segment.orphaned === SegmentOrphanedReason.SCRATCHPAD + s.segment._id === refPart.segmentId || + !s.segment.isHidden || + s.segment.orphaned === SegmentOrphanedReason.SCRATCHPAD ) - const refSegmentIndex = considerSegments.findIndex((s) => s.Segment._id === refPart.segmentId) + const refSegmentIndex = considerSegments.findIndex((s) => s.segment._id === refPart.segmentId) if (refSegmentIndex === -1) throw new Error(`Segment "${refPart.segmentId}" not found!`) const targetSegmentIndex = refSegmentIndex + segmentDelta @@ -57,7 +57,7 @@ export async function moveNextPart( // Iterate through segments and find the first part let selectedPart: ReadonlyDeep | undefined for (const segment of allowedSegments) { - const parts = playablePartsBySegment.get(segment.Segment._id) ?? [] + const parts = playablePartsBySegment.get(segment.segment._id) ?? [] // Cant go to the current part (yet) const filteredParts = parts.filter((p) => p._id !== currentPartInstance?.part._id) if (filteredParts.length > 0) { @@ -84,7 +84,7 @@ export async function moveNextPart( const tmpRefPart = { ...refPart, invalid: true } // make sure it won't be found as playable playabaleParts = sortPartsInSortedSegments( [...playabaleParts, tmpRefPart], - rawSegments.map((s) => s.Segment) + rawSegments.map((s) => s.segment) ) refPartIndex = playabaleParts.findIndex((p) => p._id === refPart._id) if (refPartIndex === -1) throw new Error(`Part "${refPart._id}" not found after insert!`) diff --git a/packages/job-worker/src/playout/resolvedPieces.ts b/packages/job-worker/src/playout/resolvedPieces.ts index c4d2b3d157..f3eea28027 100644 --- a/packages/job-worker/src/playout/resolvedPieces.ts +++ b/packages/job-worker/src/playout/resolvedPieces.ts @@ -27,12 +27,12 @@ export function getResolvedPiecesForCurrentPartInstance( ): ResolvedPieceInstance[] { if (now === undefined) now = getCurrentTime() - const partStarted = partInstance.PartInstance.timings?.plannedStartedPlayback + const partStarted = partInstance.partInstance.timings?.plannedStartedPlayback const nowInPart = partStarted ? now - partStarted : 0 const preprocessedPieces = processAndPrunePieceInstanceTimings( sourceLayers, - partInstance.PieceInstances.map((p) => p.PieceInstance), + partInstance.pieceInstances.map((p) => p.pieceInstance), nowInPart ) return preprocessedPieces.map((instance) => resolvePrunedPieceInstance(nowInPart, instance)) diff --git a/packages/job-worker/src/playout/scratchpad.ts b/packages/job-worker/src/playout/scratchpad.ts index 7e37f0b6f5..3205351b9c 100644 --- a/packages/job-worker/src/playout/scratchpad.ts +++ b/packages/job-worker/src/playout/scratchpad.ts @@ -16,13 +16,13 @@ export async function handleActivateScratchpad(context: JobContext, data: Activa context, data, async (playoutModel) => { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown) if (playlist.currentPartInfo) throw UserError.create(UserErrorMessage.RundownAlreadyActive) }, async (playoutModel) => { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist if (!playlist.activationId) throw new Error(`Playlist has no activationId!`) const rundown = playoutModel.getRundown(data.rundownId) @@ -60,20 +60,20 @@ export function validateScratchpartPartInstanceProperties( playoutModel: PlayoutModel, partInstance: PlayoutPartInstanceModel ): void { - const rundown = playoutModel.getRundown(partInstance.PartInstance.rundownId) + const rundown = playoutModel.getRundown(partInstance.partInstance.rundownId) if (!rundown) throw new Error( - `Failed to find Rundown "${partInstance.PartInstance.rundownId}" for PartInstance "${partInstance.PartInstance._id}"` + `Failed to find Rundown "${partInstance.partInstance.rundownId}" for PartInstance "${partInstance.partInstance._id}"` ) - const segment = rundown.getSegment(partInstance.PartInstance.segmentId) + const segment = rundown.getSegment(partInstance.partInstance.segmentId) if (!segment) throw new Error( - `Failed to find Segment "${partInstance.PartInstance.segmentId}" for PartInstance "${partInstance.PartInstance._id}"` + `Failed to find Segment "${partInstance.partInstance.segmentId}" for PartInstance "${partInstance.partInstance._id}"` ) // Check if this applies - if (segment.Segment.orphaned !== SegmentOrphanedReason.SCRATCHPAD) return + if (segment.segment.orphaned !== SegmentOrphanedReason.SCRATCHPAD) return partInstance.validateScratchpadSegmentProperties() } diff --git a/packages/job-worker/src/playout/selectNextPart.ts b/packages/job-worker/src/playout/selectNextPart.ts index 3d85fb0a6d..607574c4d1 100644 --- a/packages/job-worker/src/playout/selectNextPart.ts +++ b/packages/job-worker/src/playout/selectNextPart.ts @@ -97,12 +97,12 @@ export function selectNextPart( searchFromIndex = nextInSegmentIndex ?? segmentStartIndex } else { // If we didn't find the segment in the list of parts, then look for segments after this one. - const segmentIndex = segments.findIndex((s) => s.Segment._id === previousPartInstance.segmentId) + const segmentIndex = segments.findIndex((s) => s.segment._id === previousPartInstance.segmentId) let followingSegmentStart: number | undefined if (segmentIndex !== -1) { // Find the first segment with parts that lies after this for (let i = segmentIndex + 1; i < segments.length; i++) { - const segmentStart = segmentStarts.get(segments[i].Segment._id) + const segmentStart = segmentStarts.get(segments[i].segment._id) if (segmentStart !== undefined) { followingSegmentStart = segmentStart break diff --git a/packages/job-worker/src/playout/setNext.ts b/packages/job-worker/src/playout/setNext.ts index b8ee8f6c97..d2a6e0da78 100644 --- a/packages/job-worker/src/playout/setNext.ts +++ b/packages/job-worker/src/playout/setNext.ts @@ -40,25 +40,25 @@ export async function setNextPart( const span = context.startSpan('setNextPart') const rundownIds = playoutModel.getRundownIds() - const currentPartInstance = playoutModel.CurrentPartInstance - const nextPartInstance = playoutModel.NextPartInstance + const currentPartInstance = playoutModel.currentPartInstance + const nextPartInstance = playoutModel.nextPartInstance if (rawNextPart) { - if (!playoutModel.Playlist.activationId) - throw new Error(`RundownPlaylist "${playoutModel.Playlist._id}" is not active`) + if (!playoutModel.playlist.activationId) + throw new Error(`RundownPlaylist "${playoutModel.playlist._id}" is not active`) // create new instance let newPartInstance: PlayoutPartInstanceModel let consumesQueuedSegmentId: boolean - if ('PartInstance' in rawNextPart) { + if ('partInstance' in rawNextPart) { const inputPartInstance: PlayoutPartInstanceModel = rawNextPart - if (inputPartInstance.PartInstance.part.invalid) { + if (inputPartInstance.partInstance.part.invalid) { throw new Error('Part is marked as invalid, cannot set as next.') } - if (!rundownIds.includes(inputPartInstance.PartInstance.rundownId)) { + if (!rundownIds.includes(inputPartInstance.partInstance.rundownId)) { throw new Error( - `PartInstance "${inputPartInstance.PartInstance._id}" of rundown "${inputPartInstance.PartInstance.rundownId}" not part of RundownPlaylist "${playoutModel.Playlist._id}"` + `PartInstance "${inputPartInstance.partInstance._id}" of rundown "${inputPartInstance.partInstance.rundownId}" not part of RundownPlaylist "${playoutModel.playlist._id}"` ) } @@ -72,13 +72,13 @@ export async function setNextPart( if (!rundownIds.includes(selectedPart.part.rundownId)) { throw new Error( - `Part "${selectedPart.part._id}" of rundown "${selectedPart.part.rundownId}" not part of RundownPlaylist "${playoutModel.Playlist._id}"` + `Part "${selectedPart.part._id}" of rundown "${selectedPart.part.rundownId}" not part of RundownPlaylist "${playoutModel.playlist._id}"` ) } consumesQueuedSegmentId = selectedPart.consumesQueuedSegmentId ?? false - if (nextPartInstance && nextPartInstance.PartInstance.part._id === selectedPart.part._id) { + if (nextPartInstance && nextPartInstance.partInstance.part._id === selectedPart.part._id) { // Re-use existing newPartInstance = await prepareExistingPartInstanceForBeingNexted( @@ -98,16 +98,16 @@ export async function setNextPart( } const selectedPartInstanceIds = _.compact([ - newPartInstance.PartInstance._id, - playoutModel.Playlist.currentPartInfo?.partInstanceId, - playoutModel.Playlist.previousPartInfo?.partInstanceId, + newPartInstance.partInstance._id, + playoutModel.playlist.currentPartInfo?.partInstanceId, + playoutModel.playlist.previousPartInfo?.partInstanceId, ]) // reset any previous instances of this part resetPartInstancesWithPieceInstances(context, playoutModel, { _id: { $nin: selectedPartInstanceIds }, - rundownId: newPartInstance.PartInstance.rundownId, - 'part._id': newPartInstance.PartInstance.part._id, + rundownId: newPartInstance.partInstance.rundownId, + 'part._id': newPartInstance.partInstance.part._id, }) playoutModel.setPartInstanceAsNext(newPartInstance, setManually, consumesQueuedSegmentId, nextTimeOffset) @@ -131,7 +131,7 @@ async function prepareExistingPartInstanceForBeingNexted( playoutModel: PlayoutModel, instance: PlayoutPartInstanceModel ): Promise { - await syncPlayheadInfinitesForNextPartInstance(context, playoutModel, playoutModel.CurrentPartInstance, instance) + await syncPlayheadInfinitesForNextPartInstance(context, playoutModel, playoutModel.currentPartInstance, instance) return instance } @@ -164,24 +164,24 @@ async function preparePartInstanceForPartBeingNexted( * In theory the new segment should already be reset, as we do that upon leaving, but it wont be if jumping to earlier in the same segment or maybe if the rundown wasnt reset */ function resetPartInstancesWhenChangingSegment(context: JobContext, playoutModel: PlayoutModel) { - const currentPartInstance = playoutModel.CurrentPartInstance?.PartInstance - const nextPartInstance = playoutModel.NextPartInstance?.PartInstance + const currentPartInstance = playoutModel.currentPartInstance?.partInstance + const nextPartInstance = playoutModel.nextPartInstance?.partInstance if (nextPartInstance) { const resetPartInstanceIds = new Set() if (currentPartInstance) { // Always clean the current segment, anything after the current part (except the next part) - const trailingInOldSegment = playoutModel.LoadedPartInstances.filter( + const trailingInOldSegment = playoutModel.loadedPartInstances.filter( (p) => - !p.PartInstance.reset && - p.PartInstance._id !== currentPartInstance._id && - p.PartInstance._id !== nextPartInstance._id && - p.PartInstance.segmentId === currentPartInstance.segmentId && - p.PartInstance.part._rank > currentPartInstance.part._rank + !p.partInstance.reset && + p.partInstance._id !== currentPartInstance._id && + p.partInstance._id !== nextPartInstance._id && + p.partInstance.segmentId === currentPartInstance.segmentId && + p.partInstance.part._rank > currentPartInstance.part._rank ) for (const part of trailingInOldSegment) { - resetPartInstanceIds.add(part.PartInstance._id) + resetPartInstanceIds.add(part.partInstance._id) } } @@ -192,15 +192,15 @@ function resetPartInstancesWhenChangingSegment(context: JobContext, playoutModel nextPartInstance.part._rank < currentPartInstance.part._rank) ) { // clean the whole segment if new, or jumping backwards - const newSegmentParts = playoutModel.LoadedPartInstances.filter( + const newSegmentParts = playoutModel.loadedPartInstances.filter( (p) => - !p.PartInstance.reset && - p.PartInstance._id !== nextPartInstance._id && - p.PartInstance._id !== currentPartInstance?._id && - p.PartInstance.segmentId === nextPartInstance.segmentId + !p.partInstance.reset && + p.partInstance._id !== nextPartInstance._id && + p.partInstance._id !== currentPartInstance?._id && + p.partInstance.segmentId === nextPartInstance.segmentId ) for (const part of newSegmentParts) { - resetPartInstanceIds.add(part.PartInstance._id) + resetPartInstanceIds.add(part.partInstance._id) } } @@ -217,38 +217,38 @@ function resetPartInstancesWhenChangingSegment(context: JobContext, playoutModel * @param playoutModel */ async function cleanupOrphanedItems(context: JobContext, playoutModel: PlayoutModel) { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist const selectedPartInstancesSegmentIds = new Set() - const currentPartInstance = playoutModel.CurrentPartInstance?.PartInstance - const nextPartInstance = playoutModel.NextPartInstance?.PartInstance + const currentPartInstance = playoutModel.currentPartInstance?.partInstance + const nextPartInstance = playoutModel.nextPartInstance?.partInstance if (currentPartInstance) selectedPartInstancesSegmentIds.add(currentPartInstance.segmentId) if (nextPartInstance) selectedPartInstancesSegmentIds.add(nextPartInstance.segmentId) // Cleanup any orphaned segments once they are no longer being played. This also cleans up any adlib-parts, that have been marked as deleted as a deferred cleanup operation - const segments = playoutModel.getAllOrderedSegments().filter((s) => !!s.Segment.orphaned) - const orphanedSegmentIds = new Set(segments.map((s) => s.Segment._id)) + const segments = playoutModel.getAllOrderedSegments().filter((s) => !!s.segment.orphaned) + const orphanedSegmentIds = new Set(segments.map((s) => s.segment._id)) const alterSegmentsFromRundowns = new Map() for (const segment of segments) { // If the segment is orphaned and not the segment for the next or current partinstance - if (!selectedPartInstancesSegmentIds.has(segment.Segment._id)) { - let rundownSegments = alterSegmentsFromRundowns.get(segment.Segment.rundownId) + if (!selectedPartInstancesSegmentIds.has(segment.segment._id)) { + let rundownSegments = alterSegmentsFromRundowns.get(segment.segment.rundownId) if (!rundownSegments) { rundownSegments = { deleted: [], hidden: [] } - alterSegmentsFromRundowns.set(segment.Segment.rundownId, rundownSegments) + alterSegmentsFromRundowns.set(segment.segment.rundownId, rundownSegments) } // The segment is finished with. Queue it for attempted removal or reingest - switch (segment.Segment.orphaned) { + switch (segment.segment.orphaned) { case SegmentOrphanedReason.DELETED: { - rundownSegments.deleted.push(segment.Segment._id) + rundownSegments.deleted.push(segment.segment._id) break } case SegmentOrphanedReason.HIDDEN: { // The segment is finished with. Queue it for attempted resync - rundownSegments.hidden.push(segment.Segment._id) + rundownSegments.hidden.push(segment.segment._id) break } case SegmentOrphanedReason.SCRATCHPAD: @@ -258,7 +258,7 @@ async function cleanupOrphanedItems(context: JobContext, playoutModel: PlayoutMo // Not orphaned break default: - assertNever(segment.Segment.orphaned) + assertNever(segment.segment.orphaned) break } } @@ -267,12 +267,12 @@ async function cleanupOrphanedItems(context: JobContext, playoutModel: PlayoutMo // We need to run this outside of the current lock, and within an ingest lock, so defer to the work queue for (const [rundownId, candidateSegmentIds] of alterSegmentsFromRundowns) { const rundown = playoutModel.getRundown(rundownId) - if (rundown?.Rundown?.restoredFromSnapshotId) { + if (rundown?.rundown?.restoredFromSnapshotId) { // This is not valid as the rundownId won't match the externalId, so ingest will fail // For now do nothing } else if (rundown) { await context.queueIngestJob(IngestJobs.RemoveOrphanedSegments, { - rundownExternalId: rundown.Rundown.externalId, + rundownExternalId: rundown.rundown.externalId, peripheralDeviceId: null, orphanedHiddenSegmentIds: candidateSegmentIds.hidden, orphanedDeletedSegmentIds: candidateSegmentIds.deleted, @@ -282,20 +282,20 @@ async function cleanupOrphanedItems(context: JobContext, playoutModel: PlayoutMo const removePartInstanceIds: PartInstanceId[] = [] // Cleanup any orphaned partinstances once they are no longer being played (and the segment isnt orphaned) - const orphanedInstances = playoutModel.LoadedPartInstances.filter( - (p) => p.PartInstance.orphaned === 'deleted' && !p.PartInstance.reset + const orphanedInstances = playoutModel.loadedPartInstances.filter( + (p) => p.partInstance.orphaned === 'deleted' && !p.partInstance.reset ) for (const partInstance of orphanedInstances) { - if (PRESERVE_UNSYNCED_PLAYING_SEGMENT_CONTENTS && orphanedSegmentIds.has(partInstance.PartInstance.segmentId)) { + if (PRESERVE_UNSYNCED_PLAYING_SEGMENT_CONTENTS && orphanedSegmentIds.has(partInstance.partInstance.segmentId)) { // If the segment is also orphaned, then don't delete it until it is clear continue } if ( - partInstance.PartInstance._id !== playlist.currentPartInfo?.partInstanceId && - partInstance.PartInstance._id !== playlist.nextPartInfo?.partInstanceId + partInstance.partInstance._id !== playlist.currentPartInfo?.partInstanceId && + partInstance.partInstance._id !== playlist.nextPartInfo?.partInstanceId ) { - removePartInstanceIds.push(partInstance.PartInstance._id) + removePartInstanceIds.push(partInstance.partInstance._id) } } @@ -318,14 +318,14 @@ export async function queueNextSegment( ): Promise { const span = context.startSpan('queueNextSegment') if (queuedSegment) { - if (queuedSegment.Segment.orphaned === SegmentOrphanedReason.SCRATCHPAD) - throw new Error(`Segment "${queuedSegment.Segment._id}" is a scratchpad, and cannot be queued!`) + if (queuedSegment.segment.orphaned === SegmentOrphanedReason.SCRATCHPAD) + throw new Error(`Segment "${queuedSegment.segment._id}" is a scratchpad, and cannot be queued!`) // Just run so that errors will be thrown if something wrong: const firstPlayablePart = findFirstPlayablePartOrThrow(queuedSegment) - const currentPartInstance = playoutModel.CurrentPartInstance?.PartInstance - const nextPartInstance = playoutModel.NextPartInstance?.PartInstance + const currentPartInstance = playoutModel.currentPartInstance?.partInstance + const nextPartInstance = playoutModel.nextPartInstance?.partInstance // if there is not currentPartInstance or the nextPartInstance is not in the current segment // behave as if user chose SetNextPart on the first playable part of the segment @@ -352,7 +352,7 @@ export async function queueNextSegment( playoutModel.setQueuedSegment(null) } span?.end() - return { queuedSegmentId: queuedSegment?.Segment?._id ?? null } + return { queuedSegmentId: queuedSegment?.segment?._id ?? null } } /** @@ -387,7 +387,7 @@ export async function setNextSegment( } function findFirstPlayablePartOrThrow(segment: PlayoutSegmentModel): ReadonlyDeep { - const firstPlayablePart = segment.Parts.find((p) => isPartPlayable(p)) + const firstPlayablePart = segment.parts.find((p) => isPartPlayable(p)) if (!firstPlayablePart) { throw new Error('Segment contains no valid parts') } @@ -409,7 +409,7 @@ export async function setNextPartFromPart( setManually: boolean, nextTimeOffset?: number | undefined ): Promise { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown) if (playlist.holdState === RundownHoldState.ACTIVE || playlist.holdState === RundownHoldState.PENDING) { throw UserError.create(UserErrorMessage.DuringHold) @@ -422,8 +422,8 @@ export async function setNextPartFromPart( function doesPartConsumeQueuedSegmentId(playoutModel: PlayoutModel, nextPart: ReadonlyDeep) { // If we're setting the next point to somewhere other than the current segment, and in the queued segment, clear the queued segment - const playlist = playoutModel.Playlist - const currentPartInstance = playoutModel.CurrentPartInstance?.PartInstance + const playlist = playoutModel.playlist + const currentPartInstance = playoutModel.currentPartInstance?.partInstance return !!( currentPartInstance && diff --git a/packages/job-worker/src/playout/setNextJobs.ts b/packages/job-worker/src/playout/setNextJobs.ts index 9f6f6f7df4..eaaec41949 100644 --- a/packages/job-worker/src/playout/setNextJobs.ts +++ b/packages/job-worker/src/playout/setNextJobs.ts @@ -25,7 +25,7 @@ export async function handleSetNextPart(context: JobContext, data: SetNextPartPr context, data, async (playoutModel) => { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown, undefined, 412) if (playlist.holdState && playlist.holdState !== RundownHoldState.COMPLETE) { @@ -56,7 +56,7 @@ export async function handleMoveNextPart(context: JobContext, data: MoveNextPart if (!data.partDelta && !data.segmentDelta) throw new Error(`rundownMoveNext: invalid delta: (${data.partDelta}, ${data.segmentDelta})`) - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown, undefined, 412) if (playlist.holdState === RundownHoldState.ACTIVE || playlist.holdState === RundownHoldState.PENDING) { @@ -85,7 +85,7 @@ export async function handleSetNextSegment(context: JobContext, data: SetNextSeg context, data, async (playoutModel) => { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown, undefined, 412) if (playlist.holdState && playlist.holdState !== RundownHoldState.COMPLETE) { @@ -117,7 +117,7 @@ export async function handleQueueNextSegment( context, data, async (playoutModel) => { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown, undefined, 412) if (playlist.holdState && playlist.holdState !== RundownHoldState.COMPLETE) { diff --git a/packages/job-worker/src/playout/take.ts b/packages/job-worker/src/playout/take.ts index 9504a26bc8..712c8139c3 100644 --- a/packages/job-worker/src/playout/take.ts +++ b/packages/job-worker/src/playout/take.ts @@ -42,7 +42,7 @@ export async function handleTakeNextPart(context: JobContext, data: TakeNextPart context, data, async (playoutModel) => { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown, undefined, 412) @@ -53,12 +53,12 @@ export async function handleTakeNextPart(context: JobContext, data: TakeNextPart throw UserError.create(UserErrorMessage.TakeFromIncorrectPart, undefined, 412) }, async (playoutModel) => { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist let lastTakeTime = playlist.lastTakeTime ?? 0 if (playlist.currentPartInfo) { - const currentPartInstance = playoutModel.CurrentPartInstance?.PartInstance + const currentPartInstance = playoutModel.currentPartInstance?.partInstance if (currentPartInstance?.timings?.plannedStartedPlayback) { lastTakeTime = Math.max(lastTakeTime, currentPartInstance.timings.plannedStartedPlayback) } else { @@ -98,32 +98,32 @@ export async function performTakeToNextedPart( ): Promise { const span = context.startSpan('takeNextPartInner') - if (!playoutModel.Playlist.activationId) - throw new Error(`Rundown Playlist "${playoutModel.Playlist._id}" is not active!`) + if (!playoutModel.playlist.activationId) + throw new Error(`Rundown Playlist "${playoutModel.playlist._id}" is not active!`) - const timeOffset: number | null = playoutModel.Playlist.nextTimeOffset || null + const timeOffset: number | null = playoutModel.playlist.nextTimeOffset || null - const currentPartInstance = playoutModel.CurrentPartInstance - const nextPartInstance = playoutModel.NextPartInstance - const previousPartInstance = playoutModel.PreviousPartInstance + const currentPartInstance = playoutModel.currentPartInstance + const nextPartInstance = playoutModel.nextPartInstance + const previousPartInstance = playoutModel.previousPartInstance const currentOrNextPartInstance = nextPartInstance ?? currentPartInstance if (!currentOrNextPartInstance) { // Some temporary logging to diagnose some cases where this route is hit - logger.warn(`No partinstance was found during take`, JSON.stringify(playoutModel.Playlist)) + logger.warn(`No partinstance was found during take`, JSON.stringify(playoutModel.playlist)) logger.warn( 'All PartInstances in cache', - playoutModel.SortedLoadedPartInstances.map((p) => p.PartInstance._id) + playoutModel.sortedLoadedPartInstances.map((p) => p.partInstance._id) ) - logger.warn('Deleted PartInstances in cache', playoutModel.HackDeletedPartInstanceIds) + logger.warn('Deleted PartInstances in cache', playoutModel.hackDeletedPartInstanceIds) logger.warn( 'Parts in cache', playoutModel.getAllOrderedParts().map((d) => d._id) ) const validIds = _.compact([ - playoutModel.Playlist.currentPartInfo?.partInstanceId, - playoutModel.Playlist.nextPartInfo?.partInstanceId, + playoutModel.playlist.currentPartInfo?.partInstanceId, + playoutModel.playlist.nextPartInfo?.partInstanceId, ]) if (validIds.length) { const mongoDocs = await context.directCollections.PartInstances.findFetch({ _id: { $in: validIds } }) @@ -133,49 +133,49 @@ export async function performTakeToNextedPart( throw new Error(`No partInstance could be found!`) } const currentRundown = currentOrNextPartInstance - ? playoutModel.getRundown(currentOrNextPartInstance.PartInstance.rundownId) + ? playoutModel.getRundown(currentOrNextPartInstance.partInstance.rundownId) : undefined if (!currentRundown) - throw new Error(`Rundown "${currentOrNextPartInstance?.PartInstance?.rundownId ?? ''}" could not be found!`) + throw new Error(`Rundown "${currentOrNextPartInstance?.partInstance?.rundownId ?? ''}" could not be found!`) const pShowStyle = context.getShowStyleCompound( - currentRundown.Rundown.showStyleVariantId, - currentRundown.Rundown.showStyleBaseId + currentRundown.rundown.showStyleVariantId, + currentRundown.rundown.showStyleBaseId ) if (currentPartInstance) { const now = getCurrentTime() - if (currentPartInstance.PartInstance.blockTakeUntil && currentPartInstance.PartInstance.blockTakeUntil > now) { - const remainingTime = currentPartInstance.PartInstance.blockTakeUntil - now + if (currentPartInstance.partInstance.blockTakeUntil && currentPartInstance.partInstance.blockTakeUntil > now) { + const remainingTime = currentPartInstance.partInstance.blockTakeUntil - now // Adlib-actions can arbitrarily block takes from being done logger.debug( - `Take is blocked until ${currentPartInstance.PartInstance.blockTakeUntil}. Which is in: ${remainingTime}` + `Take is blocked until ${currentPartInstance.partInstance.blockTakeUntil}. Which is in: ${remainingTime}` ) throw UserError.create(UserErrorMessage.TakeBlockedDuration, { duration: remainingTime }) } // If there was a transition from the previous Part, then ensure that has finished before another take is permitted - const allowTransition = previousPartInstance && !previousPartInstance.PartInstance.part.disableNextInTransition - const start = currentPartInstance.PartInstance.timings?.plannedStartedPlayback + const allowTransition = previousPartInstance && !previousPartInstance.partInstance.part.disableNextInTransition + const start = currentPartInstance.partInstance.timings?.plannedStartedPlayback if ( allowTransition && - currentPartInstance.PartInstance.part.inTransition && + currentPartInstance.partInstance.part.inTransition && start && - now < start + currentPartInstance.PartInstance.part.inTransition.blockTakeDuration + now < start + currentPartInstance.partInstance.part.inTransition.blockTakeDuration ) { throw UserError.create(UserErrorMessage.TakeDuringTransition) } - if (isTooCloseToAutonext(currentPartInstance.PartInstance, true)) { + if (isTooCloseToAutonext(currentPartInstance.partInstance, true)) { throw UserError.create(UserErrorMessage.TakeCloseToAutonext) } } - if (playoutModel.Playlist.holdState === RundownHoldState.COMPLETE) { + if (playoutModel.playlist.holdState === RundownHoldState.COMPLETE) { playoutModel.setHoldState(RundownHoldState.NONE) // If hold is active, then this take is to clear it - } else if (playoutModel.Playlist.holdState === RundownHoldState.ACTIVE) { + } else if (playoutModel.playlist.holdState === RundownHoldState.ACTIVE) { await completeHold(context, playoutModel, await pShowStyle, currentPartInstance) if (span) span.end() @@ -185,23 +185,23 @@ export async function performTakeToNextedPart( const takePartInstance = nextPartInstance if (!takePartInstance) throw new Error('takePart not found!') - const takeRundown = playoutModel.getRundown(takePartInstance.PartInstance.rundownId) + const takeRundown = playoutModel.getRundown(takePartInstance.partInstance.rundownId) if (!takeRundown) - throw new Error(`takeRundown: takeRundown not found! ("${takePartInstance.PartInstance.rundownId}")`) + throw new Error(`takeRundown: takeRundown not found! ("${takePartInstance.partInstance.rundownId}")`) // Autonext may have setup the plannedStartedPlayback. Clear it so that a new value is generated takePartInstance.setPlannedStartedPlayback(undefined) takePartInstance.setPlannedStoppedPlayback(undefined) // it is only a first take if the Playlist has no startedPlayback and the taken PartInstance is not untimed - const isFirstTake = !playoutModel.Playlist.startedPlayback && !takePartInstance.PartInstance.part.untimed + const isFirstTake = !playoutModel.playlist.startedPlayback && !takePartInstance.partInstance.part.untimed - clearQueuedSegmentId(playoutModel, takePartInstance.PartInstance, playoutModel.Playlist.nextPartInfo) + clearQueuedSegmentId(playoutModel, takePartInstance.partInstance, playoutModel.playlist.nextPartInfo) const nextPart = selectNextPart( context, - playoutModel.Playlist, - takePartInstance.PartInstance, + playoutModel.playlist, + takePartInstance.partInstance, null, playoutModel.getAllOrderedSegments(), playoutModel.getAllOrderedParts() @@ -219,8 +219,8 @@ export async function performTakeToNextedPart( context.getStudioBlueprintConfig(), showStyle, context.getShowStyleBlueprintConfig(showStyle), - takeRundown.Rundown, - takePartInstance.PartInstance + takeRundown.rundown, + takePartInstance.partInstance ) ) } catch (err) { @@ -231,10 +231,10 @@ export async function performTakeToNextedPart( updatePartInstanceOnTake( context, - playoutModel.Playlist, + playoutModel.playlist, showStyle, blueprint, - takeRundown.Rundown, + takeRundown.rundown, takePartInstance, currentPartInstance ) @@ -250,8 +250,8 @@ export async function performTakeToNextedPart( // Setup the parts for the HOLD we are starting if ( - playoutModel.Playlist.previousPartInfo && - (playoutModel.Playlist.holdState as RundownHoldState) === RundownHoldState.ACTIVE + playoutModel.playlist.previousPartInfo && + (playoutModel.playlist.holdState as RundownHoldState) === RundownHoldState.ACTIVE ) { startHold(context, currentPartInstance, nextPartInstance) } @@ -279,7 +279,7 @@ export function clearQueuedSegmentId( if ( takenPartInfo?.consumesQueuedSegmentId && takenPartInstance && - playoutModel.Playlist.queuedSegmentId === takenPartInstance.segmentId + playoutModel.playlist.queuedSegmentId === takenPartInstance.segmentId ) { // clear the queuedSegmentId if the newly taken partInstance says it was selected because of it playoutModel.setQueuedSegment(null) @@ -291,20 +291,20 @@ export function clearQueuedSegmentId( * @param playoutModel Cache for the active Playlist */ export function resetPreviousSegment(playoutModel: PlayoutModel): void { - const previousPartInstance = playoutModel.PreviousPartInstance - const currentPartInstance = playoutModel.CurrentPartInstance + const previousPartInstance = playoutModel.previousPartInstance + const currentPartInstance = playoutModel.currentPartInstance // If the playlist is looping and // If the previous and current part are not in the same segment, then we have just left a segment if ( - playoutModel.Playlist.loop && + playoutModel.playlist.loop && previousPartInstance && - previousPartInstance.PartInstance.segmentId !== currentPartInstance?.PartInstance?.segmentId + previousPartInstance.partInstance.segmentId !== currentPartInstance?.partInstance?.segmentId ) { // Reset the old segment - const segmentId = previousPartInstance.PartInstance.segmentId - for (const partInstance of playoutModel.LoadedPartInstances) { - if (partInstance.PartInstance.segmentId === segmentId) { + const segmentId = previousPartInstance.partInstance.segmentId + for (const partInstance of playoutModel.loadedPartInstances) { + if (partInstance.partInstance.segmentId === segmentId) { partInstance.markAsReset() } } @@ -319,16 +319,16 @@ async function afterTakeUpdateTimingsAndEvents( isFirstTake: boolean, takeDoneTime: number ): Promise { - const takePartInstance = playoutModel.CurrentPartInstance - const previousPartInstance = playoutModel.PreviousPartInstance + const takePartInstance = playoutModel.currentPartInstance + const previousPartInstance = playoutModel.previousPartInstance if (takePartInstance) { // Simulate playout, if no gateway - const playoutDevices = playoutModel.PeripheralDevices.filter((d) => d.type === PeripheralDeviceType.PLAYOUT) + const playoutDevices = playoutModel.peripheralDevices.filter((d) => d.type === PeripheralDeviceType.PLAYOUT) if (playoutDevices.length === 0) { logger.info( `No Playout gateway attached to studio, reporting PartInstance "${ - takePartInstance.PartInstance._id + takePartInstance.partInstance._id }" to have started playback on timestamp ${new Date(takeDoneTime).toISOString()}` ) reportPartInstanceHasStarted(context, playoutModel, takePartInstance, takeDoneTime) @@ -336,7 +336,7 @@ async function afterTakeUpdateTimingsAndEvents( if (previousPartInstance) { logger.info( `Also reporting PartInstance "${ - previousPartInstance.PartInstance._id + previousPartInstance.partInstance._id }" to have stopped playback on timestamp ${new Date(takeDoneTime).toISOString()}` ) reportPartInstanceHasStopped(context, playoutModel, previousPartInstance, takeDoneTime) @@ -346,7 +346,7 @@ async function afterTakeUpdateTimingsAndEvents( } const takeRundown = takePartInstance - ? playoutModel.getRundown(takePartInstance.PartInstance.rundownId) + ? playoutModel.getRundown(takePartInstance.partInstance.rundownId) : undefined if (isFirstTake && takeRundown) { @@ -360,8 +360,8 @@ async function afterTakeUpdateTimingsAndEvents( context.getStudioBlueprintConfig(), showStyle, context.getShowStyleBlueprintConfig(showStyle), - takeRundown.Rundown, - takePartInstance.PartInstance + takeRundown.rundown, + takePartInstance.partInstance ) ) } catch (err) { @@ -381,8 +381,8 @@ async function afterTakeUpdateTimingsAndEvents( context.getStudioBlueprintConfig(), showStyle, context.getShowStyleBlueprintConfig(showStyle), - takeRundown.Rundown, - takePartInstance.PartInstance + takeRundown.rundown, + takePartInstance.partInstance ) ) } catch (err) { @@ -419,7 +419,7 @@ export function updatePartInstanceOnTake( { name: `${playlist.name}`, identifier: `playlist=${playlist._id},currentPartInstance=${ - currentPartInstance.PartInstance._id + currentPartInstance.partInstance._id },execution=${getRandomId()}`, }, context.studio, @@ -431,7 +431,7 @@ export function updatePartInstanceOnTake( previousPartEndState = blueprint.blueprint.getEndStateForPart( context2, playlist.previousPersistentState, - convertPartInstanceToBlueprints(currentPartInstance.PartInstance), + convertPartInstanceToBlueprints(currentPartInstance.partInstance), resolvedPieces.map(convertResolvedPieceInstanceToBlueprints), time ) @@ -446,14 +446,14 @@ export function updatePartInstanceOnTake( // calculate and cache playout timing properties, so that we don't depend on the previousPartInstance: const tmpTakePieces = processAndPrunePieceInstanceTimings( showStyle.sourceLayers, - takePartInstance.PieceInstances.map((p) => p.PieceInstance), + takePartInstance.pieceInstances.map((p) => p.pieceInstance), 0 ) const partPlayoutTimings = calculatePartTimings( playlist.holdState, - currentPartInstance?.PartInstance?.part, - currentPartInstance?.PieceInstances?.map((p) => p.PieceInstance.piece) ?? [], - takePartInstance.PartInstance.part, + currentPartInstance?.partInstance?.part, + currentPartInstance?.pieceInstances?.map((p) => p.pieceInstance.piece) ?? [], + takePartInstance.partInstance.part, tmpTakePieces.filter((p) => !p.infinite || p.infinite.infiniteInstanceIndex === 0).map((p) => p.piece) ) @@ -472,7 +472,7 @@ export async function afterTake( await updateTimeline(context, playoutModel, timeOffsetIntoPart || undefined) - playoutModel.queueNotifyCurrentlyPlayingPartEvent(takePartInstance.PartInstance.rundownId, takePartInstance) + playoutModel.queueNotifyCurrentlyPlayingPartEvent(takePartInstance.partInstance.rundownId, takePartInstance) if (span) span.end() } @@ -490,20 +490,20 @@ function startHold( const span = context.startSpan('startHold') // Make a copy of any item which is flagged as an 'infinite' extension - const pieceInstancesToCopy = holdFromPartInstance.PieceInstances.filter((p) => !!p.PieceInstance.piece.extendOnHold) + const pieceInstancesToCopy = holdFromPartInstance.pieceInstances.filter((p) => !!p.pieceInstance.piece.extendOnHold) pieceInstancesToCopy.forEach((instance) => { - if (!instance.PieceInstance.infinite) { + if (!instance.pieceInstance.infinite) { // mark current one as infinite instance.prepareForHold() // This gets deleted once the nextpart is activated, so it doesnt linger for long const extendedPieceInstance = holdToPartInstance.insertHoldPieceInstance(instance) - const content = clone(instance.PieceInstance.piece.content) as VTContent | undefined - if (content?.fileName && content.sourceDuration && instance.PieceInstance.plannedStartedPlayback) { + const content = clone(instance.pieceInstance.piece.content) as VTContent | undefined + if (content?.fileName && content.sourceDuration && instance.pieceInstance.plannedStartedPlayback) { content.seek = Math.min( content.sourceDuration, - getCurrentTime() - instance.PieceInstance.plannedStartedPlayback + getCurrentTime() - instance.pieceInstance.plannedStartedPlayback ) } extendedPieceInstance.updatePieceProps({ content }) @@ -520,7 +520,7 @@ async function completeHold( ): Promise { playoutModel.setHoldState(RundownHoldState.COMPLETE) - if (playoutModel.Playlist.currentPartInfo) { + if (playoutModel.playlist.currentPartInfo) { if (!currentPartInstance) throw new Error('currentPart not found!') // Clear the current extension line diff --git a/packages/job-worker/src/playout/timeline/generate.ts b/packages/job-worker/src/playout/timeline/generate.ts index 0bd5c86568..5e8de5b068 100644 --- a/packages/job-worker/src/playout/timeline/generate.ts +++ b/packages/job-worker/src/playout/timeline/generate.ts @@ -86,7 +86,7 @@ export async function updateStudioTimeline( throw new Error(`Studio has an active playlist`) } } else { - if (playoutModel.Playlist.activationId) { + if (playoutModel.playlist.activationId) { throw new Error(`Studio has an active playlist`) } } @@ -151,8 +151,8 @@ export async function updateTimeline( const span = context.startSpan('updateTimeline') logger.debug('updateTimeline running...') - if (!playoutModel.Playlist.activationId) { - throw new Error(`RundownPlaylist ("${playoutModel.Playlist._id}") is not active")`) + if (!playoutModel.playlist.activationId) { + throw new Error(`RundownPlaylist ("${playoutModel.playlist._id}") is not active")`) } const { versions, objs: timelineObjs, timingContext: timingInfo } = await getTimelineRundown(context, playoutModel) @@ -178,7 +178,7 @@ function preserveOrReplaceNowTimesInObjects( studioPlayoutModel: StudioPlayoutModelBase, timelineObjs: Array ) { - const timeline = studioPlayoutModel.Timeline + const timeline = studioPlayoutModel.timeline const oldTimelineObjsMap = normalizeArray( (timeline?.timelineBlob !== undefined && deserializeTimelineBlob(timeline.timelineBlob)) || [], 'id' @@ -275,21 +275,21 @@ function getPartInstanceTimelineInfo( partInstance: PlayoutPartInstanceModel | null ): SelectedPartInstanceTimelineInfo | undefined { if (partInstance) { - const partStarted = partInstance.PartInstance.timings?.plannedStartedPlayback + const partStarted = partInstance.partInstance.timings?.plannedStartedPlayback const nowInPart = partStarted === undefined ? 0 : currentTime - partStarted const pieceInstances = processAndPrunePieceInstanceTimings( sourceLayers, - partInstance.PieceInstances.map((p) => p.PieceInstance), + partInstance.pieceInstances.map((p) => p.pieceInstance), nowInPart ) return { - partInstance: partInstance.PartInstance, + partInstance: partInstance.partInstance, pieceInstances, nowInPart, partStarted, // Approximate `calculatedTimings`, for the partInstances which already have it cached - calculatedTimings: getPartTimingsOrDefaults(partInstance.PartInstance, pieceInstances), + calculatedTimings: getPartTimingsOrDefaults(partInstance.partInstance, pieceInstances), } } else { return undefined @@ -311,23 +311,23 @@ async function getTimelineRundown( try { let timelineObjs: Array = [] - const currentPartInstance = playoutModel.CurrentPartInstance - const nextPartInstance = playoutModel.NextPartInstance - const previousPartInstance = playoutModel.PreviousPartInstance + const currentPartInstance = playoutModel.currentPartInstance + const nextPartInstance = playoutModel.nextPartInstance + const previousPartInstance = playoutModel.previousPartInstance const partForRundown = currentPartInstance || nextPartInstance - const activeRundown = partForRundown && playoutModel.getRundown(partForRundown.PartInstance.rundownId) + const activeRundown = partForRundown && playoutModel.getRundown(partForRundown.partInstance.rundownId) let timelineVersions: TimelineCompleteGenerationVersions | undefined if (activeRundown) { // Fetch showstyle blueprint: const showStyle = await context.getShowStyleCompound( - activeRundown.Rundown.showStyleVariantId, - activeRundown.Rundown.showStyleBaseId + activeRundown.rundown.showStyleVariantId, + activeRundown.rundown.showStyleBaseId ) if (!showStyle) { throw new Error( - `ShowStyleBase "${activeRundown.Rundown.showStyleBaseId}" not found! (referenced by Rundown "${activeRundown.Rundown._id}")` + `ShowStyleBase "${activeRundown.rundown.showStyleBaseId}" not found! (referenced by Rundown "${activeRundown.rundown._id}")` ) } @@ -340,7 +340,7 @@ async function getTimelineRundown( if (partInstancesInfo.next) { // the nextPartInstance doesn't have accurate cached `calculatedTimings` yet, so calculate a prediction partInstancesInfo.next.calculatedTimings = calculatePartTimings( - playoutModel.Playlist.holdState, + playoutModel.playlist.holdState, partInstancesInfo.current?.partInstance?.part, partInstancesInfo.current?.pieceInstances?.map?.((p) => p.piece), partInstancesInfo.next.partInstance.part, @@ -352,17 +352,17 @@ async function getTimelineRundown( // next (on pvw (or on pgm if first)) const pLookaheadObjs = getLookeaheadObjects(context, playoutModel, partInstancesInfo) - const rawBaselineItems = activeRundown.BaselineObjects + const rawBaselineItems = activeRundown.baselineObjects if (rawBaselineItems.length > 0) { timelineObjs = timelineObjs.concat(transformBaselineItemsIntoTimeline(rawBaselineItems)) } else { - logger.warn(`Missing Baseline objects for Rundown "${activeRundown.Rundown._id}"`) + logger.warn(`Missing Baseline objects for Rundown "${activeRundown.rundown._id}"`) } const rundownTimelineResult = buildTimelineObjsForRundown( context, playoutModel, - activeRundown.Rundown, + activeRundown.rundown, partInstancesInfo ) @@ -387,11 +387,11 @@ async function getTimelineRundown( context.getStudioBlueprintConfig(), showStyle, context.getShowStyleBlueprintConfig(showStyle), - playoutModel.Playlist, - activeRundown.Rundown, - previousPartInstance?.PartInstance, - currentPartInstance?.PartInstance, - nextPartInstance?.PartInstance, + playoutModel.playlist, + activeRundown.rundown, + previousPartInstance?.partInstance, + currentPartInstance?.partInstance, + nextPartInstance?.partInstance, resolvedPieces ) try { @@ -401,7 +401,7 @@ async function getTimelineRundown( abHelper, blueprint, showStyle, - playoutModel.Playlist, + playoutModel.playlist, resolvedPieces, timelineObjs ) @@ -413,8 +413,8 @@ async function getTimelineRundown( tlGenRes = await blueprint.blueprint.onTimelineGenerate( blueprintContext, timelineObjs, - clone(playoutModel.Playlist.previousPersistentState), - clone(currentPartInstance?.PartInstance?.previousPartEndState), + clone(playoutModel.playlist.previousPersistentState), + clone(currentPartInstance?.partInstance?.previousPartEndState), resolvedPieces.map(convertResolvedPieceInstanceToBlueprints) ) sendTrace(endTrace(influxTrace)) diff --git a/packages/job-worker/src/playout/timeline/multi-gateway.ts b/packages/job-worker/src/playout/timeline/multi-gateway.ts index a4cb38eb75..91039a369f 100644 --- a/packages/job-worker/src/playout/timeline/multi-gateway.ts +++ b/packages/job-worker/src/playout/timeline/multi-gateway.ts @@ -36,7 +36,7 @@ export function deNowifyMultiGatewayTimeline( const targetNowTime = getCurrentTime() + (nowOffsetLatency ?? 0) // Replace `start: 'now'` in currentPartInstance on timeline - const currentPartInstance = playoutModel.CurrentPartInstance + const currentPartInstance = playoutModel.currentPartInstance if (!currentPartInstance) return const partGroupTimings = updatePartInstancePlannedTimes( @@ -44,7 +44,7 @@ export function deNowifyMultiGatewayTimeline( targetNowTime, timingContext, currentPartInstance, - playoutModel.NextPartInstance + playoutModel.nextPartInstance ) deNowifyCurrentPieces( @@ -67,7 +67,7 @@ export function calculateNowOffsetLatency( let nowOffsetLatency: Time | undefined if (studioPlayoutModel.isMultiGatewayMode) { - const playoutDevices = studioPlayoutModel.PeripheralDevices.filter( + const playoutDevices = studioPlayoutModel.peripheralDevices.filter( (device) => device.type === PeripheralDeviceType.PLAYOUT ) const worstLatency = Math.max(0, ...playoutDevices.map((device) => getExpectedLatency(device).safe)) @@ -98,7 +98,7 @@ function updatePartInstancePlannedTimes( nextPartInstance: PlayoutPartInstanceModel | null ): PartGroupTimings { let currentPartGroupStartTime: number - if (!currentPartInstance.PartInstance.timings?.plannedStartedPlayback) { + if (!currentPartInstance.partInstance.timings?.plannedStartedPlayback) { // Looks like the part is just being taken currentPartInstance.setPlannedStartedPlayback(targetNowTime) @@ -106,11 +106,11 @@ function updatePartInstancePlannedTimes( timingContext.currentPartGroup.enable.start = targetNowTime currentPartGroupStartTime = targetNowTime } else { - currentPartGroupStartTime = currentPartInstance.PartInstance.timings.plannedStartedPlayback + currentPartGroupStartTime = currentPartInstance.partInstance.timings.plannedStartedPlayback } // Also mark the previous as ended - const previousPartInstance = playoutModel.PreviousPartInstance + const previousPartInstance = playoutModel.previousPartInstance if (previousPartInstance) { const previousPartEndTime = currentPartGroupStartTime + (timingContext.previousPartOverlap ?? 0) previousPartInstance.setPlannedStoppedPlayback(previousPartEndTime) @@ -153,11 +153,11 @@ function deNowifyCurrentPieces( const nowInPart = targetNowTime - currentPartGroupStartTime // Ensure any pieces in the currentPartInstance have their now replaced - for (const pieceInstance of currentPartInstance.PieceInstances) { - if (pieceInstance.PieceInstance.piece.enable.start === 'now') { + for (const pieceInstance of currentPartInstance.pieceInstances) { + if (pieceInstance.pieceInstance.piece.enable.start === 'now') { pieceInstance.updatePieceProps({ enable: { - ...pieceInstance.PieceInstance.piece.enable, + ...pieceInstance.pieceInstance.piece.enable, start: nowInPart, }, }) @@ -178,23 +178,23 @@ function deNowifyCurrentPieces( } // Ensure any pieces with an unconfirmed userDuration is confirmed - for (const pieceInstance of currentPartInstance.PieceInstances) { + for (const pieceInstance of currentPartInstance.pieceInstances) { if ( - pieceInstance.PieceInstance.userDuration && - 'endRelativeToNow' in pieceInstance.PieceInstance.userDuration + pieceInstance.pieceInstance.userDuration && + 'endRelativeToNow' in pieceInstance.pieceInstance.userDuration ) { - const relativeToNow = pieceInstance.PieceInstance.userDuration.endRelativeToNow + const relativeToNow = pieceInstance.pieceInstance.userDuration.endRelativeToNow const endRelativeToPart = relativeToNow + nowInPart pieceInstance.setDuration({ endRelativeToPart }) // Update the piece control obj - const controlObj = timelineObjsMap[getPieceControlObjectId(pieceInstance.PieceInstance)] + const controlObj = timelineObjsMap[getPieceControlObjectId(pieceInstance.pieceInstance)] if (controlObj && !Array.isArray(controlObj.enable) && controlObj.enable.end === 'now') { controlObj.enable.end = endRelativeToPart } // If the piece is an infinite, there may be a now in the parent group - const infiniteGroup = timelineObjsMap[getInfinitePartGroupId(pieceInstance.PieceInstance._id)] + const infiniteGroup = timelineObjsMap[getInfinitePartGroupId(pieceInstance.pieceInstance._id)] if (infiniteGroup && !Array.isArray(infiniteGroup.enable) && infiniteGroup.enable.end === 'now') { infiniteGroup.enable.end = targetNowTime + relativeToNow } @@ -209,15 +209,15 @@ function updatePlannedTimingsForPieceInstances( timelineObjsMap: Record ) { const existingInfiniteTimings = new Map() - const previousPartInstance = playoutModel.PreviousPartInstance + const previousPartInstance = playoutModel.previousPartInstance if (previousPartInstance) { - const pieceInstances = previousPartInstance.PieceInstances + const pieceInstances = previousPartInstance.pieceInstances for (const pieceInstance of pieceInstances) { // Track the timings for the infinites - const plannedStartedPlayback = pieceInstance.PieceInstance.plannedStartedPlayback - if (pieceInstance.PieceInstance.infinite && plannedStartedPlayback) { + const plannedStartedPlayback = pieceInstance.pieceInstance.plannedStartedPlayback + if (pieceInstance.pieceInstance.infinite && plannedStartedPlayback) { existingInfiniteTimings.set( - pieceInstance.PieceInstance.infinite.infiniteInstanceId, + pieceInstance.pieceInstance.infinite.infiniteInstanceId, plannedStartedPlayback ) } @@ -225,7 +225,7 @@ function updatePlannedTimingsForPieceInstances( } // Ensure any pieces have up to date timings - for (const pieceInstance of currentPartInstance.PieceInstances) { + for (const pieceInstance of currentPartInstance.pieceInstances) { setPlannedTimingsOnPieceInstance( pieceInstance, partGroupTimings.currentStartTime, @@ -234,10 +234,10 @@ function updatePlannedTimingsForPieceInstances( preserveOrTrackInfiniteTimings(existingInfiniteTimings, timelineObjsMap, pieceInstance) } - const nextPartInstance = playoutModel.NextPartInstance + const nextPartInstance = playoutModel.nextPartInstance if (nextPartInstance && partGroupTimings.nextStartTime) { const nextPartGroupStartTime0 = partGroupTimings.nextStartTime - for (const pieceInstance of nextPartInstance.PieceInstances) { + for (const pieceInstance of nextPartInstance.pieceInstances) { setPlannedTimingsOnPieceInstance(pieceInstance, nextPartGroupStartTime0, undefined) preserveOrTrackInfiniteTimings(existingInfiniteTimings, timelineObjsMap, pieceInstance) } @@ -250,26 +250,26 @@ function setPlannedTimingsOnPieceInstance( partPlannedEnd: Time | undefined ): void { if ( - pieceInstance.PieceInstance.infinite && - pieceInstance.PieceInstance.infinite.infiniteInstanceIndex > 0 && - pieceInstance.PieceInstance.plannedStartedPlayback + pieceInstance.pieceInstance.infinite && + pieceInstance.pieceInstance.infinite.infiniteInstanceIndex > 0 && + pieceInstance.pieceInstance.plannedStartedPlayback ) { // If not the start of an infinite chain, then the plannedStartedPlayback flows differently return } - if (typeof pieceInstance.PieceInstance.piece.enable.start === 'number') { - const plannedStart = partPlannedStart + pieceInstance.PieceInstance.piece.enable.start + if (typeof pieceInstance.pieceInstance.piece.enable.start === 'number') { + const plannedStart = partPlannedStart + pieceInstance.pieceInstance.piece.enable.start pieceInstance.setPlannedStartedPlayback(plannedStart) const userDurationEnd = - pieceInstance.PieceInstance.userDuration && 'endRelativeToPart' in pieceInstance.PieceInstance.userDuration - ? pieceInstance.PieceInstance.userDuration.endRelativeToPart + pieceInstance.pieceInstance.userDuration && 'endRelativeToPart' in pieceInstance.pieceInstance.userDuration + ? pieceInstance.pieceInstance.userDuration.endRelativeToPart : null const plannedEnd = userDurationEnd ?? - (pieceInstance.PieceInstance.piece.enable.duration - ? plannedStart + pieceInstance.PieceInstance.piece.enable.duration + (pieceInstance.pieceInstance.piece.enable.duration + ? plannedStart + pieceInstance.pieceInstance.piece.enable.duration : partPlannedEnd) pieceInstance.setPlannedStoppedPlayback(plannedEnd) @@ -281,23 +281,23 @@ function preserveOrTrackInfiniteTimings( timelineObjsMap: Record, pieceInstance: PlayoutPieceInstanceModel ): void { - if (!pieceInstance.PieceInstance.infinite) return + if (!pieceInstance.pieceInstance.infinite) return - const plannedStartedPlayback = existingInfiniteTimings.get(pieceInstance.PieceInstance.infinite.infiniteInstanceId) + const plannedStartedPlayback = existingInfiniteTimings.get(pieceInstance.pieceInstance.infinite.infiniteInstanceId) if (plannedStartedPlayback) { // Found a value from the previousPartInstance, lets preserve it pieceInstance.setPlannedStartedPlayback(plannedStartedPlayback) } else { - const plannedStartedPlayback = pieceInstance.PieceInstance.plannedStartedPlayback + const plannedStartedPlayback = pieceInstance.pieceInstance.plannedStartedPlayback if (plannedStartedPlayback) { - existingInfiniteTimings.set(pieceInstance.PieceInstance.infinite.infiniteInstanceId, plannedStartedPlayback) + existingInfiniteTimings.set(pieceInstance.pieceInstance.infinite.infiniteInstanceId, plannedStartedPlayback) } } // Update the timeline group - const startedPlayback = plannedStartedPlayback ?? pieceInstance.PieceInstance.plannedStartedPlayback + const startedPlayback = plannedStartedPlayback ?? pieceInstance.pieceInstance.plannedStartedPlayback if (startedPlayback) { - const infinitePartGroupId = getInfinitePartGroupId(pieceInstance.PieceInstance._id) + const infinitePartGroupId = getInfinitePartGroupId(pieceInstance.pieceInstance._id) const infinitePartGroupObj = timelineObjsMap[infinitePartGroupId] if ( infinitePartGroupObj && diff --git a/packages/job-worker/src/playout/timeline/rundown.ts b/packages/job-worker/src/playout/timeline/rundown.ts index 7a5d38a2f3..d8ee36ab7d 100644 --- a/packages/job-worker/src/playout/timeline/rundown.ts +++ b/packages/job-worker/src/playout/timeline/rundown.ts @@ -57,7 +57,7 @@ export function buildTimelineObjsForRundown( const span = context.startSpan('buildTimelineObjsForRundown') const timelineObjs: Array = [] - const activePlaylist = playoutModel.Playlist + const activePlaylist = playoutModel.playlist const currentTime = getCurrentTime() timelineObjs.push( diff --git a/packages/job-worker/src/playout/timelineJobs.ts b/packages/job-worker/src/playout/timelineJobs.ts index d23f85fb24..3083dd3ee8 100644 --- a/packages/job-worker/src/playout/timelineJobs.ts +++ b/packages/job-worker/src/playout/timelineJobs.ts @@ -32,7 +32,7 @@ async function shouldUpdateStudioBaselineInner( if (playoutModel.getActiveRundownPlaylists().length > 0) return false - const timeline = playoutModel.Timeline + const timeline = playoutModel.timeline const blueprint = studio.blueprintId ? await context.directCollections.Blueprints.findOne(studio.blueprintId) : null if (!blueprint) return 'missingBlueprint' @@ -51,11 +51,11 @@ export async function handleUpdateTimelineAfterIngest( if (playlist?.activationId && (playlist.currentPartInfo || playlist.nextPartInfo)) { // TODO - r37 added a retry mechanic to this. should that be kept? await runWithPlayoutModel(context, playlist, lock, null, async (playoutModel) => { - const currentPartInstance = playoutModel.CurrentPartInstance + const currentPartInstance = playoutModel.currentPartInstance if ( !playoutModel.isMultiGatewayMode && currentPartInstance && - !currentPartInstance.PartInstance.timings?.reportedStartedPlayback + !currentPartInstance.partInstance.timings?.reportedStartedPlayback ) { // HACK: The current PartInstance doesn't have a start time yet, so we know an updateTimeline is coming as part of onPartPlaybackStarted // We mustn't run before that does, or we will get the timings in playout-gateway confused. diff --git a/packages/job-worker/src/playout/timings/partPlayback.ts b/packages/job-worker/src/playout/timings/partPlayback.ts index db2ab543a2..39194e2088 100644 --- a/packages/job-worker/src/playout/timings/partPlayback.ts +++ b/packages/job-worker/src/playout/timings/partPlayback.ts @@ -29,11 +29,11 @@ export async function onPartPlaybackStarted( const playingPartInstance = playoutModel.getPartInstance(data.partInstanceId) if (!playingPartInstance) throw new Error( - `PartInstance "${data.partInstanceId}" in RundownPlayst "${playoutModel.PlaylistId}" not found!` + `PartInstance "${data.partInstanceId}" in RundownPlayst "${playoutModel.playlistId}" not found!` ) // make sure we don't run multiple times, even if TSR calls us multiple times - const hasStartedPlaying = !!playingPartInstance.PartInstance.timings?.reportedStartedPlayback + const hasStartedPlaying = !!playingPartInstance.partInstance.timings?.reportedStartedPlayback if (!hasStartedPlaying) { logger.debug( `Playout reports PartInstance "${data.partInstanceId}" has started playback on timestamp ${new Date( @@ -41,12 +41,12 @@ export async function onPartPlaybackStarted( ).toISOString()}` ) - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist - const rundown = playoutModel.getRundown(playingPartInstance.PartInstance.rundownId) - if (!rundown) throw new Error(`Rundown "${playingPartInstance.PartInstance.rundownId}" not found!`) + const rundown = playoutModel.getRundown(playingPartInstance.partInstance.rundownId) + if (!rundown) throw new Error(`Rundown "${playingPartInstance.partInstance.rundownId}" not found!`) - const currentPartInstance = playoutModel.CurrentPartInstance + const currentPartInstance = playoutModel.currentPartInstance if (playlist.currentPartInfo?.partInstanceId === data.partInstanceId) { // this is the current part, it has just started playback @@ -63,32 +63,32 @@ export async function onPartPlaybackStarted( // Update generated properties on the newly playing partInstance const currentRundown = currentPartInstance - ? playoutModel.getRundown(currentPartInstance.PartInstance.rundownId) + ? playoutModel.getRundown(currentPartInstance.partInstance.rundownId) : undefined const showStyleRundown = currentRundown ?? rundown const showStyle = await context.getShowStyleCompound( - showStyleRundown.Rundown.showStyleVariantId, - showStyleRundown.Rundown.showStyleBaseId + showStyleRundown.rundown.showStyleVariantId, + showStyleRundown.rundown.showStyleBaseId ) const blueprint = await context.getShowStyleBlueprint(showStyle._id) updatePartInstanceOnTake( context, - playoutModel.Playlist, + playoutModel.playlist, showStyle, blueprint, - rundown.Rundown, + rundown.rundown, playingPartInstance, currentPartInstance ) - clearQueuedSegmentId(playoutModel, playingPartInstance.PartInstance, playlist.nextPartInfo) + clearQueuedSegmentId(playoutModel, playingPartInstance.partInstance, playlist.nextPartInfo) resetPreviousSegment(playoutModel) // Update the next partinstance const nextPart = selectNextPart( context, playlist, - playingPartInstance.PartInstance, + playingPartInstance.partInstance, null, playoutModel.getAllOrderedSegments(), playoutModel.getAllOrderedParts() @@ -116,7 +116,7 @@ export async function onPartPlaybackStarted( } logger.error( - `PartInstance "${playingPartInstance.PartInstance._id}" has started playback by the playout gateway, but has not been selected for playback!` + `PartInstance "${playingPartInstance.partInstance._id}" has started playback by the playout gateway, but has not been selected for playback!` ) } } @@ -136,15 +136,15 @@ export function onPartPlaybackStopped( stoppedPlayback: Time } ): void { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist const partInstance = playoutModel.getPartInstance(data.partInstanceId) if (partInstance) { // make sure we don't run multiple times, even if TSR calls us multiple times const isPlaying = - partInstance.PartInstance.timings?.reportedStartedPlayback && - !partInstance.PartInstance.timings?.reportedStoppedPlayback + partInstance.partInstance.timings?.reportedStartedPlayback && + !partInstance.partInstance.timings?.reportedStoppedPlayback if (isPlaying) { logger.debug( `onPartPlaybackStopped: Playout reports PartInstance "${ @@ -182,19 +182,19 @@ export function reportPartInstanceHasStarted( partInstance.setPlannedStartedPlayback(timestamp) } - const previousPartInstance = playoutModel.PreviousPartInstance + const previousPartInstance = playoutModel.previousPartInstance if (timestampUpdated && !playoutModel.isMultiGatewayMode && previousPartInstance) { // Ensure the plannedStoppedPlayback is set for the previous partinstance too previousPartInstance.setPlannedStoppedPlayback(timestamp) } // Update the playlist: - if (!partInstance.PartInstance.part.untimed) { - playoutModel.setRundownStartedPlayback(partInstance.PartInstance.rundownId, timestamp) + if (!partInstance.partInstance.part.untimed) { + playoutModel.setRundownStartedPlayback(partInstance.partInstance.rundownId, timestamp) } if (timestampUpdated) { - playoutModel.queuePartInstanceTimingEvent(partInstance.PartInstance._id) + playoutModel.queuePartInstanceTimingEvent(partInstance.partInstance._id) } } } @@ -218,6 +218,6 @@ export function reportPartInstanceHasStopped( } if (timestampUpdated) { - playoutModel.queuePartInstanceTimingEvent(partInstance.PartInstance._id) + playoutModel.queuePartInstanceTimingEvent(partInstance.partInstance._id) } } diff --git a/packages/job-worker/src/playout/timings/piecePlayback.ts b/packages/job-worker/src/playout/timings/piecePlayback.ts index 1fe2b1a87c..6ce6a3483b 100644 --- a/packages/job-worker/src/playout/timings/piecePlayback.ts +++ b/packages/job-worker/src/playout/timings/piecePlayback.ts @@ -21,7 +21,7 @@ export function onPiecePlaybackStarted( startedPlayback: Time } ): void { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist const partInstance = playoutModel.getPartInstance(data.partInstanceId) if (!partInstance) { @@ -44,7 +44,7 @@ export function onPiecePlaybackStarted( } const isPlaying = !!( - pieceInstance.PieceInstance.reportedStartedPlayback && !pieceInstance.PieceInstance.reportedStoppedPlayback + pieceInstance.pieceInstance.reportedStartedPlayback && !pieceInstance.pieceInstance.reportedStoppedPlayback ) if (!isPlaying) { logger.debug( @@ -73,7 +73,7 @@ export function onPiecePlaybackStopped( stoppedPlayback: Time } ): void { - const playlist = playoutModel.Playlist + const playlist = playoutModel.playlist const partInstance = playoutModel.getPartInstance(data.partInstanceId) if (!partInstance) { @@ -92,7 +92,7 @@ export function onPiecePlaybackStopped( } const isPlaying = !!( - pieceInstance.PieceInstance.reportedStartedPlayback && !pieceInstance.PieceInstance.reportedStoppedPlayback + pieceInstance.pieceInstance.reportedStartedPlayback && !pieceInstance.pieceInstance.reportedStoppedPlayback ) if (isPlaying) { logger.debug( @@ -126,17 +126,17 @@ function reportPieceHasStarted( } // Update the copy in the next-part if there is one, so that the infinite has the same start after a take - const nextPartInstance = playoutModel.NextPartInstance + const nextPartInstance = playoutModel.nextPartInstance if ( - pieceInstance.PieceInstance.infinite && + pieceInstance.pieceInstance.infinite && nextPartInstance && - nextPartInstance.PartInstance._id !== partInstance.PartInstance._id + nextPartInstance.partInstance._id !== partInstance.partInstance._id ) { - const infiniteInstanceId = pieceInstance.PieceInstance.infinite.infiniteInstanceId - for (const nextPieceInstance of nextPartInstance.PieceInstances) { + const infiniteInstanceId = pieceInstance.pieceInstance.infinite.infiniteInstanceId + for (const nextPieceInstance of nextPartInstance.pieceInstances) { if ( - !!nextPieceInstance.PieceInstance.infinite && - nextPieceInstance.PieceInstance.infinite.infiniteInstanceId === infiniteInstanceId + !!nextPieceInstance.pieceInstance.infinite && + nextPieceInstance.pieceInstance.infinite.infiniteInstanceId === infiniteInstanceId ) { nextPieceInstance.setReportedStartedPlayback(timestamp) @@ -147,7 +147,7 @@ function reportPieceHasStarted( } } - playoutModel.queuePartInstanceTimingEvent(partInstance.PartInstance._id) + playoutModel.queuePartInstanceTimingEvent(partInstance.partInstance._id) } } @@ -171,6 +171,6 @@ function reportPieceHasStopped( pieceInstance.setPlannedStoppedPlayback(timestamp) } - playoutModel.queuePartInstanceTimingEvent(pieceInstance.PieceInstance.partInstanceId) + playoutModel.queuePartInstanceTimingEvent(pieceInstance.pieceInstance.partInstanceId) } } diff --git a/packages/job-worker/src/playout/timings/timelineTriggerTime.ts b/packages/job-worker/src/playout/timings/timelineTriggerTime.ts index 81fc11fbba..a2551f7fcd 100644 --- a/packages/job-worker/src/playout/timings/timelineTriggerTime.ts +++ b/packages/job-worker/src/playout/timings/timelineTriggerTime.ts @@ -76,7 +76,7 @@ function timelineTriggerTimeInner( let lastTakeTime: number | undefined // ------------------------------ - const timeline = studioPlayoutModel.Timeline + const timeline = studioPlayoutModel.timeline if (timeline) { const timelineObjs = deserializeTimelineBlob(timeline.timelineBlob) let tlChanged = false diff --git a/packages/job-worker/src/rundown.ts b/packages/job-worker/src/rundown.ts index b30fbaf72f..4d9cfd8453 100644 --- a/packages/job-worker/src/rundown.ts +++ b/packages/job-worker/src/rundown.ts @@ -116,12 +116,12 @@ export async function updatePartInstanceRanks( * Syncs the ranks from matching Parts to PartInstances. */ export function updatePartInstanceRanksAfterAdlib(playoutModel: PlayoutModel, segmentId: SegmentId): void { - const newParts = playoutModel.findSegment(segmentId)?.Parts ?? [] + const newParts = playoutModel.findSegment(segmentId)?.parts ?? [] const segmentPartInstances = _.sortBy( - playoutModel.LoadedPartInstances.filter((p) => p.PartInstance.segmentId === segmentId).map((p) => - clone(p.PartInstance) - ), + playoutModel.loadedPartInstances + .filter((p) => p.partInstance.segmentId === segmentId) + .map((p) => clone(p.partInstance)), (p) => p.part._rank ) diff --git a/packages/job-worker/src/rundownPlaylists.ts b/packages/job-worker/src/rundownPlaylists.ts index d05a4877e5..5b8029ca85 100644 --- a/packages/job-worker/src/rundownPlaylists.ts +++ b/packages/job-worker/src/rundownPlaylists.ts @@ -80,7 +80,7 @@ export async function handleRegenerateRundownPlaylist( await runWithPlayoutModel(context, playlist, playlistLock, null, async (cache) => { await resetRundownPlaylist(context, cache) - if (cache.Playlist.activationId) { + if (cache.playlist.activationId) { await updateTimeline(context, cache) } }) diff --git a/packages/job-worker/src/studio/cleanup.ts b/packages/job-worker/src/studio/cleanup.ts index a1d052773f..5dcb3e9469 100644 --- a/packages/job-worker/src/studio/cleanup.ts +++ b/packages/job-worker/src/studio/cleanup.ts @@ -9,7 +9,7 @@ import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' export async function handleRemoveEmptyPlaylists(context: JobContext, _data: void): Promise { await runJobWithStudioPlayoutModel(context, async (studioPlayoutModel) => { // Skip any playlists which are active - const tmpPlaylists = studioPlayoutModel.RundownPlaylists.filter((p) => !p.activationId, { fields: { _id: 1 } }) + const tmpPlaylists = studioPlayoutModel.rundownPlaylists.filter((p) => !p.activationId, { fields: { _id: 1 } }) // We want to run them all in parallel await Promise.allSettled( diff --git a/packages/job-worker/src/studio/model/StudioPlayoutModel.ts b/packages/job-worker/src/studio/model/StudioPlayoutModel.ts index 66359f7ff2..03d687b728 100644 --- a/packages/job-worker/src/studio/model/StudioPlayoutModel.ts +++ b/packages/job-worker/src/studio/model/StudioPlayoutModel.ts @@ -15,12 +15,12 @@ export interface StudioPlayoutModelBaseReadonly { /** * All of the PeripheralDevices that belong to the Studio of this RundownPlaylist */ - readonly PeripheralDevices: ReadonlyDeep + readonly peripheralDevices: ReadonlyDeep /** * Get the Timeline for the current Studio */ - get Timeline(): TimelineComplete | null + get timeline(): TimelineComplete | null /** * Whether this Studio is operating in multi-gateway mode @@ -57,7 +57,7 @@ export interface StudioPlayoutModel extends StudioPlayoutModelBase, BaseModel { /** * The unwrapped RundownPlaylists in this Studio */ - readonly RundownPlaylists: ReadonlyDeep + readonly rundownPlaylists: ReadonlyDeep /** * Get any activated RundownPlaylists in this Studio diff --git a/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts b/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts index 39adf47aec..f98af573c7 100644 --- a/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts +++ b/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts @@ -28,14 +28,14 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel { public readonly isStudio = true - public readonly PeripheralDevices: ReadonlyDeep + public readonly peripheralDevices: ReadonlyDeep - public readonly RundownPlaylists: ReadonlyDeep + public readonly rundownPlaylists: ReadonlyDeep - #TimelineHasChanged = false - #Timeline: TimelineComplete | null - public get Timeline(): TimelineComplete | null { - return this.#Timeline + #timelineHasChanged = false + #timeline: TimelineComplete | null + public get timeline(): TimelineComplete | null { + return this.#timeline } public constructor( @@ -48,18 +48,18 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel { this.#baselineHelper = new StudioBaselineHelper(context) - this.PeripheralDevices = peripheralDevices + this.peripheralDevices = peripheralDevices - this.RundownPlaylists = rundownPlaylists - this.#Timeline = timeline ?? null + this.rundownPlaylists = rundownPlaylists + this.#timeline = timeline ?? null } - public get DisplayName(): string { + public get displayName(): string { return `CacheForStudio` } public getActiveRundownPlaylists(excludeRundownPlaylistId?: RundownPlaylistId): ReadonlyDeep { - return this.RundownPlaylists.filter((p) => !!p.activationId && p._id !== excludeRundownPlaylistId) + return this.rundownPlaylists.filter((p) => !!p.activationId && p._id !== excludeRundownPlaylistId) } #isMultiGatewayMode: boolean | undefined = undefined @@ -68,7 +68,7 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel { if (this.context.studio.settings.forceMultiGatewayMode) { this.#isMultiGatewayMode = true } else { - const playoutDevices = this.PeripheralDevices.filter( + const playoutDevices = this.peripheralDevices.filter( (device) => device.type === PeripheralDeviceType.PLAYOUT ) this.#isMultiGatewayMode = playoutDevices.length > 1 @@ -85,14 +85,14 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel { } setTimeline(timelineObjs: TimelineObjGeneric[], generationVersions: TimelineCompleteGenerationVersions): void { - this.#Timeline = { + this.#timeline = { _id: this.context.studioId, timelineHash: getRandomId(), generated: getCurrentTime(), timelineBlob: serializeTimelineBlob(timelineObjs), generationVersions: generationVersions, } - this.#TimelineHasChanged = true + this.#timelineHasChanged = true } /** @@ -110,10 +110,10 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel { const span = this.context.startSpan('StudioPlayoutModelImpl.saveAllToDatabase') // Prioritise the timeline for publication reasons - if (this.#TimelineHasChanged && this.#Timeline) { - await this.context.directCollections.Timelines.replace(this.#Timeline) + if (this.#timelineHasChanged && this.#timeline) { + await this.context.directCollections.Timelines.replace(this.#timeline) } - this.#TimelineHasChanged = false + this.#timelineHasChanged = false await this.#baselineHelper.saveAllToDatabase() @@ -143,7 +143,7 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel { ) ) - if (this.#TimelineHasChanged) + if (this.#timelineHasChanged) logOrThrowError(new Error(`Failed no changes in cache assertion, Timeline has been changed`)) if (span) span.end() diff --git a/packages/job-worker/src/workers/context.ts b/packages/job-worker/src/workers/context.ts index ba5f8e8dd2..85e57ad9a6 100644 --- a/packages/job-worker/src/workers/context.ts +++ b/packages/job-worker/src/workers/context.ts @@ -368,7 +368,7 @@ export class JobContextImpl extends StudioCacheContextImpl implements JobContext try { cache.assertNoChanges() } catch (e) { - logger.warn(`${cache.DisplayName} has unsaved changes: ${stringifyError(e)}`) + logger.warn(`${cache.displayName} has unsaved changes: ${stringifyError(e)}`) } } } From a5401fe48d88798d06f7c50b38c150c5be7bef63 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Fri, 27 Oct 2023 13:00:45 +0200 Subject: [PATCH 126/479] chore: fix test --- .../src/blueprints/__tests__/context-adlibActions.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts b/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts index 9cb3c7da0a..34e7eee3a7 100644 --- a/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts +++ b/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts @@ -259,7 +259,7 @@ describe('Test blueprint api context', () => { manuallySelected: false, consumesQueuedSegmentId: false, } - } else if ('PartInstance' in info) { + } else if ('partInstance' in info) { return { partInstanceId: info.partInstance._id, rundownId: info.partInstance.rundownId, From 2a0b344be30fa363ff16719232f441f8d541798e Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Fri, 27 Oct 2023 13:02:50 +0200 Subject: [PATCH 127/479] chore: fix more tests --- .../src/ingest/__tests__/updateNext.test.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/job-worker/src/ingest/__tests__/updateNext.test.ts b/packages/job-worker/src/ingest/__tests__/updateNext.test.ts index 660207799f..476c359b76 100644 --- a/packages/job-worker/src/ingest/__tests__/updateNext.test.ts +++ b/packages/job-worker/src/ingest/__tests__/updateNext.test.ts @@ -357,7 +357,7 @@ describe('ensureNextPartIsValid', () => { expect(setNextPartMock).toHaveBeenCalledTimes(1) expect(setNextPartMock).toHaveBeenCalledWith( context, - expect.objectContaining({ PlaylistId: rundownPlaylistId }), + expect.objectContaining({ playlistId: rundownPlaylistId }), expect.objectContaining({ part: expect.objectContaining({ _id: 'mock_part1' }) }), false ) @@ -370,7 +370,7 @@ describe('ensureNextPartIsValid', () => { expect(setNextPartMock).toHaveBeenCalledTimes(1) expect(setNextPartMock).toHaveBeenCalledWith( context, - expect.objectContaining({ PlaylistId: rundownPlaylistId }), + expect.objectContaining({ playlistId: rundownPlaylistId }), expect.objectContaining({ part: expect.objectContaining({ _id: 'mock_part4' }) }), false ) @@ -392,7 +392,7 @@ describe('ensureNextPartIsValid', () => { expect(setNextPartMock).toHaveBeenCalledTimes(1) expect(setNextPartMock).toHaveBeenCalledWith( context, - expect.objectContaining({ PlaylistId: rundownPlaylistId }), + expect.objectContaining({ playlistId: rundownPlaylistId }), expect.objectContaining({ part: expect.objectContaining({ _id: 'mock_part1' }) }), false ) @@ -419,7 +419,7 @@ describe('ensureNextPartIsValid', () => { expect(setNextPartMock).toHaveBeenCalledTimes(1) expect(setNextPartMock).toHaveBeenCalledWith( context, - expect.objectContaining({ PlaylistId: rundownPlaylistId }), + expect.objectContaining({ playlistId: rundownPlaylistId }), expect.objectContaining({ part: expect.objectContaining({ _id: 'mock_part4' }) }), false ) @@ -432,7 +432,7 @@ describe('ensureNextPartIsValid', () => { expect(setNextPartMock).toHaveBeenCalledTimes(1) expect(setNextPartMock).toHaveBeenCalledWith( context, - expect.objectContaining({ PlaylistId: rundownPlaylistId }), + expect.objectContaining({ playlistId: rundownPlaylistId }), expect.objectContaining({ part: expect.objectContaining({ _id: 'mock_part4' }) }), false ) @@ -445,7 +445,7 @@ describe('ensureNextPartIsValid', () => { expect(setNextPartMock).toHaveBeenCalledTimes(1) expect(setNextPartMock).toHaveBeenCalledWith( context, - expect.objectContaining({ PlaylistId: rundownPlaylistId }), + expect.objectContaining({ playlistId: rundownPlaylistId }), expect.objectContaining({ part: expect.objectContaining({ _id: 'mock_part9' }) }), false ) @@ -458,7 +458,7 @@ describe('ensureNextPartIsValid', () => { expect(setNextPartMock).toHaveBeenCalledTimes(1) expect(setNextPartMock).toHaveBeenCalledWith( context, - expect.objectContaining({ PlaylistId: rundownPlaylistId }), + expect.objectContaining({ playlistId: rundownPlaylistId }), expect.objectContaining({ part: expect.objectContaining({ _id: 'mock_part9' }) }), false ) @@ -495,7 +495,7 @@ describe('ensureNextPartIsValid', () => { expect(setNextPartMock).toHaveBeenCalledTimes(1) expect(setNextPartMock).toHaveBeenCalledWith( context, - expect.objectContaining({ PlaylistId: rundownPlaylistId }), + expect.objectContaining({ playlistId: rundownPlaylistId }), expect.objectContaining({ part: expect.objectContaining({ _id: 'mock_part1' }) }), false ) @@ -532,7 +532,7 @@ describe('ensureNextPartIsValid', () => { expect(setNextPartMock).toHaveBeenCalledTimes(1) expect(setNextPartMock).toHaveBeenCalledWith( context, - expect.objectContaining({ PlaylistId: rundownPlaylistId }), + expect.objectContaining({ playlistId: rundownPlaylistId }), expect.objectContaining({ part: expect.objectContaining({ _id: 'mock_part1' }) }), false ) @@ -606,7 +606,7 @@ describe('ensureNextPartIsValid', () => { expect(setNextPartMock).toHaveBeenCalledTimes(1) expect(setNextPartMock).toHaveBeenCalledWith( expect.objectContaining({}), - expect.objectContaining({ PlaylistId: rundownPlaylistId }), + expect.objectContaining({ playlistId: rundownPlaylistId }), expect.objectContaining({ part: expect.objectContaining({ _id: 'tmp_part_1' }) }), false ) @@ -624,7 +624,7 @@ describe('ensureNextPartIsValid', () => { expect(setNextPartMock).toHaveBeenCalledTimes(1) expect(setNextPartMock).toHaveBeenCalledWith( expect.objectContaining({}), - expect.objectContaining({ PlaylistId: rundownPlaylistId }), + expect.objectContaining({ playlistId: rundownPlaylistId }), null, false ) From cc9401a9f5abf7ee387c654a729a5e6fc254bd3c Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Fri, 27 Oct 2023 14:07:57 +0200 Subject: [PATCH 128/479] fix(MediaStatus PopUp): improve scroll to Part --- .../MediaStatusPopUp/MediaStatusPopUp.scss | 14 ++++++++++--- .../MediaStatusPopUp/MediaStatusPopUpItem.tsx | 21 ++++++++++++------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUp.scss b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUp.scss index 9f02a92e90..1b2c927638 100644 --- a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUp.scss +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUp.scss @@ -22,12 +22,20 @@ .media-status-panel__pop-out { position: absolute; - top: 20px; - right: 20px; - left: auto; + top: 20px; + right: 20px; + left: auto; opacity: 0.5; + visibility: visible; + transition: opacity 200ms, visibility 0ms 0ms; &:hover { opacity: 1; } } + +.media-status-panel.velocity-animating .media-status-panel__pop-out { + opacity: 0; + visibility: hidden; + transition: opacity 200ms, visibility 0ms 200ms; +} diff --git a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.tsx b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.tsx index 3f2cbb1e8c..7bfb1a131f 100644 --- a/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.tsx +++ b/meteor/client/ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.tsx @@ -7,7 +7,8 @@ import { TimingDataResolution, TimingTickResolution, withTiming } from '../Rundo import { RundownUtils } from '../../../lib/rundown' import classNames from 'classnames' import { MediaStatusIndicator } from '../../MediaStatus/MediaStatusIndicator' -import RundownViewEventBus, { RundownViewEvents } from '../../../../lib/api/triggers/RundownViewEventBus' +import { scrollToPart, scrollToSegment } from '../../../lib/viewPort' +import { logger } from '../../../../lib/logging' export const MediaStatusPopUpItem = withTiming< { @@ -58,17 +59,19 @@ export const MediaStatusPopUpItem = withTiming< const onPartIdentifierClick = useCallback(() => { if (!segmentId || !partId) return - RundownViewEventBus.emit(RundownViewEvents.GO_TO_PART, { - segmentId, - partId, - zoomInToFit: true, - }) + scrollToPart(partId, false, false, false).catch(logger.error) }, [segmentId, partId]) + const onSegmentIdentifierClick = useCallback(() => { + if (!segmentId) return + + scrollToSegment(segmentId, false, false).catch(logger.error) + }, [segmentId]) + return ( - {isNext ?
: null} + {isNext && !isLive ?
: null} {isLive ?
: null} @@ -76,7 +79,9 @@ export const MediaStatusPopUpItem = withTiming< {segmentIdentifier ? ( - + ) : null} {partIdentifier ? (
+ Segments:
diff --git a/packages/live-status-gateway/sample-client/script.js b/packages/live-status-gateway/sample-client/script.js index 2dd151592f..c02ddb99b2 100644 --- a/packages/live-status-gateway/sample-client/script.js +++ b/packages/live-status-gateway/sample-client/script.js @@ -59,6 +59,8 @@ const TIME_OF_DAY_SPAN_ID = 'time-of-day' const SEGMENT_DURATION_SPAN_CLASS = 'segment-duration' const SEGMENT_REMAINIG_SPAN_ID = 'segment-remaining' const PART_REMAINIG_SPAN_ID = 'part-remaining' +const ACTIVE_PIECES_SPAN_ID = 'active-pieces' +const NEXT_PIECES_SPAN_ID = 'next-pieces' const SEGMENTS_DIV_ID = 'segments' const ENABLE_SYNCED_TICKS = true @@ -66,6 +68,16 @@ let activePlaylist = {} function handleActivePlaylist(data) { activePlaylist = data + const activePiecesEl = document.getElementById(ACTIVE_PIECES_SPAN_ID) + const nextPiecesEl = document.getElementById(NEXT_PIECES_SPAN_ID) + activePiecesEl.innerHTML = + '
  • ' + + activePlaylist.activePieces.map((p) => `${p.name} [${p.tags || []}]`).join('
  • ') + + '
    • ' + nextPiecesEl.innerHTML = + '
      • ' + + activePlaylist.nextPart.pieces.map((p) => `${p.name} [${p.tags || []}]`).join('
      • ') + + '
        • ' } setInterval(() => { @@ -85,6 +97,7 @@ setInterval(() => { } if (segmentEndTime) segmentRemainingEl.textContent = formatMillisecondsToTime(segmentEndTime - now) if (partEndTime) partRemainingEl.textContent = formatMillisecondsToTime(Math.ceil(partEndTime / 1000) * 1000 - now) + updateClock() }, 100) diff --git a/packages/live-status-gateway/src/collections/adLibActionsHandler.ts b/packages/live-status-gateway/src/collections/adLibActionsHandler.ts index 7148ba2b18..402f9fc939 100644 --- a/packages/live-status-gateway/src/collections/adLibActionsHandler.ts +++ b/packages/live-status-gateway/src/collections/adLibActionsHandler.ts @@ -6,7 +6,6 @@ import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibActio import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' -import _ = require('underscore') import { SelectedPartInstances } from './partInstancesHandler' export class AdLibActionsHandler @@ -36,14 +35,13 @@ export class AdLibActionsHandler async update(source: string, data: SelectedPartInstances | undefined): Promise { this._logger.info(`${this._name} received partInstances update from ${source}`) const prevRundownId = this._curRundownId - const prevCurPartInstance = this._curPartInstance this._curPartInstance = data ? data.current ?? data.next : undefined this._curRundownId = this._curPartInstance ? unprotectString(this._curPartInstance.rundownId) : undefined await new Promise(process.nextTick.bind(this)) if (!this._collectionName) return if (!this._publicationName) return - if (prevRundownId !== this._curRundownId || !_.isEqual(prevCurPartInstance, this._curPartInstance)) { + if (prevRundownId !== this._curRundownId) { if (this._subscriptionId) this._coreHandler.unsubscribe(this._subscriptionId) if (this._dbObserver) this._dbObserver.stop() if (this._curRundownId && this._curPartInstance) { @@ -62,7 +60,6 @@ export class AdLibActionsHandler if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) this._collectionData = collection.find({ rundownId: this._curRundownId, - partId: this._curPartInstance.part._id, }) await this.notify(this._collectionData) } diff --git a/packages/live-status-gateway/src/collections/adLibsHandler.ts b/packages/live-status-gateway/src/collections/adLibsHandler.ts index 689a36d977..7cbaeef4d2 100644 --- a/packages/live-status-gateway/src/collections/adLibsHandler.ts +++ b/packages/live-status-gateway/src/collections/adLibsHandler.ts @@ -6,7 +6,6 @@ import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' -import _ = require('underscore') import { SelectedPartInstances } from './partInstancesHandler' export class AdLibsHandler @@ -36,7 +35,6 @@ export class AdLibsHandler async update(source: string, data: SelectedPartInstances | undefined): Promise { this._logger.info(`${this._name} received adLibs update from ${source}`) const prevRundownId = this._currentRundownId - const prevCurPartInstance = this._currentPartInstance this._currentPartInstance = data ? data.current ?? data.next : undefined this._currentRundownId = this._currentPartInstance ? unprotectString(this._currentPartInstance.rundownId) @@ -45,7 +43,7 @@ export class AdLibsHandler await new Promise(process.nextTick.bind(this)) if (!this._collectionName) return if (!this._publicationName) return - if (prevRundownId !== this._currentRundownId || !_.isEqual(prevCurPartInstance, this._currentPartInstance)) { + if (prevRundownId !== this._currentRundownId) { if (this._subscriptionId) this._coreHandler.unsubscribe(this._subscriptionId) if (this._dbObserver) this._dbObserver.stop() if (this._currentRundownId && this._currentPartInstance) { @@ -67,7 +65,6 @@ export class AdLibsHandler if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) this._collectionData = collection.find({ rundownId: this._currentRundownId, - partId: this._currentPartInstance.part._id, }) await this.notify(this._collectionData) } diff --git a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts new file mode 100644 index 0000000000..7d22e25c13 --- /dev/null +++ b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts @@ -0,0 +1,160 @@ +import { Logger } from 'winston' +import { CoreHandler } from '../coreHandler' +import { CollectionBase, Collection, CollectionObserver } from '../wsHandler' +import { CoreConnection } from '@sofie-automation/server-core-integration' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' +import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' +import areElementsShallowEqual from '@sofie-automation/shared-lib/dist/lib/isShallowEqual' +import _ = require('underscore') + +export interface SelectedPieceInstances { + // Pieces reported by the Playout Gateway as active + active: PieceInstance[] + + // Pieces present in the current part instance + currentPartInstance: PieceInstance[] + + // Pieces present in the current part instance + nextPartInstance: PieceInstance[] +} + +export class PieceInstancesHandler + extends CollectionBase + implements Collection, CollectionObserver +{ + public observerName: string + private _core: CoreConnection + private _currentPlaylist: DBRundownPlaylist | undefined + private _partInstanceIds: string[] = [] + private _activationId: string | undefined + + constructor(logger: Logger, coreHandler: CoreHandler) { + super(PieceInstancesHandler.name, CollectionName.PieceInstances, 'pieceInstances', logger, coreHandler) + this._core = coreHandler.coreConnection + this.observerName = this._name + this._collectionData = { + active: [], + currentPartInstance: [], + nextPartInstance: [], + } + } + + async changed(id: string, changeType: string): Promise { + this._logger.info(`${this._name} ${changeType} ${id}`) + if (!this._collectionName) return + this.updateCollectionData() + + await this.notify(this._collectionData) + } + + private updateCollectionData(): boolean { + if (!this._collectionName || !this._collectionData) return false + const collection = this._core.getCollection(this._collectionName) + if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) + const active = this._currentPlaylist?.currentPartInfo?.partInstanceId + ? collection.find( + (pieceInstance: PieceInstance) => + (pieceInstance.partInstanceId === this._currentPlaylist?.previousPartInfo?.partInstanceId || + pieceInstance.partInstanceId === this._currentPlaylist?.currentPartInfo?.partInstanceId) && + (pieceInstance.reportedStartedPlayback != null || + pieceInstance.piece.enable.start === 0 || + pieceInstance.infinite?.fromPreviousPart) + ) + : [] + const inCurrentPartInstance = this._currentPlaylist?.currentPartInfo?.partInstanceId + ? collection.find({ partInstanceId: this._currentPlaylist.currentPartInfo.partInstanceId }) + : [] + const inNextPartInstance = this._currentPlaylist?.nextPartInfo?.partInstanceId + ? collection.find({ partInstanceId: this._currentPlaylist.nextPartInfo.partInstanceId }) + : [] + + let hasAnythingChanged = false + if (!areElementsShallowEqual(this._collectionData.active, active)) { + this._collectionData.active = active + hasAnythingChanged = true + } + if (!areElementsShallowEqual(this._collectionData.currentPartInstance, inCurrentPartInstance)) { + this._collectionData.currentPartInstance = inCurrentPartInstance + hasAnythingChanged = true + } + if (!areElementsShallowEqual(this._collectionData.nextPartInstance, inNextPartInstance)) { + this._collectionData.nextPartInstance = inNextPartInstance + hasAnythingChanged = true + } + return hasAnythingChanged + } + + private clearCollectionData() { + if (!this._collectionName || !this._collectionData) return + this._collectionData.active = [] + this._collectionData.currentPartInstance = [] + this._collectionData.nextPartInstance = [] + } + + async update(source: string, data: DBRundownPlaylist | undefined): Promise { + const prevPartInstanceIds = this._partInstanceIds + const prevActivationId = this._activationId + + this._logger.info( + `${this._name} received playlist update ${data?._id}, active ${ + data?.activationId ? true : false + } from ${source}` + ) + this._currentPlaylist = data + if (!this._collectionName) return + + this._partInstanceIds = this._currentPlaylist + ? _.compact([ + unprotectString(this._currentPlaylist.previousPartInfo?.partInstanceId), + unprotectString(this._currentPlaylist.nextPartInfo?.partInstanceId), + unprotectString(this._currentPlaylist.currentPartInfo?.partInstanceId), + ]) + : [] + this._activationId = unprotectString(this._currentPlaylist?.activationId) + if (this._currentPlaylist && this._partInstanceIds.length && this._activationId) { + const sameSubscription = + areElementsShallowEqual(this._partInstanceIds, prevPartInstanceIds) && + prevActivationId === this._activationId + if (!sameSubscription) { + await new Promise(process.nextTick.bind(this)) + if (!this._collectionName) return + if (!this._publicationName) return + if (!this._currentPlaylist) return + if (this._subscriptionId) this._coreHandler.unsubscribe(this._subscriptionId) + this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, { + partInstanceId: { $in: this._partInstanceIds }, + playlistActivationId: this._activationId, + reportedStartedPlayback: { $exists: false }, + }) + this._dbObserver = this._coreHandler.setupObserver(this._collectionName) + this._dbObserver.added = (id: string) => { + void this.changed(id, 'added').catch(this._logger.error) + } + this._dbObserver.changed = (id: string) => { + void this.changed(id, 'changed').catch(this._logger.error) + } + this._dbObserver.removed = (id: string) => { + void this.changed(id, 'removed').catch(this._logger.error) + } + + const hasAnythingChanged = this.updateCollectionData() + if (hasAnythingChanged) { + await this.notify(this._collectionData) + } + } else if (this._subscriptionId) { + const hasAnythingChanged = this.updateCollectionData() + if (hasAnythingChanged) { + await this.notify(this._collectionData) + } + } else { + this.clearCollectionData() + await this.notify(this._collectionData) + } + } else { + this.clearCollectionData() + await this.notify(this._collectionData) + } + } +} diff --git a/packages/live-status-gateway/src/liveStatusServer.ts b/packages/live-status-gateway/src/liveStatusServer.ts index 6949271a7c..461fdf3ef3 100644 --- a/packages/live-status-gateway/src/liveStatusServer.ts +++ b/packages/live-status-gateway/src/liveStatusServer.ts @@ -20,6 +20,8 @@ import { SegmentsTopic } from './topics/segmentsTopic' import { SegmentsHandler } from './collections/segmentsHandler' import { PartHandler } from './collections/partHandler' import { PartsHandler } from './collections/partsHandler' +import { PieceInstancesHandler } from './collections/pieceInstancesHandler' +import { AdLibsTopic } from './topics/adLibsTopic' export class LiveStatusServer { _logger: Logger @@ -39,10 +41,12 @@ export class LiveStatusServer { const studioTopic = new StudioTopic(this._logger) const activePlaylistTopic = new ActivePlaylistTopic(this._logger) const segmentsTopic = new SegmentsTopic(this._logger) + const adLibsTopic = new AdLibsTopic(this._logger) rootChannel.addTopic('studio', studioTopic) rootChannel.addTopic('activePlaylist', activePlaylistTopic) rootChannel.addTopic('segments', segmentsTopic) + rootChannel.addTopic('adLibs', adLibsTopic) const studioHandler = new StudioHandler(this._logger, this._coreHandler) await studioHandler.init() @@ -64,6 +68,8 @@ export class LiveStatusServer { await partHandler.init() const partInstancesHandler = new PartInstancesHandler(this._logger, this._coreHandler) await partInstancesHandler.init() + const pieceInstancesHandler = new PieceInstancesHandler(this._logger, this._coreHandler) + await pieceInstancesHandler.init() const adLibActionsHandler = new AdLibActionsHandler(this._logger, this._coreHandler) await adLibActionsHandler.init() const adLibsHandler = new AdLibsHandler(this._logger, this._coreHandler) @@ -78,6 +84,7 @@ export class LiveStatusServer { await playlistHandler.subscribe(segmentHandler) await playlistHandler.subscribe(partHandler) await playlistHandler.subscribe(partInstancesHandler) + await playlistHandler.subscribe(pieceInstancesHandler) await rundownHandler.subscribe(showStyleBaseHandler) await partInstancesHandler.subscribe(rundownHandler) await partInstancesHandler.subscribe(segmentHandler) @@ -90,19 +97,24 @@ export class LiveStatusServer { // add observers for websocket topic updates await studioHandler.subscribe(studioTopic) await playlistHandler.playlistsHandler.subscribe(studioTopic) + await playlistHandler.subscribe(activePlaylistTopic) await showStyleBaseHandler.subscribe(activePlaylistTopic) await partInstancesHandler.subscribe(activePlaylistTopic) - await adLibActionsHandler.subscribe(activePlaylistTopic) - await adLibsHandler.subscribe(activePlaylistTopic) - await globalAdLibActionsHandler.subscribe(activePlaylistTopic) - await globalAdLibsHandler.subscribe(activePlaylistTopic) await partsHandler.subscribe(activePlaylistTopic) + await pieceInstancesHandler.subscribe(activePlaylistTopic) await playlistHandler.subscribe(segmentsTopic) await segmentsHandler.subscribe(segmentsTopic) await partsHandler.subscribe(segmentsTopic) + await showStyleBaseHandler.subscribe(adLibsTopic) + await playlistHandler.subscribe(adLibsTopic) + await adLibActionsHandler.subscribe(adLibsTopic) + await adLibsHandler.subscribe(adLibsTopic) + await globalAdLibActionsHandler.subscribe(adLibsTopic) + await globalAdLibsHandler.subscribe(adLibsTopic) + const wss = new WebSocketServer({ port: 8080 }) wss.on('connection', (ws, request) => { this._logger.info(`WebSocket connection requested for path '${request.url}'`) diff --git a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts index 28ecce1256..eec3e4943f 100644 --- a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts +++ b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts @@ -4,39 +4,36 @@ import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protected import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { DBShowStyleBase, OutputLayers, SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' -import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' -import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' -import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' -import { - IBlueprintActionManifestDisplayContent, - IOutputLayer, - ISourceLayer, -} from '@sofie-automation/blueprints-integration' +import { IOutputLayer, ISourceLayer } from '@sofie-automation/blueprints-integration' import { literal } from '@sofie-automation/shared-lib/dist/lib/lib' import { WebSocketTopicBase, WebSocketTopic, CollectionObserver } from '../wsHandler' import { SelectedPartInstances, PartInstancesHandler } from '../collections/partInstancesHandler' import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' -import { interpollateTranslation } from '@sofie-automation/corelib/dist/TranslatableMessage' -import { AdLibsHandler } from '../collections/adLibsHandler' -import { GlobalAdLibsHandler } from '../collections/globalAdLibsHandler' import { PlaylistHandler } from '../collections/playlistHandler' import { ShowStyleBaseHandler } from '../collections/showStyleBaseHandler' -import { AdLibActionsHandler } from '../collections/adLibActionsHandler' -import { GlobalAdLibActionsHandler } from '../collections/globalAdLibActionsHandler' import { CurrentSegmentTiming, calculateCurrentSegmentTiming } from './helpers/segmentTiming' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { PartsHandler } from '../collections/partsHandler' import _ = require('underscore') import { PartTiming, calculateCurrentPartTiming } from './helpers/partTiming' +import { SelectedPieceInstances } from '../collections/pieceInstancesHandler' +import { PieceInstancesHandler } from '../collections/pieceInstancesHandler' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -const THROTTLE_PERIOD_MS = 20 +const THROTTLE_PERIOD_MS = 100 + +interface PieceStatus { + id: string + name: string + tags?: string[] +} interface PartStatus { id: string segmentId: string name: string autoNext?: boolean + pieces: PieceStatus[] } interface CurrentPartStatus extends PartStatus { @@ -48,20 +45,6 @@ interface CurrentSegmentStatus { timing: CurrentSegmentTiming } -interface AdLibActionType { - name: string - label: string -} - -interface AdLibStatus { - id: string - name: string - sourceLayer: string - outputLayer: string - actionType: AdLibActionType[] - tags?: string[] -} - export interface ActivePlaylistStatus { event: string id: string | null @@ -70,8 +53,7 @@ export interface ActivePlaylistStatus { currentPart: CurrentPartStatus | null currentSegment: CurrentSegmentStatus | null nextPart: PartStatus | null - adLibs: AdLibStatus[] - globalAdLibs: AdLibStatus[] + activePieces: PieceStatus[] } export class ActivePlaylistTopic @@ -80,9 +62,8 @@ export class ActivePlaylistTopic WebSocketTopic, CollectionObserver, CollectionObserver, - CollectionObserver, - CollectionObserver, - CollectionObserver + CollectionObserver, + CollectionObserver { public observerName = ActivePlaylistTopic.name private _sourceLayersMap: Map = new Map() @@ -92,11 +73,8 @@ export class ActivePlaylistTopic private _nextPartInstance: DBPartInstance | undefined private _firstInstanceInSegmentPlayout: DBPartInstance | undefined private _partInstancesInCurrentSegment: DBPartInstance[] = [] - private _adLibActions: AdLibAction[] | undefined - private _abLibs: AdLibPiece[] | undefined - private _globalAdLibActions: RundownBaselineAdLibAction[] | undefined - private _globalAdLibs: RundownBaselineAdLibItem[] | undefined private _partsBySegmentId: Record = {} + private _pieceInstances: SelectedPieceInstances | undefined private throttledSendStatusToAll: () => void constructor(logger: Logger) { @@ -115,7 +93,11 @@ export class ActivePlaylistTopic sendStatus(subscribers: Iterable): void { if ( this._currentPartInstance?._id !== this._activePlaylist?.currentPartInfo?.partInstanceId || - this._nextPartInstance?._id !== this._activePlaylist?.nextPartInfo?.partInstanceId + this._nextPartInstance?._id !== this._activePlaylist?.nextPartInfo?.partInstanceId || + (this._pieceInstances?.currentPartInstance[0] && + this._pieceInstances.currentPartInstance[0].partInstanceId !== this._currentPartInstance?._id) || + (this._pieceInstances?.nextPartInstance[0] && + this._pieceInstances.nextPartInstance[0].partInstanceId !== this._nextPartInstance?._id) ) { // data is inconsistent, let's wait return @@ -123,100 +105,6 @@ export class ActivePlaylistTopic const currentPart = this._currentPartInstance ? this._currentPartInstance.part : null const nextPart = this._nextPartInstance ? this._nextPartInstance.part : null - const adLibs: AdLibStatus[] = [] - const globalAdLibs: AdLibStatus[] = [] - - if (this._adLibActions) { - adLibs.push( - ...this._adLibActions.map((action) => { - const sourceLayerName = this._sourceLayersMap.get( - (action.display as IBlueprintActionManifestDisplayContent).sourceLayerId - ) - const outputLayerName = this._outputLayersMap.get( - (action.display as IBlueprintActionManifestDisplayContent).outputLayerId - ) - const triggerModes = action.triggerModes - ? action.triggerModes.map((t) => - literal({ - name: t.data, - label: interpollateTranslation(t.display.label.key, t.display.label.args), - }) - ) - : [] - return literal({ - id: unprotectString(action._id), - name: interpollateTranslation(action.display.label.key, action.display.label.args), - sourceLayer: sourceLayerName ?? 'invalid', - outputLayer: outputLayerName ?? 'invalid', - actionType: triggerModes, - tags: action.display.tags, - }) - }) - ) - } - - if (this._abLibs) { - adLibs.push( - ...this._abLibs.map((adLib) => { - const sourceLayerName = this._sourceLayersMap.get(adLib.sourceLayerId) - const outputLayerName = this._outputLayersMap.get(adLib.outputLayerId) - return literal({ - id: unprotectString(adLib._id), - name: adLib.name, - sourceLayer: sourceLayerName ?? 'invalid', - outputLayer: outputLayerName ?? 'invalid', - actionType: [], - tags: adLib.tags, - }) - }) - ) - } - - if (this._globalAdLibActions) { - globalAdLibs.push( - ...this._globalAdLibActions.map((action) => { - const sourceLayerName = this._sourceLayersMap.get( - (action.display as IBlueprintActionManifestDisplayContent).sourceLayerId - ) - const outputLayerName = this._outputLayersMap.get( - (action.display as IBlueprintActionManifestDisplayContent).outputLayerId - ) - const triggerModes = action.triggerModes - ? action.triggerModes.map((t) => - literal({ - name: t.data, - label: interpollateTranslation(t.display.label.key, t.display.label.args), - }) - ) - : [] - return literal({ - id: unprotectString(action._id), - name: interpollateTranslation(action.display.label.key, action.display.label.args), - sourceLayer: sourceLayerName ?? 'invalid', - outputLayer: outputLayerName ?? 'invalid', - actionType: triggerModes, - tags: action.display.tags, - }) - }) - ) - } - - if (this._globalAdLibs) { - globalAdLibs.push( - ...this._globalAdLibs.map((adLib) => { - const sourceLayerName = this._sourceLayersMap.get(adLib.sourceLayerId) - const outputLayerName = this._outputLayersMap.get(adLib.outputLayerId) - return literal({ - id: unprotectString(adLib._id), - name: adLib.name, - sourceLayer: sourceLayerName ?? 'invalid', - outputLayer: outputLayerName ?? 'invalid', - actionType: [], - tags: adLib.tags, - }) - }) - ) - } const message = this._activePlaylist ? literal({ @@ -235,6 +123,7 @@ export class ActivePlaylistTopic this._currentPartInstance, this._partInstancesInCurrentSegment ), + pieces: this._pieceInstances?.currentPartInstance.map(this.toPieceStatus) ?? [], }) : null, currentSegment: @@ -255,10 +144,10 @@ export class ActivePlaylistTopic name: nextPart.title, autoNext: nextPart.autoNext, segmentId: unprotectString(nextPart.segmentId), + pieces: this._pieceInstances?.nextPartInstance.map(this.toPieceStatus) ?? [], }) : null, - adLibs, - globalAdLibs, + activePieces: this._pieceInstances?.active.map(this.toPieceStatus) ?? [], }) : literal({ event: 'activePlaylist', @@ -268,8 +157,7 @@ export class ActivePlaylistTopic currentPart: null, currentSegment: null, nextPart: null, - adLibs: [], - globalAdLibs: [], + activePieces: [], }) for (const subscriber of subscribers) { @@ -283,11 +171,8 @@ export class ActivePlaylistTopic | DBRundownPlaylist | DBShowStyleBase | SelectedPartInstances - | AdLibAction[] - | RundownBaselineAdLibAction[] - | AdLibPiece[] - | RundownBaselineAdLibItem[] | DBPart[] + | SelectedPieceInstances | undefined ): Promise { switch (source) { @@ -345,35 +230,17 @@ export class ActivePlaylistTopic this._partInstancesInCurrentSegment = partInstances.inCurrentSegment break } - case AdLibActionsHandler.name: { - const adLibActions = data ? (data as AdLibAction[]) : [] - this._logger.info(`${this._name} received adLibActions update from ${source}`) - this._adLibActions = adLibActions - break - } - case GlobalAdLibActionsHandler.name: { - const globalAdLibActions = data ? (data as RundownBaselineAdLibAction[]) : [] - this._logger.info(`${this._name} received globalAdLibActions update from ${source}`) - this._globalAdLibActions = globalAdLibActions - break - } - case AdLibsHandler.name: { - const adLibs = data ? (data as AdLibPiece[]) : [] - this._logger.info(`${this._name} received adLibs update from ${source}`) - this._abLibs = adLibs - break - } - case GlobalAdLibsHandler.name: { - const globalAdLibs = data ? (data as RundownBaselineAdLibItem[]) : [] - this._logger.info(`${this._name} received globalAdLibs update from ${source}`) - this._globalAdLibs = globalAdLibs - break - } case PartsHandler.name: { this._partsBySegmentId = _.groupBy(data as DBPart[], 'segmentId') this._logger.info(`${this._name} received parts update from ${source}`) break } + case PieceInstancesHandler.name: { + const pieceInstances = data as SelectedPieceInstances + this._logger.info(`${this._name} received pieceInstances update from ${source}`) + this._pieceInstances = pieceInstances + break + } default: throw new Error(`${this._name} received unsupported update from ${source}}`) } @@ -384,4 +251,12 @@ export class ActivePlaylistTopic private sendStatusToAll() { this.sendStatus(this._subscribers) } + + private toPieceStatus(this: void, pieceInstance: PieceInstance): PieceStatus { + return { + id: unprotectString(pieceInstance._id), + name: pieceInstance.piece.name, + tags: pieceInstance.piece.tags, + } + } } diff --git a/packages/live-status-gateway/src/topics/adLibsTopic.ts b/packages/live-status-gateway/src/topics/adLibsTopic.ts new file mode 100644 index 0000000000..17a7ae99b3 --- /dev/null +++ b/packages/live-status-gateway/src/topics/adLibsTopic.ts @@ -0,0 +1,279 @@ +import { Logger } from 'winston' +import { WebSocket } from 'ws' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { WebSocketTopicBase, WebSocketTopic, CollectionObserver } from '../wsHandler' +import { PlaylistHandler } from '../collections/playlistHandler' +import { literal } from '@sofie-automation/corelib/dist/lib' +import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' +import _ = require('underscore') +import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' +import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' +import { AdLibActionsHandler } from '../collections/adLibActionsHandler' +import { GlobalAdLibActionsHandler } from '../collections/globalAdLibActionsHandler' +import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' +import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' +import { + IBlueprintActionManifestDisplayContent, + IOutputLayer, + ISourceLayer, +} from '@sofie-automation/blueprints-integration' +import { ShowStyleBaseHandler } from '../collections/showStyleBaseHandler' +import { DBShowStyleBase, OutputLayers, SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' +import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { interpollateTranslation } from '@sofie-automation/corelib/dist/TranslatableMessage' +import { AdLibsHandler } from '../collections/adLibsHandler' +import { GlobalAdLibsHandler } from '../collections/globalAdLibsHandler' + +const THROTTLE_PERIOD_MS = 100 + +export interface AdLibsStatus { + event: 'adLibs' + rundownPlaylistId: string | null + adLibs: AdLibStatus[] + globalAdLibs: AdLibStatus[] +} + +interface AdLibActionType { + name: string + label: string +} + +interface AdLibStatus { + id: string + name: string + sourceLayer: string + outputLayer: string + actionType: AdLibActionType[] + tags?: string[] +} + +export class AdLibsTopic + extends WebSocketTopicBase + implements + WebSocketTopic, + CollectionObserver, + CollectionObserver, + CollectionObserver +{ + public observerName = AdLibsTopic.name + private _activePlaylist: DBRundownPlaylist | undefined + private _sourceLayersMap: Map = new Map() + private _outputLayersMap: Map = new Map() + private _adLibActions: AdLibAction[] | undefined + private _abLibs: AdLibPiece[] | undefined + private _globalAdLibActions: RundownBaselineAdLibAction[] | undefined + private _globalAdLibs: RundownBaselineAdLibItem[] | undefined + private throttledSendStatusToAll: () => void + + constructor(logger: Logger) { + super(AdLibsTopic.name, logger) + this.throttledSendStatusToAll = _.throttle(this.sendStatusToAll.bind(this), THROTTLE_PERIOD_MS, { + leading: true, + trailing: true, + }) + } + + addSubscriber(ws: WebSocket): void { + super.addSubscriber(ws) + this.sendStatus([ws]) + } + + sendStatus(subscribers: Iterable): void { + const adLibs: AdLibStatus[] = [] + const globalAdLibs: AdLibStatus[] = [] + + if (this._adLibActions) { + adLibs.push( + ...this._adLibActions.map((action) => { + const sourceLayerName = this._sourceLayersMap.get( + (action.display as IBlueprintActionManifestDisplayContent).sourceLayerId + ) + const outputLayerName = this._outputLayersMap.get( + (action.display as IBlueprintActionManifestDisplayContent).outputLayerId + ) + const triggerModes = action.triggerModes + ? action.triggerModes.map((t) => + literal({ + name: t.data, + label: interpollateTranslation(t.display.label.key, t.display.label.args), + }) + ) + : [] + return literal({ + id: unprotectString(action._id), + name: interpollateTranslation(action.display.label.key, action.display.label.args), + sourceLayer: sourceLayerName ?? 'invalid', + outputLayer: outputLayerName ?? 'invalid', + actionType: triggerModes, + tags: action.display.tags, + }) + }) + ) + } + + if (this._abLibs) { + adLibs.push( + ...this._abLibs.map((adLib) => { + const sourceLayerName = this._sourceLayersMap.get(adLib.sourceLayerId) + const outputLayerName = this._outputLayersMap.get(adLib.outputLayerId) + return literal({ + id: unprotectString(adLib._id), + name: adLib.name, + sourceLayer: sourceLayerName ?? 'invalid', + outputLayer: outputLayerName ?? 'invalid', + actionType: [], + tags: adLib.tags, + }) + }) + ) + } + + if (this._globalAdLibActions) { + globalAdLibs.push( + ...this._globalAdLibActions.map((action) => { + const sourceLayerName = this._sourceLayersMap.get( + (action.display as IBlueprintActionManifestDisplayContent).sourceLayerId + ) + const outputLayerName = this._outputLayersMap.get( + (action.display as IBlueprintActionManifestDisplayContent).outputLayerId + ) + const triggerModes = action.triggerModes + ? action.triggerModes.map((t) => + literal({ + name: t.data, + label: interpollateTranslation(t.display.label.key, t.display.label.args), + }) + ) + : [] + return literal({ + id: unprotectString(action._id), + name: interpollateTranslation(action.display.label.key, action.display.label.args), + sourceLayer: sourceLayerName ?? 'invalid', + outputLayer: outputLayerName ?? 'invalid', + actionType: triggerModes, + tags: action.display.tags, + }) + }) + ) + } + + if (this._globalAdLibs) { + globalAdLibs.push( + ...this._globalAdLibs.map((adLib) => { + const sourceLayerName = this._sourceLayersMap.get(adLib.sourceLayerId) + const outputLayerName = this._outputLayersMap.get(adLib.outputLayerId) + return literal({ + id: unprotectString(adLib._id), + name: adLib.name, + sourceLayer: sourceLayerName ?? 'invalid', + outputLayer: outputLayerName ?? 'invalid', + actionType: [], + tags: adLib.tags, + }) + }) + ) + } + + const adLibsStatus: AdLibsStatus = this._activePlaylist + ? { + event: 'adLibs', + rundownPlaylistId: unprotectString(this._activePlaylist._id), + adLibs, + globalAdLibs, + } + : { event: 'adLibs', rundownPlaylistId: null, adLibs: [], globalAdLibs: [] } + + for (const subscriber of subscribers) { + this.sendMessage(subscriber, adLibsStatus) + } + } + + async update( + source: string, + data: + | DBRundownPlaylist + | DBShowStyleBase + | AdLibAction[] + | RundownBaselineAdLibAction[] + | AdLibPiece[] + | RundownBaselineAdLibItem[] + | undefined + ): Promise { + switch (source) { + case PlaylistHandler.name: { + const previousPlaylistId = this._activePlaylist?._id + this._activePlaylist = data as DBRundownPlaylist | undefined + this._logger.info(`${this._name} received playlist update from ${source}`) + if (previousPlaylistId === this._activePlaylist?._id) return + break + } + case AdLibActionsHandler.name: { + const adLibActions = data ? (data as AdLibAction[]) : [] + this._logger.info(`${this._name} received adLibActions update from ${source}`) + this._adLibActions = adLibActions + break + } + case GlobalAdLibActionsHandler.name: { + const globalAdLibActions = data ? (data as RundownBaselineAdLibAction[]) : [] + this._logger.info(`${this._name} received globalAdLibActions update from ${source}`) + this._globalAdLibActions = globalAdLibActions + break + } + case AdLibsHandler.name: { + const adLibs = data ? (data as AdLibPiece[]) : [] + this._logger.info(`${this._name} received adLibs update from ${source}`) + this._abLibs = adLibs + break + } + case GlobalAdLibsHandler.name: { + const globalAdLibs = data ? (data as RundownBaselineAdLibItem[]) : [] + this._logger.info(`${this._name} received globalAdLibs update from ${source}`) + this._globalAdLibs = globalAdLibs + break + } + case ShowStyleBaseHandler.name: { + const sourceLayers: SourceLayers = data + ? applyAndValidateOverrides((data as DBShowStyleBase).sourceLayersWithOverrides).obj + : {} + const outputLayers: OutputLayers = data + ? applyAndValidateOverrides((data as DBShowStyleBase).outputLayersWithOverrides).obj + : {} + this._logger.info( + `${this._name} received showStyleBase update with sourceLayers [${Object.values< + ISourceLayer | undefined + >(sourceLayers).map( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (s) => s!.name + )}]` + ) + this._logger.info( + `${this._name} received showStyleBase update with outputLayers [${Object.values< + IOutputLayer | undefined + >(outputLayers).map( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (s) => s!.name + )}]` + ) + this._sourceLayersMap.clear() + this._outputLayersMap.clear() + for (const [layerId, sourceLayer] of Object.entries(sourceLayers)) { + if (sourceLayer === undefined || sourceLayer === null) continue + this._sourceLayersMap.set(layerId, sourceLayer.name) + } + for (const [layerId, outputLayer] of Object.entries(outputLayers)) { + if (outputLayer === undefined || outputLayer === null) continue + this._outputLayersMap.set(layerId, outputLayer.name) + } + break + } + default: + throw new Error(`${this._name} received unsupported update from ${source}}`) + } + + this.throttledSendStatusToAll() + } + + private sendStatusToAll() { + this.sendStatus(this._subscribers) + } +} diff --git a/packages/live-status-gateway/src/topics/root.ts b/packages/live-status-gateway/src/topics/root.ts index 42e6c70ad9..698478e86c 100644 --- a/packages/live-status-gateway/src/topics/root.ts +++ b/packages/live-status-gateway/src/topics/root.ts @@ -35,6 +35,7 @@ export enum StatusChannels { studio = 'studio', activePlaylist = 'activePlaylist', segments = 'segments', + adLibs = 'adLibs', } interface RootMsg { From 876de7638ca4b4c5bc827a822042df01796bd144 Mon Sep 17 00:00:00 2001 From: ianshade Date: Thu, 9 Nov 2023 11:45:17 +0100 Subject: [PATCH 138/479] wip(live-status-gw): piece status --- .../api/schemas/activePlaylist.yaml | 50 ++++++++++++++++++- .../src/topics/activePlaylistTopic.ts | 19 +++++-- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/packages/live-status-gateway/api/schemas/activePlaylist.yaml b/packages/live-status-gateway/api/schemas/activePlaylist.yaml index efdaa6dea8..8f65cf4999 100644 --- a/packages/live-status-gateway/api/schemas/activePlaylist.yaml +++ b/packages/live-status-gateway/api/schemas/activePlaylist.yaml @@ -27,7 +27,12 @@ $defs: nextPart: description: The next Part - if empty, no part will follow live part $ref: '#/$defs/part' - required: [event, id, name, rundownIds, currentPart, currentSegment, nextPart] + activePieces: + description: Pieces that are currently active (live) + type: array + items: + $ref: '#/$defs/piece' + required: [event, id, name, rundownIds, currentPart, currentSegment, nextPart, activePieces] additionalProperties: false examples: - event: activePlaylist @@ -40,6 +45,8 @@ $defs: $ref: '#/$defs/currentSegment/examples/0' nextPart: $ref: '#/$defs/part/examples/0' + activePieces: + - $ref: '#/$defs/piece/examples/0' partBase: type: object properties: @@ -56,13 +63,20 @@ $defs: description: Should this part progress to the next automatically type: boolean default: false - required: [id, name, segmentId] + pieces: + description: All pieces in this part + type: array + items: + $ref: '#/$defs/piece' + required: [id, name, segmentId, pieces] additionalProperties: false examples: - id: 'H5CBGYjThrMSmaYvRaa5FVKJIzk_' name: 'Intro' segmentId: 'n1mOVd5_K5tt4sfk6HYfTuwumGQ_' autoNext: false + pieces: + - $ref: '#/$defs/piece/examples/0' part: oneOf: - $ref: '#/$defs/partBase' @@ -72,6 +86,8 @@ $defs: name: 'Intro' segmentId: 'n1mOVd5_K5tt4sfk6HYfTuwumGQ_' autoNext: false + pieces: + - $ref: '#/$defs/piece/examples/0' currentPart: oneOf: - allOf: @@ -103,6 +119,8 @@ $defs: startTime: 1600000060000 expectedDurationMs: 15000 expectedEndTime: 1600000075000 + pieces: + - $ref: '#/$defs/piece/examples/0' currentSegment: type: object properties: @@ -131,3 +149,31 @@ $defs: expectedDurationMs: 15000 budgetDurationMs: 20000 expectedEndTime: 1600000075000 + piece: + type: object + properties: + id: + description: Unique id of the Piece + type: string + name: + description: User-facing name of the Piece + type: string + sourceLayer: + description: The source layer name for this Piece + type: string + outputLayer: + description: The output layer name for this Piece + type: string + tags: + description: Tags attached to this Piece + type: array + items: + type: string + required: [id, name, sourceLayer, outputLayer] + additionalProperties: false + examples: + - id: 'H5CBGYjThrMSmaYvRaa5FVKJIzk_' + name: 'Camera 1' + sourceLayer: 'Camera' + outputLayer: 'PGM' + tags: ['camera'] diff --git a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts index eec3e4943f..e912b92bfb 100644 --- a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts +++ b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts @@ -25,6 +25,8 @@ const THROTTLE_PERIOD_MS = 100 interface PieceStatus { id: string name: string + sourceLayer: string + outputLayer: string tags?: string[] } @@ -123,7 +125,10 @@ export class ActivePlaylistTopic this._currentPartInstance, this._partInstancesInCurrentSegment ), - pieces: this._pieceInstances?.currentPartInstance.map(this.toPieceStatus) ?? [], + pieces: + this._pieceInstances?.currentPartInstance.map((piece) => + this.toPieceStatus(piece) + ) ?? [], }) : null, currentSegment: @@ -144,10 +149,12 @@ export class ActivePlaylistTopic name: nextPart.title, autoNext: nextPart.autoNext, segmentId: unprotectString(nextPart.segmentId), - pieces: this._pieceInstances?.nextPartInstance.map(this.toPieceStatus) ?? [], + pieces: + this._pieceInstances?.nextPartInstance.map((piece) => this.toPieceStatus(piece)) ?? + [], }) : null, - activePieces: this._pieceInstances?.active.map(this.toPieceStatus) ?? [], + activePieces: this._pieceInstances?.active.map((piece) => this.toPieceStatus(piece)) ?? [], }) : literal({ event: 'activePlaylist', @@ -252,10 +259,14 @@ export class ActivePlaylistTopic this.sendStatus(this._subscribers) } - private toPieceStatus(this: void, pieceInstance: PieceInstance): PieceStatus { + private toPieceStatus(pieceInstance: PieceInstance): PieceStatus { + const sourceLayerName = this._sourceLayersMap.get(pieceInstance.piece.sourceLayerId) + const outputLayerName = this._outputLayersMap.get(pieceInstance.piece.outputLayerId) return { id: unprotectString(pieceInstance._id), name: pieceInstance.piece.name, + sourceLayer: sourceLayerName ?? 'invalid', + outputLayer: outputLayerName ?? 'invalid', tags: pieceInstance.piece.tags, } } From d5df6f35f7cb7e23506ca49c0ddf134a3a27533d Mon Sep 17 00:00:00 2001 From: ianshade Date: Thu, 9 Nov 2023 14:08:42 +0100 Subject: [PATCH 139/479] chore(live-status-gateway): update tests --- .../topics/__tests__/activePlaylist.spec.ts | 185 +++++++++--------- .../src/topics/__tests__/adLibs.spec.ts | 104 ++++++++++ .../src/topics/__tests__/utils.ts | 18 ++ 3 files changed, 218 insertions(+), 89 deletions(-) create mode 100644 packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts diff --git a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts index 569db0fe6d..1e2d48cd76 100644 --- a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts @@ -1,35 +1,19 @@ import { ActivePlaylistStatus, ActivePlaylistTopic } from '../activePlaylistTopic' -import { makeMockLogger, makeMockSubscriber, makeTestPlaylist } from './utils' +import { makeMockLogger, makeMockSubscriber, makeTestPlaylist, makeTestShowStyleBase } from './utils' import { PlaylistHandler } from '../../collections/playlistHandler' import { ShowStyleBaseHandler } from '../../collections/showStyleBaseHandler' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' -import { SourceLayerType } from '@sofie-automation/blueprints-integration/dist' import { PartInstancesHandler, SelectedPartInstances } from '../../collections/partInstancesHandler' -import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' import { protectString, unprotectString, unprotectStringArray } from '@sofie-automation/server-core-integration/dist' -import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' -import { AdLibActionsHandler } from '../../collections/adLibActionsHandler' -import { GlobalAdLibActionsHandler } from '../../collections/globalAdLibActionsHandler' import { PartialDeep } from 'type-fest' import { literal } from '@sofie-automation/corelib/dist/lib' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { PartsHandler } from '../../collections/partsHandler' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import { PieceInstancesHandler, SelectedPieceInstances } from '../../collections/pieceInstancesHandler' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -function makeTestShowStyleBase(): Pick { - return { - sourceLayersWithOverrides: { - defaults: { layer0: { _id: 'layer0', name: 'Layer 0', _rank: 0, type: SourceLayerType.VT } }, - overrides: [], - }, - outputLayersWithOverrides: { - defaults: { pgm: { _id: 'pgm', name: 'PGM', _rank: 0, isPGM: true } }, - overrides: [], - }, - } -} - -function makeTestPartInstanceMap(): SelectedPartInstances { +function makeEmptyTestPartInstances(): SelectedPartInstances { return { current: undefined, firstInSegmentPlayout: undefined, @@ -38,45 +22,30 @@ function makeTestPartInstanceMap(): SelectedPartInstances { } } -function makeTestAdLibActions(): AdLibAction[] { - return [ - { - _id: protectString('ACTION_0'), - actionId: 'ACTION_0', - partId: protectString('part0'), - rundownId: protectString('RUNDOWN_0'), - display: { - content: {}, - label: { key: 'An Action' }, - sourceLayerId: 'layer0', - outputLayerId: 'pgm', - tags: ['adlib_tag'], - }, - externalId: 'NCS_ACTION_0', - userData: {}, - userDataManifest: {}, - }, - ] -} - -function makeTestGlobalAdLibActions(): RundownBaselineAdLibAction[] { - return [ - { - _id: protectString('GLOBAL_ACTION_0'), - actionId: 'GLOBAL_ACTION_0', - rundownId: protectString('RUNDOWN_0'), - display: { - content: {}, - label: { key: 'A Global Action' }, - sourceLayerId: 'layer0', - outputLayerId: 'pgm', - tags: ['global_adlib_tag'], - }, - externalId: 'NCS_GLOBAL_ACTION_0', - userData: {}, - userDataManifest: {}, +function makeTestPartInstances(currentPartInstanceId: string): PartialDeep { + const part1: Partial = { + _id: protectString('PART_1'), + title: 'Test Part', + segmentId: protectString('SEGMENT_1'), + expectedDurationWithPreroll: 10000, + expectedDuration: 10000, + } + const testPartInstances: PartialDeep = { + current: { + _id: currentPartInstanceId, + part: part1, + timings: { plannedStartedPlayback: 1600000060000 }, }, - ] + firstInSegmentPlayout: {}, + inCurrentSegment: [ + literal>({ + _id: protectString(currentPartInstanceId), + part: part1, + timings: { plannedStartedPlayback: 1600000060000 }, + }), + ] as DBPartInstance[], + } + return testPartInstances } describe('ActivePlaylistTopic', () => { @@ -91,47 +60,20 @@ describe('ActivePlaylistTopic', () => { const testShowStyleBase = makeTestShowStyleBase() await topic.update(ShowStyleBaseHandler.name, testShowStyleBase as DBShowStyleBase) - const testPartInstancesMap = makeTestPartInstanceMap() + const testPartInstancesMap = makeEmptyTestPartInstances() await topic.update(PartInstancesHandler.name, testPartInstancesMap) - const testAdLibActions = makeTestAdLibActions() - await topic.update(AdLibActionsHandler.name, testAdLibActions) - - const testGlobalAdLibActions = makeTestGlobalAdLibActions() - await topic.update(GlobalAdLibActionsHandler.name, testGlobalAdLibActions) - - // TODO: AdLibPieces and Global AdLibPieces - topic.addSubscriber(mockSubscriber) const expectedStatus: ActivePlaylistStatus = { event: 'activePlaylist', name: playlist.name, id: unprotectString(playlist._id), - adLibs: [ - { - actionType: [], - id: 'ACTION_0', - name: 'An Action', - outputLayer: 'PGM', - sourceLayer: 'Layer 0', - tags: ['adlib_tag'], - }, - ], currentPart: null, nextPart: null, currentSegment: null, - globalAdLibs: [ - { - actionType: [], - id: 'GLOBAL_ACTION_0', - name: 'A Global Action', - outputLayer: 'PGM', - sourceLayer: 'Layer 0', - tags: ['global_adlib_tag'], - }, - ], rundownIds: unprotectStringArray(playlist.rundownIdsInOrder), + activePieces: [], } // eslint-disable-next-line @typescript-eslint/unbound-method @@ -189,12 +131,12 @@ describe('ActivePlaylistTopic', () => { event: 'activePlaylist', name: playlist.name, id: unprotectString(playlist._id), - adLibs: [], currentPart: { id: 'PART_1', name: 'Test Part', segmentId: 'SEGMENT_1', timing: { startTime: 1600000060000, expectedDurationMs: 10000, expectedEndTime: 1600000070000 }, + pieces: [], }, nextPart: null, currentSegment: { @@ -204,8 +146,73 @@ describe('ActivePlaylistTopic', () => { expectedEndTime: 1600000070000, }, }, - globalAdLibs: [], rundownIds: unprotectStringArray(playlist.rundownIdsInOrder), + activePieces: [], + } + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockSubscriber.send).toHaveBeenCalledTimes(1) + expect(JSON.parse(mockSubscriber.send.mock.calls[0][0] as string)).toMatchObject(expectedStatus) + }) + + it('provides active pieces', async () => { + const topic = new ActivePlaylistTopic(makeMockLogger()) + const mockSubscriber = makeMockSubscriber() + + const currentPartInstanceId = 'CURRENT_PART_INSTANCE_ID' + + const playlist = makeTestPlaylist() + playlist.activationId = protectString('somethingRandom') + playlist.currentPartInfo = { + consumesQueuedSegmentId: false, + manuallySelected: false, + partInstanceId: protectString(currentPartInstanceId), + rundownId: playlist.rundownIdsInOrder[0], + } + await topic.update(PlaylistHandler.name, playlist) + + const testPartInstances = makeTestPartInstances(currentPartInstanceId) + await topic.update(PartInstancesHandler.name, testPartInstances as SelectedPartInstances) + + const testShowStyleBase = makeTestShowStyleBase() + await topic.update(ShowStyleBaseHandler.name, testShowStyleBase as DBShowStyleBase) + + const testPieceInstances: PartialDeep = { + currentPartInstance: [], + nextPartInstance: [], + active: [ + literal>({ + _id: protectString('PIECE_1'), + piece: { + name: 'Piece 1', + outputLayerId: 'pgm', + sourceLayerId: 'layer0', + tags: ['my_tag'], + }, + }), + ] as PieceInstance[], + } + await topic.update(PieceInstancesHandler.name, testPieceInstances as SelectedPieceInstances) + + await topic.update(PartsHandler.name, [testPartInstances.current!.part] as DBPart[]) + + topic.addSubscriber(mockSubscriber) + + const expectedStatus: Partial = { + event: 'activePlaylist', + name: playlist.name, + id: unprotectString(playlist._id), + nextPart: null, + rundownIds: unprotectStringArray(playlist.rundownIdsInOrder), + activePieces: [ + { + id: 'PIECE_1', + name: 'Piece 1', + sourceLayer: 'Layer 0', + outputLayer: 'PGM', + tags: ['my_tag'], + }, + ], } // eslint-disable-next-line @typescript-eslint/unbound-method diff --git a/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts b/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts new file mode 100644 index 0000000000..2fb5a406d5 --- /dev/null +++ b/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts @@ -0,0 +1,104 @@ +import { protectString, unprotectString } from '@sofie-automation/server-core-integration' +import { makeMockLogger, makeMockSubscriber, makeTestPlaylist, makeTestShowStyleBase } from './utils' +import { AdLibsStatus, AdLibsTopic } from '../adLibsTopic' +import { PlaylistHandler } from '../../collections/playlistHandler' +import { ShowStyleBaseHandler } from '../../collections/showStyleBaseHandler' +import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' +import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' +import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' +import { AdLibActionsHandler } from '../../collections/adLibActionsHandler' +import { GlobalAdLibActionsHandler } from '../../collections/globalAdLibActionsHandler' + +function makeTestAdLibActions(): AdLibAction[] { + return [ + { + _id: protectString('ACTION_0'), + actionId: 'ACTION_0', + partId: protectString('part0'), + rundownId: protectString('RUNDOWN_0'), + display: { + content: {}, + label: { key: 'An Action' }, + sourceLayerId: 'layer0', + outputLayerId: 'pgm', + tags: ['adlib_tag'], + }, + externalId: 'NCS_ACTION_0', + userData: {}, + userDataManifest: {}, + }, + ] +} + +function makeTestGlobalAdLibActions(): RundownBaselineAdLibAction[] { + return [ + { + _id: protectString('GLOBAL_ACTION_0'), + actionId: 'GLOBAL_ACTION_0', + rundownId: protectString('RUNDOWN_0'), + display: { + content: {}, + label: { key: 'A Global Action' }, + sourceLayerId: 'layer0', + outputLayerId: 'pgm', + tags: ['global_adlib_tag'], + }, + externalId: 'NCS_GLOBAL_ACTION_0', + userData: {}, + userDataManifest: {}, + }, + ] +} + +describe('ActivePlaylistTopic', () => { + it('notifies subscribers', async () => { + const topic = new AdLibsTopic(makeMockLogger()) + const mockSubscriber = makeMockSubscriber() + + const playlist = makeTestPlaylist() + playlist.activationId = protectString('somethingRandom') + await topic.update(PlaylistHandler.name, playlist) + + const testShowStyleBase = makeTestShowStyleBase() + await topic.update(ShowStyleBaseHandler.name, testShowStyleBase as DBShowStyleBase) + + const testAdLibActions = makeTestAdLibActions() + await topic.update(AdLibActionsHandler.name, testAdLibActions) + + const testGlobalAdLibActions = makeTestGlobalAdLibActions() + await topic.update(GlobalAdLibActionsHandler.name, testGlobalAdLibActions) + + // TODO: AdLibPieces and Global AdLibPieces + + topic.addSubscriber(mockSubscriber) + + const expectedStatus: AdLibsStatus = { + event: 'adLibs', + rundownPlaylistId: unprotectString(playlist._id), + adLibs: [ + { + actionType: [], + id: 'ACTION_0', + name: 'An Action', + outputLayer: 'PGM', + sourceLayer: 'Layer 0', + tags: ['adlib_tag'], + }, + ], + globalAdLibs: [ + { + actionType: [], + id: 'GLOBAL_ACTION_0', + name: 'A Global Action', + outputLayer: 'PGM', + sourceLayer: 'Layer 0', + tags: ['global_adlib_tag'], + }, + ], + } + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockSubscriber.send).toHaveBeenCalledTimes(1) + expect(JSON.parse(mockSubscriber.send.mock.calls[0][0] as string)).toMatchObject(expectedStatus) + }) +}) diff --git a/packages/live-status-gateway/src/topics/__tests__/utils.ts b/packages/live-status-gateway/src/topics/__tests__/utils.ts index c091820a31..b1fb414be5 100644 --- a/packages/live-status-gateway/src/topics/__tests__/utils.ts +++ b/packages/live-status-gateway/src/topics/__tests__/utils.ts @@ -1,5 +1,7 @@ +import { SourceLayerType } from '@sofie-automation/blueprints-integration' import { PlaylistTimingType } from '@sofie-automation/blueprints-integration/dist/documents/playlistTiming' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { mock, MockProxy } from 'jest-mock-extended' import { Logger } from 'winston' @@ -32,3 +34,19 @@ export function makeTestPlaylist(id?: string): DBRundownPlaylist { timing: { type: PlaylistTimingType.None }, } } + +export function makeTestShowStyleBase(): Pick< + DBShowStyleBase, + 'sourceLayersWithOverrides' | 'outputLayersWithOverrides' +> { + return { + sourceLayersWithOverrides: { + defaults: { layer0: { _id: 'layer0', name: 'Layer 0', _rank: 0, type: SourceLayerType.VT } }, + overrides: [], + }, + outputLayersWithOverrides: { + defaults: { pgm: { _id: 'pgm', name: 'PGM', _rank: 0, isPGM: true } }, + overrides: [], + }, + } +} From 89f155c4ae903920af91cbc586c34eb222946623 Mon Sep 17 00:00:00 2001 From: ianshade Date: Fri, 10 Nov 2023 09:47:58 +0100 Subject: [PATCH 140/479] fix(live-status-gw): active piece conditions --- .../src/collections/pieceInstancesHandler.ts | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts index 7d22e25c13..760154c379 100644 --- a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts +++ b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts @@ -54,14 +54,7 @@ export class PieceInstancesHandler const collection = this._core.getCollection(this._collectionName) if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) const active = this._currentPlaylist?.currentPartInfo?.partInstanceId - ? collection.find( - (pieceInstance: PieceInstance) => - (pieceInstance.partInstanceId === this._currentPlaylist?.previousPartInfo?.partInstanceId || - pieceInstance.partInstanceId === this._currentPlaylist?.currentPartInfo?.partInstanceId) && - (pieceInstance.reportedStartedPlayback != null || - pieceInstance.piece.enable.start === 0 || - pieceInstance.infinite?.fromPreviousPart) - ) + ? collection.find((pieceInstance: PieceInstance) => this.isPieceInstanceActive(pieceInstance)) : [] const inCurrentPartInstance = this._currentPlaylist?.currentPartInfo?.partInstanceId ? collection.find({ partInstanceId: this._currentPlaylist.currentPartInfo.partInstanceId }) @@ -126,7 +119,7 @@ export class PieceInstancesHandler this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, { partInstanceId: { $in: this._partInstanceIds }, playlistActivationId: this._activationId, - reportedStartedPlayback: { $exists: false }, + reportedStoppedPlayback: { $exists: false }, }) this._dbObserver = this._coreHandler.setupObserver(this._collectionName) this._dbObserver.added = (id: string) => { @@ -157,4 +150,15 @@ export class PieceInstancesHandler await this.notify(this._collectionData) } } + + private isPieceInstanceActive(pieceInstance: PieceInstance) { + return ( + (pieceInstance.partInstanceId === this._currentPlaylist?.previousPartInfo?.partInstanceId || // a piece from previous part instance may be active during transition + pieceInstance.partInstanceId === this._currentPlaylist?.currentPartInfo?.partInstanceId) && + (pieceInstance.reportedStartedPlayback != null || // has been reported to have started by the Playout Gateway + (pieceInstance.partInstanceId === this._currentPlaylist?.currentPartInfo?.partInstanceId && + pieceInstance.piece.enable.start === 0) || // this is to speed things up immediately after a part instance is taken when not yet reported by the Playout Gateway + pieceInstance.infinite?.fromPreviousPart) // infinites from previous part also are on air from the start of the current part + ) + } } From 0c3c1bfd2bb779034976dc34e49aa6e664ea874b Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 10 Nov 2023 11:44:56 +0100 Subject: [PATCH 141/479] feat: typed publications in gateways/peripheraldevices SOFIE-1183 (#1056) --- .../lib/ConnectionStatusNotification.tsx | 4 +- meteor/client/lib/MeteorReactComponent.ts | 7 +- .../lib/ReactMeteorData/ReactMeteorData.tsx | 11 +- .../lib/reactiveData/reactiveDataHelper.ts | 4 +- .../client/lib/triggers/TriggersHandler.tsx | 19 +- meteor/client/ui/Account/OrganizationPage.tsx | 4 +- meteor/client/ui/ActiveRundownView.tsx | 7 +- meteor/client/ui/App.tsx | 6 +- .../client/ui/ClipTrimPanel/ClipTrimPanel.tsx | 4 +- .../ui/ClockView/CameraScreen/Rundown.tsx | 4 +- .../ui/ClockView/CameraScreen/index.tsx | 17 +- meteor/client/ui/ClockView/ClockView.tsx | 4 +- .../ui/ClockView/OverlayScreenSaver.tsx | 7 +- .../client/ui/ClockView/PresenterScreen.tsx | 23 +- meteor/client/ui/MediaStatus/MediaStatus.tsx | 29 +- .../client/ui/PieceIcons/PieceCountdown.tsx | 7 +- meteor/client/ui/PieceIcons/PieceIcon.tsx | 7 +- meteor/client/ui/PieceIcons/PieceName.tsx | 7 +- meteor/client/ui/Prompter/PrompterView.tsx | 23 +- meteor/client/ui/RundownList.tsx | 15 +- meteor/client/ui/RundownView.tsx | 41 +-- .../client/ui/RundownView/RundownNotifier.tsx | 7 +- .../ui/RundownView/RundownSystemStatus.tsx | 4 +- .../LinePartAdLibIndicator.tsx | 6 +- .../ui/SegmentList/SegmentListContainer.tsx | 9 +- .../SegmentScratchpadContainer.tsx | 9 +- .../SegmentStoryboardContainer.tsx | 9 +- .../SegmentTimelineContainer.tsx | 9 +- meteor/client/ui/Settings.tsx | 12 +- .../ui/Settings/RundownLayoutEditor.tsx | 4 +- meteor/client/ui/Settings/SettingsMenu.tsx | 13 +- .../BlueprintConfiguration/index.tsx | 4 +- meteor/client/ui/Settings/SnapshotsView.tsx | 7 +- .../Studio/BlueprintConfiguration/index.tsx | 4 +- .../client/ui/Settings/SystemManagement.tsx | 4 +- meteor/client/ui/Settings/Upgrades/View.tsx | 4 +- .../TriggeredActionsEditor.tsx | 13 +- .../triggerEditors/DeviceEditor.tsx | 4 +- meteor/client/ui/Shelf/BucketPanel.tsx | 15 +- meteor/client/ui/Shelf/DashboardPanel.tsx | 4 +- meteor/client/ui/Status.tsx | 11 +- meteor/client/ui/Status/Evaluations.tsx | 4 +- meteor/client/ui/Status/ExternalMessages.tsx | 7 +- meteor/client/ui/Status/MediaManager.tsx | 6 +- meteor/client/ui/Status/SystemStatus.tsx | 4 +- meteor/client/ui/Status/UserActivity.tsx | 4 +- .../client/ui/Status/media-status/index.tsx | 4 +- .../client/ui/Status/package-status/index.tsx | 10 +- .../StudioScreenSaver/StudioScreenSaver.tsx | 7 +- meteor/client/ui/TestTools/DeviceTriggers.tsx | 21 +- meteor/client/ui/TestTools/Mappings.tsx | 11 +- meteor/client/ui/TestTools/Timeline.tsx | 11 +- .../client/ui/TestTools/TimelineDatastore.tsx | 4 +- meteor/client/ui/TestTools/index.tsx | 9 +- meteor/client/ui/i18n.ts | 4 +- meteor/lib/api/__tests__/pubsub.test.ts | 24 +- meteor/lib/api/pubsub.ts | 314 ++++++------------ meteor/lib/collections/lib.ts | 33 +- meteor/server/api/rest/v0/index.ts | 18 +- .../server/lib/customPublication/publish.ts | 18 +- meteor/server/publications/_publications.ts | 16 + .../blueprintUpgradeStatus/publication.ts | 6 +- meteor/server/publications/buckets.ts | 32 +- .../publications/deviceTriggersPreview.ts | 8 +- meteor/server/publications/lib.ts | 14 +- meteor/server/publications/mountedTriggers.ts | 13 +- meteor/server/publications/organization.ts | 50 +-- .../expectedPackages/publication.ts | 11 +- .../packageManager/packageContainers.ts | 11 +- .../packageManager/playoutContext.ts | 11 +- .../server/publications/peripheralDevice.ts | 27 +- .../publications/peripheralDeviceForDevice.ts | 11 +- .../bucket/publication.ts | 6 +- .../rundown/publication.ts | 6 +- meteor/server/publications/rundown.ts | 123 ++++--- meteor/server/publications/rundownPlaylist.ts | 4 +- .../segmentPartNotesUI/publication.ts | 6 +- meteor/server/publications/showStyle.ts | 11 +- meteor/server/publications/showStyleUI.ts | 6 +- meteor/server/publications/studio.ts | 27 +- meteor/server/publications/studioUI.ts | 38 ++- meteor/server/publications/system.ts | 39 ++- meteor/server/publications/timeline.ts | 42 ++- .../publications/translationsBundles.ts | 4 +- .../server/publications/triggeredActionsUI.ts | 6 +- .../src/documents/expectedPlayoutItem.ts | 14 +- packages/corelib/src/pubsub.ts | 212 ++++++++++++ .../src/collections/adLibActions.ts | 24 +- .../src/collections/adLibs.ts | 24 +- .../src/collections/globalAdLibActions.ts | 28 +- .../src/collections/globalAdLibs.ts | 28 +- .../src/collections/part.ts | 28 +- .../src/collections/partInstances.ts | 34 +- .../src/collections/playlist.ts | 33 +- .../src/collections/rundownHandler.ts | 23 +- .../src/collections/rundownsHandler.ts | 5 +- .../src/collections/segmentHandler.ts | 18 +- .../src/collections/segmentsHandler.ts | 5 +- .../src/collections/showStyleBase.ts | 24 +- .../src/collections/studio.ts | 27 +- .../live-status-gateway/src/coreHandler.ts | 74 +++-- packages/live-status-gateway/src/wsHandler.ts | 34 +- .../mos-gateway/src/CoreMosDeviceHandler.ts | 7 +- packages/mos-gateway/src/coreHandler.ts | 40 ++- packages/mos-gateway/src/mosHandler.ts | 13 +- packages/playout-gateway/src/coreHandler.ts | 105 ++---- packages/playout-gateway/src/tsrHandler.ts | 81 +++-- packages/server-core-integration/README.md | 6 +- .../examples/client.ts | 6 +- .../src/__tests__/index.spec.ts | 14 +- packages/server-core-integration/src/index.ts | 1 + .../src/integrationTests/index.spec.ts | 15 +- .../src/lib/CoreConnectionChild.ts | 38 ++- .../src/lib/coreConnection.ts | 49 ++- .../src/lib/ddpClient.ts | 38 +-- .../src/lib/subscriptions.ts | 28 +- .../shared-lib/src/expectedPlayoutItem.ts | 24 ++ .../shared-lib/src/pubsub/peripheralDevice.ts | 139 ++++++++ 118 files changed, 1595 insertions(+), 1087 deletions(-) create mode 100644 packages/corelib/src/pubsub.ts create mode 100644 packages/shared-lib/src/expectedPlayoutItem.ts create mode 100644 packages/shared-lib/src/pubsub/peripheralDevice.ts diff --git a/meteor/client/lib/ConnectionStatusNotification.tsx b/meteor/client/lib/ConnectionStatusNotification.tsx index 0850522e35..1f21207093 100644 --- a/meteor/client/lib/ConnectionStatusNotification.tsx +++ b/meteor/client/lib/ConnectionStatusNotification.tsx @@ -16,7 +16,7 @@ import { import { WithManagedTracker } from './reactiveData/reactiveDataHelper' import { withTranslation } from 'react-i18next' import { NotificationCenterPopUps } from './notifications/NotificationCenterPanel' -import { PubSub } from '../../lib/api/pubsub' +import { MeteorPubSub } from '../../lib/api/pubsub' import { ICoreSystem, ServiceMessage, Criticality } from '../../lib/collections/CoreSystem' import { TFunction } from 'react-i18next' import { getRandomId } from '@sofie-automation/corelib/dist/lib' @@ -31,7 +31,7 @@ export class ConnectionStatusNotifier extends WithManagedTracker { constructor(t: TFunction) { super() - this.subscribe(PubSub.coreSystem) + this.subscribe(MeteorPubSub.coreSystem) this._translator = t diff --git a/meteor/client/lib/MeteorReactComponent.ts b/meteor/client/lib/MeteorReactComponent.ts index 5f4895cd70..82c2a0fb5b 100644 --- a/meteor/client/lib/MeteorReactComponent.ts +++ b/meteor/client/lib/MeteorReactComponent.ts @@ -2,7 +2,7 @@ import { Tracker } from 'meteor/tracker' import * as React from 'react' import { stringifyObjects } from '../../lib/lib' import { Meteor } from 'meteor/meteor' -import { PubSubTypes } from '../../lib/api/pubsub' +import { AllPubSubTypes } from '../../lib/api/pubsub' import { catchError } from './lib' export class MeteorReactComponent extends React.Component { private _subscriptions: { [id: string]: Meteor.SubscriptionHandle } = {} @@ -14,7 +14,10 @@ export class MeteorReactComponent extends React.Component(name: K, ...args: Parameters): Meteor.SubscriptionHandle { + subscribe( + name: K, + ...args: Parameters + ): Meteor.SubscriptionHandle { return Tracker.nonreactive(() => { // let id = name + '_' + JSON.stringify(args.join()) const id = name + '_' + stringifyObjects(args) diff --git a/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx b/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx index abc8924c63..55eb4d9d72 100644 --- a/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx +++ b/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx @@ -6,7 +6,7 @@ import { Mongo } from 'meteor/mongo' import { Tracker } from 'meteor/tracker' import { withTranslation, WithTranslation } from 'react-i18next' import { MeteorReactComponent } from '../MeteorReactComponent' -import { meteorSubscribe, PubSubTypes } from '../../../lib/api/pubsub' +import { meteorSubscribe, AllPubSubTypes } from '../../../lib/api/pubsub' import { stringifyObjects } from '../../../lib/lib' const globalTrackerQueue: Array = [] @@ -340,7 +340,10 @@ export function useTracker( * @param {...any[]} args A list of arugments for the subscription. This is used for optimizing the subscription across * renders so that it isn't torn down and created for every render. */ -export function useSubscription(sub: K, ...args: Parameters): boolean { +export function useSubscription( + sub: K, + ...args: Parameters +): boolean { const [ready, setReady] = useState(false) useEffect(() => { @@ -359,9 +362,9 @@ export function useSubscription(sub: K, ...args: Pa /** * Sets up multiple subscriptions of the same type, but with different arguments */ -export function useSubscriptions( +export function useSubscriptions( sub: K, - argsArray: Parameters[] + argsArray: Parameters[] ): boolean { const [ready, setReady] = useState(false) diff --git a/meteor/client/lib/reactiveData/reactiveDataHelper.ts b/meteor/client/lib/reactiveData/reactiveDataHelper.ts index 4215a65f74..965e5083d5 100644 --- a/meteor/client/lib/reactiveData/reactiveDataHelper.ts +++ b/meteor/client/lib/reactiveData/reactiveDataHelper.ts @@ -1,5 +1,5 @@ import { Tracker } from 'meteor/tracker' -import { meteorSubscribe, PubSubTypes } from '../../../lib/api/pubsub' +import { meteorSubscribe, AllPubSubTypes } from '../../../lib/api/pubsub' import { Meteor } from 'meteor/meteor' /** @@ -70,7 +70,7 @@ export abstract class WithManagedTracker { return this._subs.every((e) => e.ready()) } - protected subscribe(sub: K, ...args: Parameters): void { + protected subscribe(sub: K, ...args: Parameters): void { this._subs.push(meteorSubscribe(sub, ...args)) } diff --git a/meteor/client/lib/triggers/TriggersHandler.tsx b/meteor/client/lib/triggers/TriggersHandler.tsx index ac48e5bc40..de630f935e 100644 --- a/meteor/client/lib/triggers/TriggersHandler.tsx +++ b/meteor/client/lib/triggers/TriggersHandler.tsx @@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from 'react' import { TFunction } from 'i18next' import { useTranslation } from 'react-i18next' import Sorensen from '@sofie-automation/sorensen' -import { PubSub } from '../../../lib/api/pubsub' +import { MeteorPubSub } from '../../../lib/api/pubsub' import { useSubscription, useTracker } from '../ReactMeteorData/ReactMeteorData' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PlayoutActions, SomeAction, SomeBlueprintTrigger, TriggerType } from '@sofie-automation/blueprints-integration' @@ -43,6 +43,7 @@ import { isHotkeyTrigger } from '../../../lib/api/triggers/triggerTypeSelectors' import { RundownPlaylistCollectionUtil } from '../../../lib/collections/rundownPlaylistUtil' import { catchError } from '../lib' import { logger } from '../../../lib/logging' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' type HotkeyTriggerListener = (e: KeyboardEvent) => void @@ -72,32 +73,32 @@ function useSubscriptions( showStyleBaseId: ShowStyleBaseId ) { const allReady = [ - useSubscription(PubSub.rundownPlaylists, { + useSubscription(CorelibPubSub.rundownPlaylists, { _id: rundownPlaylistId, }), - useSubscription(PubSub.rundowns, [rundownPlaylistId], null), + useSubscription(CorelibPubSub.rundowns, [rundownPlaylistId], null), - useSubscription(PubSub.adLibActions, { + useSubscription(CorelibPubSub.adLibActions, { rundownId: { $in: rundownIds, }, }), - useSubscription(PubSub.adLibPieces, { + useSubscription(CorelibPubSub.adLibPieces, { rundownId: { $in: rundownIds, }, }), - useSubscription(PubSub.rundownBaselineAdLibActions, { + useSubscription(CorelibPubSub.rundownBaselineAdLibActions, { rundownId: { $in: rundownIds, }, }), - useSubscription(PubSub.rundownBaselineAdLibPieces, { + useSubscription(CorelibPubSub.rundownBaselineAdLibPieces, { rundownId: { $in: rundownIds, }, }), - useSubscription(PubSub.uiShowStyleBase, showStyleBaseId), + useSubscription(MeteorPubSub.uiShowStyleBase, showStyleBaseId), ] return !allReady.some((state) => state === false) @@ -371,7 +372,7 @@ export const TriggersHandler: React.FC = function TriggersHandler( JSON.stringify(props.nextSegmentPartIds), ]) - const triggerSubReady = useSubscription(PubSub.uiTriggeredActions, props.showStyleBaseId) + const triggerSubReady = useSubscription(MeteorPubSub.uiTriggeredActions, props.showStyleBaseId) const rundownIds = useTracker(() => { diff --git a/meteor/client/ui/Account/OrganizationPage.tsx b/meteor/client/ui/Account/OrganizationPage.tsx index 1c263cc071..b00c218b35 100644 --- a/meteor/client/ui/Account/OrganizationPage.tsx +++ b/meteor/client/ui/Account/OrganizationPage.tsx @@ -3,7 +3,7 @@ import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/reac import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { getUser, User, getUserRoles, DBUser } from '../../../lib/collections/Users' import { Spinner } from '../../lib/Spinner' -import { PubSub } from '../../../lib/api/pubsub' +import { MeteorPubSub } from '../../../lib/api/pubsub' import { DBOrganization, UserRoles } from '../../../lib/collections/Organization' import { unprotectString } from '../../../lib/lib' import { MeteorCall } from '../../../lib/api/methods' @@ -53,7 +53,7 @@ export const OrganizationPage = translateWithTracker(() => { componentDidMount(): void { this.autorun(() => { if (this.props.organization) { - this.subscribe(PubSub.usersInOrganization, { organizationId: this.props.organization._id }) + this.subscribe(MeteorPubSub.usersInOrganization, { organizationId: this.props.organization._id }) } }) } diff --git a/meteor/client/ui/ActiveRundownView.tsx b/meteor/client/ui/ActiveRundownView.tsx index 0c269d745a..fcc5785385 100644 --- a/meteor/client/ui/ActiveRundownView.tsx +++ b/meteor/client/ui/ActiveRundownView.tsx @@ -4,19 +4,20 @@ import { useSubscription, useTracker } from '../lib/ReactMeteorData/ReactMeteorD import { Spinner } from '../lib/Spinner' import { RundownView } from './RundownView' -import { PubSub } from '../../lib/api/pubsub' +import { MeteorPubSub } from '../../lib/api/pubsub' import { UIStudios } from './Collections' import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { RundownPlaylists } from '../collections' import { useTranslation } from 'react-i18next' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' export function ActiveRundownView({ studioId }: { studioId: StudioId }): JSX.Element | null { const { t } = useTranslation() const { path } = useRouteMatch() - const studioReady = useSubscription(PubSub.uiStudio, studioId) - const playlistReady = useSubscription(PubSub.rundownPlaylists, { + const studioReady = useSubscription(MeteorPubSub.uiStudio, studioId) + const playlistReady = useSubscription(CorelibPubSub.rundownPlaylists, { activationId: { $exists: true }, studioId, }) diff --git a/meteor/client/ui/App.tsx b/meteor/client/ui/App.tsx index 78877a92c8..e101c78d83 100644 --- a/meteor/client/ui/App.tsx +++ b/meteor/client/ui/App.tsx @@ -52,7 +52,7 @@ import { ResetPasswordPage } from './Account/NotLoggedIn/ResetPasswordPage' import { AccountPage } from './Account/AccountPage' import { OrganizationPage } from './Account/OrganizationPage' import { getUser, User } from '../../lib/collections/Users' -import { PubSub } from '../../lib/api/pubsub' +import { MeteorPubSub } from '../../lib/api/pubsub' import { useTracker, useSubscription } from '../lib/ReactMeteorData/ReactMeteorData' import { DocumentTitleProvider } from '../lib/DocumentTitleProvider' import { Spinner } from '../lib/Spinner' @@ -82,8 +82,8 @@ export const App: React.FC = function App() { const [lastStart] = useState(Date.now()) const [requestedRoute, setRequestedRoute] = useState() - const userReady = useSubscription(PubSub.loggedInUser) - const orgReady = useSubscription(PubSub.organization, user?.organizationId ?? null) + const userReady = useSubscription(MeteorPubSub.loggedInUser) + const orgReady = useSubscription(MeteorPubSub.organization, user?.organizationId ?? null) const subsReady = userReady && orgReady diff --git a/meteor/client/ui/ClipTrimPanel/ClipTrimPanel.tsx b/meteor/client/ui/ClipTrimPanel/ClipTrimPanel.tsx index 1ef2eef98a..76fdee741b 100644 --- a/meteor/client/ui/ClipTrimPanel/ClipTrimPanel.tsx +++ b/meteor/client/ui/ClipTrimPanel/ClipTrimPanel.tsx @@ -9,9 +9,9 @@ import { UIStudio } from '../../../lib/api/studios' import { useTranslation } from 'react-i18next' import { useContentStatusForPiece } from '../SegmentTimeline/withMediaObjectStatus' import { useSubscription, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' -import { PubSub } from '../../../lib/api/pubsub' import { PieceId, RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { Pieces } from '../../collections' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' export interface IProps { studio: UIStudio @@ -86,7 +86,7 @@ export function ClipTrimPanel({ }: IProps): JSX.Element { const { t } = useTranslation() - useSubscription(PubSub.pieces, { _id: pieceId, startRundownId: rundownId }) + useSubscription(CorelibPubSub.pieces, { _id: pieceId, startRundownId: rundownId }) const piece = useTracker(() => Pieces.findOne(pieceId), [pieceId]) diff --git a/meteor/client/ui/ClockView/CameraScreen/Rundown.tsx b/meteor/client/ui/ClockView/CameraScreen/Rundown.tsx index 5916e60adc..3325145f04 100644 --- a/meteor/client/ui/ClockView/CameraScreen/Rundown.tsx +++ b/meteor/client/ui/ClockView/CameraScreen/Rundown.tsx @@ -1,7 +1,7 @@ import React, { useContext } from 'react' import { useSubscription, useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData' import { Rundown as RundownObj } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { PubSub } from '../../../../lib/api/pubsub' +import { MeteorPubSub } from '../../../../lib/api/pubsub' import { Segments } from '../../../collections' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { Segment as SegmentComponent } from './Segment' @@ -20,7 +20,7 @@ interface IProps { export function Rundown({ playlist, rundown, rundownIdsBefore }: IProps): JSX.Element | null { const rundownId = rundown._id - useSubscription(PubSub.uiShowStyleBase, rundown.showStyleBaseId) + useSubscription(MeteorPubSub.uiShowStyleBase, rundown.showStyleBaseId) const segments = useTracker(() => Segments.find({ rundownId }).fetch(), [rundownId], [] as DBSegment[]) diff --git a/meteor/client/ui/ClockView/CameraScreen/index.tsx b/meteor/client/ui/ClockView/CameraScreen/index.tsx index 431ab4be7e..9d73e9319d 100644 --- a/meteor/client/ui/ClockView/CameraScreen/index.tsx +++ b/meteor/client/ui/ClockView/CameraScreen/index.tsx @@ -3,7 +3,7 @@ import { CameraContent, RemoteContent, SourceLayerType, SplitsContent } from '@s import { RundownId, ShowStyleBaseId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' -import { PubSub } from '../../../../lib/api/pubsub' +import { MeteorPubSub } from '../../../../lib/api/pubsub' import { UIStudio } from '../../../../lib/api/studios' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PieceExtended } from '../../../../lib/Rundown' @@ -21,6 +21,7 @@ import { Spinner } from '../../../lib/Spinner' import { useBlackBrowserTheme } from '../../../lib/useBlackBrowserTheme' import { useWakeLock } from './useWakeLock' import { catchError, useDebounce } from '../../../lib/lib' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' interface IProps { playlist: DBRundownPlaylist | undefined @@ -94,25 +95,25 @@ export function CameraScreen({ playlist, studioId }: IProps): JSX.Element | null const rundownIds = useMemo(() => rundowns.map((rundown) => rundown._id), [rundowns]) const showStyleBaseIds = useMemo(() => rundowns.map((rundown) => rundown.showStyleBaseId), [rundowns]) - const rundownsReady = useSubscription(PubSub.rundowns, playlistIds, null) - useSubscription(PubSub.segments, { + const rundownsReady = useSubscription(CorelibPubSub.rundowns, playlistIds, null) + useSubscription(CorelibPubSub.segments, { rundownId: { $in: rundownIds, }, }) - const studioReady = useSubscription(PubSub.uiStudio, studioId) - useSubscription(PubSub.partInstances, rundownIds, playlist?.activationId) + const studioReady = useSubscription(MeteorPubSub.uiStudio, studioId) + useSubscription(CorelibPubSub.partInstances, rundownIds, playlist?.activationId) - useSubscription(PubSub.parts, rundownIds) + useSubscription(CorelibPubSub.parts, rundownIds) - useSubscription(PubSub.pieceInstancesSimple, { + useSubscription(CorelibPubSub.pieceInstancesSimple, { rundownId: { $in: rundownIds, }, }) - const piecesReady = useSubscription(PubSub.pieces, { + const piecesReady = useSubscription(CorelibPubSub.pieces, { startRundownId: { $in: rundownIds, }, diff --git a/meteor/client/ui/ClockView/ClockView.tsx b/meteor/client/ui/ClockView/ClockView.tsx index 70afd06fae..2173c07ac4 100644 --- a/meteor/client/ui/ClockView/ClockView.tsx +++ b/meteor/client/ui/ClockView/ClockView.tsx @@ -4,7 +4,6 @@ import { useSubscription, useTracker } from '../../lib/ReactMeteorData/react-met import { RundownTimingProvider } from '../RundownView/RundownTiming/RundownTimingProvider' -import { PubSub } from '../../../lib/api/pubsub' import { StudioScreenSaver } from '../StudioScreenSaver/StudioScreenSaver' import { PresenterScreen } from './PresenterScreen' import { OverlayScreen } from './OverlayScreen' @@ -12,9 +11,10 @@ import { OverlayScreenSaver } from './OverlayScreenSaver' import { RundownPlaylists } from '../../collections' import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { CameraScreen } from './CameraScreen' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' export function ClockView({ studioId }: { studioId: StudioId }): JSX.Element { - useSubscription(PubSub.rundownPlaylists, { + useSubscription(CorelibPubSub.rundownPlaylists, { activationId: { $exists: true }, studioId, }) diff --git a/meteor/client/ui/ClockView/OverlayScreenSaver.tsx b/meteor/client/ui/ClockView/OverlayScreenSaver.tsx index 39b6cf7680..c8bc813118 100644 --- a/meteor/client/ui/ClockView/OverlayScreenSaver.tsx +++ b/meteor/client/ui/ClockView/OverlayScreenSaver.tsx @@ -1,11 +1,12 @@ import React, { useEffect, useRef } from 'react' import { Clock } from '../StudioScreenSaver/Clock' import { useTracker, useSubscription } from '../../lib/ReactMeteorData/ReactMeteorData' -import { PubSub } from '../../../lib/api/pubsub' +import { MeteorPubSub } from '../../../lib/api/pubsub' import { findNextPlaylist } from '../StudioScreenSaver/StudioScreenSaver' // @ts-expect-error No types available import Velocity from 'velocity-animate' import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' export function OverlayScreenSaver({ studioId }: { studioId: StudioId }): JSX.Element { const studioNameRef = useRef(null) @@ -17,8 +18,8 @@ export function OverlayScreenSaver({ studioId }: { studioId: StudioId }): JSX.El } }) - useSubscription(PubSub.uiStudio, studioId) - useSubscription(PubSub.rundownPlaylists, { studioId: studioId, activationId: { $exists: false } }) + useSubscription(MeteorPubSub.uiStudio, studioId) + useSubscription(CorelibPubSub.rundownPlaylists, { studioId: studioId, activationId: { $exists: false } }) const data = useTracker(() => findNextPlaylist({ studioId }), [studioId]) diff --git a/meteor/client/ui/ClockView/PresenterScreen.tsx b/meteor/client/ui/ClockView/PresenterScreen.tsx index 2d507f6b8b..d63a150e18 100644 --- a/meteor/client/ui/ClockView/PresenterScreen.tsx +++ b/meteor/client/ui/ClockView/PresenterScreen.tsx @@ -10,7 +10,7 @@ import { Translated, withTracker } from '../../lib/ReactMeteorData/ReactMeteorDa import { extendMandadory, getCurrentTime, protectString, unprotectString } from '../../../lib/lib' import { PartInstance } from '../../../lib/collections/PartInstances' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' -import { PubSub } from '../../../lib/api/pubsub' +import { MeteorPubSub } from '../../../lib/api/pubsub' import { PieceIconContainer } from '../PieceIcons/PieceIcon' import { PieceNameContainer } from '../PieceIcons/PieceName' import { Timediff } from './Timediff' @@ -41,6 +41,7 @@ import { UIStudio } from '../../../lib/api/studios' import { PieceInstances, RundownLayouts, RundownPlaylists, Rundowns, ShowStyleVariants } from '../../collections' import { RundownPlaylistCollectionUtil } from '../../../lib/collections/rundownPlaylistUtil' import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' interface SegmentUi extends DBSegment { items: Array @@ -305,7 +306,7 @@ export class PresenterScreenBase extends MeteorReactComponent< protected subscribeToData(): void { this.autorun(() => { - this.subscribe(PubSub.uiStudio, this.props.studioId) + this.subscribe(MeteorPubSub.uiStudio, this.props.studioId) const playlist = RundownPlaylists.findOne(this.props.playlistId, { fields: { @@ -314,7 +315,7 @@ export class PresenterScreenBase extends MeteorReactComponent< }, }) as Pick | undefined if (playlist) { - this.subscribe(PubSub.rundowns, [playlist._id], null) + this.subscribe(CorelibPubSub.rundowns, [playlist._id], null) this.autorun(() => { const rundowns = RundownPlaylistCollectionUtil.getRundownsUnordered(playlist, undefined, { @@ -328,22 +329,22 @@ export class PresenterScreenBase extends MeteorReactComponent< const showStyleBaseIds = rundowns.map((r) => r.showStyleBaseId) const showStyleVariantIds = rundowns.map((r) => r.showStyleVariantId) - this.subscribe(PubSub.segments, { + this.subscribe(CorelibPubSub.segments, { rundownId: { $in: rundownIds }, }) - this.subscribe(PubSub.parts, rundownIds) - this.subscribe(PubSub.partInstances, rundownIds, playlist.activationId) + this.subscribe(CorelibPubSub.parts, rundownIds) + this.subscribe(CorelibPubSub.partInstances, rundownIds, playlist.activationId) for (const rundown of rundowns) { - this.subscribe(PubSub.uiShowStyleBase, rundown.showStyleBaseId) + this.subscribe(MeteorPubSub.uiShowStyleBase, rundown.showStyleBaseId) } - this.subscribe(PubSub.showStyleVariants, { + this.subscribe(CorelibPubSub.showStyleVariants, { _id: { $in: showStyleVariantIds, }, }) - this.subscribe(PubSub.rundownLayouts, { + this.subscribe(MeteorPubSub.rundownLayouts, { showStyleBaseId: { $in: showStyleBaseIds, }, @@ -362,13 +363,13 @@ export class PresenterScreenBase extends MeteorReactComponent< const { nextPartInstance, currentPartInstance } = RundownPlaylistCollectionUtil.getSelectedPartInstances(playlistR) if (currentPartInstance) { - this.subscribe(PubSub.pieceInstances, { + this.subscribe(CorelibPubSub.pieceInstances, { rundownId: currentPartInstance.rundownId, partInstanceId: currentPartInstance._id, }) } if (nextPartInstance) { - this.subscribe(PubSub.pieceInstances, { + this.subscribe(CorelibPubSub.pieceInstances, { rundownId: nextPartInstance.rundownId, partInstanceId: nextPartInstance._id, }) diff --git a/meteor/client/ui/MediaStatus/MediaStatus.tsx b/meteor/client/ui/MediaStatus/MediaStatus.tsx index 42f3b250a7..298bb0f8c8 100644 --- a/meteor/client/ui/MediaStatus/MediaStatus.tsx +++ b/meteor/client/ui/MediaStatus/MediaStatus.tsx @@ -1,6 +1,6 @@ import { useMemo, JSX } from 'react' import { useSubscription, useSubscriptions, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' -import { PubSub } from '../../../lib/api/pubsub' +import { MeteorPubSub } from '../../../lib/api/pubsub' import { getSegmentsWithPartInstances } from '../../../lib/Rundown' import { AdLibActionId, @@ -35,6 +35,7 @@ import { assertNever, literal } from '@sofie-automation/corelib/dist/lib' import { UIPieceContentStatuses, UIShowStyleBases } from '../Collections' import { isTranslatableMessage, translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import { i18nTranslator } from '../i18n' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' export function MediaStatus({ playlistIds, @@ -256,24 +257,24 @@ function useMediaStatusSubscriptions( ): boolean { const readyStatus: boolean[] = [] let counter = 0 - readyStatus[counter++] = useSubscription(PubSub.rundownPlaylists, { + readyStatus[counter++] = useSubscription(CorelibPubSub.rundownPlaylists, { _id: { $in: playlistIds, }, }) - readyStatus[counter++] = useSubscription(PubSub.rundowns, playlistIds, null) + readyStatus[counter++] = useSubscription(CorelibPubSub.rundowns, playlistIds, null) const uiShowStyleBaseSubArguments = useMemo( () => showStyleBaseIds.map((showStyleBaseId) => [showStyleBaseId] as [ShowStyleBaseId]), [showStyleBaseIds] ) - readyStatus[counter++] = useSubscriptions(PubSub.uiShowStyleBase, uiShowStyleBaseSubArguments) - readyStatus[counter++] = useSubscription(PubSub.segments, { + readyStatus[counter++] = useSubscriptions(MeteorPubSub.uiShowStyleBase, uiShowStyleBaseSubArguments) + readyStatus[counter++] = useSubscription(CorelibPubSub.segments, { rundownId: { $in: rundownIds, }, }) - readyStatus[counter++] = useSubscription(PubSub.parts, rundownIds) - readyStatus[counter++] = useSubscription(PubSub.partInstancesSimple, { + readyStatus[counter++] = useSubscription(CorelibPubSub.parts, rundownIds) + readyStatus[counter++] = useSubscription(CorelibPubSub.partInstancesSimple, { rundownId: { $in: rundownIds, }, @@ -281,32 +282,32 @@ function useMediaStatusSubscriptions( $ne: true, }, }) - readyStatus[counter++] = useSubscription(PubSub.pieceInstancesSimple, { + readyStatus[counter++] = useSubscription(CorelibPubSub.pieceInstancesSimple, { rundownId: { $in: rundownIds, }, }) - readyStatus[counter++] = useSubscription(PubSub.pieces, { + readyStatus[counter++] = useSubscription(CorelibPubSub.pieces, { startRundownId: { $in: rundownIds, }, }) - readyStatus[counter++] = useSubscription(PubSub.adLibActions, { + readyStatus[counter++] = useSubscription(CorelibPubSub.adLibActions, { rundownId: { $in: rundownIds, }, }) - readyStatus[counter++] = useSubscription(PubSub.adLibPieces, { + readyStatus[counter++] = useSubscription(CorelibPubSub.adLibPieces, { rundownId: { $in: rundownIds, }, }) - readyStatus[counter++] = useSubscription(PubSub.rundownBaselineAdLibActions, { + readyStatus[counter++] = useSubscription(CorelibPubSub.rundownBaselineAdLibActions, { rundownId: { $in: rundownIds, }, }) - readyStatus[counter++] = useSubscription(PubSub.rundownBaselineAdLibPieces, { + readyStatus[counter++] = useSubscription(CorelibPubSub.rundownBaselineAdLibPieces, { rundownId: { $in: rundownIds, }, @@ -315,7 +316,7 @@ function useMediaStatusSubscriptions( () => playlistIds.map((playlistIds) => [playlistIds] as [RundownPlaylistId]), [playlistIds] ) - readyStatus[counter++] = useSubscriptions(PubSub.uiPieceContentStatuses, uiPieceContentStatusesSubArguments) + readyStatus[counter++] = useSubscriptions(MeteorPubSub.uiPieceContentStatuses, uiPieceContentStatusesSubArguments) return readyStatus.reduce((mem, current) => mem && current, true) } diff --git a/meteor/client/ui/PieceIcons/PieceCountdown.tsx b/meteor/client/ui/PieceIcons/PieceCountdown.tsx index 94c0d0c1fd..2a59e7dfa2 100644 --- a/meteor/client/ui/PieceIcons/PieceCountdown.tsx +++ b/meteor/client/ui/PieceIcons/PieceCountdown.tsx @@ -1,7 +1,7 @@ import React from 'react' import { useSubscription, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { SourceLayerType, VTContent } from '@sofie-automation/blueprints-integration' -import { PubSub } from '../../../lib/api/pubsub' +import { MeteorPubSub } from '../../../lib/api/pubsub' import { findPieceInstanceToShow } from './utils' import { Timediff } from '../ClockView/Timediff' import { getCurrentTime } from '../../../lib/lib' @@ -11,6 +11,7 @@ import { RundownPlaylistActivationId, ShowStyleBaseId, } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' export interface IPropsHeader { partInstanceId: PartInstanceId @@ -41,12 +42,12 @@ export function PieceCountdownContainer(props: IPropsHeader): JSX.Element | null } ) - useSubscription(PubSub.pieceInstancesSimple, { + useSubscription(CorelibPubSub.pieceInstancesSimple, { rundownId: { $in: props.rundownIds }, playlistActivationId: props.playlistActivationId, }) - useSubscription(PubSub.uiShowStyleBase, props.showStyleBaseId) + useSubscription(MeteorPubSub.uiShowStyleBase, props.showStyleBaseId) const piece = pieceInstance ? pieceInstance.piece : undefined const sourceDuration = (piece?.content as VTContent)?.sourceDuration diff --git a/meteor/client/ui/PieceIcons/PieceIcon.tsx b/meteor/client/ui/PieceIcons/PieceIcon.tsx index 0ada6d4793..b6e017c0f8 100644 --- a/meteor/client/ui/PieceIcons/PieceIcon.tsx +++ b/meteor/client/ui/PieceIcons/PieceIcon.tsx @@ -14,7 +14,7 @@ import RemoteInputIcon from './Renderers/RemoteInputIcon' import LiveSpeakInputIcon from './Renderers/LiveSpeakInputIcon' import GraphicsInputIcon from './Renderers/GraphicsInputIcon' import UnknownInputIcon from './Renderers/UnknownInputIcon' -import { PubSub } from '../../../lib/api/pubsub' +import { MeteorPubSub } from '../../../lib/api/pubsub' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { findPieceInstanceToShow, findPieceInstanceToShowFromInstances } from './utils' import LocalInputIcon from './Renderers/LocalInputIcon' @@ -25,6 +25,7 @@ import { ShowStyleBaseId, } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ReadonlyDeep } from 'type-fest' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' export interface IPropsHeader { partInstanceId: PartInstanceId @@ -129,12 +130,12 @@ export function PieceIconContainer(props: IPropsHeader): JSX.Element | null { } ) - useSubscription(PubSub.pieceInstancesSimple, { + useSubscription(CorelibPubSub.pieceInstancesSimple, { rundownId: { $in: props.rundownIds }, playlistActivationId: props.playlistActivationId, }) - useSubscription(PubSub.uiShowStyleBase, props.showStyleBaseId) + useSubscription(MeteorPubSub.uiShowStyleBase, props.showStyleBaseId) return } diff --git a/meteor/client/ui/PieceIcons/PieceName.tsx b/meteor/client/ui/PieceIcons/PieceName.tsx index 30acdeed39..c8ace6b1bb 100644 --- a/meteor/client/ui/PieceIcons/PieceName.tsx +++ b/meteor/client/ui/PieceIcons/PieceName.tsx @@ -2,12 +2,13 @@ import React from 'react' import { useSubscription, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { EvsContent, SourceLayerType } from '@sofie-automation/blueprints-integration' -import { PubSub } from '../../../lib/api/pubsub' +import { MeteorPubSub } from '../../../lib/api/pubsub' import { IPropsHeader } from './PieceIcon' import { findPieceInstanceToShow } from './utils' import { PieceGeneric } from '@sofie-automation/corelib/dist/dataModel/Piece' import { RundownPlaylistActivationId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ReadonlyDeep } from 'type-fest' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' interface INamePropsHeader extends IPropsHeader { partName: string @@ -54,12 +55,12 @@ export function PieceNameContainer(props: INamePropsHeader): JSX.Element | null } ) - useSubscription(PubSub.pieceInstancesSimple, { + useSubscription(CorelibPubSub.pieceInstancesSimple, { rundownId: { $in: props.rundownIds }, playlistActivationId: props.playlistActivationId, }) - useSubscription(PubSub.uiShowStyleBase, props.showStyleBaseId) + useSubscription(MeteorPubSub.uiShowStyleBase, props.showStyleBaseId) if (pieceInstance && sourceLayer && supportedLayers.has(sourceLayer.type)) { return getPieceLabel(pieceInstance.piece, sourceLayer.type) diff --git a/meteor/client/ui/Prompter/PrompterView.tsx b/meteor/client/ui/Prompter/PrompterView.tsx index d7969babe0..7ecffa7721 100644 --- a/meteor/client/ui/Prompter/PrompterView.tsx +++ b/meteor/client/ui/Prompter/PrompterView.tsx @@ -14,7 +14,7 @@ import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { firstIfArray, literal, protectString } from '../../../lib/lib' import { PrompterData, PrompterAPI, PrompterDataPart } from './prompter' import { PrompterControlManager } from './controller/manager' -import { PubSub } from '../../../lib/api/pubsub' +import { MeteorPubSub } from '../../../lib/api/pubsub' import { documentTitle } from '../../lib/DocumentTitleProvider' import { StudioScreenSaver } from '../StudioScreenSaver/StudioScreenSaver' import { RundownTimingProvider } from '../RundownView/RundownTiming/RundownTimingProvider' @@ -26,6 +26,7 @@ import { UIStudio } from '../../../lib/api/studios' import { RundownPlaylists, Rundowns } from '../../collections' import { RundownPlaylistCollectionUtil } from '../../../lib/collections/rundownPlaylistUtil' import { logger } from '../../../lib/logging' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' const DEFAULT_UPDATE_THROTTLE = 250 //ms const PIECE_MISSING_UPDATE_THROTTLE = 2000 //ms @@ -203,9 +204,9 @@ export class PrompterViewInner extends MeteorReactComponent | undefined if (playlist?._id) { - this.subscribe(PubSub.rundowns, [playlist._id], null) + this.subscribe(CorelibPubSub.rundowns, [playlist._id], null) } }) @@ -598,7 +599,7 @@ export const Prompter = translateWithTracker, } componentDidMount(): void { - this.subscribe(PubSub.rundowns, [this.props.rundownPlaylistId], null) + this.subscribe(CorelibPubSub.rundowns, [this.props.rundownPlaylistId], null) this.autorun(() => { const playlist = RundownPlaylists.findOne(this.props.rundownPlaylistId, { @@ -609,15 +610,15 @@ export const Prompter = translateWithTracker, }) as Pick | undefined if (playlist) { const rundownIDs = RundownPlaylistCollectionUtil.getRundownUnorderedIDs(playlist) - this.subscribe(PubSub.segments, { + this.subscribe(CorelibPubSub.segments, { rundownId: { $in: rundownIDs }, }) - this.subscribe(PubSub.parts, rundownIDs) - this.subscribe(PubSub.partInstances, rundownIDs, playlist.activationId) - this.subscribe(PubSub.pieces, { + this.subscribe(CorelibPubSub.parts, rundownIDs) + this.subscribe(CorelibPubSub.partInstances, rundownIDs, playlist.activationId) + this.subscribe(CorelibPubSub.pieces, { startRundownId: { $in: rundownIDs }, }) - this.subscribe(PubSub.pieceInstancesSimple, { + this.subscribe(CorelibPubSub.pieceInstancesSimple, { rundownId: { $in: rundownIDs }, reset: { $ne: true }, }) @@ -635,7 +636,7 @@ export const Prompter = translateWithTracker, } ).fetch() as Pick[] for (const rundown of rundowns) { - this.subscribe(PubSub.uiShowStyleBase, rundown.showStyleBaseId) + this.subscribe(MeteorPubSub.uiShowStyleBase, rundown.showStyleBaseId) } }) } diff --git a/meteor/client/ui/RundownList.tsx b/meteor/client/ui/RundownList.tsx index 27571d3d06..2c33d7de25 100644 --- a/meteor/client/ui/RundownList.tsx +++ b/meteor/client/ui/RundownList.tsx @@ -1,6 +1,6 @@ import Tooltip from 'rc-tooltip' import * as React from 'react' -import { PubSub } from '../../lib/api/pubsub' +import { MeteorPubSub } from '../../lib/api/pubsub' import { GENESIS_SYSTEM_VERSION } from '../../lib/collections/CoreSystem' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { getAllowConfigure, getHelpMode } from '../lib/localStorage' @@ -19,6 +19,7 @@ import { getCoreSystem, RundownLayouts, RundownPlaylists, Rundowns } from '../co import { RundownPlaylistCollectionUtil } from '../../lib/collections/rundownPlaylistUtil' import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' export enum ToolTipStep { TOOLTIP_START_HERE = 'TOOLTIP_START_HERE', @@ -59,17 +60,17 @@ export function RundownList(): JSX.Element { ) const baseSubsReady = [ - useSubscription(PubSub.rundownPlaylists, {}), - useSubscription(PubSub.uiStudio, null), - useSubscription(PubSub.rundownLayouts, {}), + useSubscription(CorelibPubSub.rundownPlaylists, {}), + useSubscription(MeteorPubSub.uiStudio, null), + useSubscription(MeteorPubSub.rundownLayouts, {}), - useSubscription(PubSub.rundowns, playlistIds, null), + useSubscription(CorelibPubSub.rundowns, playlistIds, null), - useSubscription(PubSub.showStyleBases, { + useSubscription(CorelibPubSub.showStyleBases, { _id: { $in: showStyleBaseIds }, }), - useSubscription(PubSub.showStyleVariants, { + useSubscription(CorelibPubSub.showStyleVariants, { _id: { $in: showStyleVariantIds }, }), ].reduce((prev, current) => prev && current, true) diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx index 12fa41923c..c8affdb093 100644 --- a/meteor/client/ui/RundownView.tsx +++ b/meteor/client/ui/RundownView.tsx @@ -63,7 +63,7 @@ import { PeripheralDevice, PeripheralDeviceType } from '@sofie-automation/coreli import { doUserAction, UserAction } from '../../lib/clientUserAction' import { hashSingleUseToken, ReloadRundownPlaylistResponse, TriggerReloadDataResponse } from '../../lib/api/userActions' import { ClipTrimDialog } from './ClipTrimPanel/ClipTrimDialog' -import { meteorSubscribe, PubSub } from '../../lib/api/pubsub' +import { meteorSubscribe, MeteorPubSub } from '../../lib/api/pubsub' import { RundownLayoutType, RundownLayoutBase, @@ -148,6 +148,7 @@ import { PromiseButton } from '../lib/Components/PromiseButton' import { logger } from '../../lib/logging' import { isTranslatableMessage, translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import { i18nTranslator } from './i18n' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' export const MAGIC_TIME_SCALE_FACTOR = 0.03 @@ -1578,10 +1579,10 @@ export const RundownView = translateWithTracker(( componentDidMount(): void { const playlistId = this.props.rundownPlaylistId - this.subscribe(PubSub.rundownPlaylists, { + this.subscribe(CorelibPubSub.rundownPlaylists, { _id: playlistId, }) - this.subscribe(PubSub.rundowns, [playlistId], null) + this.subscribe(CorelibPubSub.rundowns, [playlistId], null) this.autorun(() => { const playlist = RundownPlaylists.findOne(playlistId, { fields: { @@ -1591,10 +1592,10 @@ export const RundownView = translateWithTracker(( }) as Pick | undefined if (!playlist) return - this.subscribe(PubSub.uiSegmentPartNotes, playlistId) - this.subscribe(PubSub.uiPieceContentStatuses, playlistId) - this.subscribe(PubSub.uiStudio, playlist.studioId) - this.subscribe(PubSub.buckets, playlist.studioId, null) + this.subscribe(MeteorPubSub.uiSegmentPartNotes, playlistId) + this.subscribe(MeteorPubSub.uiPieceContentStatuses, playlistId) + this.subscribe(MeteorPubSub.uiStudio, playlist.studioId) + this.subscribe(MeteorPubSub.buckets, playlist.studioId, null) }) this.autorun(() => { @@ -1615,47 +1616,47 @@ export const RundownView = translateWithTracker(( }) as Pick[] for (const rundown of rundowns) { - this.subscribe(PubSub.uiShowStyleBase, rundown.showStyleBaseId) + this.subscribe(MeteorPubSub.uiShowStyleBase, rundown.showStyleBaseId) } - this.subscribe(PubSub.showStyleVariants, { + this.subscribe(CorelibPubSub.showStyleVariants, { _id: { $in: rundowns.map((i) => i.showStyleVariantId), }, }) - this.subscribe(PubSub.rundownLayouts, { + this.subscribe(MeteorPubSub.rundownLayouts, { showStyleBaseId: { $in: rundowns.map((i) => i.showStyleBaseId), }, }) const rundownIDs = rundowns.map((i) => i._id) - this.subscribe(PubSub.segments, { + this.subscribe(CorelibPubSub.segments, { rundownId: { $in: rundownIDs, }, }) - this.subscribe(PubSub.adLibPieces, { + this.subscribe(CorelibPubSub.adLibPieces, { rundownId: { $in: rundownIDs, }, }) - this.subscribe(PubSub.rundownBaselineAdLibPieces, { + this.subscribe(CorelibPubSub.rundownBaselineAdLibPieces, { rundownId: { $in: rundownIDs, }, }) - this.subscribe(PubSub.adLibActions, { + this.subscribe(CorelibPubSub.adLibActions, { rundownId: { $in: rundownIDs, }, }) - this.subscribe(PubSub.rundownBaselineAdLibActions, { + this.subscribe(CorelibPubSub.rundownBaselineAdLibActions, { rundownId: { $in: rundownIDs, }, }) - this.subscribe(PubSub.parts, rundownIDs) - this.subscribe(PubSub.partInstances, rundownIDs, playlist.activationId) + this.subscribe(CorelibPubSub.parts, rundownIDs) + this.subscribe(CorelibPubSub.partInstances, rundownIDs, playlist.activationId) }) this.autorun(() => { const playlist = RundownPlaylists.findOne(playlistId, { @@ -1670,7 +1671,7 @@ export const RundownView = translateWithTracker(( // Use meteorSubscribe so that this subscription doesn't mess with this.subscriptionsReady() // it's run in this.autorun, so the subscription will be stopped along with the autorun, // so we don't have to manually clean up after ourselves. - meteorSubscribe(PubSub.pieceInstances, { + meteorSubscribe(CorelibPubSub.pieceInstances, { rundownId: { $in: rundownIds, }, @@ -1689,13 +1690,13 @@ export const RundownView = translateWithTracker(( RundownPlaylistCollectionUtil.getSelectedPartInstances(playlist) if (previousPartInstance) { - meteorSubscribe(PubSub.partInstancesForSegmentPlayout, { + meteorSubscribe(CorelibPubSub.partInstancesForSegmentPlayout, { rundownId: previousPartInstance.rundownId, segmentPlayoutId: previousPartInstance.segmentPlayoutId, }) } if (currentPartInstance) { - meteorSubscribe(PubSub.partInstancesForSegmentPlayout, { + meteorSubscribe(CorelibPubSub.partInstancesForSegmentPlayout, { rundownId: currentPartInstance.rundownId, segmentPlayoutId: currentPartInstance.segmentPlayoutId, }) diff --git a/meteor/client/ui/RundownView/RundownNotifier.tsx b/meteor/client/ui/RundownView/RundownNotifier.tsx index 1defeb10e9..6a58df9995 100644 --- a/meteor/client/ui/RundownView/RundownNotifier.tsx +++ b/meteor/client/ui/RundownView/RundownNotifier.tsx @@ -14,7 +14,7 @@ import { WithManagedTracker } from '../../lib/reactiveData/reactiveDataHelper' import { reactiveData } from '../../lib/reactiveData/reactiveData' import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { getCurrentTime, unprotectString } from '../../../lib/lib' -import { PubSub, meteorSubscribe } from '../../../lib/api/pubsub' +import { meteorSubscribe } from '../../../lib/api/pubsub' import { ReactiveVar } from 'meteor/reactive-var' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { doModalDialog } from '../../lib/ModalDialog' @@ -44,6 +44,7 @@ import { import { UIPieceContentStatuses, UISegmentPartNotes } from '../Collections' import { RundownPlaylistCollectionUtil } from '../../../lib/collections/rundownPlaylistUtil' import { logger } from '../../../lib/logging' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' export const onRONotificationClick = new ReactiveVar<((e: RONotificationEvent) => void) | undefined>(undefined) export const reloadRundownPlaylistClick = new ReactiveVar<((e: any) => void) | undefined>(undefined) @@ -292,7 +293,7 @@ class RundownViewNotifier extends WithManagedTracker { | ReactiveVar[]> | undefined if (studioId) { - meteorSubscribe(PubSub.peripheralDevicesAndSubDevices, { studioId: studioId }) + meteorSubscribe(CorelibPubSub.peripheralDevicesAndSubDevices, { studioId: studioId }) reactivePeripheralDevices = reactiveData.getRPeripheralDevices(studioId, { fields: { name: 1, @@ -576,7 +577,7 @@ class RundownViewNotifier extends WithManagedTracker { } private reactiveQueueStatus(studioId: StudioId, playlistId: RundownPlaylistId) { - meteorSubscribe(PubSub.externalMessageQueue, { studioId: studioId, playlistId }) + meteorSubscribe(CorelibPubSub.externalMessageQueue, { studioId: studioId, playlistId }) const reactiveUnsentMessageCount = reactiveData.getUnsentExternalMessageCount(studioId, playlistId) this.autorun(() => { if (reactiveUnsentMessageCount.get() > 0 && this._unsentExternalMessagesStatus === undefined) { diff --git a/meteor/client/ui/RundownView/RundownSystemStatus.tsx b/meteor/client/ui/RundownView/RundownSystemStatus.tsx index c975f1f078..43185fb373 100644 --- a/meteor/client/ui/RundownView/RundownSystemStatus.tsx +++ b/meteor/client/ui/RundownView/RundownSystemStatus.tsx @@ -13,11 +13,11 @@ import { Time, getCurrentTime, unprotectString } from '../../../lib/lib' import { withTranslation, WithTranslation } from 'react-i18next' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { PubSub } from '../../../lib/api/pubsub' import { StatusCode } from '@sofie-automation/blueprints-integration' import { UIStudio } from '../../../lib/api/studios' import { RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PeripheralDevices } from '../../collections' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' interface IMOSStatusProps { lastUpdate: Time @@ -180,7 +180,7 @@ export const RundownSystemStatus = translateWithTracker( } componentDidMount(): void { - this.subscribe(PubSub.peripheralDevicesAndSubDevices, { + this.subscribe(CorelibPubSub.peripheralDevicesAndSubDevices, { studioId: this.props.studio._id, }) } diff --git a/meteor/client/ui/SegmentList/LinePartPieceIndicator/LinePartAdLibIndicator.tsx b/meteor/client/ui/SegmentList/LinePartPieceIndicator/LinePartAdLibIndicator.tsx index 8d2b27f386..9601340e09 100644 --- a/meteor/client/ui/SegmentList/LinePartPieceIndicator/LinePartAdLibIndicator.tsx +++ b/meteor/client/ui/SegmentList/LinePartPieceIndicator/LinePartAdLibIndicator.tsx @@ -1,5 +1,4 @@ import React, { useCallback, useMemo } from 'react' -import { PubSub } from '../../../../lib/api/pubsub' import { PartId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' @@ -11,6 +10,7 @@ import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMes import StudioContext from '../../RundownView/StudioContext' import { AdLibActions, AdLibPieces } from '../../../collections' import RundownViewEventBus, { RundownViewEvents } from '../../../../lib/api/triggers/RundownViewEventBus' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' interface IProps { sourceLayers: ISourceLayerExtended[] @@ -24,14 +24,14 @@ export const LinePartAdLibIndicator: React.FC = function LinePartAdLibIn const sourceLayerIds = useMemo(() => sourceLayers.map((sourceLayer) => sourceLayer._id), [sourceLayers]) const label = useMemo(() => sourceLayers[0]?.name ?? '', [sourceLayers]) - useSubscription(PubSub.adLibPieces, { + useSubscription(CorelibPubSub.adLibPieces, { partId, sourceLayerId: { $in: sourceLayerIds, }, }) - useSubscription(PubSub.adLibActions, { + useSubscription(CorelibPubSub.adLibActions, { partId, 'display.sourceLayerId': { $in: sourceLayerIds, diff --git a/meteor/client/ui/SegmentList/SegmentListContainer.tsx b/meteor/client/ui/SegmentList/SegmentListContainer.tsx index 7e9509abd8..5e9b1b22d4 100644 --- a/meteor/client/ui/SegmentList/SegmentListContainer.tsx +++ b/meteor/client/ui/SegmentList/SegmentListContainer.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef } from 'react' import { PieceLifespan } from '@sofie-automation/blueprints-integration' -import { PubSub, meteorSubscribe } from '../../../lib/api/pubsub' +import { meteorSubscribe } from '../../../lib/api/pubsub' import { useSubscription, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { withResolvedSegment, @@ -12,6 +12,7 @@ import { SegmentList } from './SegmentList' import { unprotectString } from '../../../lib/lib' import { LIVELINE_HISTORY_SIZE as TIMELINE_LIVELINE_HISTORY_SIZE } from '../SegmentTimeline/SegmentTimelineContainer' import { PartInstances, Parts, Segments } from '../../collections' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' export const LIVELINE_HISTORY_SIZE = TIMELINE_LIVELINE_HISTORY_SIZE @@ -41,7 +42,7 @@ export const SegmentListContainer = withResolvedSegment(function Segment [segmentId] ) - useSubscription(PubSub.pieces, { + useSubscription(CorelibPubSub.pieces, { startRundownId: rundownId, startPartId: { $in: partIds, @@ -67,7 +68,7 @@ export const SegmentListContainer = withResolvedSegment(function Segment [segmentId] ) - useSubscription(PubSub.pieceInstances, { + useSubscription(CorelibPubSub.pieceInstances, { rundownId: rundownId, partInstanceId: { $in: partInstanceIds, @@ -85,7 +86,7 @@ export const SegmentListContainer = withResolvedSegment(function Segment }, }) segment && - meteorSubscribe(PubSub.pieces, { + meteorSubscribe(CorelibPubSub.pieces, { invalid: { $ne: true, }, diff --git a/meteor/client/ui/SegmentScratchpad/SegmentScratchpadContainer.tsx b/meteor/client/ui/SegmentScratchpad/SegmentScratchpadContainer.tsx index 7074116dcc..26f0da062f 100644 --- a/meteor/client/ui/SegmentScratchpad/SegmentScratchpadContainer.tsx +++ b/meteor/client/ui/SegmentScratchpad/SegmentScratchpadContainer.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react' import { PieceLifespan } from '@sofie-automation/blueprints-integration' -import { meteorSubscribe, PubSub } from '../../../lib/api/pubsub' +import { meteorSubscribe } from '../../../lib/api/pubsub' import { useSubscription, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { // PartUi, @@ -16,6 +16,7 @@ import { PartInstances, Parts, Segments } from '../../collections' import { literal } from '@sofie-automation/shared-lib/dist/lib/lib' import { MongoFieldSpecifierOnes } from '@sofie-automation/corelib/dist/mongo' import { PartInstance } from '../../../lib/collections/PartInstances' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' export const LIVELINE_HISTORY_SIZE = TIMELINE_LIVELINE_HISTORY_SIZE @@ -45,7 +46,7 @@ export const SegmentScratchpadContainer = withResolvedSegment(function S [segmentId] ) - const piecesReady = useSubscription(PubSub.pieces, { + const piecesReady = useSubscription(CorelibPubSub.pieces, { startRundownId: rundownId, startPartId: { $in: partIds, @@ -71,7 +72,7 @@ export const SegmentScratchpadContainer = withResolvedSegment(function S [segmentId] ) - const pieceInstancesReady = useSubscription(PubSub.pieceInstances, { + const pieceInstancesReady = useSubscription(CorelibPubSub.pieceInstances, { rundownId: rundownId, partInstanceId: { $in: partInstanceIds, @@ -89,7 +90,7 @@ export const SegmentScratchpadContainer = withResolvedSegment(function S }, }) segment && - meteorSubscribe(PubSub.pieces, { + meteorSubscribe(CorelibPubSub.pieces, { invalid: { $ne: true, }, diff --git a/meteor/client/ui/SegmentStoryboard/SegmentStoryboardContainer.tsx b/meteor/client/ui/SegmentStoryboard/SegmentStoryboardContainer.tsx index f722285a09..ced92a4863 100644 --- a/meteor/client/ui/SegmentStoryboard/SegmentStoryboardContainer.tsx +++ b/meteor/client/ui/SegmentStoryboard/SegmentStoryboardContainer.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react' import { PieceLifespan } from '@sofie-automation/blueprints-integration' -import { meteorSubscribe, PubSub } from '../../../lib/api/pubsub' +import { meteorSubscribe } from '../../../lib/api/pubsub' import { useSubscription, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { // PartUi, @@ -16,6 +16,7 @@ import { PartInstances, Parts, Segments } from '../../collections' import { literal } from '@sofie-automation/shared-lib/dist/lib/lib' import { MongoFieldSpecifierOnes } from '@sofie-automation/corelib/dist/mongo' import { PartInstance } from '../../../lib/collections/PartInstances' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' export const LIVELINE_HISTORY_SIZE = TIMELINE_LIVELINE_HISTORY_SIZE @@ -45,7 +46,7 @@ export const SegmentStoryboardContainer = withResolvedSegment(function S [segmentId] ) - const piecesReady = useSubscription(PubSub.pieces, { + const piecesReady = useSubscription(CorelibPubSub.pieces, { startRundownId: rundownId, startPartId: { $in: partIds, @@ -71,7 +72,7 @@ export const SegmentStoryboardContainer = withResolvedSegment(function S [segmentId] ) - const pieceInstancesReady = useSubscription(PubSub.pieceInstances, { + const pieceInstancesReady = useSubscription(CorelibPubSub.pieceInstances, { rundownId: rundownId, partInstanceId: { $in: partInstanceIds, @@ -89,7 +90,7 @@ export const SegmentStoryboardContainer = withResolvedSegment(function S }, }) segment && - meteorSubscribe(PubSub.pieces, { + meteorSubscribe(CorelibPubSub.pieces, { invalid: { $ne: true, }, diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx index e32f46601c..e9ae6d6a2d 100644 --- a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx +++ b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx @@ -11,7 +11,7 @@ import { MAGIC_TIME_SCALE_FACTOR } from '../RundownView' import { SpeechSynthesiser } from '../../lib/speechSynthesis' import { getElementWidth } from '../../utils/dimensions' import { isMaintainingFocus, scrollToSegment, getHeaderHeight } from '../../lib/viewPort' -import { meteorSubscribe, PubSub } from '../../../lib/api/pubsub' +import { meteorSubscribe } from '../../../lib/api/pubsub' import { unprotectString, equalSets, equivalentArrays } from '../../../lib/lib' import { Settings } from '../../../lib/Settings' import { Tracker } from 'meteor/tracker' @@ -34,6 +34,7 @@ import { RundownViewShelf } from '../RundownView/RundownViewShelf' import { PartInstanceId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PartInstances, Parts, Segments } from '../../collections' import { catchError } from '../../lib/lib' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' // Kept for backwards compatibility export { SegmentUi, PartUi, PieceUi, ISourceLayerUi, IOutputLayerUi } from '../SegmentContainer/withResolvedSegment' @@ -145,7 +146,7 @@ export const SegmentTimelineContainer = withResolvedSegment( } ).map((part) => part._id) - this.subscribe(PubSub.pieces, { + this.subscribe(CorelibPubSub.pieces, { startRundownId: this.props.rundownId, startPartId: { $in: partIds, @@ -178,7 +179,7 @@ export const SegmentTimelineContainer = withResolvedSegment( }, }) segment && - this.subscribe(PubSub.pieces, { + this.subscribe(CorelibPubSub.pieces, { invalid: { $ne: true, }, @@ -423,7 +424,7 @@ export const SegmentTimelineContainer = withResolvedSegment( this.partInstanceSub.stop() } // we handle this subscription manually - this.partInstanceSub = meteorSubscribe(PubSub.pieceInstances, { + this.partInstanceSub = meteorSubscribe(CorelibPubSub.pieceInstances, { rundownId: this.props.rundownId, partInstanceId: { $in: partInstanceIds, diff --git a/meteor/client/ui/Settings.tsx b/meteor/client/ui/Settings.tsx index b770d93b58..15f553ef8d 100644 --- a/meteor/client/ui/Settings.tsx +++ b/meteor/client/ui/Settings.tsx @@ -11,23 +11,23 @@ import BlueprintSettings from './Settings/BlueprintSettings' import SystemManagement from './Settings/SystemManagement' import { MigrationView } from './Settings/Migration' -import { PubSub } from '../../lib/api/pubsub' import { getUser } from '../../lib/collections/Users' import { Settings as MeteorSettings } from '../../lib/Settings' import { SettingsMenu } from './Settings/SettingsMenu' import { getAllowConfigure } from '../lib/localStorage' import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' export function Settings(): JSX.Element | null { const user = useTracker(() => getUser(), [], null) const history = useHistory() - useSubscription(PubSub.peripheralDevices, {}) - useSubscription(PubSub.studios, {}) - useSubscription(PubSub.showStyleBases, {}) - useSubscription(PubSub.showStyleVariants, {}) - useSubscription(PubSub.blueprints, {}) + useSubscription(CorelibPubSub.peripheralDevices, {}) + useSubscription(CorelibPubSub.studios, {}) + useSubscription(CorelibPubSub.showStyleBases, {}) + useSubscription(CorelibPubSub.showStyleVariants, {}) + useSubscription(CorelibPubSub.blueprints, {}) useEffect(() => { if (MeteorSettings.enableUserAccounts && user) { diff --git a/meteor/client/ui/Settings/RundownLayoutEditor.tsx b/meteor/client/ui/Settings/RundownLayoutEditor.tsx index 4d1aa21eb2..fbabfe9c30 100644 --- a/meteor/client/ui/Settings/RundownLayoutEditor.tsx +++ b/meteor/client/ui/Settings/RundownLayoutEditor.tsx @@ -20,7 +20,7 @@ import { CustomizableRegionSettingsManifest, RundownLayoutsAPI, } from '../../../lib/api/rundownLayouts' -import { PubSub } from '../../../lib/api/pubsub' +import { MeteorPubSub } from '../../../lib/api/pubsub' import { getRandomString, literal, unprotectString } from '../../../lib/lib' import { UploadButton } from '../../lib/uploadButton' import { doModalDialog } from '../../lib/ModalDialog' @@ -83,7 +83,7 @@ export default translateWithTracker((props: IProp componentDidMount(): void { super.componentDidMount && super.componentDidMount() - this.subscribe(PubSub.rundownLayouts, {}) + this.subscribe(MeteorPubSub.rundownLayouts, {}) } onAddLayout = () => { diff --git a/meteor/client/ui/Settings/SettingsMenu.tsx b/meteor/client/ui/Settings/SettingsMenu.tsx index d6a8f13bf7..6f494e1a31 100644 --- a/meteor/client/ui/Settings/SettingsMenu.tsx +++ b/meteor/client/ui/Settings/SettingsMenu.tsx @@ -14,7 +14,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' -import { PubSub, meteorSubscribe } from '../../../lib/api/pubsub' +import { meteorSubscribe } from '../../../lib/api/pubsub' import { MeteorCall } from '../../../lib/api/methods' import { Settings as MeteorSettings } from '../../../lib/Settings' import { IOutputLayer, StatusCode } from '@sofie-automation/blueprints-integration' @@ -23,6 +23,7 @@ import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts' import { Blueprints, PeripheralDevices, ShowStyleBases, Studios } from '../../collections' import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { catchError } from '../../lib/lib' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' interface ISettingsMenuProps { superAdmin?: boolean @@ -38,11 +39,11 @@ export const SettingsMenu = translateWithTracker { // TODO: add organizationId: - meteorSubscribe(PubSub.studios, {}) - meteorSubscribe(PubSub.showStyleBases, {}) - meteorSubscribe(PubSub.showStyleVariants, {}) - meteorSubscribe(PubSub.blueprints, {}) - meteorSubscribe(PubSub.peripheralDevices, {}) + meteorSubscribe(CorelibPubSub.studios, {}) + meteorSubscribe(CorelibPubSub.showStyleBases, {}) + meteorSubscribe(CorelibPubSub.showStyleVariants, {}) + meteorSubscribe(CorelibPubSub.blueprints, {}) + meteorSubscribe(CorelibPubSub.peripheralDevices, {}) return { studios: Studios.find({}).fetch(), diff --git a/meteor/client/ui/Settings/ShowStyle/BlueprintConfiguration/index.tsx b/meteor/client/ui/Settings/ShowStyle/BlueprintConfiguration/index.tsx index a09b55b851..ce7873a974 100644 --- a/meteor/client/ui/Settings/ShowStyle/BlueprintConfiguration/index.tsx +++ b/meteor/client/ui/Settings/ShowStyle/BlueprintConfiguration/index.tsx @@ -8,7 +8,7 @@ import { MappingsExt } from '@sofie-automation/corelib/dist/dataModel/Studio' import { DBShowStyleBase, SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { SelectConfigPreset } from './SelectConfigPreset' import { SelectBlueprint } from './SelectBlueprint' -import { PubSub } from '../../../../../lib/api/pubsub' +import { MeteorPubSub } from '../../../../../lib/api/pubsub' import { useSubscription, useTracker } from '../../../../lib/ReactMeteorData/ReactMeteorData' import { UIBlueprintUpgradeStatuses } from '../../../Collections' import { getUpgradeStatusMessage, UpgradeStatusButtons } from '../../Upgrades/Components' @@ -27,7 +27,7 @@ export function ShowStyleBaseBlueprintConfigurationSettings( ): JSX.Element { const { t } = useTranslation() - const isStatusReady = useSubscription(PubSub.uiBlueprintUpgradeStatuses) + const isStatusReady = useSubscription(MeteorPubSub.uiBlueprintUpgradeStatuses) const status = useTracker( () => UIBlueprintUpgradeStatuses.findOne({ diff --git a/meteor/client/ui/Settings/SnapshotsView.tsx b/meteor/client/ui/Settings/SnapshotsView.tsx index e2bef3278e..e50f02fbb8 100644 --- a/meteor/client/ui/Settings/SnapshotsView.tsx +++ b/meteor/client/ui/Settings/SnapshotsView.tsx @@ -13,12 +13,13 @@ import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { multilineText, fetchFrom } from '../../lib/lib' import { NotificationCenter, Notification, NoticeLevel } from '../../../lib/notifications/notifications' import { UploadButton } from '../../lib/uploadButton' -import { PubSub } from '../../../lib/api/pubsub' +import { MeteorPubSub } from '../../../lib/api/pubsub' import { MeteorCall } from '../../../lib/api/methods' import { SnapshotId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { Snapshots, Studios } from '../../collections' import { ClientAPI } from '../../../lib/api/client' import { hashSingleUseToken } from '../../../lib/api/userActions' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' interface IProps { match: { @@ -59,12 +60,12 @@ export default translateWithTracker(() => { } } componentDidMount(): void { - this.subscribe(PubSub.snapshots, { + this.subscribe(MeteorPubSub.snapshots, { created: { $gt: getCurrentTime() - 30 * 24 * 3600 * 1000, // last 30 days }, }) - this.subscribe(PubSub.studios, {}) + this.subscribe(CorelibPubSub.studios, {}) } onUploadFile(e: React.ChangeEvent) { diff --git a/meteor/client/ui/Settings/Studio/BlueprintConfiguration/index.tsx b/meteor/client/ui/Settings/Studio/BlueprintConfiguration/index.tsx index 7da0ace786..1ea97f6a46 100644 --- a/meteor/client/ui/Settings/Studio/BlueprintConfiguration/index.tsx +++ b/meteor/client/ui/Settings/Studio/BlueprintConfiguration/index.tsx @@ -12,7 +12,7 @@ import { useTranslation } from 'react-i18next' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { SelectConfigPreset } from './SelectConfigPreset' import { SelectBlueprint } from './SelectBlueprint' -import { PubSub } from '../../../../../lib/api/pubsub' +import { MeteorPubSub } from '../../../../../lib/api/pubsub' import { UIBlueprintUpgradeStatuses } from '../../../Collections' import { getUpgradeStatusMessage, UpgradeStatusButtons } from '../../Upgrades/Components' @@ -23,7 +23,7 @@ interface StudioBlueprintConfigurationSettingsProps { export function StudioBlueprintConfigurationSettings(props: StudioBlueprintConfigurationSettingsProps): JSX.Element { const { t } = useTranslation() - const isStatusReady = useSubscription(PubSub.uiBlueprintUpgradeStatuses) + const isStatusReady = useSubscription(MeteorPubSub.uiBlueprintUpgradeStatuses) const status = useTracker( () => UIBlueprintUpgradeStatuses.findOne({ diff --git a/meteor/client/ui/Settings/SystemManagement.tsx b/meteor/client/ui/Settings/SystemManagement.tsx index d2683b7114..878a6aa480 100644 --- a/meteor/client/ui/Settings/SystemManagement.tsx +++ b/meteor/client/ui/Settings/SystemManagement.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { translateWithTracker, Translated } from '../../lib/ReactMeteorData/ReactMeteorData' import { ICoreSystem, SofieLogo } from '../../../lib/collections/CoreSystem' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' -import { meteorSubscribe, PubSub } from '../../../lib/api/pubsub' +import { meteorSubscribe, MeteorPubSub } from '../../../lib/api/pubsub' import { EditAttribute } from '../../lib/EditAttribute' import { doModalDialog } from '../../lib/ModalDialog' import { MeteorCall } from '../../../lib/api/methods' @@ -30,7 +30,7 @@ export default translateWithTracker((_props: IProps) })( class SystemManagement extends MeteorReactComponent> { componentDidMount(): void { - meteorSubscribe(PubSub.coreSystem) + meteorSubscribe(MeteorPubSub.coreSystem) } cleanUpOldDatabaseIndexes(): void { const { t } = this.props diff --git a/meteor/client/ui/Settings/Upgrades/View.tsx b/meteor/client/ui/Settings/Upgrades/View.tsx index 059755e852..ca555e9fc3 100644 --- a/meteor/client/ui/Settings/Upgrades/View.tsx +++ b/meteor/client/ui/Settings/Upgrades/View.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next' import { Spinner } from '../../../lib/Spinner' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { useSubscription, useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData' -import { PubSub } from '../../../../lib/api/pubsub' +import { MeteorPubSub } from '../../../../lib/api/pubsub' import { UIBlueprintUpgradeStatuses } from '../../Collections' import { UIBlueprintUpgradeStatusShowStyle, UIBlueprintUpgradeStatusStudio } from '../../../../lib/api/upgradeStatus' import { getUpgradeStatusMessage, UpgradeStatusButtons } from './Components' @@ -11,7 +11,7 @@ import { getUpgradeStatusMessage, UpgradeStatusButtons } from './Components' export function UpgradesView(): JSX.Element { const { t } = useTranslation() - const isReady = useSubscription(PubSub.uiBlueprintUpgradeStatuses) + const isReady = useSubscription(MeteorPubSub.uiBlueprintUpgradeStatuses) const statuses = useTracker(() => UIBlueprintUpgradeStatuses.find().fetch(), []) diff --git a/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx b/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx index 5a539a3bba..9a9cc5d2fa 100644 --- a/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx +++ b/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useContext, useCallback } from 'react' import { useTranslation } from 'react-i18next' import { useSubscription, useTracker } from '../../../../lib/ReactMeteorData/ReactMeteorData' -import { PubSub } from '../../../../../lib/api/pubsub' +import { MeteorPubSub } from '../../../../../lib/api/pubsub' import { TriggeredActionsObj } from '../../../../../lib/collections/TriggeredActions' import { faCaretDown, faCaretRight, faDownload, faPlus, faUpload } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' @@ -31,6 +31,7 @@ import { PartInstances, Parts, RundownPlaylists, Rundowns, TriggeredActions } fr import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { SourceLayers, OutputLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { RundownPlaylistCollectionUtil } from '../../../../../lib/collections/rundownPlaylistUtil' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' export interface PreviewContext { rundownPlaylist: DBRundownPlaylist | null @@ -90,8 +91,8 @@ export const TriggeredActionsEditor: React.FC = function TriggeredAction ]), } - useSubscription(PubSub.triggeredActions, showStyleBaseSelector) - useSubscription(PubSub.rundowns, null, showStyleBaseId ? [showStyleBaseId] : []) + useSubscription(MeteorPubSub.triggeredActions, showStyleBaseSelector) + useSubscription(CorelibPubSub.rundowns, null, showStyleBaseId ? [showStyleBaseId] : []) useEffect(() => { const debounce = setTimeout(() => { @@ -166,7 +167,7 @@ export const TriggeredActionsEditor: React.FC = function TriggeredAction [showStyleBaseId, parsedTriggerFilter] ) - useSubscription(PubSub.rundownPlaylists, {}) + useSubscription(CorelibPubSub.rundownPlaylists, {}) const rundown = useTracker(() => { const activePlaylists = RundownPlaylists.find( @@ -205,8 +206,8 @@ export const TriggeredActionsEditor: React.FC = function TriggeredAction null ) - useSubscription(PubSub.partInstances, rundown ? [rundown._id] : [], rundownPlaylist?.activationId) - useSubscription(PubSub.parts, rundown ? [rundown._id] : []) + useSubscription(CorelibPubSub.partInstances, rundown ? [rundown._id] : [], rundownPlaylist?.activationId) + useSubscription(CorelibPubSub.parts, rundown ? [rundown._id] : []) const previewContext = useTracker( () => { diff --git a/meteor/client/ui/Settings/components/triggeredActions/triggerEditors/DeviceEditor.tsx b/meteor/client/ui/Settings/components/triggeredActions/triggerEditors/DeviceEditor.tsx index 6a6ceb05af..755c582518 100644 --- a/meteor/client/ui/Settings/components/triggeredActions/triggerEditors/DeviceEditor.tsx +++ b/meteor/client/ui/Settings/components/triggeredActions/triggerEditors/DeviceEditor.tsx @@ -2,7 +2,7 @@ import { IBlueprintDeviceTrigger } from '@sofie-automation/blueprints-integratio import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' import classNames from 'classnames' import React, { useMemo } from 'react' -import { PubSub } from '../../../../../../lib/api/pubsub' +import { MeteorPubSub } from '../../../../../../lib/api/pubsub' import { Studios } from '../../../../../collections' import { getCurrentTime } from '../../../../../../lib/lib' import { UIDeviceTriggerPreview } from '../../../../../../server/publications/deviceTriggersPreview' @@ -33,7 +33,7 @@ export const DeviceEditor = function DeviceEditor({ trigger, modified, readonly, ) const studio = useTracker(() => Studios.findOne(), [], undefined) - useSubscription(PubSub.deviceTriggersPreview, studio?._id ?? protectString('')) + useSubscription(MeteorPubSub.deviceTriggersPreview, studio?._id ?? protectString('')) return ( <> diff --git a/meteor/client/ui/Shelf/BucketPanel.tsx b/meteor/client/ui/Shelf/BucketPanel.tsx index 5a13eaf6cd..30f3d58ef6 100644 --- a/meteor/client/ui/Shelf/BucketPanel.tsx +++ b/meteor/client/ui/Shelf/BucketPanel.tsx @@ -23,7 +23,7 @@ import { IBlueprintActionTriggerMode, SomeContent, } from '@sofie-automation/blueprints-integration' -import { PubSub } from '../../../lib/api/pubsub' +import { MeteorPubSub } from '../../../lib/api/pubsub' import { doUserAction, getEventTimestamp, UserAction } from '../../../lib/clientUserAction' import { NotificationCenter, Notification, NoticeLevel } from '../../../lib/notifications/notifications' import { literal, unprotectString, partial, protectString } from '../../../lib/lib' @@ -72,6 +72,7 @@ import { ShowStyleVariantId, } from '@sofie-automation/corelib/dist/dataModel/Ids' import { RundownPlaylistCollectionUtil } from '../../../lib/collections/rundownPlaylistUtil' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' interface IBucketPanelDragObject { id: BucketId @@ -385,9 +386,9 @@ export const BucketPanel = translateWithTracker, I } componentDidMount(): void { - this.subscribe(PubSub.buckets, this.props.playlist.studioId, this.props.bucket._id) - this.subscribe(PubSub.uiBucketContentStatuses, this.props.playlist.studioId, this.props.bucket._id) - this.subscribe(PubSub.uiStudio, this.props.playlist.studioId) + this.subscribe(MeteorPubSub.buckets, this.props.playlist.studioId, this.props.bucket._id) + this.subscribe(MeteorPubSub.uiBucketContentStatuses, this.props.playlist.studioId, this.props.bucket._id) + this.subscribe(MeteorPubSub.uiStudio, this.props.playlist.studioId) this.autorun(() => { const showStyles: Array<[ShowStyleBaseId, ShowStyleVariantId]> = RundownPlaylistCollectionUtil.getRundownsUnordered(this.props.playlist).map((rundown) => [ @@ -396,14 +397,14 @@ export const BucketPanel = translateWithTracker, I ]) const showStyleBases = showStyles.map((showStyle) => showStyle[0]) const showStyleVariants = showStyles.map((showStyle) => showStyle[1]) - this.subscribe(PubSub.bucketAdLibPieces, { + this.subscribe(CorelibPubSub.bucketAdLibPieces, { bucketId: this.props.bucket._id, studioId: this.props.playlist.studioId, showStyleVariantId: { $in: [null, ...showStyleVariants], // null = valid for all variants }, }) - this.subscribe(PubSub.bucketAdLibActions, { + this.subscribe(CorelibPubSub.bucketAdLibActions, { bucketId: this.props.bucket._id, studioId: this.props.playlist.studioId, showStyleVariantId: { @@ -411,7 +412,7 @@ export const BucketPanel = translateWithTracker, I }, }) for (const showStyleBaseId of _.uniq(showStyleBases)) { - this.subscribe(PubSub.uiShowStyleBase, showStyleBaseId) + this.subscribe(MeteorPubSub.uiShowStyleBase, showStyleBaseId) } }) diff --git a/meteor/client/ui/Shelf/DashboardPanel.tsx b/meteor/client/ui/Shelf/DashboardPanel.tsx index 4364466e51..fddf988c5a 100644 --- a/meteor/client/ui/Shelf/DashboardPanel.tsx +++ b/meteor/client/ui/Shelf/DashboardPanel.tsx @@ -6,7 +6,6 @@ import ClassNames from 'classnames' import { Spinner } from '../../lib/Spinner' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { ISourceLayer, IBlueprintActionTriggerMode } from '@sofie-automation/blueprints-integration' -import { PubSub } from '../../../lib/api/pubsub' import { doUserAction, UserAction } from '../../../lib/clientUserAction' import { NotificationCenter, Notification, NoticeLevel } from '../../../lib/notifications/notifications' import { DashboardLayoutFilter, DashboardPanelUnit } from '../../../lib/collections/RundownLayouts' @@ -34,6 +33,7 @@ import { UIStudios } from '../Collections' import { Meteor } from 'meteor/meteor' import { PieceId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { RundownPlaylistCollectionUtil } from '../../../lib/collections/rundownPlaylistUtil' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' interface IState { outputLayers: OutputLayers @@ -167,7 +167,7 @@ export class DashboardPanelInner extends MeteorReactComponent< this.autorun(() => { const unorderedRundownIds = RundownPlaylistCollectionUtil.getRundownUnorderedIDs(this.props.playlist) if (unorderedRundownIds.length > 0) { - this.subscribe(PubSub.pieceInstances, { + this.subscribe(CorelibPubSub.pieceInstances, { rundownId: { $in: unorderedRundownIds, }, diff --git a/meteor/client/ui/Status.tsx b/meteor/client/ui/Status.tsx index 755ecbeea9..a3985854fc 100644 --- a/meteor/client/ui/Status.tsx +++ b/meteor/client/ui/Status.tsx @@ -9,9 +9,10 @@ import { ExternalMessages } from './Status/ExternalMessages' import { UserActivity } from './Status/UserActivity' import { EvaluationView } from './Status/Evaluations' import { MeteorReactComponent } from '../lib/MeteorReactComponent' -import { PubSub } from '../../lib/api/pubsub' +import { MeteorPubSub } from '../../lib/api/pubsub' import { ExpectedPackagesStatus } from './Status/package-status' import { MediaStatus } from './Status/media-status' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' interface IStatusMenuProps { match?: any @@ -79,10 +80,10 @@ class Status extends MeteorReactComponent> { componentDidMount(): void { // Subscribe to data: - this.subscribe(PubSub.peripheralDevices, {}) - this.subscribe(PubSub.uiStudio, null) - this.subscribe(PubSub.showStyleBases, {}) - this.subscribe(PubSub.showStyleVariants, {}) + this.subscribe(CorelibPubSub.peripheralDevices, {}) + this.subscribe(MeteorPubSub.uiStudio, null) + this.subscribe(CorelibPubSub.showStyleBases, {}) + this.subscribe(CorelibPubSub.showStyleVariants, {}) } render(): JSX.Element { // const { t } = this.props diff --git a/meteor/client/ui/Status/Evaluations.tsx b/meteor/client/ui/Status/Evaluations.tsx index 0de5e28ff3..2a630bf91f 100644 --- a/meteor/client/ui/Status/Evaluations.tsx +++ b/meteor/client/ui/Status/Evaluations.tsx @@ -9,7 +9,7 @@ import { Evaluation } from '../../../lib/collections/Evaluations' import { DatePickerFromTo } from '../../lib/datePicker' import moment from 'moment' import { getQuestionOptions } from '../AfterBroadcastForm' -import { PubSub, meteorSubscribe } from '../../../lib/api/pubsub' +import { MeteorPubSub, meteorSubscribe } from '../../../lib/api/pubsub' import { Evaluations } from '../../collections' interface IEvaluationProps {} @@ -63,7 +63,7 @@ const EvaluationView = translateWithTracker { this.setState({ @@ -159,7 +160,7 @@ const ExternalMessagesInStudio = translateWithTracker< if (this._sub) { this._sub.stop() } - this._sub = meteorSubscribe(PubSub.externalMessageQueue, { + this._sub = meteorSubscribe(CorelibPubSub.externalMessageQueue, { studioId: this.props.studioId, created: { $gte: this.state.dateFrom, diff --git a/meteor/client/ui/Status/MediaManager.tsx b/meteor/client/ui/Status/MediaManager.tsx index cb50493026..d03bcc95f3 100644 --- a/meteor/client/ui/Status/MediaManager.tsx +++ b/meteor/client/ui/Status/MediaManager.tsx @@ -14,7 +14,7 @@ import * as i18next from 'react-i18next' import { extendMandadory, unprotectString } from '../../../lib/lib' import * as _ from 'underscore' import { MeteorReactComponent } from '../../lib/MeteorReactComponent' -import { PubSub } from '../../../lib/api/pubsub' +import { MeteorPubSub } from '../../../lib/api/pubsub' import { Spinner } from '../../lib/Spinner' import { sofieWarningIcon as WarningIcon } from '../../lib/notifications/warningIcon' import { doUserAction, UserAction } from '../../../lib/clientUserAction' @@ -378,8 +378,8 @@ export const MediaManagerStatus = translateWithTracker { diff --git a/meteor/client/ui/Status/SystemStatus.tsx b/meteor/client/ui/Status/SystemStatus.tsx index c01c9e406d..4347cf39bc 100644 --- a/meteor/client/ui/Status/SystemStatus.tsx +++ b/meteor/client/ui/Status/SystemStatus.tsx @@ -19,7 +19,6 @@ import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { callPeripheralDeviceAction, PeripheralDevicesAPI } from '../../lib/clientAPI' import { NotificationCenter, NoticeLevel, Notification } from '../../../lib/notifications/notifications' import { getAllowConfigure, getAllowDeveloper, getAllowStudio, getHelpMode } from '../../lib/localStorage' -import { PubSub } from '../../../lib/api/pubsub' import ClassNames from 'classnames' import { StatusCode, TSR } from '@sofie-automation/blueprints-integration' import { ICoreSystem } from '../../../lib/collections/CoreSystem' @@ -40,6 +39,7 @@ import { JSONBlobParse } from '@sofie-automation/shared-lib/dist/lib/JSONBlob' import { ClientAPI } from '../../../lib/api/client' import { catchError } from '../../lib/lib' import { logger } from '../../../lib/logging' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' interface IDeviceItemProps { parentDevice: PeripheralDevice | null @@ -546,7 +546,7 @@ export default translateWithTracker(moment().startOf('day').valueOf()) const [dateTo, setDateTo] = useState
+
+ ) +} + +interface EvaluationRowProps { + questionOptions: Omit, 'i'>[] + evaluation: Evaluation +} + +function EvaluationRow({ questionOptions, evaluation }: Readonly): JSX.Element { + const tds = Object.entries(evaluation.answers).map(([key, answer]) => { + let str: string = answer + if (key === 'q0') { + for (const option of questionOptions) { + if (option.value === str) { + str = option.name + break } - this._sub = meteorSubscribe(MeteorPubSub.evaluations, this.state.dateFrom, this.state.dateTo) - } - } - componentWillUnmount(): void { - if (this._sub) { - this._sub.stop() } } - renderMessageHead() { - const { t } = this.props - return ( - - - {t('Timestamp')} - {t('User Name')} - {t('Rundown')} - - {t('Answers')} - - - - ) - } - handleChangeDate = (from: Time, to: Time) => { - this.setState({ - dateFrom: from, - dateTo: to, - }) - } + return ( + + {str} + + ) + }) - renderEvaluation() { - const { t } = this.props - return ( -
-
- -
- - {this.renderMessageHead()} - - {_.map( - _.filter(this.props.evaluations, (e) => { - return e.timestamp >= this.state.dateFrom && e.timestamp < this.state.dateTo - }), - (evaluation) => { - let tds = [ - , - , - , - ] - tds = tds.concat( - _.map(evaluation.answers, (answer, key) => { - let str: string = answer - if (key === 'q0') { - _.find(getQuestionOptions(t), (o) => { - if (o.value === str) { - str = o.name - return true - } - return false - }) - } - return ( - - ) - }) - ) - return {tds} - } - )} - -
- {evaluation.timestamp} - - {evaluation.answers && evaluation.answers.q2} - - {unprotectString(evaluation.playlistId)} - - {str} -
-
- ) - } + return ( + + + {evaluation.timestamp} + + + {evaluation.answers.q2 || ''} + + + {unprotectString(evaluation.playlistId)} + - render(): JSX.Element { - const { t } = this.props - return ( -
-
-

{t('Evaluations')}

-
-
{this.renderEvaluation()}
-
- ) - } - } -) -export { EvaluationView } + {tds} + + ) +} From 4813d8f8d355f0b0b595f4e2933551b5db2503d5 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 12 Dec 2023 10:44:11 +0000 Subject: [PATCH 175/479] chore: refactor `withMediaObjectStatus` to be functional SOFIE-2751 (#1082) Co-authored-by: Jan Starzak --- .../ui/ClockView/CameraScreen/Segment.tsx | 4 +- meteor/client/ui/RundownView.tsx | 2 +- .../SegmentContainer/withResolvedSegment.ts | 16 +- .../ui/SegmentList/SegmentListContainer.tsx | 4 +- .../SegmentScratchpadContainer.tsx | 4 +- .../SegmentStoryboardContainer.tsx | 4 +- .../SegmentTimelineContainer.tsx | 12 +- .../SegmentTimeline/withMediaObjectStatus.tsx | 151 +++++++----------- 8 files changed, 86 insertions(+), 111 deletions(-) diff --git a/meteor/client/ui/ClockView/CameraScreen/Segment.tsx b/meteor/client/ui/ClockView/CameraScreen/Segment.tsx index 9149083a68..be7d7d087b 100644 --- a/meteor/client/ui/ClockView/CameraScreen/Segment.tsx +++ b/meteor/client/ui/ClockView/CameraScreen/Segment.tsx @@ -4,8 +4,8 @@ import React, { useContext, useMemo } from 'react' import { ActivePartInstancesContext, PieceFilter } from '.' import { withResolvedSegment, - IProps as IWithResolvedSegmentProps, - ITrackedProps as IWithResolvedSegmentInjectedProps, + IResolvedSegmentProps as IWithResolvedSegmentProps, + ITrackedResolvedSegmentProps as IWithResolvedSegmentInjectedProps, } from '../../SegmentContainer/withResolvedSegment' import { OrderedPartsContext } from './OrderedPartsProvider' import { Part } from './Part' diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx index e9cecfd24a..d4fb3061d9 100644 --- a/meteor/client/ui/RundownView.tsx +++ b/meteor/client/ui/RundownView.tsx @@ -119,7 +119,7 @@ import { matchFilter } from './Shelf/AdLibListView' import { ExecuteActionResult } from '@sofie-automation/corelib/dist/worker/studio' import { SegmentListContainer } from './SegmentList/SegmentListContainer' import { getNextMode as getNextSegmentViewMode } from './SegmentContainer/SwitchViewModeButton' -import { IProps as IResolvedSegmentProps } from './SegmentContainer/withResolvedSegment' +import { IResolvedSegmentProps } from './SegmentContainer/withResolvedSegment' import { UIShowStyleBases, UIStudios } from './Collections' import { UIStudio } from '../../lib/api/studios' import { diff --git a/meteor/client/ui/SegmentContainer/withResolvedSegment.ts b/meteor/client/ui/SegmentContainer/withResolvedSegment.ts index 3fd0fa997c..a0f56852a1 100644 --- a/meteor/client/ui/SegmentContainer/withResolvedSegment.ts +++ b/meteor/client/ui/SegmentContainer/withResolvedSegment.ts @@ -67,7 +67,7 @@ export type MinimalRundown = Pick pieces: Map @@ -124,10 +124,10 @@ type IWrappedComponent = | React.ComponentClass | ((props: IProps & TrackedProps) => JSX.Element | null) -export function withResolvedSegment( - WrappedComponent: IWrappedComponent +export function withResolvedSegment( + WrappedComponent: IWrappedComponent ): new (props: T) => React.Component { - return withTracker( + return withTracker( (props: T) => { const segment = Segments.findOne(props.segmentId) as SegmentUi | undefined @@ -314,7 +314,11 @@ export function withResolvedSegment( isScratchpad, } }, - (data: ITrackedProps, props: IProps, nextProps: IProps): boolean => { + ( + data: ITrackedResolvedSegmentProps, + props: IResolvedSegmentProps, + nextProps: IResolvedSegmentProps + ): boolean => { // This is a potentailly very dangerous hook into the React component lifecycle. Re-use with caution. // Check obvious primitive changes if ( diff --git a/meteor/client/ui/SegmentList/SegmentListContainer.tsx b/meteor/client/ui/SegmentList/SegmentListContainer.tsx index e2e05107f8..62aeb70687 100644 --- a/meteor/client/ui/SegmentList/SegmentListContainer.tsx +++ b/meteor/client/ui/SegmentList/SegmentListContainer.tsx @@ -3,8 +3,8 @@ import { meteorSubscribe } from '../../../lib/api/pubsub' import { useSubscription, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { withResolvedSegment, - IProps as IResolvedSegmentProps, - ITrackedProps as ITrackedResolvedSegmentProps, + IResolvedSegmentProps, + ITrackedResolvedSegmentProps, } from '../SegmentContainer/withResolvedSegment' import { SpeechSynthesiser } from '../../lib/speechSynthesis' import { SegmentList } from './SegmentList' diff --git a/meteor/client/ui/SegmentScratchpad/SegmentScratchpadContainer.tsx b/meteor/client/ui/SegmentScratchpad/SegmentScratchpadContainer.tsx index 1ca6b8f2bc..faf192ae46 100644 --- a/meteor/client/ui/SegmentScratchpad/SegmentScratchpadContainer.tsx +++ b/meteor/client/ui/SegmentScratchpad/SegmentScratchpadContainer.tsx @@ -4,8 +4,8 @@ import { useSubscription, useTracker } from '../../lib/ReactMeteorData/ReactMete import { // PartUi, withResolvedSegment, - IProps as IResolvedSegmentProps, - ITrackedProps as ITrackedResolvedSegmentProps, + IResolvedSegmentProps, + ITrackedResolvedSegmentProps, } from '../SegmentContainer/withResolvedSegment' import { SpeechSynthesiser } from '../../lib/speechSynthesis' import { SegmentScratchpad } from './SegmentScratchpad' diff --git a/meteor/client/ui/SegmentStoryboard/SegmentStoryboardContainer.tsx b/meteor/client/ui/SegmentStoryboard/SegmentStoryboardContainer.tsx index dc58c1020a..157951927d 100644 --- a/meteor/client/ui/SegmentStoryboard/SegmentStoryboardContainer.tsx +++ b/meteor/client/ui/SegmentStoryboard/SegmentStoryboardContainer.tsx @@ -4,8 +4,8 @@ import { useSubscription, useTracker } from '../../lib/ReactMeteorData/ReactMete import { // PartUi, withResolvedSegment, - IProps as IResolvedSegmentProps, - ITrackedProps as ITrackedResolvedSegmentProps, + IResolvedSegmentProps, + ITrackedResolvedSegmentProps, } from '../SegmentContainer/withResolvedSegment' import { SpeechSynthesiser } from '../../lib/speechSynthesis' import { SegmentStoryboard } from './SegmentStoryboard' diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx index 5912c2796f..c3620af664 100644 --- a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx +++ b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx @@ -24,8 +24,8 @@ import { SegmentTimelinePartClass } from './Parts/SegmentTimelinePart' import { PartUi, withResolvedSegment, - IProps as IResolvedSegmentProps, - ITrackedProps, + IResolvedSegmentProps, + ITrackedResolvedSegmentProps, IOutputLayerUi, } from '../SegmentContainer/withResolvedSegment' import { computeSegmentDuration, getPartInstanceTimingId, RundownTimingContext } from '../../lib/rundownTiming' @@ -80,7 +80,7 @@ interface IProps extends IResolvedSegmentProps { } export const SegmentTimelineContainer = withResolvedSegment( - class SegmentTimelineContainer extends MeteorReactComponent { + class SegmentTimelineContainer extends MeteorReactComponent { static contextTypes = { durations: PropTypes.object.isRequired, syncedDurations: PropTypes.object.isRequired, @@ -100,7 +100,7 @@ export const SegmentTimelineContainer = withResolvedSegment( private pastInfinitesComp: Tracker.Computation | undefined - constructor(props: IProps & ITrackedProps) { + constructor(props: IProps & ITrackedResolvedSegmentProps) { super(props) this.state = { @@ -128,7 +128,7 @@ export const SegmentTimelineContainer = withResolvedSegment( this.isVisible = false } - shouldComponentUpdate(nextProps: IProps & ITrackedProps, nextState: IState) { + shouldComponentUpdate(nextProps: IProps & ITrackedResolvedSegmentProps, nextState: IState) { return !_.isMatch(this.props, nextProps) || !_.isMatch(this.state, nextState) } @@ -204,7 +204,7 @@ export const SegmentTimelineContainer = withResolvedSegment( .catch(catchError('updateMaxTimeScale')) } - componentDidUpdate(prevProps: IProps & ITrackedProps) { + componentDidUpdate(prevProps: IProps & ITrackedResolvedSegmentProps) { let isLiveSegment = false let isNextSegment = false let currentLivePart: PartExtended | undefined = undefined diff --git a/meteor/client/ui/SegmentTimeline/withMediaObjectStatus.tsx b/meteor/client/ui/SegmentTimeline/withMediaObjectStatus.tsx index 6f14c827d8..32d44771d6 100644 --- a/meteor/client/ui/SegmentTimeline/withMediaObjectStatus.tsx +++ b/meteor/client/ui/SegmentTimeline/withMediaObjectStatus.tsx @@ -1,7 +1,5 @@ -import * as React from 'react' -import { Tracker } from 'meteor/tracker' +import React, { useEffect, useState } from 'react' import { PieceUi } from './SegmentTimelineContainer' -import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { ISourceLayer } from '@sofie-automation/blueprints-integration' import { RundownUtils } from '../../lib/rundown' import { IAdLibListItem } from '../Shelf/AdLibListItem' @@ -45,113 +43,86 @@ const DEFAULT_STATUS = deepFreeze({ contentDuration: undefined, }) +function unwrapPieceInstance(piece: BucketAdLibUi | IAdLibListItem | AdLibPieceUi | PieceUi | BucketAdLibActionUi) { + if (RundownUtils.isPieceInstance(piece)) { + return piece.instance.piece + } else { + return piece + } +} + /** * @deprecated This can now be achieved by a simple minimongo query against either UIPieceContentStatuses or UIBucketContentStatuses */ export function withMediaObjectStatus(): ( WrappedComponent: IWrappedComponent | React.FC -) => new (props: IProps, context: any) => React.Component { +) => React.FC { return (WrappedComponent) => { - return class WithMediaObjectStatusHOCComponent extends MeteorReactComponent { - private statusComp: Tracker.Computation - private overrides: Partial - private destroyed: boolean - - private shouldDataTrackerUpdate(prevProps: IProps): boolean { - if (this.props.piece !== prevProps.piece) return true - if (this.props.studio !== prevProps.studio) return true - if (this.props.isLiveLine !== prevProps.isLiveLine) return true - return false - } - - private static unwrapPieceInstance( - piece: BucketAdLibUi | IAdLibListItem | AdLibPieceUi | PieceUi | BucketAdLibActionUi - ) { - if (RundownUtils.isPieceInstance(piece)) { - return piece.instance.piece - } else { - return piece + return function WithMediaObjectStatusHOCComponent(props: IProps) { + const [invalidationToken, setInvalidationToken] = useState(Date.now()) + useEffect(() => { + // Force an invalidation shortly after mounting + const callback = window.requestIdleCallback( + () => { + setInvalidationToken(Date.now()) + }, + { + timeout: 500, + } + ) + return () => { + window.cancelIdleCallback(callback) } - } - - updateDataTracker() { - if (this.destroyed) return - - this.statusComp = this.autorun(() => { - const { piece, studio, layer } = this.props - this.overrides = {} - const overrides = this.overrides - - // Check item status - if (piece && (piece.sourceLayer || layer) && studio) { - const pieceUnwrapped = WithMediaObjectStatusHOCComponent.unwrapPieceInstance(piece) - const statusDoc = RundownUtils.isBucketAdLibItem(piece) - ? UIBucketContentStatuses.findOne({ - bucketId: piece.bucketId, - docId: pieceUnwrapped._id, - }) - : UIPieceContentStatuses.findOne({ - // Future: It would be good for this to be stricter. - pieceId: pieceUnwrapped._id, - }) - - // Extract the status or populate some default values - const statusObj = statusDoc?.status ?? DEFAULT_STATUS - - if (RundownUtils.isAdLibPieceOrAdLibListItem(piece)) { - if (!overrides.piece || !_.isEqual(statusObj, (overrides.piece as AdLibPieceUi).contentStatus)) { - // Deep clone the required bits - const origPiece = (overrides.piece || this.props.piece) as AdLibPieceUi - const pieceCopy: AdLibPieceUi = { - ...origPiece, - - contentStatus: statusObj, - } - - overrides.piece = pieceCopy - } - } else if (!overrides.piece || !_.isEqual(statusObj, (overrides.piece as PieceUi).contentStatus)) { + }, []) + + const overrides = useTracker(() => { + const { piece, studio, layer } = props + const overrides: Partial = {} + + // Check item status + if (piece && (piece.sourceLayer || layer) && studio) { + const pieceUnwrapped = unwrapPieceInstance(piece) + const statusDoc = RundownUtils.isBucketAdLibItem(piece) + ? UIBucketContentStatuses.findOne({ + bucketId: piece.bucketId, + docId: pieceUnwrapped._id, + }) + : UIPieceContentStatuses.findOne({ + // Future: It would be good for this to be stricter. + pieceId: pieceUnwrapped._id, + }) + + // Extract the status or populate some default values + const statusObj = statusDoc?.status ?? DEFAULT_STATUS + + if (RundownUtils.isAdLibPieceOrAdLibListItem(piece)) { + if (!overrides.piece || !_.isEqual(statusObj, (overrides.piece as AdLibPieceUi).contentStatus)) { // Deep clone the required bits - const pieceCopy: PieceUi = { - ...((overrides.piece || piece) as PieceUi), + const origPiece = (overrides.piece || props.piece) as AdLibPieceUi + const pieceCopy: AdLibPieceUi = { + ...origPiece, contentStatus: statusObj, } overrides.piece = pieceCopy } - } + } else if (!overrides.piece || !_.isEqual(statusObj, (overrides.piece as PieceUi).contentStatus)) { + // Deep clone the required bits + const pieceCopy: PieceUi = { + ...((overrides.piece || piece) as PieceUi), - this.forceUpdate() - }) - } + contentStatus: statusObj, + } - componentDidMount(): void { - window.requestIdleCallback( - () => { - this.updateDataTracker() - }, - { - timeout: 500, + overrides.piece = pieceCopy } - ) - } - - componentDidUpdate(prevProps: IProps) { - if (this.shouldDataTrackerUpdate(prevProps)) { - if (this.statusComp) this.statusComp.invalidate() } - } - - componentWillUnmount(): void { - this.destroyed = true - super.componentWillUnmount() - } + return overrides + }, [props.piece, props.studio, props.isLiveLine, invalidationToken]) - render(): JSX.Element { - return - } + return } } } From b4648d2e03cce09eb1fff8e3b9292dc1c8739801 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 14 Dec 2023 18:22:43 +0000 Subject: [PATCH 176/479] fix: add `RundownOrphanedReason` enum --- packages/corelib/src/dataModel/Rundown.ts | 11 +++++++- .../src/ingest/__tests__/ingest.test.ts | 26 +++++++++++-------- packages/job-worker/src/ingest/commit.ts | 4 +-- .../src/ingest/ingestRundownJobs.ts | 4 +-- .../mosDevice/__tests__/mosIngest.test.ts | 22 +++++++++------- packages/job-worker/src/playout/snapshot.ts | 3 ++- 6 files changed, 44 insertions(+), 26 deletions(-) diff --git a/packages/corelib/src/dataModel/Rundown.ts b/packages/corelib/src/dataModel/Rundown.ts index 41fab73264..c282b55e5a 100644 --- a/packages/corelib/src/dataModel/Rundown.ts +++ b/packages/corelib/src/dataModel/Rundown.ts @@ -10,6 +10,15 @@ import { } from './Ids' import { RundownNote } from './Notes' +export enum RundownOrphanedReason { + /** Rundown is deleted from the NRCS but we still need it */ + DELETED = 'deleted', + /** Rundown was restored from a snapshot and does not correspond with a rundown in the NRCS */ + FROM_SNAPSHOT = 'from-snapshot', + /** Rundown was unsynced by the user */ + MANUAL = 'manual', +} + export interface RundownImportVersions { studio: string showStyleBase: string @@ -47,7 +56,7 @@ export interface Rundown { // There should be something like a Owner user here somewhere? /** Is the rundown in an unsynced (has been unpublished from ENPS) state? */ - orphaned?: 'deleted' | 'from-snapshot' | 'manual' + orphaned?: RundownOrphanedReason /** Last sent storyStatus to ingestDevice (MOS) */ notifiedCurrentPlayingPartExternalId?: string diff --git a/packages/job-worker/src/ingest/__tests__/ingest.test.ts b/packages/job-worker/src/ingest/__tests__/ingest.test.ts index ed49bf6fad..358bc6cc72 100644 --- a/packages/job-worker/src/ingest/__tests__/ingest.test.ts +++ b/packages/job-worker/src/ingest/__tests__/ingest.test.ts @@ -14,7 +14,7 @@ import { PeripheralDeviceType, PERIPHERAL_SUBTYPE_PROCESS, } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' -import { DBRundown, Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { DBRundown, Rundown, RundownOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { DBSegment, SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' import { getRandomString, literal } from '@sofie-automation/corelib/dist/lib' @@ -733,11 +733,11 @@ describe('Test ingest actions for rundowns and segments', () => { }) await expect(context.mockCollections.Rundowns.findOne()).resolves.toBeTruthy() - await context.mockCollections.Rundowns.update({}, { $set: { orphaned: 'deleted' } }) + await context.mockCollections.Rundowns.update({}, { $set: { orphaned: RundownOrphanedReason.DELETED } }) const rundown0 = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown expect(rundown0).toBeTruthy() - expect(rundown0.orphaned).toEqual('deleted') + expect(rundown0.orphaned).toEqual(RundownOrphanedReason.DELETED) await expect(context.mockCollections.Segments.findFetch({ rundownId: rundown0._id })).resolves.toHaveLength(2) const rundownData: IngestRundown = { @@ -771,12 +771,14 @@ describe('Test ingest actions for rundowns and segments', () => { // Segment count should not have changed const rundown1 = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown expect(rundown1).toBeTruthy() - expect(rundown1.orphaned).toEqual('deleted') + expect(rundown1.orphaned).toEqual(RundownOrphanedReason.DELETED) await expect(context.mockCollections.Segments.findFetch({ rundownId: rundown1._id })).resolves.toHaveLength(2) }) test('dataRundownCreate replace orphaned rundown', async () => { - await expect(context.mockCollections.Rundowns.findFetch({ orphaned: 'deleted' })).resolves.toHaveLength(1) + await expect( + context.mockCollections.Rundowns.findFetch({ orphaned: RundownOrphanedReason.DELETED }) + ).resolves.toHaveLength(1) const rundownData: IngestRundown = { externalId: externalId, @@ -817,16 +819,18 @@ describe('Test ingest actions for rundowns and segments', () => { isCreateAction: true, }) - await expect(context.mockCollections.Rundowns.findFetch({ orphaned: 'deleted' })).resolves.toHaveLength(0) + await expect( + context.mockCollections.Rundowns.findFetch({ orphaned: RundownOrphanedReason.DELETED }) + ).resolves.toHaveLength(0) }) test('dataSegmentCreate in deleted rundown', async () => { await expect(context.mockCollections.Rundowns.findOne()).resolves.toBeTruthy() - await context.mockCollections.Rundowns.update({}, { $set: { orphaned: 'deleted' } }) + await context.mockCollections.Rundowns.update({}, { $set: { orphaned: RundownOrphanedReason.DELETED } }) const rundown0 = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown expect(rundown0).toBeTruthy() - expect(rundown0.orphaned).toEqual('deleted') + expect(rundown0.orphaned).toEqual(RundownOrphanedReason.DELETED) await expect(context.mockCollections.Segments.findFetch({ rundownId: rundown0._id })).resolves.toHaveLength(2) const ingestSegment: IngestSegment = { @@ -1001,7 +1005,7 @@ describe('Test ingest actions for rundowns and segments', () => { test('dataSegmentUpdate deleted rundown', async () => { const rundown = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown expect(rundown).toBeTruthy() - await context.mockCollections.Rundowns.update({}, { $set: { orphaned: 'deleted' } }) + await context.mockCollections.Rundowns.update({}, { $set: { orphaned: RundownOrphanedReason.DELETED } }) await context.mockCollections.Segments.update({ rundownId: rundown._id }, { $unset: { orphaned: 1 } }) await expect(context.mockCollections.Segments.findFetch({ rundownId: rundown._id })).resolves.toHaveLength(3) @@ -1238,7 +1242,7 @@ describe('Test ingest actions for rundowns and segments', () => { context.mockCollections.Segments.findFetch({ rundownId: rundown._id, externalId: segExternalId }) ).resolves.toHaveLength(1) - await context.mockCollections.Rundowns.update({}, { $set: { orphaned: 'deleted' } }) + await context.mockCollections.Rundowns.update({}, { $set: { orphaned: RundownOrphanedReason.DELETED } }) await context.mockCollections.Segments.update({ rundownId: rundown._id }, { $unset: { orphaned: 1 } }) await expect(context.mockCollections.Segments.findFetch({ rundownId: rundown._id })).resolves.toHaveLength(3) @@ -1652,7 +1656,7 @@ describe('Test ingest actions for rundowns and segments', () => { rundownExternalId: rundownData.externalId, }) ).rejects.toMatchUserError(UserErrorMessage.RundownRemoveWhileActive) - await expect(getRundownOrphaned()).resolves.toEqual('deleted') + await expect(getRundownOrphaned()).resolves.toEqual(RundownOrphanedReason.DELETED) await resyncRundown() await expect(getRundownOrphaned()).resolves.toBeUndefined() diff --git a/packages/job-worker/src/ingest/commit.ts b/packages/job-worker/src/ingest/commit.ts index 664f33ee8e..3f6f6f1633 100644 --- a/packages/job-worker/src/ingest/commit.ts +++ b/packages/job-worker/src/ingest/commit.ts @@ -5,7 +5,7 @@ import { RundownId, PartInstanceId, } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { DBRundown, RundownOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { unprotectString, protectString } from '@sofie-automation/corelib/dist/protectedString' import { logger } from '../logging' import { PlayoutModel } from '../playout/model/PlayoutModel' @@ -625,7 +625,7 @@ function setRundownAsTrapepdInPlaylist( if (rundownIsToBeRemoved) { // Orphan the deleted rundown ingestCache.Rundown.update((rd) => { - rd.orphaned = 'deleted' + rd.orphaned = RundownOrphanedReason.DELETED return rd }) } else { diff --git a/packages/job-worker/src/ingest/ingestRundownJobs.ts b/packages/job-worker/src/ingest/ingestRundownJobs.ts index 6211ba86c7..96b932585b 100644 --- a/packages/job-worker/src/ingest/ingestRundownJobs.ts +++ b/packages/job-worker/src/ingest/ingestRundownJobs.ts @@ -6,7 +6,7 @@ import { canRundownBeUpdated, getRundown } from './lib' import { CommitIngestData, runIngestJob, runWithRundownLock, UpdateIngestRundownAction } from './lock' import { removeRundownFromDb } from '../rundownPlaylists' import { literal } from '@sofie-automation/corelib/dist/lib' -import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { DBRundown, RundownOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { IngestRegenerateRundownProps, IngestRemoveRundownProps, @@ -183,7 +183,7 @@ export async function handleUserUnsyncRundown(context: JobContext, data: UserUns if (!rundown.orphaned) { await context.directCollections.Rundowns.update(rundown._id, { $set: { - orphaned: 'manual', + orphaned: RundownOrphanedReason.MANUAL, }, }) } else { diff --git a/packages/job-worker/src/ingest/mosDevice/__tests__/mosIngest.test.ts b/packages/job-worker/src/ingest/mosDevice/__tests__/mosIngest.test.ts index 7797eed4a3..56c6fafb6a 100644 --- a/packages/job-worker/src/ingest/mosDevice/__tests__/mosIngest.test.ts +++ b/packages/job-worker/src/ingest/mosDevice/__tests__/mosIngest.test.ts @@ -18,7 +18,7 @@ import { parseMosString } from '../lib' import { MockJobContext, setupDefaultJobEnvironment } from '../../../__mocks__/context' import { setupMockIngestDevice, setupMockShowStyleCompound } from '../../../__mocks__/presetCollections' import { fixSnapshot } from '../../../__mocks__/helpers/snapshot' -import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { DBRundown, RundownOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { MongoQuery } from '../../../db' import { handleRemovedRundown } from '../../ingestRundownJobs' @@ -207,7 +207,7 @@ describe('Test recieved mos ingest payloads', () => { await context.mockCollections.Rundowns.update( { externalId: mosTypes.mosString128.stringify(roData.ID) }, - { $set: { orphaned: 'deleted' } } + { $set: { orphaned: RundownOrphanedReason.DELETED } } ) expect( await context.mockCollections.Rundowns.findOne({ externalId: mosTypes.mosString128.stringify(roData.ID) }) @@ -237,7 +237,7 @@ describe('Test recieved mos ingest payloads', () => { const roData = mockRO.roCreate() await context.mockCollections.Rundowns.update( { externalId: mosTypes.mosString128.stringify(roData.ID) }, - { $set: { orphaned: 'deleted' } } + { $set: { orphaned: RundownOrphanedReason.DELETED } } ) const rundown = (await context.mockCollections.Rundowns.findOne({ @@ -317,7 +317,7 @@ describe('Test recieved mos ingest payloads', () => { }) test('mosRoStatus: orphaned rundown', async () => { - await context.mockCollections.Rundowns.update({}, { $set: { orphaned: 'deleted' } }) + await context.mockCollections.Rundowns.update({}, { $set: { orphaned: RundownOrphanedReason.DELETED } }) const newStatus = MOS.IMOSObjectStatus.UPDATED @@ -377,7 +377,7 @@ describe('Test recieved mos ingest payloads', () => { }) test('mosRoReadyToAir: orphaned rundown', async () => { - await context.mockCollections.Rundowns.update({}, { $set: { orphaned: 'deleted' } }) + await context.mockCollections.Rundowns.update({}, { $set: { orphaned: RundownOrphanedReason.DELETED } }) const newStatus = MOS.IMOSObjectAirStatus.NOT_READY @@ -451,7 +451,7 @@ describe('Test recieved mos ingest payloads', () => { }) test('mosRoStoryInsert: orphaned rundown', async () => { - await context.mockCollections.Rundowns.update({}, { $set: { orphaned: 'deleted' } }) + await context.mockCollections.Rundowns.update({}, { $set: { orphaned: RundownOrphanedReason.DELETED } }) const playlist = (await context.mockCollections.RundownPlaylists.findOne()) as DBRundownPlaylist expect(playlist).toBeTruthy() @@ -471,7 +471,9 @@ describe('Test recieved mos ingest payloads', () => { const { parts } = await getRundownData({ _id: rundown._id }) - expect((await context.mockCollections.Rundowns.findOne(rundown._id))?.orphaned).toEqual('deleted') + expect((await context.mockCollections.Rundowns.findOne(rundown._id))?.orphaned).toEqual( + RundownOrphanedReason.DELETED + ) expect(parts.find((p) => p.externalId === mosTypes.mosString128.stringify(newPartData.ID))).toBeUndefined() }) @@ -619,7 +621,7 @@ describe('Test recieved mos ingest payloads', () => { }) test('mosRoStoryReplace: orphaned rundown', async () => { - await context.mockCollections.Rundowns.update({}, { $set: { orphaned: 'deleted' } }) + await context.mockCollections.Rundowns.update({}, { $set: { orphaned: RundownOrphanedReason.DELETED } }) const playlist = (await context.mockCollections.RundownPlaylists.findOne()) as DBRundownPlaylist expect(playlist).toBeTruthy() @@ -638,7 +640,9 @@ describe('Test recieved mos ingest payloads', () => { }) const { parts } = await getRundownData({ _id: rundown._id }) - expect((await context.mockCollections.Rundowns.findOne(rundown._id))?.orphaned).toEqual('deleted') + expect((await context.mockCollections.Rundowns.findOne(rundown._id))?.orphaned).toEqual( + RundownOrphanedReason.DELETED + ) expect(parts.find((p) => p.externalId === mosTypes.mosString128.stringify(newPartData.ID))).toBeUndefined() }) diff --git a/packages/job-worker/src/playout/snapshot.ts b/packages/job-worker/src/playout/snapshot.ts index e1d4300b4a..3ad9c1a21d 100644 --- a/packages/job-worker/src/playout/snapshot.ts +++ b/packages/job-worker/src/playout/snapshot.ts @@ -34,6 +34,7 @@ import { assertNever, getRandomId, literal } from '@sofie-automation/corelib/dis import { logger } from '../logging' import { JSONBlobParse, JSONBlobStringify } from '@sofie-automation/shared-lib/dist/lib/JSONBlob' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { RundownOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Rundown' /** * Generate the Playlist owned portions of a Playlist snapshot @@ -161,7 +162,7 @@ export async function handleRestorePlaylistSnapshot( for (const rd of snapshot.rundowns) { if (!rd.orphaned) { - rd.orphaned = 'from-snapshot' + rd.orphaned = RundownOrphanedReason.FROM_SNAPSHOT } rd.playlistId = playlistId From ebdd1e9ddd78ba8a647ade8a58a1f88bd334a8f2 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 14 Dec 2023 18:23:33 +0000 Subject: [PATCH 177/479] chore: remove unused `baselineModifyHash` field --- packages/corelib/src/dataModel/Rundown.ts | 2 - .../src/__mocks__/helpers/snapshot.ts | 1 - .../src/ingest/generationRundown.ts | 64 ++++++++----------- 3 files changed, 26 insertions(+), 41 deletions(-) diff --git a/packages/corelib/src/dataModel/Rundown.ts b/packages/corelib/src/dataModel/Rundown.ts index c282b55e5a..2eb95fd633 100644 --- a/packages/corelib/src/dataModel/Rundown.ts +++ b/packages/corelib/src/dataModel/Rundown.ts @@ -87,8 +87,6 @@ export interface Rundown { playlistId: RundownPlaylistId /** If the playlistId has ben set manually by a user in Sofie */ playlistIdIsSetInSofie?: boolean - /** Whenever the baseline (RundownBaselineObjs, RundownBaselineAdLibItems, RundownBaselineAdLibActions) changes, this is changed too */ - baselineModifyHash?: string } /** Note: Use Rundown instead */ diff --git a/packages/job-worker/src/__mocks__/helpers/snapshot.ts b/packages/job-worker/src/__mocks__/helpers/snapshot.ts index 4d5b35f92a..09b0f0e27d 100644 --- a/packages/job-worker/src/__mocks__/helpers/snapshot.ts +++ b/packages/job-worker/src/__mocks__/helpers/snapshot.ts @@ -69,7 +69,6 @@ export function fixSnapshot(data: Data | Array, sortData?: boolean): Data } else if (isRundown(o)) { o['created'] = 0 o['modified'] = 0 - delete o['baselineModifyHash'] // TODO - is this ok? if (o.importVersions.core) { // re-write the core version so something static, so tests won't fail just because the version has changed o.importVersions.core = '0.0.0-test' diff --git a/packages/job-worker/src/ingest/generationRundown.ts b/packages/job-worker/src/ingest/generationRundown.ts index 4e27632c89..df8c88f844 100644 --- a/packages/job-worker/src/ingest/generationRundown.ts +++ b/packages/job-worker/src/ingest/generationRundown.ts @@ -19,7 +19,6 @@ import { postProcessRundownBaselineItems, } from '../blueprints/postProcess' import { saveIntoCache } from '../cache/lib' -import { sumChanges, anythingChanged } from '../db/changes' import { getCurrentTime, getSystemVersion } from '../lib' import { logger } from '../logging' import _ = require('underscore') @@ -367,9 +366,7 @@ export async function getRundownFromIngestData( // validated later playlistId: protectString(''), - ...(cache.Rundown.doc - ? _.pick(cache.Rundown.doc, 'playlistId', 'baselineModifyHash', 'airStatus', 'status') - : {}), + ...(cache.Rundown.doc ? _.pick(cache.Rundown.doc, 'playlistId', 'airStatus', 'status') : {}), }) return { dbRundownData, rundownRes } @@ -400,43 +397,34 @@ export async function saveChangesForRundown( logger.info(`... got ${(rundownRes.globalActions || []).length} adLib actions from baseline.`) const { baselineObjects, baselineAdlibPieces, baselineAdlibActions } = await cache.loadBaselineCollections() - const rundownBaselineChanges = sumChanges( - saveIntoCache(context, baselineObjects, null, [ - { - _id: getRandomId(7), - rundownId: dbRundown._id, - timelineObjectsString: serializePieceTimelineObjectsBlob( - postProcessRundownBaselineItems(showStyle.base.blueprintId, rundownRes.baseline.timelineObjects) - ), - }, - ]), - // Save the global adlibs - saveIntoCache( - context, - baselineAdlibPieces, - null, - postProcessAdLibPieces( - context, - showStyle.base.blueprintId, - dbRundown._id, - undefined, - rundownRes.globalAdLibPieces - ) - ), - saveIntoCache( + saveIntoCache(context, baselineObjects, null, [ + { + _id: getRandomId(7), + rundownId: dbRundown._id, + timelineObjectsString: serializePieceTimelineObjectsBlob( + postProcessRundownBaselineItems(showStyle.base.blueprintId, rundownRes.baseline.timelineObjects) + ), + }, + ]) + // Save the global adlibs + saveIntoCache( + context, + baselineAdlibPieces, + null, + postProcessAdLibPieces( context, - baselineAdlibActions, - null, - postProcessGlobalAdLibActions(showStyle.base.blueprintId, dbRundown._id, rundownRes.globalActions || []) + showStyle.base.blueprintId, + dbRundown._id, + undefined, + rundownRes.globalAdLibPieces ) ) - if (anythingChanged(rundownBaselineChanges)) { - // If any of the rundown baseline datas was modified, we'll update the baselineModifyHash of the rundown - cache.Rundown.update((rd) => { - rd.baselineModifyHash = getCurrentTime() + '' - return rd - }) - } + saveIntoCache( + context, + baselineAdlibActions, + null, + postProcessGlobalAdLibActions(showStyle.base.blueprintId, dbRundown._id, rundownRes.globalActions || []) + ) return dbRundown } From 2a502c3551e8df57eab2028d26a681c835e65a8d Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 14 Dec 2023 18:20:53 +0000 Subject: [PATCH 178/479] chore: various renaming and tidying. --- .../corelib/src/dataModel/ExpectedPackages.ts | 3 +- packages/corelib/src/dataModel/Part.ts | 5 +-- .../src/blueprints/context/adlibActions.ts | 4 +- .../job-worker/src/blueprints/context/lib.ts | 19 ++++++---- packages/job-worker/src/db/changes.ts | 4 +- packages/job-worker/src/ingest/updateNext.ts | 2 +- packages/job-worker/src/modelBase.ts | 4 +- .../src/playout/activePlaylistJobs.ts | 4 +- .../job-worker/src/playout/adlibAction.ts | 10 ++--- packages/job-worker/src/playout/adlibUtils.ts | 2 +- packages/job-worker/src/playout/debug.ts | 10 ++--- packages/job-worker/src/playout/lib.ts | 8 ++-- packages/job-worker/src/playout/lock.ts | 16 ++++---- .../model/implementation/LoadPlayoutModel.ts | 10 ++--- .../model/implementation/PlayoutModelImpl.ts | 38 +++++++++---------- .../job-worker/src/playout/resolvedPieces.ts | 1 - packages/job-worker/src/playout/setNext.ts | 8 ++-- packages/job-worker/src/playout/take.ts | 10 ++--- .../src/playout/timeline/generate.ts | 2 +- .../src/playout/timings/partPlayback.ts | 8 ++-- .../src/playout/timings/piecePlayback.ts | 8 ++-- packages/job-worker/src/rundownPlaylists.ts | 10 ++--- .../studio/model/StudioPlayoutModelImpl.ts | 14 +++---- 23 files changed, 101 insertions(+), 99 deletions(-) diff --git a/packages/corelib/src/dataModel/ExpectedPackages.ts b/packages/corelib/src/dataModel/ExpectedPackages.ts index 2940e10409..d1f5194c2b 100644 --- a/packages/corelib/src/dataModel/ExpectedPackages.ts +++ b/packages/corelib/src/dataModel/ExpectedPackages.ts @@ -13,6 +13,7 @@ import { SegmentId, StudioId, } from './Ids' +import { ReadonlyDeep } from 'type-fest' /* Expected Packages are created from Pieces in the rundown. @@ -126,7 +127,7 @@ export interface ExpectedPackageDBFromBucketAdLibAction extends ExpectedPackageD pieceExternalId: string } -export function getContentVersionHash(expectedPackage: Omit): string { +export function getContentVersionHash(expectedPackage: ReadonlyDeep>): string { return hashObj({ content: expectedPackage.content, version: expectedPackage.version, diff --git a/packages/corelib/src/dataModel/Part.ts b/packages/corelib/src/dataModel/Part.ts index 1ba160af5e..ea7321447f 100644 --- a/packages/corelib/src/dataModel/Part.ts +++ b/packages/corelib/src/dataModel/Part.ts @@ -1,6 +1,5 @@ -import { IBlueprintPartDB, NoteSeverity } from '@sofie-automation/blueprints-integration' +import { IBlueprintPart, NoteSeverity } from '@sofie-automation/blueprints-integration' import { ITranslatableMessage } from '../TranslatableMessage' -import { ProtectedStringProperties } from '../protectedString' import { PartId, RundownId, SegmentId } from './Ids' import { PartNote } from './Notes' import { ReadonlyDeep } from 'type-fest' @@ -12,7 +11,7 @@ export interface PartInvalidReason { } /** A "Line" in NRK Lingo. */ -export interface DBPart extends ProtectedStringProperties { +export interface DBPart extends IBlueprintPart { _id: PartId /** Position inside the segment */ _rank: number diff --git a/packages/job-worker/src/blueprints/context/adlibActions.ts b/packages/job-worker/src/blueprints/context/adlibActions.ts index 8300b5f20b..993d7f61b6 100644 --- a/packages/job-worker/src/blueprints/context/adlibActions.ts +++ b/packages/job-worker/src/blueprints/context/adlibActions.ts @@ -195,7 +195,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct if (options?.pieceMetaDataFilter) { for (const [key, value] of Object.entries(options.pieceMetaDataFilter)) { // TODO do we need better validation here? - // It should be pretty safe as we are working with the cache version (for now) + // It should be pretty safe as we are working with the in-memory version (for now) query[`piece.metaData.${key}`] = value } } @@ -228,7 +228,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct if (options?.pieceMetaDataFilter) { for (const [key, value] of Object.entries(options.pieceMetaDataFilter)) { // TODO do we need better validation here? - // It should be pretty safe as we are working with the cache version (for now) + // It should be pretty safe as we are working with the in-memory version (for now) query[`metaData.${key}`] = value } } diff --git a/packages/job-worker/src/blueprints/context/lib.ts b/packages/job-worker/src/blueprints/context/lib.ts index 8d4935388c..1df268ae94 100644 --- a/packages/job-worker/src/blueprints/context/lib.ts +++ b/packages/job-worker/src/blueprints/context/lib.ts @@ -18,6 +18,9 @@ import { ExpectedPlayoutItemGeneric, HackPartMediaObjectSubscription, IBlueprintActionManifest, + IBlueprintActionManifestDisplay, + IBlueprintActionManifestDisplayContent, + IBlueprintActionTriggerMode, IBlueprintAdLibPieceDB, IBlueprintConfig, IBlueprintMutatablePart, @@ -265,7 +268,7 @@ export function convertPartToBlueprints(part: ReadonlyDeep): IBlueprintP * @param adLib the AdLibPiece to convert * @returns a cloned complete and clean IBlueprintAdLibPieceDB */ -export function convertAdLibPieceToBlueprints(adLib: AdLibPiece): IBlueprintAdLibPieceDB { +export function convertAdLibPieceToBlueprints(adLib: ReadonlyDeep): IBlueprintAdLibPieceDB { const obj: Complete = { ...convertPieceGenericToBlueprintsInner(adLib), _id: unprotectString(adLib._id), @@ -273,8 +276,8 @@ export function convertAdLibPieceToBlueprints(adLib: AdLibPiece): IBlueprintAdLi invalid: adLib.invalid, expectedDuration: adLib.expectedDuration, floated: adLib.floated, - currentPieceTags: clone(adLib.currentPieceTags), - nextPieceTags: clone(adLib.nextPieceTags), + currentPieceTags: clone(adLib.currentPieceTags), + nextPieceTags: clone(adLib.nextPieceTags), uniquenessId: adLib.uniquenessId, invertOnAirState: adLib.invertOnAirState, } @@ -287,7 +290,7 @@ export function convertAdLibPieceToBlueprints(adLib: AdLibPiece): IBlueprintAdLi * @param action the AdLibAction to convert * @returns a cloned complete and clean IBlueprintActionManifest */ -export function convertAdLibActionToBlueprints(action: AdLibAction): IBlueprintActionManifest { +export function convertAdLibActionToBlueprints(action: ReadonlyDeep): IBlueprintActionManifest { const obj: Complete = { externalId: action.externalId, actionId: action.actionId, @@ -295,10 +298,10 @@ export function convertAdLibActionToBlueprints(action: AdLibAction): IBlueprintA partId: unprotectString(action.partId), allVariants: action.allVariants, userDataManifest: clone(action.userDataManifest), - display: clone(action.display), // TODO - type mismatch - triggerModes: clone(action.triggerModes), // TODO - type mismatch - expectedPlayoutItems: clone(action.expectedPlayoutItems), - expectedPackages: clone(action.expectedPackages), + display: clone(action.display), // TODO - type mismatch + triggerModes: clone(action.triggerModes), // TODO - type mismatch + expectedPlayoutItems: clone(action.expectedPlayoutItems), + expectedPackages: clone(action.expectedPackages), } return obj diff --git a/packages/job-worker/src/db/changes.ts b/packages/job-worker/src/db/changes.ts index 194e5addc9..dbf75daaf8 100644 --- a/packages/job-worker/src/db/changes.ts +++ b/packages/job-worker/src/db/changes.ts @@ -208,7 +208,7 @@ export function saveIntoBase }>( newData: Array, options: SaveIntoDbHooks & SaveIntoDbHandlers ): ChangedIds { - const span = context.startSpan(`DBCache.saveIntoBase.${collectionName}`) + const span = context.startSpan(`saveIntoBase.${collectionName}`) const changes: ChangedIds = { added: [], @@ -228,7 +228,7 @@ export function saveIntoBase }>( const objectsToRemove = normalizeArrayToMap(oldDocs, '_id') for (const o of newData) { - // const span2 = profiler.startSpan(`DBCache.saveIntoBase.${collectionName}.do.${o._id}`) + // const span2 = profiler.startSpan(`saveIntoBase.${collectionName}.do.${o._id}`) const oldObj = objectsToRemove.get(o._id) if (oldObj) { diff --git a/packages/job-worker/src/ingest/updateNext.ts b/packages/job-worker/src/ingest/updateNext.ts index 8fe4157ea2..369713b39a 100644 --- a/packages/job-worker/src/ingest/updateNext.ts +++ b/packages/job-worker/src/ingest/updateNext.ts @@ -10,7 +10,7 @@ import { updateTimeline } from '../playout/timeline/generate' * Make sure that the nextPartInstance for the current Playlist is still correct * This will often change the nextPartInstance * @param context Context of the job being run - * @param playoutModel Playout Cache to operate on + * @param playoutModel Playout Model to operate on */ export async function ensureNextPartIsValid(context: JobContext, playoutModel: PlayoutModel): Promise { const span = context.startSpan('api.ingest.ensureNextPartIsValid') diff --git a/packages/job-worker/src/modelBase.ts b/packages/job-worker/src/modelBase.ts index 2ad23599de..b555e48f25 100644 --- a/packages/job-worker/src/modelBase.ts +++ b/packages/job-worker/src/modelBase.ts @@ -14,8 +14,8 @@ export interface BaseModel { dispose(): void /** - * Assert that no changes should have been made to the cache, will throw an Error otherwise. This can be used in - * place of `saveAllToDatabase()`, when the code controlling the cache expects no changes to have been made and any + * Assert that no changes should have been made to the model, will throw an Error otherwise. This can be used in + * place of `saveAllToDatabase()`, when the code owning the model expects no changes to have been made and any * changes made are an error and will cause issues. */ assertNoChanges(): void diff --git a/packages/job-worker/src/playout/activePlaylistJobs.ts b/packages/job-worker/src/playout/activePlaylistJobs.ts index 9f125264f1..aba24296a2 100644 --- a/packages/job-worker/src/playout/activePlaylistJobs.ts +++ b/packages/job-worker/src/playout/activePlaylistJobs.ts @@ -92,8 +92,8 @@ export async function handleResetRundownPlaylist(context: JobContext, data: Rese // 'forceResetAndActivateRundownPlaylist', { playlistId: otherRundownPlaylist._id }, null, - async (otherCache) => { - await deactivateRundownPlaylistInner(context, otherCache) + async (otherPlayoutModel) => { + await deactivateRundownPlaylistInner(context, otherPlayoutModel) } ).catch((e) => errors.push(e)) ) diff --git a/packages/job-worker/src/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index 0a76630079..8b191411f0 100644 --- a/packages/job-worker/src/playout/adlibAction.ts +++ b/packages/job-worker/src/playout/adlibAction.ts @@ -36,9 +36,9 @@ export async function handleExecuteAdlibAction( if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown, undefined, 412) if (!playlist.currentPartInfo) throw UserError.create(UserErrorMessage.NoCurrentPart, undefined, 412) - const initCache = await loadPlayoutModelPreInit(context, lock, playlist, false) + const initPlayoutModel = await loadPlayoutModelPreInit(context, lock, playlist, false) - const rundown = initCache.getRundown(playlist.currentPartInfo.rundownId) + const rundown = initPlayoutModel.getRundown(playlist.currentPartInfo.rundownId) if (!rundown) throw new Error(`Current Rundown "${playlist.currentPartInfo.rundownId}" could not be found`) const showStyle = await context.getShowStyleCompound(rundown.showStyleVariantId, rundown.showStyleBaseId) @@ -81,11 +81,11 @@ export async function handleExecuteAdlibAction( } if (blueprint.blueprint.executeAction) { - // load a full cache for the regular actions & executet the handler - const playoutModel = await createPlayoutModelfromInitModel(context, initCache) + // load a full model for the regular actions & execute the handler + const playoutModel = await createPlayoutModelfromInitModel(context, initPlayoutModel) const fullRundown = playoutModel.getRundown(rundown._id) - if (!fullRundown) throw new Error(`Rundown "${rundown._id}" missing between caches`) + if (!fullRundown) throw new Error(`Rundown "${rundown._id}" missing between models`) try { const res: ExecuteActionResult = await executeActionInner( diff --git a/packages/job-worker/src/playout/adlibUtils.ts b/packages/job-worker/src/playout/adlibUtils.ts index 65b65b4dff..2d0125895e 100644 --- a/packages/job-worker/src/playout/adlibUtils.ts +++ b/packages/job-worker/src/playout/adlibUtils.ts @@ -109,7 +109,7 @@ export async function innerFindLastPieceOnLayer( if (span) span.end() - // Note: This does not want to use the cache, as we want to search as far back as we can + // Note: This does not want to use the in-memory model, as we want to search as far back as we can // TODO - will this cause problems? return context.directCollections.PieceInstances.findOne(query, { sort: { diff --git a/packages/job-worker/src/playout/debug.ts b/packages/job-worker/src/playout/debug.ts index 4b5fe747f5..596a0941b6 100644 --- a/packages/job-worker/src/playout/debug.ts +++ b/packages/job-worker/src/playout/debug.ts @@ -79,18 +79,18 @@ export async function handleDebugCrash(context: JobContext, data: DebugRegenerat * Debug: Regenerate the timeline for the Studio */ export async function handleDebugUpdateTimeline(context: JobContext, _data: void): Promise { - await runJobWithStudioPlayoutModel(context, async (studioCache) => { - const activePlaylists = studioCache.getActiveRundownPlaylists() + await runJobWithStudioPlayoutModel(context, async (studioPlayoutModel) => { + const activePlaylists = studioPlayoutModel.getActiveRundownPlaylists() if (activePlaylists.length > 1) { throw new Error(`Too many active playlists`) } else if (activePlaylists.length > 0) { const playlist = activePlaylists[0] - await runJobWithPlayoutModel(context, { playlistId: playlist._id }, null, async (playoutCache) => { - await updateTimeline(context, playoutCache) + await runJobWithPlayoutModel(context, { playlistId: playlist._id }, null, async (playoutModel) => { + await updateTimeline(context, playoutModel) }) } else { - await updateStudioTimeline(context, studioCache) + await updateStudioTimeline(context, studioPlayoutModel) } }) } diff --git a/packages/job-worker/src/playout/lib.ts b/packages/job-worker/src/playout/lib.ts index 3214165b2b..805fd5fced 100644 --- a/packages/job-worker/src/playout/lib.ts +++ b/packages/job-worker/src/playout/lib.ts @@ -21,7 +21,7 @@ import { selectNextPart } from './selectNextPart' export async function resetRundownPlaylist(context: JobContext, playoutModel: PlayoutModel): Promise { logger.info('resetRundownPlaylist ' + playoutModel.playlist._id) // Remove all dunamically inserted pieces (adlibs etc) - // const rundownIds = new Set((cache.getRundownIds())) + // const rundownIds = new Set((playoutModel.getRundownIds())) playoutModel.resetPlaylist(!!playoutModel.playlist.activationId) @@ -70,7 +70,7 @@ export function resetPartInstancesWithPieceInstances( // Defer ones which arent loaded playoutModel.deferAfterSave(async (playoutModel) => { const rundownIds = playoutModel.getRundownIds() - const partInstanceIdsInCache = playoutModel.loadedPartInstances.map((p) => p.partInstance._id) + const partInstanceIdsInModel = playoutModel.loadedPartInstances.map((p) => p.partInstance._id) // Find all the partInstances which are not loaded, but should be reset const resetInDb = await context.directCollections.PartInstances.findFetch( @@ -78,8 +78,8 @@ export function resetPartInstancesWithPieceInstances( $and: [ selector ?? {}, { - // Not any which are in the cache, as they have already been done if needed - _id: { $nin: partInstanceIdsInCache }, + // Not any which are in the model, as they have already been done if needed + _id: { $nin: partInstanceIdsInModel }, rundownId: { $in: rundownIds }, reset: { $ne: true }, }, diff --git a/packages/job-worker/src/playout/lock.ts b/packages/job-worker/src/playout/lock.ts index 4015b74aea..c8be6844cf 100644 --- a/packages/job-worker/src/playout/lock.ts +++ b/packages/job-worker/src/playout/lock.ts @@ -35,7 +35,7 @@ export async function runJobWithPlayoutModel( /** * Run a minimal playout job - * This avoids loading the cache + * This avoids loading the model */ export async function runJobWithPlaylistLock( context: JobContext, @@ -58,7 +58,7 @@ export async function runJobWithPlaylistLock( } /** - * Lock the playlist for a quick task without the cache + * Lock the playlist for a quick task without the model */ export async function runWithPlaylistLock( context: JobContext, @@ -82,22 +82,22 @@ export async function runWithPlayoutModel( preInitFcn: null | ((playoutModel: PlayoutModelPreInit) => Promise | void), fcn: (playoutModel: PlayoutModel) => Promise | TRes ): Promise { - const initCache = await loadPlayoutModelPreInit(context, lock, playlist, false) + const initPlayoutModel = await loadPlayoutModelPreInit(context, lock, playlist, false) if (preInitFcn) { - await preInitFcn(initCache) + await preInitFcn(initPlayoutModel) } - const fullCache = await createPlayoutModelfromInitModel(context, initCache) + const fullPlayoutModel = await createPlayoutModelfromInitModel(context, initPlayoutModel) try { - const res = await fcn(fullCache) + const res = await fcn(fullPlayoutModel) logger.silly('runWithPlayoutModel: saveAllToDatabase') - await fullCache.saveAllToDatabase() + await fullPlayoutModel.saveAllToDatabase() return res } catch (err) { - fullCache.dispose() + fullPlayoutModel.dispose() throw err } } diff --git a/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts b/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts index cc82067dff..f9d5c0f435 100644 --- a/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts +++ b/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts @@ -36,11 +36,11 @@ export async function loadPlayoutModelPreInit( tmpPlaylist: ReadonlyDeep, reloadPlaylist = true ): Promise { - const span = context.startSpan('CacheForPlayoutPreInit.createPreInit') + const span = context.startSpan('PlayoutModelPreInit.createPreInit') if (span) span.setLabel('playlistId', unprotectString(tmpPlaylist._id)) if (!playlistLock.isLocked) { - throw new Error('Cannot create cache with released playlist lock') + throw new Error('Cannot create model with released playlist lock') } const [PeripheralDevices, Playlist, Rundowns] = await Promise.all([ @@ -140,11 +140,11 @@ export async function createPlayoutModelfromInitModel( context: JobContext, initModel: PlayoutModelPreInit ): Promise { - const span = context.startSpan('CacheForPlayout.fromInit') + const span = context.startSpan('PlayoutModel.fromInit') if (span) span.setLabel('playlistId', unprotectString(initModel.playlistId)) if (!initModel.playlistLock.isLocked) { - throw new Error('Cannot create cache with released playlist lock') + throw new Error('Cannot create model with released playlist lock') } const rundownIds = initModel.rundowns.map((r) => r._id) @@ -228,7 +228,7 @@ async function loadRundowns( } /** - * Intitialise the full content of the cache + * Intitialise the full content of the model * @param ingestCache A CacheForIngest that is pending saving, if this is following an ingest operation */ async function loadPartInstances( diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts index 380ca28514..19375b50cb 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts @@ -224,9 +224,9 @@ export class PlayoutModelReadonlyImpl implements PlayoutModelReadonly { } /** - * This is a cache used for playout operations. + * This is a model used for playout operations. * It contains everything that is needed to generate the timeline, and everything except for pieces needed to update the partinstances. - * Anything not in this cache should not be needed often, and only for specific operations (eg, AdlibActions needed to run one). + * Anything not in this model should not be needed often, and only for specific operations (eg, AdlibActions needed to run one). */ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements PlayoutModel, DatabasePersistedModel { readonly #baselineHelper: StudioBaselineHelper @@ -461,14 +461,14 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou // Defer ones which arent loaded this.deferAfterSave(async (playoutModel) => { const rundownIds = playoutModel.getRundownIds() - // We need to keep any for PartInstances which are still existent in the cache (as they werent removed) - const partInstanceIdsInCache = playoutModel.loadedPartInstances.map((p) => p.partInstance._id) + // We need to keep any for PartInstances which are still existent in the model (as they werent removed) + const partInstanceIdsInModel = playoutModel.loadedPartInstances.map((p) => p.partInstance._id) // Find all the partInstances which are not loaded, but should be removed const removeFromDb = await this.context.directCollections.PartInstances.findFetch( { - // Not any which are in the cache, as they have already been done if needed - _id: { $nin: partInstanceIdsInCache }, + // Not any which are in the model, as they have already been done if needed + _id: { $nin: partInstanceIdsInModel }, rundownId: { $in: rundownIds }, rehearsal: true, }, @@ -536,7 +536,7 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou const span = this.context.startSpan('PlayoutModelImpl.saveAllToDatabase') - // Execute cache.deferBeforeSave()'s + // Execute deferBeforeSave()'s for (const fn of this.#deferredBeforeSaveFunctions) { await fn(this as any) } @@ -565,7 +565,7 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou this.#playlistHasChanged = false - // Execute cache.deferAfterSave()'s + // Execute deferAfterSave()'s for (const fn of this.#deferredAfterSaveFunctions) { await fn(this as any) } @@ -695,15 +695,15 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou this.#deferredAfterSaveFunctions.push(fcn) } - /** ICacheBase */ + /** BaseModel */ /** - * Assert that no changes should have been made to the cache, will throw an Error otherwise. This can be used in - * place of `saveAllToDatabase()`, when the code controlling the cache expects no changes to have been made and any + * Assert that no changes should have been made to the model, will throw an Error otherwise. This can be used in + * place of `saveAllToDatabase()`, when the code controlling the model expects no changes to have been made and any * changes made are an error and will cause issues. */ assertNoChanges(): void { - const span = this.context.startSpan('Cache.assertNoChanges') + const span = this.context.startSpan('PlayoutModelImpl.assertNoChanges') function logOrThrowError(error: Error) { if (!IS_PRODUCTION) { @@ -716,7 +716,7 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou if (this.#deferredBeforeSaveFunctions.length > 0) logOrThrowError( new Error( - `Failed no changes in cache assertion, there were ${ + `Failed no changes in model assertion, there were ${ this.#deferredBeforeSaveFunctions.length } deferred functions` ) @@ -725,33 +725,33 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou if (this.#deferredAfterSaveFunctions.length > 0) logOrThrowError( new Error( - `Failed no changes in cache assertion, there were ${ + `Failed no changes in model assertion, there were ${ this.#deferredAfterSaveFunctions.length } after-save deferred functions` ) ) if (this.#timelineHasChanged) - logOrThrowError(new Error(`Failed no changes in cache assertion, Timeline has been changed`)) + logOrThrowError(new Error(`Failed no changes in model assertion, Timeline has been changed`)) if (this.#playlistHasChanged) - logOrThrowError(new Error(`Failed no changes in cache assertion, Playlist has been changed`)) + logOrThrowError(new Error(`Failed no changes in model assertion, Playlist has been changed`)) if (this.rundownsImpl.find((rd) => rd.ScratchPadSegmentHasChanged)) - logOrThrowError(new Error(`Failed no changes in cache assertion, a scratchpad Segment has been changed`)) + logOrThrowError(new Error(`Failed no changes in model assertion, a scratchpad Segment has been changed`)) if ( Array.from(this.allPartInstances.values()).find( (part) => !part || part.partInstanceHasChanges || part.changedPieceInstanceIds().length > 0 ) ) - logOrThrowError(new Error(`Failed no changes in cache assertion, a PartInstance has been changed`)) + logOrThrowError(new Error(`Failed no changes in model assertion, a PartInstance has been changed`)) if (span) span.end() } /** - * Discards all documents in this cache, and marks it as unusable + * Discards all documents in this model, and marks it as unusable */ dispose(): void { this.#disposed = true diff --git a/packages/job-worker/src/playout/resolvedPieces.ts b/packages/job-worker/src/playout/resolvedPieces.ts index f3eea28027..46c8c83067 100644 --- a/packages/job-worker/src/playout/resolvedPieces.ts +++ b/packages/job-worker/src/playout/resolvedPieces.ts @@ -14,7 +14,6 @@ import { PlayoutPartInstanceModel } from './model/PlayoutPartInstanceModel' * Resolve the PieceInstances for a PartInstance * Uses the getCurrentTime() as approximation for 'now' * @param context Context for current job - * @param cache Cache for the active Playlist * @param sourceLayers SourceLayers for the current ShowStyle * @param partInstance PartInstance to resolve * @returns ResolvedPieceInstances sorted by startTime diff --git a/packages/job-worker/src/playout/setNext.ts b/packages/job-worker/src/playout/setNext.ts index d2a6e0da78..7f3b08e641 100644 --- a/packages/job-worker/src/playout/setNext.ts +++ b/packages/job-worker/src/playout/setNext.ts @@ -25,7 +25,7 @@ import { protectString } from '@sofie-automation/corelib/dist/protectedString' /** * Set or clear the nexted part, from a given PartInstance, or SelectNextPartResult * @param context Context for the running job - * @param playoutModel The playout cache of the playlist + * @param playoutModel The playout model of the playlist * @param rawNextPart The Part to set as next * @param setManually Whether this was manually chosen by the user * @param nextTimeOffset The offset into the Part to start playback @@ -308,7 +308,7 @@ async function cleanupOrphanedItems(context: JobContext, playoutModel: PlayoutMo /** * Set or clear the queued segment. * @param context Context for the running job - * @param playoutModel The playout cache of the playlist + * @param playoutModel The playout model of the playlist * @param queuedSegment The segment to queue, or null to clear it */ export async function queueNextSegment( @@ -358,7 +358,7 @@ export async function queueNextSegment( /** * Set the first playable part of a given segment as next. * @param context Context for the running job - * @param playoutModel The playout cache of the playlist + * @param playoutModel The playout model of the playlist * @param nextSegment The segment, whose first part is to be set as next */ export async function setNextSegment( @@ -397,7 +397,7 @@ function findFirstPlayablePartOrThrow(segment: PlayoutSegmentModel): ReadonlyDee /** * Set the nexted part, from a given DBPart * @param context Context for the running job - * @param playoutModel The playout cache of the playlist + * @param playoutModel The playout model of the playlist * @param nextPart The Part to set as next * @param setManually Whether this was manually chosen by the user * @param nextTimeOffset The offset into the Part to start playback diff --git a/packages/job-worker/src/playout/take.ts b/packages/job-worker/src/playout/take.ts index 95d95af1ae..7cf38075e4 100644 --- a/packages/job-worker/src/playout/take.ts +++ b/packages/job-worker/src/playout/take.ts @@ -88,7 +88,7 @@ export async function handleTakeNextPart(context: JobContext, data: TakeNextPart /** * Perform a Take into the nexted Part, and prepare a new nexted Part * @param context Context for current job - * @param playoutModel Cache for the active Playlist + * @param playoutModel Model for the active Playlist * @param now Current timestamp */ export async function performTakeToNextedPart( @@ -259,8 +259,8 @@ export async function performTakeToNextedPart( // Last: const takeDoneTime = getCurrentTime() - playoutModel.deferBeforeSave(async (cache2) => { - await afterTakeUpdateTimingsAndEvents(context, cache2, showStyle, blueprint, isFirstTake, takeDoneTime) + playoutModel.deferBeforeSave(async (playoutModel2) => { + await afterTakeUpdateTimingsAndEvents(context, playoutModel2, showStyle, blueprint, isFirstTake, takeDoneTime) }) if (span) span.end() @@ -268,7 +268,7 @@ export async function performTakeToNextedPart( /** * Clear the nexted Segment, if taking into a PartInstance that consumes it - * @param playoutModel Cache for the active Playlist + * @param playoutModel Model for the active Playlist * @param takenPartInstance PartInstance to check */ export function clearQueuedSegmentId( @@ -288,7 +288,7 @@ export function clearQueuedSegmentId( /** * Reset the Segment of the previousPartInstance, if playback has left that Segment and the Rundown is looping - * @param playoutModel Cache for the active Playlist + * @param playoutModel Model for the active Playlist */ export function resetPreviousSegment(playoutModel: PlayoutModel): void { const previousPartInstance = playoutModel.previousPartInstance diff --git a/packages/job-worker/src/playout/timeline/generate.ts b/packages/job-worker/src/playout/timeline/generate.ts index ff5ca1e2ea..cb9e55df68 100644 --- a/packages/job-worker/src/playout/timeline/generate.ts +++ b/packages/job-worker/src/playout/timeline/generate.ts @@ -231,7 +231,7 @@ function logAnyRemainingNowTimes(_context: JobContext, timelineObjs: Array { - await resetRundownPlaylist(context, cache) + await runWithPlayoutModel(context, playlist, playlistLock, null, async (playoutModel) => { + await resetRundownPlaylist(context, playoutModel) - if (cache.playlist.activationId) { - await updateTimeline(context, cache) + if (playoutModel.playlist.activationId) { + await updateTimeline(context, playoutModel) } }) - // exit the sync function, so the cache is written back + // exit the sync function, so the lock is released is written back return rundowns.map((rundown) => ({ rundownExternalId: rundown.externalId, })) diff --git a/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts b/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts index f98af573c7..65430a2081 100644 --- a/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts +++ b/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts @@ -55,7 +55,7 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel { } public get displayName(): string { - return `CacheForStudio` + return `StudioPlayoutModel` } public getActiveRundownPlaylists(excludeRundownPlaylistId?: RundownPlaylistId): ReadonlyDeep { @@ -96,7 +96,7 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel { } /** - * Discards all documents in this cache, and marks it as unusable + * Discards all documents in this model, and marks it as unusable */ dispose(): void { this.#disposed = true @@ -121,12 +121,12 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel { } /** - * Assert that no changes should have been made to the cache, will throw an Error otherwise. This can be used in - * place of `saveAllToDatabase()`, when the code controlling the cache expects no changes to have been made and any + * Assert that no changes should have been made to the model, will throw an Error otherwise. This can be used in + * place of `saveAllToDatabase()`, when the code controlling the model expects no changes to have been made and any * changes made are an error and will cause issues. */ assertNoChanges(): void { - const span = this.context.startSpan('Cache.assertNoChanges') + const span = this.context.startSpan('StudioPlayoutModelImpl.assertNoChanges') function logOrThrowError(error: Error) { if (!IS_PRODUCTION) { @@ -139,12 +139,12 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel { if (this.#baselineHelper.hasChanges()) logOrThrowError( new Error( - `Failed no changes in cache assertion, baseline ExpectedPackages or ExpectedPlayoutItems has changes` + `Failed no changes in model assertion, baseline ExpectedPackages or ExpectedPlayoutItems has changes` ) ) if (this.#timelineHasChanged) - logOrThrowError(new Error(`Failed no changes in cache assertion, Timeline has been changed`)) + logOrThrowError(new Error(`Failed no changes in model assertion, Timeline has been changed`)) if (span) span.end() } From 11d705c33f4eed7c352ad2e9eb356b8fb778133d Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 15 Dec 2023 10:22:39 +0000 Subject: [PATCH 179/479] chore: refactor Status/MediaManager to avoid MeteorReactComponent SOFIE-2751 (#1094) --- .../lib/forms/SchemaFormTable/ArrayTable.tsx | 2 +- .../lib/forms/SchemaFormTable/ObjectTable.tsx | 2 +- .../Settings/BlueprintConfigSchema/index.tsx | 2 +- .../ui/Settings/ShowStyle/OutputLayer.tsx | 2 +- .../ui/Settings/ShowStyle/SourceLayer.tsx | 2 +- .../Studio/Devices/GenericSubDevices.tsx | 2 +- meteor/client/ui/Settings/Studio/Mappings.tsx | 2 +- meteor/client/ui/Status/MediaManager.tsx | 231 ++++++++---------- .../useToggleExpandHelper.tsx} | 5 +- 9 files changed, 111 insertions(+), 139 deletions(-) rename meteor/client/ui/{Settings/util/ToggleExpandedHelper.tsx => util/useToggleExpandHelper.tsx} (73%) diff --git a/meteor/client/lib/forms/SchemaFormTable/ArrayTable.tsx b/meteor/client/lib/forms/SchemaFormTable/ArrayTable.tsx index 8f18022bc3..5fd6e10e8c 100644 --- a/meteor/client/lib/forms/SchemaFormTable/ArrayTable.tsx +++ b/meteor/client/lib/forms/SchemaFormTable/ArrayTable.tsx @@ -7,7 +7,7 @@ import { WrappedOverridableItemNormal, OverrideOpHelperForItemContents, } from '../../../ui/Settings/util/OverrideOpHelper' -import { useToggleExpandHelper } from '../../../ui/Settings/util/ToggleExpandedHelper' +import { useToggleExpandHelper } from '../../../ui/util/useToggleExpandHelper' import { doModalDialog } from '../../ModalDialog' import { getSchemaSummaryFieldsForObject, diff --git a/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx b/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx index b279cdbf71..653aef06da 100644 --- a/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx +++ b/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx @@ -8,7 +8,7 @@ import { OverrideOpHelperForItemContents, getAllCurrentAndDeletedItemsFromOverrides, } from '../../../ui/Settings/util/OverrideOpHelper' -import { useToggleExpandHelper } from '../../../ui/Settings/util/ToggleExpandedHelper' +import { useToggleExpandHelper } from '../../../ui/util/useToggleExpandHelper' import { doModalDialog } from '../../ModalDialog' import { getSchemaSummaryFieldsForObject, diff --git a/meteor/client/ui/Settings/BlueprintConfigSchema/index.tsx b/meteor/client/ui/Settings/BlueprintConfigSchema/index.tsx index efb65a4aca..8328300b7b 100644 --- a/meteor/client/ui/Settings/BlueprintConfigSchema/index.tsx +++ b/meteor/client/ui/Settings/BlueprintConfigSchema/index.tsx @@ -12,7 +12,7 @@ import { useOverrideOpHelper, WrappedOverridableItemNormal } from '../util/Overr import { JSONSchema } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes' import deepmerge from 'deepmerge' import { SchemaFormSofieEnumDefinition, translateStringIfHasNamespaces } from '../../../lib/forms/schemaFormUtil' -import { useToggleExpandHelper } from '../util/ToggleExpandedHelper' +import { useToggleExpandHelper } from '../../util/useToggleExpandHelper' import { SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { ConfigCategoryEntry } from './CategoryEntry' diff --git a/meteor/client/ui/Settings/ShowStyle/OutputLayer.tsx b/meteor/client/ui/Settings/ShowStyle/OutputLayer.tsx index df09d96375..a187da9d51 100644 --- a/meteor/client/ui/Settings/ShowStyle/OutputLayer.tsx +++ b/meteor/client/ui/Settings/ShowStyle/OutputLayer.tsx @@ -20,7 +20,7 @@ import { } from '../util/OverrideOpHelper' import { TextInputControl } from '../../../lib/Components/TextInput' import { IntInputControl } from '../../../lib/Components/IntInput' -import { useToggleExpandHelper } from '../util/ToggleExpandedHelper' +import { useToggleExpandHelper } from '../../util/useToggleExpandHelper' import { LabelActual, LabelAndOverrides, diff --git a/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx b/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx index 44d5978313..3202f1db91 100644 --- a/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx +++ b/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx @@ -10,7 +10,7 @@ import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowSt import { getHelpMode } from '../../../lib/localStorage' import { doModalDialog } from '../../../lib/ModalDialog' import { findHighestRank } from '../StudioSettings' -import { useToggleExpandHelper } from '../util/ToggleExpandedHelper' +import { useToggleExpandHelper } from '../../util/useToggleExpandHelper' import { ObjectOverrideSetOp, SomeObjectOverrideOp } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { getAllCurrentAndDeletedItemsFromOverrides, diff --git a/meteor/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx b/meteor/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx index 64b042d8af..762e63de97 100644 --- a/meteor/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx +++ b/meteor/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx @@ -11,7 +11,7 @@ import { faCheck, faPencilAlt, faSync, faTrash } from '@fortawesome/free-solid-s import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { JSONBlob, JSONBlobParse, JSONSchema } from '@sofie-automation/blueprints-integration' import { DropdownInputControl, DropdownInputOption } from '../../../../lib/Components/DropdownInput' -import { useToggleExpandHelper } from '../../util/ToggleExpandedHelper' +import { useToggleExpandHelper } from '../../../util/useToggleExpandHelper' import { doModalDialog } from '../../../../lib/ModalDialog' import classNames from 'classnames' import { SubdeviceManifest } from '@sofie-automation/corelib/dist/deviceConfig' diff --git a/meteor/client/ui/Settings/Studio/Mappings.tsx b/meteor/client/ui/Settings/Studio/Mappings.tsx index a82773d165..84bfb74afd 100644 --- a/meteor/client/ui/Settings/Studio/Mappings.tsx +++ b/meteor/client/ui/Settings/Studio/Mappings.tsx @@ -9,7 +9,7 @@ import { faTrash, faPencilAlt, faCheck, faPlus, faSync } from '@fortawesome/free import { useTranslation } from 'react-i18next' import { LookaheadMode, TSR } from '@sofie-automation/blueprints-integration' import { LOOKAHEAD_DEFAULT_SEARCH_DISTANCE } from '@sofie-automation/shared-lib/dist/core/constants' -import { useToggleExpandHelper } from '../util/ToggleExpandedHelper' +import { useToggleExpandHelper } from '../../util/useToggleExpandHelper' import { getAllCurrentAndDeletedItemsFromOverrides, OverrideOpHelper, diff --git a/meteor/client/ui/Status/MediaManager.tsx b/meteor/client/ui/Status/MediaManager.tsx index 618666ae8c..c8a47260f6 100644 --- a/meteor/client/ui/Status/MediaManager.tsx +++ b/meteor/client/ui/Status/MediaManager.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import React, { useCallback } from 'react' import CoreIcons from '@nrk/core-icons/jsx' import { faChevronDown, faChevronRight, faCheck, faStopCircle, faRedo, faFlag } from '@fortawesome/free-solid-svg-icons' // @ts-expect-error No types available @@ -7,13 +7,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import ClassNames from 'classnames' import { MomentFromNow } from '../../lib/Moment' import { CircularProgressbar } from 'react-circular-progressbar' -import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/react-meteor-data' +import { useSubscription, useTracker } from '../../lib/ReactMeteorData/react-meteor-data' import { MediaWorkFlow } from '@sofie-automation/shared-lib/dist/core/model/MediaWorkFlows' import { MediaWorkFlowStep } from '@sofie-automation/shared-lib/dist/core/model/MediaWorkFlowSteps' -import * as i18next from 'react-i18next' +import { useTranslation } from 'react-i18next' import { extendMandadory, unprotectString } from '../../../lib/lib' -import * as _ from 'underscore' -import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { MeteorPubSub } from '../../../lib/api/pubsub' import { Spinner } from '../../lib/Spinner' import { sofieWarningIcon as WarningIcon } from '../../lib/notifications/warningIcon' @@ -24,26 +22,15 @@ import { MediaManagerAPI } from '../../../lib/api/mediaManager' import { getAllowConfigure, getAllowStudio } from '../../lib/localStorage' import { MediaWorkFlowId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { MediaWorkFlows, MediaWorkFlowSteps } from '../../collections' - -interface IMediaManagerStatusProps {} +import { useToggleExpandHelper } from '../util/useToggleExpandHelper' interface MediaWorkFlowUi extends MediaWorkFlow { steps: MediaWorkFlowStep[] } -interface IMediaManagerStatusTrackedProps { - workFlows: MediaWorkFlowUi[] -} - -interface IMediaManagerStatusState { - expanded: { - [mediaWorkFlowId: string]: boolean - } -} - interface IItemProps { item: MediaWorkFlowUi - expanded: _.Dictionary + isExpanded: boolean toggleExpanded: (id: MediaWorkFlowId) => void actionRestart: (event: React.MouseEvent, workflow: MediaWorkFlowUi) => void actionAbort: (event: React.MouseEvent, workflow: MediaWorkFlowUi) => void @@ -148,13 +135,12 @@ function workStepStatusLabel(t: TFunc, step: MediaWorkFlowStep): string { } } -const MediaManagerWorkFlowItem: React.FunctionComponent = ( - props: IItemProps & i18next.WithTranslation -) => { +function MediaManagerWorkFlowItem(props: Readonly): JSX.Element { + const { t } = useTranslation() + const mediaWorkflow = props.item - const t = props.t - const expanded = props.expanded[unprotectString(mediaWorkflow._id)] === true + const isExpanded = props.isExpanded const finishedOK = mediaWorkflow.success && mediaWorkflow.finished const finishedError = !mediaWorkflow.success && mediaWorkflow.finished const criticalSteps = mediaWorkflow.steps.filter((j) => j.criticalStep) @@ -186,7 +172,7 @@ const MediaManagerWorkFlowItem: React.FunctionComponent{mediaWorkflow.created}
props.toggleExpanded(mediaWorkflow._id)}> - {expanded ? t('Collapse') : t('Details')} - {expanded ? : } + {isExpanded ? t('Collapse') : t('Details')} + {isExpanded ? : }
{workFlowStatusLabel(t, mediaWorkflow.success, mediaWorkflow.finished, keyFinishedOK, currentTask)} @@ -310,7 +296,7 @@ const MediaManagerWorkFlowItem: React.FunctionComponent - {expanded && ( + {isExpanded && (
{mediaWorkflow.steps .sort((a, b) => b.priority - a.priority) @@ -349,118 +335,103 @@ const MediaManagerWorkFlowItem: React.FunctionComponent( - (_props: IMediaManagerStatusProps) => { - return { - workFlows: MediaWorkFlows.find({}) - .fetch() - .map((i) => - extendMandadory(i, { - steps: MediaWorkFlowSteps.find({ - workFlowId: i._id, - }).fetch(), - }) - ), - } - } -)( - class MediaManagerStatus extends MeteorReactComponent< - Translated, - IMediaManagerStatusState - > { - constructor(props: Translated) { - super(props) +export function MediaManagerStatus(): JSX.Element { + const { t } = useTranslation() - this.state = { - expanded: {}, - } - } + // Subscribe to data: + useSubscription(MeteorPubSub.mediaWorkFlows) // TODO: add some limit + useSubscription(MeteorPubSub.mediaWorkFlowSteps) - componentDidMount(): void { - // Subscribe to data: - this.subscribe(MeteorPubSub.mediaWorkFlows) // TODO: add some limit - this.subscribe(MeteorPubSub.mediaWorkFlowSteps) - } + const actionRestartAll = useCallback( + (event: React.MouseEvent) => { + doUserAction(t, event, UserAction.RESTART_MEDIA_WORKFLOW, (e, ts) => + MeteorCall.userAction.mediaRestartAllWorkflows(e, ts) + ) + }, + [t] + ) + const actionAbortAll = useCallback( + (event: React.MouseEvent) => { + doUserAction(t, event, UserAction.ABORT_ALL_MEDIA_WORKFLOWS, (e, ts) => + MeteorCall.userAction.mediaAbortAllWorkflows(e, ts) + ) + }, + [t] + ) - toggleExpanded = (workFlowId: MediaWorkFlowId) => { - this.state.expanded[unprotectString(workFlowId)] = !this.state.expanded[unprotectString(workFlowId)] - this.setState({ - expanded: this.state.expanded, - }) - } - actionRestart = (event: React.MouseEvent, workflow: MediaWorkFlowUi) => { - doUserAction(this.props.t, event, UserAction.RESTART_MEDIA_WORKFLOW, (e, ts) => + const actionRestart = useCallback( + (event: React.MouseEvent, workflow: MediaWorkFlowUi) => { + doUserAction(t, event, UserAction.RESTART_MEDIA_WORKFLOW, (e, ts) => MeteorCall.userAction.mediaRestartWorkflow(e, ts, workflow._id) ) - } - actionAbort = (event: React.MouseEvent, workflow: MediaWorkFlowUi) => { - doUserAction(this.props.t, event, UserAction.ABORT_MEDIA_WORKFLOW, (e, ts) => + }, + [t] + ) + const actionAbort = useCallback( + (event: React.MouseEvent, workflow: MediaWorkFlowUi) => { + doUserAction(t, event, UserAction.ABORT_MEDIA_WORKFLOW, (e, ts) => MeteorCall.userAction.mediaAbortWorkflow(e, ts, workflow._id) ) - } - actionPrioritize = (event: React.MouseEvent, workflow: MediaWorkFlowUi) => { - doUserAction(this.props.t, event, UserAction.PRIORITIZE_MEDIA_WORKFLOW, (e, ts) => + }, + [t] + ) + const actionPrioritize = useCallback( + (event: React.MouseEvent, workflow: MediaWorkFlowUi) => { + doUserAction(t, event, UserAction.PRIORITIZE_MEDIA_WORKFLOW, (e, ts) => MeteorCall.userAction.mediaPrioritizeWorkflow(e, ts, workflow._id) ) - } - actionRestartAll = (event: React.MouseEvent) => { - doUserAction(this.props.t, event, UserAction.RESTART_MEDIA_WORKFLOW, (e, ts) => - MeteorCall.userAction.mediaRestartAllWorkflows(e, ts) - ) - } - actionAbortAll = (event: React.MouseEvent) => { - doUserAction(this.props.t, event, UserAction.ABORT_ALL_MEDIA_WORKFLOWS, (e, ts) => - MeteorCall.userAction.mediaAbortAllWorkflows(e, ts) - ) - } - - renderWorkFlows() { - const { t, i18n, tReady } = this.props + }, + [t] + ) - return this.props.workFlows - .sort((a, b) => b.created - a.created) - .sort((a, b) => b.priority - a.priority) - .map((mediaWorkflow) => { - return ( - - ) - }) - } + const allWorkFlows = useTracker( + () => + MediaWorkFlows.find({}) + .fetch() + .map((i) => + extendMandadory(i, { + steps: MediaWorkFlowSteps.find({ + workFlowId: i._id, + }).fetch(), + }) + ), + [], + [] + ) + const sortedWorkflows = allWorkFlows.sort((a, b) => b.created - a.created).sort((a, b) => b.priority - a.priority) - render(): JSX.Element { - const { t } = this.props + const expandedHelper = useToggleExpandHelper() - return ( -
-
-

{t('Media Transfer Status')}

-
-
- {getAllowStudio() || getAllowConfigure() ? ( - - - - - ) : null} -
-
{this.renderWorkFlows()}
-
- ) - } - } -) + return ( +
+
+

{t('Media Transfer Status')}

+
+
+ {getAllowStudio() || getAllowConfigure() ? ( + + + + + ) : null} +
+
+ {sortedWorkflows.map((mediaWorkflow) => ( + + ))} +
+
+ ) +} diff --git a/meteor/client/ui/Settings/util/ToggleExpandedHelper.tsx b/meteor/client/ui/util/useToggleExpandHelper.tsx similarity index 73% rename from meteor/client/ui/Settings/util/ToggleExpandedHelper.tsx rename to meteor/client/ui/util/useToggleExpandHelper.tsx index 12bee5dccb..50d8fc7b99 100644 --- a/meteor/client/ui/Settings/util/ToggleExpandedHelper.tsx +++ b/meteor/client/ui/util/useToggleExpandHelper.tsx @@ -1,8 +1,9 @@ +import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' import { useState, useCallback } from 'react' export function useToggleExpandHelper(): { - toggleExpanded(id: string | number, forceState?: boolean): void - isExpanded(id: string | number): boolean + toggleExpanded(id: ProtectedString | string | number, forceState?: boolean): void + isExpanded(id: ProtectedString | string | number): boolean } { const [expandedItemIds, setExpandedItemIds] = useState>({}) From 5d3ea65c580df155363e04a1111bf319d100c42a Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 15 Dec 2023 10:48:29 +0000 Subject: [PATCH 180/479] chore: refactor DashboardPanelInner to avoid MeteorReactComponent SOFIE-2751 (#1093) --- meteor/client/ui/Shelf/AdLibPanel.tsx | 76 ++++---- meteor/client/ui/Shelf/DashboardPanel.tsx | 172 +++++++++--------- .../ui/Shelf/TimelineDashboardPanel.tsx | 93 +++------- 3 files changed, 161 insertions(+), 180 deletions(-) diff --git a/meteor/client/ui/Shelf/AdLibPanel.tsx b/meteor/client/ui/Shelf/AdLibPanel.tsx index 7d45c73f9e..8e60f4408a 100644 --- a/meteor/client/ui/Shelf/AdLibPanel.tsx +++ b/meteor/client/ui/Shelf/AdLibPanel.tsx @@ -137,6 +137,46 @@ interface IFetchAndFilterProps { includeGlobalAdLibs?: boolean } +export function useFetchAndFilter( + playlist: DBRundownPlaylist, + showStyleBase: UIShowStyleBase, + filter: RundownLayoutFilterBase | undefined, + includeGlobalAdLibs: boolean | undefined +): AdLibFetchAndFilterProps { + return useTracker( + () => + fetchAndFilter({ + playlist: playlist as Pick< + DBRundownPlaylist, + '_id' | 'studioId' | 'currentPartInfo' | 'nextPartInfo' | 'previousPartInfo' | 'rundownIdsInOrder' + >, + showStyleBase: showStyleBase as Pick, + filter, + includeGlobalAdLibs, + }), + [ + playlist._id, + playlist.studioId, + playlist.currentPartInfo?.partInstanceId, + playlist.nextPartInfo?.partInstanceId, + playlist.previousPartInfo?.partInstanceId, + playlist.rundownIdsInOrder, + showStyleBase._id, + showStyleBase.sourceLayers, + showStyleBase.outputLayers, + filter, + includeGlobalAdLibs, + ], + { + liveSegment: undefined, + rundownBaselineAdLibs: [], + sourceLayerLookup: {}, + uiSegments: [] as AdlibSegmentUi[], + uiSegmentMap: new Map(), + } + ) +} + export function fetchAndFilter(props: IFetchAndFilterProps): AdLibFetchAndFilterProps { const sourceLayerLookup = props.showStyleBase && props.showStyleBase.sourceLayers const outputLayerLookup = props.showStyleBase && props.showStyleBase.outputLayers @@ -540,37 +580,11 @@ export function AdLibPanel({ const [selectedSegment, setSelectedSegment] = useState(undefined) const shelfFollowsOnAir = getShelfFollowsOnAir() - const { uiSegments, liveSegment, sourceLayerLookup, rundownBaselineAdLibs } = useTracker( - () => - fetchAndFilter({ - playlist: playlist as Pick< - DBRundownPlaylist, - '_id' | 'studioId' | 'currentPartInfo' | 'nextPartInfo' | 'previousPartInfo' | 'rundownIdsInOrder' - >, - showStyleBase: showStyleBase as Pick, - filter, - includeGlobalAdLibs, - }), - [ - playlist._id, - playlist.studioId, - playlist.currentPartInfo?.partInstanceId, - playlist.nextPartInfo?.partInstanceId, - playlist.previousPartInfo?.partInstanceId, - playlist.rundownIdsInOrder, - showStyleBase._id, - showStyleBase.sourceLayers, - showStyleBase.outputLayers, - filter, - includeGlobalAdLibs, - ], - { - liveSegment: undefined, - rundownBaselineAdLibs: [], - sourceLayerLookup: {}, - uiSegments: [] as AdlibSegmentUi[], - uiSegmentMap: new Map(), - } + const { uiSegments, liveSegment, sourceLayerLookup, rundownBaselineAdLibs } = useFetchAndFilter( + playlist, + showStyleBase, + filter, + includeGlobalAdLibs ) useEffect(() => { diff --git a/meteor/client/ui/Shelf/DashboardPanel.tsx b/meteor/client/ui/Shelf/DashboardPanel.tsx index 388f828556..eab5ca5215 100644 --- a/meteor/client/ui/Shelf/DashboardPanel.tsx +++ b/meteor/client/ui/Shelf/DashboardPanel.tsx @@ -1,16 +1,15 @@ import React from 'react' import _ from 'underscore' -import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/react-meteor-data' +import { Translated, useSubscription, useTracker } from '../../lib/ReactMeteorData/react-meteor-data' import ClassNames from 'classnames' import { Spinner } from '../../lib/Spinner' -import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { ISourceLayer, IBlueprintActionTriggerMode } from '@sofie-automation/blueprints-integration' import { doUserAction, UserAction } from '../../../lib/clientUserAction' import { NotificationCenter, Notification, NoticeLevel } from '../../../lib/notifications/notifications' import { DashboardLayoutFilter, DashboardPanelUnit } from '../../../lib/collections/RundownLayouts' import { unprotectString } from '../../../lib/lib' -import { IAdLibPanelProps, AdLibFetchAndFilterProps, fetchAndFilter } from './AdLibPanel' +import { IAdLibPanelProps, AdLibFetchAndFilterProps, useFetchAndFilter } from './AdLibPanel' import { AdLibPanelToolbar } from './AdLibPanelToolbar' import { matchFilter } from './AdLibListView' import { DashboardPieceButton } from './DashboardPieceButton' @@ -27,17 +26,14 @@ import { isAdLibNext, isAdLibOnAir, } from '../../lib/shelf' -import { OutputLayers, SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { UIStudio } from '../../../lib/api/studios' import { UIStudios } from '../Collections' -import { Meteor } from 'meteor/meteor' import { PieceId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { RundownPlaylistCollectionUtil } from '../../../lib/collections/rundownPlaylistUtil' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { useTranslation } from 'react-i18next' -interface IState { - outputLayers: OutputLayers - sourceLayers: SourceLayers +export interface IDashboardPanelState { searchFilter: string | undefined selectedAdLib?: AdLibPieceUi singleClickMode: boolean @@ -131,54 +127,39 @@ export function dashboardElementStyle(el: DashboardPositionableElement): React.C } } -export class DashboardPanelInner extends MeteorReactComponent< - Translated, - IState -> { - constructor( - props: Translated - ) { +export function filterOutAdLibsForDashboardPanel( + props: IAdLibPanelProps & AdLibFetchAndFilterProps, + state: IDashboardPanelState, + uniquenessIds?: Set +): AdLibPieceUi[] { + const liveSegment = props.uiSegments.find((i) => i.isLive === true) + return props.rundownBaselineAdLibs + .concat(props.uiSegments.map((seg) => seg.pieces).flat()) + .filter((item) => + matchFilter(item, props.showStyleBase, liveSegment, props.filter, state.searchFilter, uniquenessIds) + ) +} + +export type DashboardPanelInnerProps = IAdLibPanelProps & + IDashboardPanelProps & + AdLibFetchAndFilterProps & + IDashboardPanelTrackedProps + +export class DashboardPanelInner extends React.Component, IDashboardPanelState> { + constructor(props: Translated) { super(props) this.state = { - outputLayers: {}, - sourceLayers: {}, searchFilter: undefined, singleClickMode: false, } } - static getDerivedStateFromProps( - props: Translated - ): Partial | null { - if (props.showStyleBase && props.showStyleBase.outputLayers && props.showStyleBase.sourceLayers) { - const tOLayers = props.showStyleBase.outputLayers - const tSLayers = props.showStyleBase.sourceLayers - - return { - outputLayers: tOLayers, - sourceLayers: tSLayers, - } - } - return null - } - - componentDidMount(): void { - this.autorun(() => { - const unorderedRundownIds = RundownPlaylistCollectionUtil.getRundownUnorderedIDs(this.props.playlist) - if (unorderedRundownIds.length > 0) { - this.subscribe(CorelibPubSub.pieceInstances, unorderedRundownIds, null, { - onlyPlayingAdlibsOrWithTags: true, - }) - } - }) - } - - componentDidUpdate(prevProps: IAdLibPanelProps & AdLibFetchAndFilterProps, prevState: IState): void { + componentDidUpdate(prevProps: IAdLibPanelProps & AdLibFetchAndFilterProps, prevState: IDashboardPanelState): void { const { selectedAdLib } = this.state const { selectedPiece } = this.props - const newState: Partial = {} + const newState: Partial = {} // Synchronize the internal selectedAdlib state with the outer selectedPiece if ( @@ -201,7 +182,7 @@ export class DashboardPanelInner extends MeteorReactComponent< // If the outer selectedPiece is changing, we should check if it's present in this Panel. If it is // we should change our inner selectedAdLib state. If it isn't, we should leave it be, so that it // doesn't affect any selections the user may have made when using "displayTakeButtons". - const memberAdLib = DashboardPanelInner.filterOutAdLibs(this.props, this.state).find( + const memberAdLib = filterOutAdLibsForDashboardPanel(this.props, this.state).find( (adLib) => adLib._id === selectedPiece._id ) if (memberAdLib) { @@ -210,23 +191,10 @@ export class DashboardPanelInner extends MeteorReactComponent< } if (Object.keys(newState).length > 0) { - this.setState(newState as IState) + this.setState(newState as IDashboardPanelState) } } - protected static filterOutAdLibs( - props: IAdLibPanelProps & AdLibFetchAndFilterProps, - state: IState, - uniquenessIds?: Set - ): AdLibPieceUi[] { - const liveSegment = props.uiSegments.find((i) => i.isLive === true) - return props.rundownBaselineAdLibs - .concat(props.uiSegments.map((seg) => seg.pieces).flat()) - .filter((item) => - matchFilter(item, props.showStyleBase, liveSegment, props.filter, state.searchFilter, uniquenessIds) - ) - } - protected isAdLibOnAir(adLib: AdLibPieceUi): boolean { return isAdLibOnAir(this.props.unfinishedAdLibIds, this.props.unfinishedTags, adLib) } @@ -465,7 +433,7 @@ export class DashboardPanelInner extends MeteorReactComponent< render(): JSX.Element | null { const { t } = this.props const uniquenessIds = new Set() - const filteredAdLibs = this.findNext(DashboardPanelInner.filterOutAdLibs(this.props, this.state, uniquenessIds)) + const filteredAdLibs = this.findNext(filterOutAdLibsForDashboardPanel(this.props, this.state, uniquenessIds)) if (this.props.visible && this.props.showStyleBase && this.props.filter) { const filter = this.props.filter as DashboardLayoutFilter if (!this.props.uiSegments || !this.props.playlist) { @@ -513,8 +481,8 @@ export class DashboardPanelInner extends MeteorReactComponent< , - IState, - AdLibFetchAndFilterProps & IDashboardPanelTrackedProps ->( - (props: Translated) => { - const studio = UIStudios.findOne(props.playlist.studioId) - if (!studio) throw new Meteor.Error(404, 'Studio "' + props.playlist.studioId + '" not found!') - - const { unfinishedAdLibIds, unfinishedTags } = getUnfinishedPieceInstancesGrouped( - props.playlist, - props.showStyleBase +export function useDashboardPanelTrackedProps( + props: IAdLibPanelProps +): AdLibFetchAndFilterProps & IDashboardPanelTrackedProps { + const unorderedRundownIds = useTracker( + () => RundownPlaylistCollectionUtil.getRundownUnorderedIDs(props.playlist), + [props.playlist._id], + [] + ) + useSubscription(CorelibPubSub.pieceInstances, unorderedRundownIds, null, { + onlyPlayingAdlibsOrWithTags: true, + }) + + const studio = useTracker(() => UIStudios.findOne(props.playlist.studioId), [props.playlist.studioId]) + + const { unfinishedAdLibIds, unfinishedTags } = useTracker( + () => getUnfinishedPieceInstancesGrouped(props.playlist, props.showStyleBase), + [props.playlist, props.showStyleBase], + { unfinishedAdLibIds: [], unfinishedPieceInstances: [], unfinishedTags: [] } + ) + const { nextAdLibIds, nextTags } = useTracker( + () => getNextPieceInstancesGrouped(props.playlist, props.showStyleBase), + [props.playlist, props.showStyleBase], + { nextAdLibIds: [], nextPieceInstances: [], nextTags: [] } + ) + + const otherProps = useFetchAndFilter(props.playlist, props.showStyleBase, props.filter, props.includeGlobalAdLibs) + + return { + ...otherProps, + studio, + unfinishedAdLibIds, + unfinishedTags, + nextAdLibIds, + nextTags, + } +} + +export const DashboardPanel = React.memo( + function DashboardPanel(props: IAdLibPanelProps & IDashboardPanelProps) { + const i18next = useTranslation() + + const trackedProps = useDashboardPanelTrackedProps(props) + if (!trackedProps.studio) return null + + return ( + ) - const { nextAdLibIds, nextTags } = getNextPieceInstancesGrouped(props.playlist, props.showStyleBase) - return { - ...fetchAndFilter(props), - studio, - unfinishedAdLibIds, - unfinishedTags, - nextAdLibIds, - nextTags, - } }, - (_data, props: IAdLibPanelProps, nextProps: IAdLibPanelProps) => { + (props: IAdLibPanelProps, nextProps: IAdLibPanelProps) => { return !_.isEqual(props, nextProps) } -)(DashboardPanelInner) +) diff --git a/meteor/client/ui/Shelf/TimelineDashboardPanel.tsx b/meteor/client/ui/Shelf/TimelineDashboardPanel.tsx index 62253fb982..dcc976d9bb 100644 --- a/meteor/client/ui/Shelf/TimelineDashboardPanel.tsx +++ b/meteor/client/ui/Shelf/TimelineDashboardPanel.tsx @@ -1,98 +1,65 @@ -import * as React from 'react' +import React from 'react' import * as _ from 'underscore' -import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/react-meteor-data' +import { Translated } from '../../lib/ReactMeteorData/react-meteor-data' import ClassNames from 'classnames' - import { Spinner } from '../../lib/Spinner' import { DashboardLayoutFilter, PieceDisplayStyle } from '../../../lib/collections/RundownLayouts' -import { IAdLibPanelProps, AdLibFetchAndFilterProps, fetchAndFilter } from './AdLibPanel' +import { IAdLibPanelProps, AdLibFetchAndFilterProps } from './AdLibPanel' import { AdLibPanelToolbar } from './AdLibPanelToolbar' import { matchFilter } from './AdLibListView' import { DashboardPieceButton } from './DashboardPieceButton' -import { contextMenuHoldToDisplayTime, UserAgentPointer, USER_AGENT_POINTER_PROPERTY } from '../../lib/lib' +import { contextMenuHoldToDisplayTime } from '../../lib/lib' import { DashboardPanelInner, dashboardElementStyle, - IDashboardPanelTrackedProps, IDashboardPanelProps, + IDashboardPanelState, + DashboardPanelInnerProps, + useDashboardPanelTrackedProps, } from './DashboardPanel' import { unprotectString } from '../../../lib/lib' import { RundownUtils } from '../../lib/rundown' -import { AdLibPieceUi, getNextPieceInstancesGrouped, getUnfinishedPieceInstancesGrouped } from '../../lib/shelf' +import { AdLibPieceUi } from '../../lib/shelf' import { ContextMenuTrigger } from '@jstarpl/react-contextmenu' import { ContextType, setShelfContextMenuContext } from './ShelfContextMenu' -import { UIStudios } from '../Collections' -import { Meteor } from 'meteor/meteor' +import { withTranslation } from 'react-i18next' -export const TimelineDashboardPanel = translateWithTracker< - Translated, - DashboardPanelInner['state'], - AdLibFetchAndFilterProps & IDashboardPanelTrackedProps ->( - (props: Translated) => { - const studio = UIStudios.findOne(props.playlist.studioId) - if (!studio) throw new Meteor.Error(404, 'Studio "' + props.playlist.studioId + '" not found!') +export const TimelineDashboardPanel = React.memo( + function TimelineDashboardPanel(props: IAdLibPanelProps & IDashboardPanelProps) { + const trackedProps = useDashboardPanelTrackedProps(props) + if (!trackedProps.studio) return null - const { unfinishedAdLibIds, unfinishedTags } = getUnfinishedPieceInstancesGrouped( - props.playlist, - props.showStyleBase - ) - const { nextAdLibIds, nextTags } = getNextPieceInstancesGrouped(props.playlist, props.showStyleBase) - return { - ...fetchAndFilter(props), - studio, - unfinishedAdLibIds, - unfinishedTags, - nextAdLibIds, - nextTags, - } + return }, - (_data, props: IAdLibPanelProps, nextProps: IAdLibPanelProps) => { + (props: IAdLibPanelProps, nextProps: IAdLibPanelProps) => { return !_.isEqual(props, nextProps) } -)( - class TimelineDashboardPanel extends DashboardPanelInner { +) + +const TimelineDashboardPanelContent = withTranslation()( + class TimelineDashboardPanelContent extends DashboardPanelInner { liveLine: HTMLDivElement scrollIntoViewTimeout: NodeJS.Timer | undefined = undefined - constructor( - props: Translated< - IAdLibPanelProps & IDashboardPanelProps & AdLibFetchAndFilterProps & IDashboardPanelTrackedProps - > - ) { + constructor(props: Translated) { super(props) } - setRef = (ref: HTMLDivElement) => { + private setRefExt = (ref: HTMLDivElement) => { this.liveLine = ref this.ensureLiveLineVisible() - const _panel = ref - if (_panel) { - const style = window.getComputedStyle(_panel) - // check if a special variable is set through CSS to indicate that we shouldn't expect - // double clicks to trigger AdLibs - const value = style.getPropertyValue(USER_AGENT_POINTER_PROPERTY) - const shouldBeSingleClick = !!value.match(UserAgentPointer.NO_POINTER) - if (this.state.singleClickMode !== shouldBeSingleClick) { - this.setState({ - singleClickMode: shouldBeSingleClick, - }) - } - } + this.setRef(ref) } - componentDidUpdate( - prevProps: IAdLibPanelProps & AdLibFetchAndFilterProps, - prevState: DashboardPanelInner['state'] - ) { + componentDidUpdate(prevProps: IAdLibPanelProps & AdLibFetchAndFilterProps, prevState: IDashboardPanelState) { super.componentDidUpdate(prevProps, prevState) this.ensureLiveLineVisible() } componentDidMount(): void { - super.componentDidMount() + super.componentDidMount?.() this.ensureLiveLineVisible() } - ensureLiveLineVisible = _.debounce(() => { + private ensureLiveLineVisible = _.debounce(() => { if (this.liveLine) { this.liveLine.scrollIntoView({ behavior: 'smooth', @@ -163,8 +130,8 @@ export const TimelineDashboardPanel = translateWithTracker< key={unprotectString(adLibListItem._id)} piece={adLibListItem} studio={this.props.studio} - layer={this.state.sourceLayers[adLibListItem.sourceLayerId]} - outputLayer={this.state.outputLayers[adLibListItem.outputLayerId]} + layer={this.props.showStyleBase.sourceLayers[adLibListItem.sourceLayerId]} + outputLayer={this.props.showStyleBase.outputLayers[adLibListItem.outputLayerId]} onToggleAdLib={this.onToggleOrSelectAdLib} onSelectAdLib={this.onSelectAdLib} isSelected={ @@ -213,7 +180,7 @@ export const TimelineDashboardPanel = translateWithTracker< })} > {(seg.isLive || (seg.isNext && !this.props.playlist.currentPartInfo)) && ( -
+
)} {filteredPieces.map((adLibListItem: AdLibPieceUi) => { return ( @@ -245,8 +212,8 @@ export const TimelineDashboardPanel = translateWithTracker< Date: Fri, 15 Dec 2023 10:48:44 +0000 Subject: [PATCH 181/479] chore: refactor SystemStatus and SnapshotsView to avoid MeteorReactComponent SOFIE-2751 (#1092) --- meteor/client/ui/Settings/SnapshotsView.tsx | 48 ++++++++++++--------- meteor/client/ui/Status/SystemStatus.tsx | 25 +++++------ 2 files changed, 41 insertions(+), 32 deletions(-) diff --git a/meteor/client/ui/Settings/SnapshotsView.tsx b/meteor/client/ui/Settings/SnapshotsView.tsx index eed0ccc6d5..e4ef4f4c2e 100644 --- a/meteor/client/ui/Settings/SnapshotsView.tsx +++ b/meteor/client/ui/Settings/SnapshotsView.tsx @@ -1,7 +1,6 @@ import * as React from 'react' -import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/react-meteor-data' +import { Translated, useSubscription, useTracker } from '../../lib/ReactMeteorData/react-meteor-data' import { doModalDialog } from '../../lib/ModalDialog' -import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { SnapshotItem } from '../../../lib/collections/Snapshots' import { unprotectString } from '../../../lib/lib' import * as _ from 'underscore' @@ -20,6 +19,7 @@ import { Snapshots, Studios } from '../../collections' import { ClientAPI } from '../../../lib/api/client' import { hashSingleUseToken } from '../../../lib/api/userActions' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { withTranslation } from 'react-i18next' interface IProps { match: { @@ -37,20 +37,32 @@ interface ITrackedProps { snapshots: Array studios: Array } -export default translateWithTracker(() => { - return { - snapshots: Snapshots.find( - {}, - { - sort: { - created: -1, - }, - } - ).fetch(), - studios: Studios.find({}, {}).fetch(), - } -})( - class SnapshotsView extends MeteorReactComponent, IState> { + +export default function SnapshotsView(props: Readonly): JSX.Element { + // // Subscribe to data: + useSubscription(MeteorPubSub.snapshots) + useSubscription(CorelibPubSub.studios, null) + + const snapshots = useTracker( + () => + Snapshots.find( + {}, + { + sort: { + created: -1, + }, + } + ).fetch(), + [], + [] + ) + const studios = useTracker(() => Studios.find({}, {}).fetch(), [], []) + + return +} + +const SnapshotsViewContent = withTranslation()( + class SnapshotsViewContent extends React.Component, IState> { constructor(props: Translated) { super(props) this.state = { @@ -59,10 +71,6 @@ export default translateWithTracker(() => { removeSnapshots: false, } } - componentDidMount(): void { - this.subscribe(MeteorPubSub.snapshots) - this.subscribe(CorelibPubSub.studios, null) - } onUploadFile(e: React.ChangeEvent) { const { t } = this.props diff --git a/meteor/client/ui/Status/SystemStatus.tsx b/meteor/client/ui/Status/SystemStatus.tsx index 30b83b85d4..569f034890 100644 --- a/meteor/client/ui/Status/SystemStatus.tsx +++ b/meteor/client/ui/Status/SystemStatus.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/react-meteor-data' +import { Translated, useSubscription, useTracker } from '../../lib/ReactMeteorData/react-meteor-data' import { PeripheralDevice, PeripheralDeviceType, @@ -15,7 +15,6 @@ import { faTrash, faEye } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import * as _ from 'underscore' import { doModalDialog } from '../../lib/ModalDialog' -import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { callPeripheralDeviceAction, PeripheralDevicesAPI } from '../../lib/clientAPI' import { NotificationCenter, NoticeLevel, Notification } from '../../../lib/notifications/notifications' import { getAllowConfigure, getAllowDeveloper, getAllowStudio, getHelpMode } from '../../lib/localStorage' @@ -517,13 +516,18 @@ interface DeviceInHierarchy { children: Array } -export default translateWithTracker(() => { - return { - coreSystem: CoreSystem.findOne(), - devices: PeripheralDevices.find({}, { sort: { lastConnected: -1 } }).fetch(), - } -})( - class SystemStatus extends MeteorReactComponent< +export default function SystemStatus(props: Readonly): JSX.Element { + // Subscribe to data: + useSubscription(CorelibPubSub.peripheralDevices, null) + + const coreSystem = useTracker(() => CoreSystem.findOne(), []) + const devices = useTracker(() => PeripheralDevices.find({}, { sort: { lastConnected: -1 } }).fetch(), [], []) + + return +} + +const SystemStatusContent = reacti18next.withTranslation()( + class SystemStatusContent extends React.Component< Translated, ISystemStatusState > { @@ -544,9 +548,6 @@ export default translateWithTracker Date: Fri, 15 Dec 2023 10:51:13 +0000 Subject: [PATCH 182/479] chore: refactor Organization to avoid MeteorReactComponent SOFIE-2751 (#1087) --- meteor/client/ui/Account/AccountPage.tsx | 6 +- meteor/client/ui/Account/OrganizationPage.tsx | 250 +++++++++--------- meteor/lib/collections/Users.ts | 7 + 3 files changed, 136 insertions(+), 127 deletions(-) diff --git a/meteor/client/ui/Account/AccountPage.tsx b/meteor/client/ui/Account/AccountPage.tsx index 61708a231a..c08bedefbe 100644 --- a/meteor/client/ui/Account/AccountPage.tsx +++ b/meteor/client/ui/Account/AccountPage.tsx @@ -4,7 +4,7 @@ import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/reac import type { RouteComponentProps } from 'react-router' import { NotificationCenter, Notification, NoticeLevel } from '../../../lib/notifications/notifications' import { MeteorCall } from '../../../lib/api/methods' -import { getUser, User, getUserRoles } from '../../../lib/collections/Users' +import { getUser, User, getUserRolesFromLoadedDocuments } from '../../../lib/collections/Users' import { DBOrganization, UserRoles } from '../../../lib/collections/Organization' import { Spinner } from '../../lib/Spinner' import { Link } from 'react-router-dom' @@ -91,7 +91,7 @@ export const AccountPage = translateWithTracker(() => { if (user && organization) { const roles: UserRoles = organization.userRoles[unprotectString(user._id)] || {} - return getUserRoles(user, organization).admin ? ( + return getUserRolesFromLoadedDocuments(user, organization).admin ? ( {

{t('Name:')} {organization.name}

- {user && getUserRoles(user, organization).admin ? ( + {user && getUserRolesFromLoadedDocuments(user, organization).admin ? ( diff --git a/meteor/client/ui/Account/OrganizationPage.tsx b/meteor/client/ui/Account/OrganizationPage.tsx index 51c83619cb..ba537e2709 100644 --- a/meteor/client/ui/Account/OrganizationPage.tsx +++ b/meteor/client/ui/Account/OrganizationPage.tsx @@ -1,7 +1,6 @@ -import * as React from 'react' -import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/react-meteor-data' -import { MeteorReactComponent } from '../../lib/MeteorReactComponent' -import { getUser, User, getUserRoles, DBUser } from '../../../lib/collections/Users' +import React, { useCallback, useState } from 'react' +import { useSubscriptions, useTracker } from '../../lib/ReactMeteorData/react-meteor-data' +import { getUser, DBUser, getUserRolesFromLoadedDocuments } from '../../../lib/collections/Users' import { Spinner } from '../../lib/Spinner' import { MeteorPubSub } from '../../../lib/api/pubsub' import { DBOrganization, UserRoles } from '../../../lib/collections/Organization' @@ -9,138 +8,141 @@ import { unprotectString } from '../../../lib/lib' import { MeteorCall } from '../../../lib/api/methods' import { EditAttribute } from '../../lib/EditAttribute' import { Organizations, Users } from '../../collections' +import { useTranslation } from 'react-i18next' +import { logger } from '../../../lib/logging' -interface IOrganizationProps { - user: User | null - organization: DBOrganization | null - usersInOrg: User[] -} +export function OrganizationPage(): JSX.Element { + const { t } = useTranslation() -interface IOrganizationState { - newUserEmail: string - newUserName: string - editUser: string -} + const loggedInUser = useTracker(() => getUser(), [], null) + const userOrganizationId = loggedInUser?.organizationId -export const OrganizationPage = translateWithTracker(() => { - const user = getUser() - const organization = user && Organizations.findOne({ _id: user.organizationId }) + // Subscribe to data: + useSubscriptions(MeteorPubSub.usersInOrganization, userOrganizationId ? [[userOrganizationId]] : []) - const usersInOrg = user && organization && Users.find({ organizationId: user.organizationId }).fetch() - return { - user: user ? user : null, - organization: organization ? organization : null, - usersInOrg: usersInOrg || [], - } -})( - class OrganizationPage extends MeteorReactComponent, IOrganizationState> { - state: IOrganizationState = { - newUserEmail: '', - newUserName: '', - editUser: '', - } + const organization = + useTracker(() => userOrganizationId && Organizations.findOne({ _id: userOrganizationId }), [userOrganizationId]) ?? + null + + const userIsAdmin = !!getUserRolesFromLoadedDocuments(loggedInUser, organization).admin - private async createAndEnrollUser() { - if (!this.state.newUserEmail || !this.state.newUserName) { - return - } - if (!this.props.organization) return + const usersInOrg = useTracker( + () => (userOrganizationId ? Users.find({ organizationId: userOrganizationId }).fetch() : []), + [userOrganizationId], + [] + ) - await MeteorCall.user.enrollUser(this.state.newUserEmail, this.state.newUserName) - this.setState({ newUserEmail: '', newUserName: '' }) + const [newUserEmail, setNewUserEmail] = useState('') + const [newUserName, setNewUserName] = useState('') + const createAndEnrollUser = useCallback(() => { + if (!newUserEmail || !newUserName) { + return } - componentDidMount(): void { - this.autorun(() => { - if (this.props.organization) { - this.subscribe(MeteorPubSub.usersInOrganization, this.props.organization._id) - } + MeteorCall.user + .enrollUser(newUserEmail, newUserName) + .then(() => { + // Clear fields + setNewUserEmail('') + setNewUserName('') }) - } - private renderUserRole(user: DBUser, userRole: keyof UserRoles) { - const organization = this.props.organization + .catch((e) => { + logger.error('enrollUser failed: ', e) + }) + }, [newUserEmail, newUserName]) - if (user && organization) { - const roles: UserRoles = organization.userRoles[unprotectString(user._id)] || {} - return getUserRoles(this.props.user, organization).admin ? ( - - ) : ( - - ) - } else return null - } + const changeNewUserEmail = useCallback((e: React.FormEvent) => { + setNewUserEmail(e.currentTarget.value) + }, []) + const changeNewUserName = useCallback((e: React.FormEvent) => { + setNewUserName(e.currentTarget.value) + }, []) - render(): React.ReactNode { - const { t } = this.props - const org = this.props.organization - if (!getUserRoles().admin) { - return 'Not Allowed' - } - return ( -
-
-

{t(`Organization Page${org ? ' - ' + org.name : ''}`)}

- {this.props.user ? ( -
-

{t('Invite User')}

-

{t("New User's Email")}

- this.setState({ newUserEmail: e.currentTarget.value })} - /> -

{t("New User's Name")}

- this.setState({ newUserName: e.currentTarget.value })} - /> - -
- ) : ( - - )} -
-
-

{t('Users in organization')}

- - - - - - - - - - - - - {this.props.usersInOrg.map((user) => { - return ( - - - + if (!userIsAdmin) { + return
Not Allowed
+ } - - - - - - ) - })} - -
{t('Studio')}{t('Configurator')}{t('Developer')}{t('Admin')}
{user.profile.name}{user.emails.map((e) => e.address).join(', ')}{this.renderUserRole(user, 'studio')}{this.renderUserRole(user, 'configurator')}{this.renderUserRole(user, 'developer')}{this.renderUserRole(user, 'admin')}
+ return ( +
+
+

{t(`Organization Page${organization ? ' - ' + organization.name : ''}`)}

+ {loggedInUser ? ( +
+

{t('Invite User')}

+

{t("New User's Email")}

+ +

{t("New User's Name")}

+ +
-
+ ) : ( + + )} +
+
+

{t('Users in organization')}

+ + + + + + + + + + + + + {usersInOrg.map((user) => { + return ( + + ) + })} + +
{t('Studio')}{t('Configurator')}{t('Developer')}{t('Admin')}
+
+
+ ) +} +interface OrganizationPageUserRowProps { + userIsAdmin: boolean + displayUser: DBUser + organization: DBOrganization | null +} +function OrganizationPageUserRow({ userIsAdmin, displayUser, organization }: Readonly) { + const renderUserRole = (user: DBUser, userRole: keyof UserRoles) => { + if (user && organization) { + const roles: UserRoles = organization.userRoles[unprotectString(user._id)] || {} + return userIsAdmin ? ( + + ) : ( + ) - } + } else return null } -) + + return ( + + {displayUser.profile.name} + {displayUser.emails.map((e) => e.address).join(', ')} + + {renderUserRole(displayUser, 'studio')} + {renderUserRole(displayUser, 'configurator')} + {renderUserRole(displayUser, 'developer')} + {renderUserRole(displayUser, 'admin')} + + ) +} diff --git a/meteor/lib/collections/Users.ts b/meteor/lib/collections/Users.ts index 99ab909bec..29f60eddba 100644 --- a/meteor/lib/collections/Users.ts +++ b/meteor/lib/collections/Users.ts @@ -46,5 +46,12 @@ export function getUserRoles(user?: User | null, organization?: DBOrganization | return {} } if (organization === undefined) organization = Organizations.findOne({ _id: user.organizationId }) || null + return getUserRolesFromLoadedDocuments(user, organization) +} + +export function getUserRolesFromLoadedDocuments(user: User | null, organization: DBOrganization | null): UserRoles { + if (!user) { + return {} + } return (organization?.userRoles && organization.userRoles[unprotectString(user._id)]) || {} } From d0f870422780aaf8ac661d4719d68a763ba31863 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 15 Dec 2023 10:51:24 +0000 Subject: [PATCH 183/479] chore: refactor RundownLayoutEditor to avoid MeteorReactComponent SOFIE-2751 (#1086) --- .../ui/Settings/RundownLayoutEditor.tsx | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/meteor/client/ui/Settings/RundownLayoutEditor.tsx b/meteor/client/ui/Settings/RundownLayoutEditor.tsx index 00dae3ea01..24bb1c54e2 100644 --- a/meteor/client/ui/Settings/RundownLayoutEditor.tsx +++ b/meteor/client/ui/Settings/RundownLayoutEditor.tsx @@ -1,8 +1,7 @@ -import * as React from 'react' +import React, { useMemo } from 'react' import ClassNames from 'classnames' import { EditAttribute } from '../../lib/EditAttribute' -import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/react-meteor-data' -import { MeteorReactComponent } from '../../lib/MeteorReactComponent' +import { Translated, useSubscription, useTracker } from '../../lib/ReactMeteorData/react-meteor-data' import { faUpload, faPlus, faCheck, faPencilAlt, faDownload, faTrash } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { @@ -38,6 +37,7 @@ import { RundownLayoutId, ShowStyleBaseId } from '@sofie-automation/corelib/dist import { OutputLayers, SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { RundownLayouts } from '../../collections' import { LabelActual } from '../../lib/Components/LabelAndOverrides' +import { withTranslation } from 'react-i18next' export interface IProps { showStyleBaseId: ShowStyleBaseId @@ -57,20 +57,26 @@ interface ITrackedProps { layoutTypes: RundownLayoutType[] } -export default translateWithTracker((props: IProps) => { - const layoutTypes = props.customRegion.layouts.map((l) => l.type) +export default function RundownLayoutEditor(props: Readonly): JSX.Element { + useSubscription(MeteorPubSub.rundownLayouts, [props.showStyleBaseId]) - const rundownLayouts = RundownLayouts.find({ - showStyleBaseId: props.showStyleBaseId, - userId: { $exists: false }, - }).fetch() + const layoutTypes = useMemo(() => props.customRegion.layouts.map((l) => l.type), [props.customRegion]) - return { - rundownLayouts, - layoutTypes, - } -})( - class RundownLayoutEditor extends MeteorReactComponent, IState> { + const rundownLayouts = useTracker( + () => + RundownLayouts.find({ + showStyleBaseId: props.showStyleBaseId, + userId: { $exists: false }, + }).fetch(), + [props.showStyleBaseId], + [] + ) + + return +} + +const RundownLayoutEditorContent = withTranslation()( + class RundownLayoutEditorContent extends React.Component, IState> { constructor(props: Translated) { super(props) @@ -80,12 +86,6 @@ export default translateWithTracker((props: IProp } } - componentDidMount(): void { - super.componentDidMount && super.componentDidMount() - - this.subscribe(MeteorPubSub.rundownLayouts, null) - } - onAddLayout = () => { const { t, showStyleBaseId } = this.props MeteorCall.rundownLayout From 65594c7f3baa0577baf28d21dd65261ef92f0f04 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 15 Dec 2023 11:07:04 +0000 Subject: [PATCH 184/479] chore: refactor Status/ExternalMessages to avoid MeteorReactComponent SOFIE-2751 (#1097) --- meteor/client/ui/Status/ExternalMessages.tsx | 528 ++++++++----------- 1 file changed, 232 insertions(+), 296 deletions(-) diff --git a/meteor/client/ui/Status/ExternalMessages.tsx b/meteor/client/ui/Status/ExternalMessages.tsx index 794d1b5593..08de2c1b62 100644 --- a/meteor/client/ui/Status/ExternalMessages.tsx +++ b/meteor/client/ui/Status/ExternalMessages.tsx @@ -1,341 +1,277 @@ -import { Meteor } from 'meteor/meteor' -import * as React from 'react' -import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/react-meteor-data' +import React, { useCallback, useState } from 'react' +import { useSubscription, useTracker } from '../../lib/ReactMeteorData/react-meteor-data' import { getCurrentTime, Time, unprotectString } from '../../../lib/lib' import { MomentFromNow } from '../../lib/Moment' import { getAllowConfigure } from '../../lib/localStorage' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import * as _ from 'underscore' import { ExternalMessageQueueObj } from '@sofie-automation/corelib/dist/dataModel/ExternalMessageQueue' -import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { makeTableOfObject } from '../../lib/utilComponents' import ClassNames from 'classnames' import { DatePickerFromTo } from '../../lib/datePicker' import moment from 'moment' import { faTrash, faPause, faPlay, faRedo } from '@fortawesome/free-solid-svg-icons' -import { MeteorPubSub, meteorSubscribe } from '../../../lib/api/pubsub' +import { MeteorPubSub } from '../../../lib/api/pubsub' import { MeteorCall } from '../../../lib/api/methods' import { UIStudios } from '../Collections' -import { UIStudio } from '../../../lib/api/studios' import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ExternalMessageQueue } from '../../collections' import { catchError } from '../../lib/lib' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { useTranslation } from 'react-i18next' -interface IExternalMessagesProps {} -interface IExternalMessagesState { - studioId: StudioId | undefined -} -interface IExternalMessagesTrackedProps { - studios: Array -} +function ExternalMessages(): JSX.Element { + const { t } = useTranslation() -const ExternalMessages = translateWithTracker< - IExternalMessagesProps, - IExternalMessagesState, - IExternalMessagesTrackedProps ->((_props: IExternalMessagesProps) => { - return { - studios: UIStudios.find({}).fetch(), - } -})( - class ExternalMessages extends MeteorReactComponent< - Translated, - IExternalMessagesState - > { - constructor(props: Translated) { - super(props) - this.state = { - studioId: undefined, - } - } - componentDidMount(): void { - this.subscribe(MeteorPubSub.uiStudio, null) - } - onClickStudio = (studio: UIStudio) => { - this.setState({ - studioId: studio._id, - }) - } - render(): JSX.Element { - const { t } = this.props + useSubscription(MeteorPubSub.uiStudio, null) - return ( -
-
-

{t('Message Queue')}

-
-
- Studio - -
-
{this.state.studioId ? : null}
-
- ) - } - } -) + const studios = useTracker(() => UIStudios.find({}).fetch(), [], []) + + const [selectedStudioId, setSelectedStudioId] = useState(null) + + return ( +
+
+

{t('Message Queue')}

+
+
+ Studio + +
+
{selectedStudioId ? : null}
+
+ ) +} interface IExternalMessagesInStudioProps { studioId: StudioId } -interface IExternalMessagesInStudioState { - dateFrom: Time - dateTo: Time +function ExternalMessagesInStudio({ studioId }: Readonly) { + const [dateFrom, setDateFrom] = useState(() => moment().startOf('day').valueOf()) + const [dateTo, setDateTo] = useState(() => moment().add(1, 'days').startOf('day').valueOf()) + + useSubscription(CorelibPubSub.externalMessageQueue, { + studioId: studioId, + created: { + $gte: dateFrom, + $lt: dateTo, + }, + }) + + const handleChangeDate = useCallback((from: Time, to: Time) => { + setDateFrom(from) + setDateTo(to) + }, []) + + return ( +
+
+ +
+
+ + +
+
+ ) } -interface IExternalMessagesInStudioTrackedProps { - queuedMessages: Array - sentMessages: Array + +interface ExternalMessagesQueuedMessagesProps { + studioId: StudioId } +function ExternalMessagesQueuedMessages({ studioId }: Readonly) { + const { t } = useTranslation() -const ExternalMessagesInStudio = translateWithTracker< - IExternalMessagesInStudioProps, - IExternalMessagesInStudioState, - IExternalMessagesInStudioTrackedProps ->((props: IExternalMessagesInStudioProps) => { - return { - queuedMessages: ExternalMessageQueue.find( - { - studioId: props.studioId, - sent: { $not: { $gt: 0 } }, - }, - { - sort: { - created: -1, - lastTry: -1, - }, - } - ).fetch(), - sentMessages: ExternalMessageQueue.find( - { - studioId: props.studioId, - sent: { $gt: 0 }, - }, - { - sort: { - sent: -1, - lastTry: -1, + const queuedMessages = useTracker( + () => + ExternalMessageQueue.find( + { + studioId: studioId, + sent: { $gt: 0 }, }, - } - ).fetch(), - } -})( - class ExternalMessagesInStudio extends React.Component< - Translated, - IExternalMessagesInStudioState - > { - private _currentsub = '' - private _sub?: Meteor.SubscriptionHandle + { + sort: { + sent: -1, + lastTry: -1, + }, + } + ).fetch(), + [studioId], + [] + ) - constructor(props: Translated) { - super(props) + return ( +
+

{t('Queued Messages')}

+ + + {queuedMessages.map((msg) => ( + + ))} + +
+
+ ) +} - this.state = { - dateFrom: moment().startOf('day').valueOf(), - dateTo: moment().add(1, 'days').startOf('day').valueOf(), - } - } +interface ExternalMessagesSentMessagesProps { + studioId: StudioId +} +function ExternalMessagesSentMessages({ studioId }: Readonly) { + const { t } = useTranslation() - componentDidMount(): void { - // Subscribe to data: - this.updateSubscription() - } - componentDidUpdate(): void { - this.updateSubscription() - } - updateSubscription() { - const h = this.state.dateFrom + '_' + this.state.dateTo - if (h !== this._currentsub) { - this._currentsub = h - if (this._sub) { - this._sub.stop() - } - this._sub = meteorSubscribe(CorelibPubSub.externalMessageQueue, { - studioId: this.props.studioId, - created: { - $gte: this.state.dateFrom, - $lt: this.state.dateTo, + const sentMessages = useTracker( + () => + ExternalMessageQueue.find( + { + studioId: studioId, + sent: { $gt: 0 }, + }, + { + sort: { + sent: -1, + lastTry: -1, }, - }) - } - } - componentWillUnmount(): void { - if (this._sub) { - this._sub.stop() - } - } - removeMessage(msg: ExternalMessageQueueObj) { - MeteorCall.externalMessages.remove(msg._id).catch(catchError('externalMessages.remove')) - } - toggleHoldMessage(msg: ExternalMessageQueueObj) { - MeteorCall.externalMessages.toggleHold(msg._id).catch(catchError('externalMessages.toggleHold')) - } - retryMessage(msg: ExternalMessageQueueObj) { - MeteorCall.externalMessages.retry(msg._id).catch(catchError('externalMessages.retry')) - } - renderMessageRow(msg: ExternalMessageQueueObj) { - const classes: string[] = ['message-row'] - let info: JSX.Element | null = null - if (msg.sent) { - classes.push('sent') - info = ( -
- Sent: - {msg.sent} -
- ) - } else if ( - getCurrentTime() - (msg.lastTry || 0) < 10 * 1000 && - (msg.lastTry || 0) > (msg.errorMessageTime || 0) - ) { - classes.push('sending') - info = ( -
- Sending... -
- ) - } else if (msg.errorFatal) { - classes.push('fatal') - info = ( -
- Fatal error: - {msg.errorMessage} -
- ) - } else if (msg.errorMessage) { - classes.push('error') - info = ( -
- Error: - {msg.errorMessage} -
- {msg.errorMessageTime} -
-
- ) - } else { - classes.push('waiting') - if (msg.tryCount) { - info = ( -
- Tried {msg.tryCount} times -
- ) } - if (msg.lastTry) { - info = ( -
- Last try: - {msg.lastTry} -
- ) - } - } - return ( - - - {getAllowConfigure() ? ( - - - - -
-
- ) : null} - ID: {unprotectString(msg._id)} -
- Created: {msg.created} - {msg.queueForLaterReason !== undefined ? ( -
- Queued for later due to: {msg.queueForLaterReason || 'Unknown reason'} -
- ) : null} - - -
{info}
-
-
- Receiver -
- {makeTableOfObject(msg.receiver)} -
-
- Message -
- {makeTableOfObject(msg.message)} -
-
- - - ) - } + ).fetch(), + [studioId], + [] + ) - renderQueuedMessages() { - const { t } = this.props - return ( + return ( +
+

{t('Sent Messages')}

+ + + {sentMessages.map((msg) => ( + + ))} + +
+
+ ) +} + +interface ExternalMessagesRowProps { + msg: ExternalMessageQueueObj +} +function ExternalMessagesRow({ msg }: Readonly) { + const removeMessage = useCallback(() => { + MeteorCall.externalMessages.remove(msg._id).catch(catchError('externalMessages.remove')) + }, [msg._id]) + const toggleHoldMessage = useCallback(() => { + MeteorCall.externalMessages.toggleHold(msg._id).catch(catchError('externalMessages.toggleHold')) + }, [msg._id]) + const retryMessage = useCallback(() => { + MeteorCall.externalMessages.retry(msg._id).catch(catchError('externalMessages.retry')) + }, [msg._id]) + + const classes: string[] = ['message-row'] + let info: JSX.Element | null = null + if (msg.sent) { + classes.push('sent') + info = ( +
+ Sent: + {msg.sent} +
+ ) + } else if (getCurrentTime() - (msg.lastTry || 0) < 10 * 1000 && (msg.lastTry || 0) > (msg.errorMessageTime || 0)) { + classes.push('sending') + info = ( +
+ Sending... +
+ ) + } else if (msg.errorFatal) { + classes.push('fatal') + info = ( +
+ Fatal error: + {msg.errorMessage} +
+ ) + } else if (msg.errorMessage) { + classes.push('error') + info = ( +
+ Error: + {msg.errorMessage}
-

{t('Queued Messages')}

- - - {_.map(this.props.queuedMessages, (msg) => { - return this.renderMessageRow(msg) - })} - -
+ {msg.errorMessageTime} +
+
+ ) + } else { + classes.push('waiting') + if (msg.tryCount) { + info = ( +
+ Tried {msg.tryCount} times
) } - renderSentMessages() { - const { t } = this.props - return ( + if (msg.lastTry) { + info = (
-

{t('Sent Messages')}

- - - {_.map(this.props.sentMessages, (msg) => { - return this.renderMessageRow(msg) - })} - -
+ Last try: + {msg.lastTry}
) } - handleChangeDate = (from: Time, to: Time) => { - this.setState({ - dateFrom: from, - dateTo: to, - }) - } - - render(): JSX.Element { - return ( -
-
- + } + return ( + + + {getAllowConfigure() ? ( + + + + +
+
+ ) : null} + ID: {unprotectString(msg._id)} +
+ Created: {msg.created} + {msg.queueForLaterReason !== undefined ? ( +
+ Queued for later due to: {msg.queueForLaterReason || 'Unknown reason'}
-
- {this.renderQueuedMessages()} - {this.renderSentMessages()} + ) : null} + + +
{info}
+
+
+ Receiver +
+ {makeTableOfObject(msg.receiver)} +
+
+ Message +
+ {makeTableOfObject(msg.message)}
- ) - } - } -) + + + ) +} + export { ExternalMessages } From ca9acd3162ae5bd45a6d744df52a453b3f0d6b78 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 15 Dec 2023 11:07:29 +0000 Subject: [PATCH 185/479] chore: refactor TestTools to avoid MeteorReactComponent SOFIE-2751 (#1096) --- meteor/client/ui/TestTools/Mappings.tsx | 151 ++++++++++-------------- meteor/client/ui/TestTools/index.tsx | 149 +++++++++++------------ 2 files changed, 132 insertions(+), 168 deletions(-) diff --git a/meteor/client/ui/TestTools/Mappings.tsx b/meteor/client/ui/TestTools/Mappings.tsx index 57211419b9..95541df490 100644 --- a/meteor/client/ui/TestTools/Mappings.tsx +++ b/meteor/client/ui/TestTools/Mappings.tsx @@ -1,8 +1,7 @@ import * as React from 'react' -import { withTracker } from '../../lib/ReactMeteorData/react-meteor-data' +import { useSubscription, useTracker } from '../../lib/ReactMeteorData/react-meteor-data' import * as _ from 'underscore' -import { MeteorReactComponent } from '../../lib/MeteorReactComponent' -import { omit, Time, unprotectString } from '../../../lib/lib' +import { omit, unprotectString } from '../../../lib/lib' import { MeteorPubSub } from '../../../lib/api/pubsub' import { makeTableOfObject } from '../../lib/utilComponents' import { StudioSelect } from './StudioSelect' @@ -10,7 +9,6 @@ import { MappingExt } from '@sofie-automation/corelib/dist/dataModel/Studio' import { LookaheadMode, TSR } from '@sofie-automation/blueprints-integration' import { createSyncPeripheralDeviceCustomPublicationMongoCollection } from '../../../lib/collections/lib' import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { RoutedMappings } from '@sofie-automation/shared-lib/dist/core/model/Timeline' import { PeripheralDevicePubSubCollectionsNames } from '@sofie-automation/shared-lib/dist/pubsub/peripheralDevice' import { useTranslation } from 'react-i18next' @@ -44,94 +42,73 @@ function MappingsView(props: Readonly): JSX.Element { ) } -interface IMappingsTableProps { +interface ComponentMappingsTableProps { studioId: StudioId } -interface IMappingsTableTrackedProps { - mappings: RoutedMappings | null -} -interface IMappingsTableState { - time: Time | null -} -export const ComponentMappingsTable = withTracker( - (props: IMappingsTableProps) => { - try { - // These properties will be exposed under this.props - // Note that these properties are reactively recalculated - const mappings = StudioMappings.findOne(props.studioId) - return { - mappings: mappings || null, - } - } catch (e) { - return { - mappings: null, - } - } - } -)( - class ComponentMappingsTable extends MeteorReactComponent< - IMappingsTableProps & IMappingsTableTrackedProps, - IMappingsTableState - > { - constructor(props: IMappingsTableProps & IMappingsTableTrackedProps) { - super(props) +function ComponentMappingsTable({ studioId }: Readonly): JSX.Element { + useSubscription(MeteorPubSub.mappingsForStudio, studioId) - this.state = { - time: null, - } - } - componentDidMount(): void { - this.subscribe(MeteorPubSub.mappingsForStudio, this.props.studioId) - } - renderMappingsState(state: RoutedMappings) { - const rows = _.sortBy(Object.entries(state.mappings), (o) => o[0]) - return rows.map(([id, obj]) => ( - - {id} - {unprotectString(obj.deviceId)} - {TSR.DeviceType[obj.device]} - {obj.layerName} - - Mode: {LookaheadMode[obj.lookahead]} -
- Distance: {obj.lookaheadMaxSearchDistance} -
- Depth: {obj.lookaheadDepth} - - - {makeTableOfObject( - omit(obj, 'deviceId', 'device', 'lookahead', 'lookaheadDepth', 'lookaheadMaxSearchDistance', 'layerName') - )} - - - )) - } - render(): JSX.Element { - const { mappings } = this.props - return ( + const mappingsObj = useTracker( + () => { + return StudioMappings.findOne(studioId) + }, + [studioId], + null + ) + + const mappingsItems = mappingsObj ? _.sortBy(Object.entries(mappingsObj.mappings), (o) => o[0]) : [] + + return ( +
+
-
-
- - - - - - - - - - - {mappings ? this.renderMappingsState(mappings) : ''} - -
MappingDeviceIdTypeNameLookaheadData
-
-
+ + + + + + + + + + + {mappingsItems.map(([id, obj]) => ( + + ))} + +
MappingDeviceIdTypeNameLookaheadData
- ) - } - } -) +
+
+ ) +} + +interface ComponentMappingsTableRowProps { + id: string + obj: MappingExt +} +function ComponentMappingsTableRow({ id, obj }: Readonly) { + return ( + + {id} + {unprotectString(obj.deviceId)} + {TSR.DeviceType[obj.device]} + {obj.layerName} + + Mode: {LookaheadMode[obj.lookahead]} +
+ Distance: {obj.lookaheadMaxSearchDistance} +
+ Depth: {obj.lookaheadDepth} + + + {makeTableOfObject( + omit(obj, 'deviceId', 'device', 'lookahead', 'lookaheadDepth', 'lookaheadMaxSearchDistance', 'layerName') + )} + + + ) +} function MappingsStudioSelect(): JSX.Element { return diff --git a/meteor/client/ui/TestTools/index.tsx b/meteor/client/ui/TestTools/index.tsx index fd88ecb9cf..73e1e3891a 100644 --- a/meteor/client/ui/TestTools/index.tsx +++ b/meteor/client/ui/TestTools/index.tsx @@ -1,10 +1,8 @@ import * as React from 'react' -import { withTranslation } from 'react-i18next' -import { Translated } from '../../lib/ReactMeteorData/react-meteor-data' +import { useTranslation } from 'react-i18next' +import { useSubscription } from '../../lib/ReactMeteorData/react-meteor-data' import { Route, Switch, NavLink, Redirect } from 'react-router-dom' - import { TimelineView, TimelineStudioSelect } from './Timeline' -import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { MeteorPubSub } from '../../../lib/api/pubsub' import { MappingsStudioSelect, MappingsView } from './Mappings' import { TimelineDatastoreStudioSelect, TimelineDatastoreView } from './TimelineDatastore' @@ -14,93 +12,82 @@ import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' interface IStatusMenuProps { match?: any } -type IStatusMenuState = {} -const StatusMenu = withTranslation()( - class StatusMenu extends React.Component, IStatusMenuState> { - render(): JSX.Element { - const { t } = this.props +function StatusMenu(_props: Readonly) { + const { t } = useTranslation() - return ( -
- -

{t('Timeline')}

-
- -

{t('Timeline Datastore')}

-
- -

{t('Mappings')}

-
- -

{t('Device Triggers')}

-
-
- ) - } - } -) + return ( +
+ +

{t('Timeline')}

+
+ +

{t('Timeline Datastore')}

+
+ +

{t('Mappings')}

+
+ +

{t('Device Triggers')}

+
+
+ ) +} interface IStatusProps { match?: any } -class Status extends MeteorReactComponent> { - componentDidMount(): void { - // Subscribe to data: +export default function Status(props: Readonly): JSX.Element { + // Subscribe to data: + useSubscription(MeteorPubSub.uiStudio, null) + useSubscription(CorelibPubSub.showStyleBases, null) + useSubscription(CorelibPubSub.showStyleVariants, null, null) - this.subscribe(MeteorPubSub.uiStudio, null) - this.subscribe(CorelibPubSub.showStyleBases, null) - this.subscribe(CorelibPubSub.showStyleVariants, null, null) - } - render(): JSX.Element { - return ( -
- {/*
+ return ( +
+ {/*

{t('Status')}

*/} -
-
-
- -
+
+
+
+
-
-
- -
-
- - - - - - - - - - - -
+
+
+
+ +
+
+ + + + + + + + + + +
- ) - } +
+ ) } - -export default withTranslation()(Status) From 5af3cf05b6d9f6a2e2f598d88c030bd610db8311 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 15 Dec 2023 11:07:40 +0000 Subject: [PATCH 186/479] chore: refactor BucketPanel to avoid MeteorReactComponent SOFIE-2751 (#1088) --- meteor/client/ui/Shelf/BucketPanel.tsx | 231 +++++++++++++------------ 1 file changed, 120 insertions(+), 111 deletions(-) diff --git a/meteor/client/ui/Shelf/BucketPanel.tsx b/meteor/client/ui/Shelf/BucketPanel.tsx index 07e6d9e8fe..3ebbeaceb0 100644 --- a/meteor/client/ui/Shelf/BucketPanel.tsx +++ b/meteor/client/ui/Shelf/BucketPanel.tsx @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor' import * as React from 'react' import * as _ from 'underscore' -import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/react-meteor-data' +import { Translated, useSubscription, useSubscriptions, useTracker } from '../../lib/ReactMeteorData/react-meteor-data' import { IAdLibListItem } from './AdLibListItem' import ClassNames from 'classnames' import { @@ -15,7 +15,6 @@ import { } from 'react-dnd' import { faBars } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { OutputLayers, SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { ISourceLayer, @@ -73,6 +72,7 @@ import { } from '@sofie-automation/corelib/dist/dataModel/Ids' import { RundownPlaylistCollectionUtil } from '../../../lib/collections/rundownPlaylistUtil' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { withTranslation } from 'react-i18next' interface IBucketPanelDragObject { id: BucketId @@ -266,93 +266,132 @@ interface BucketTargetCollectedProps { connectDropTarget: ConnectDropTarget } -export const BucketPanel = translateWithTracker, IState, IBucketPanelTrackedProps>( - (props: Translated) => { - let showStyleBaseId: ShowStyleBaseId | undefined = undefined - let showStyleVariantId: ShowStyleVariantId | undefined = undefined - - const selectedPart = props.playlist.currentPartInfo?.partInstanceId ?? props.playlist.nextPartInfo?.partInstanceId - if (selectedPart) { - const part = PartInstances.findOne(selectedPart, { - fields: literal>({ - rundownId: 1, - //@ts-expect-error deep property - 'part._id': 1, - }), - }) as Pick | undefined - if (part) { - const rundown = Rundowns.findOne(part.rundownId, { - fields: { - showStyleBaseId: 1, - showStyleVariantId: 1, - }, - }) as Pick | undefined - if (rundown) { - showStyleBaseId = rundown.showStyleBaseId - showStyleVariantId = rundown.showStyleVariantId +export const BucketPanel = React.memo( + function BucketPanel(props: Readonly): JSX.Element | null { + // Data subscriptions: + useSubscription(MeteorPubSub.buckets, props.playlist.studioId, props.bucket._id) + useSubscription(MeteorPubSub.uiBucketContentStatuses, props.playlist.studioId, props.bucket._id) + useSubscription(MeteorPubSub.uiStudio, props.playlist.studioId) + + const { showStyleBases, showStyleVariants } = useTracker( + () => { + const rundowns = RundownPlaylistCollectionUtil.getRundownsUnordered(props.playlist) + + const showStyleBases = _.uniq(rundowns.map((rundown) => rundown.showStyleBaseId)) + const showStyleVariants = _.uniq(rundowns.map((rundown) => rundown.showStyleVariantId)) + + return { showStyleBases, showStyleVariants } + }, + [props.playlist], + { showStyleBases: [], showStyleVariants: [] } + ) + + useSubscription(CorelibPubSub.bucketAdLibPieces, props.playlist.studioId, props.bucket._id, showStyleVariants) + useSubscription(CorelibPubSub.bucketAdLibActions, props.playlist.studioId, props.bucket._id, showStyleVariants) + + useSubscriptions( + MeteorPubSub.uiShowStyleBase, + showStyleBases.map((id) => [id]) + ) + + // Data processing: + const { showStyleBaseId, showStyleVariantId } = useTracker( + () => { + const selectedPartInstanceId = + props.playlist.currentPartInfo?.partInstanceId ?? props.playlist.nextPartInfo?.partInstanceId + const partInstance = PartInstances.findOne(selectedPartInstanceId, { + fields: literal>({ + rundownId: 1, + //@ts-expect-error deep property + 'part._id': 1, + }), + }) as Pick | undefined + if (partInstance) { + const rundown = Rundowns.findOne(partInstance.rundownId, { + fields: { + showStyleBaseId: 1, + showStyleVariantId: 1, + }, + }) as Pick | undefined + if (rundown) { + return { showStyleBaseId: rundown.showStyleBaseId, showStyleVariantId: rundown.showStyleVariantId } + } } - } - } - if (showStyleVariantId === undefined) { - const rundown = RundownPlaylistCollectionUtil.getRundownsOrdered( - props.playlist, - {}, - { - fields: { - showStyleBaseId: 1, - showStyleVariantId: 1, - }, + + const rundown = RundownPlaylistCollectionUtil.getRundownsOrdered( + props.playlist, + {}, + { + fields: { + showStyleBaseId: 1, + showStyleVariantId: 1, + }, + } + )[0] as Pick | undefined + if (rundown) { + return { showStyleBaseId: rundown.showStyleBaseId, showStyleVariantId: rundown.showStyleVariantId } } - )[0] as Pick | undefined - if (rundown) { - showStyleBaseId = rundown.showStyleBaseId - showStyleVariantId = rundown.showStyleVariantId - } - } - if (!showStyleBaseId) throw new Meteor.Error(500, `No showStyleBaseId found for playlist ${props.playlist._id}`) - if (!showStyleVariantId) - throw new Meteor.Error(500, `No showStyleVariantId found for playlist ${props.playlist._id}`) - const studio = UIStudios.findOne(props.playlist.studioId) - if (!studio) throw new Meteor.Error(500, `No Studio found for playlist ${props.playlist._id}`) + return { showStyleBaseId: undefined, showStyleVariantId: undefined } + }, + [], + { showStyleBaseId: undefined, showStyleVariantId: undefined } + ) + + const studio = useTracker(() => UIStudios.findOne(props.playlist.studioId), [props.playlist.studioId]) - const tOLayers = props.showStyleBase ? props.showStyleBase.outputLayers : {} - const tSLayers = props.showStyleBase ? props.showStyleBase.sourceLayers : {} + const outputLayers = props.showStyleBase.outputLayers + const sourceLayers = props.showStyleBase.sourceLayers - const { unfinishedAdLibIds, unfinishedTags } = getUnfinishedPieceInstancesGrouped( - props.playlist, - props.showStyleBase + const { unfinishedAdLibIds, unfinishedTags } = useTracker( + () => getUnfinishedPieceInstancesGrouped(props.playlist, props.showStyleBase), + [props.playlist, props.showStyleBase], + { unfinishedPieceInstances: [], unfinishedAdLibIds: [], unfinishedTags: [] } + ) + const { nextAdLibIds, nextTags } = useTracker( + () => getNextPieceInstancesGrouped(props.playlist, props.showStyleBase), + [props.playlist, props.showStyleBase], + { nextPieceInstances: [], nextAdLibIds: [], nextTags: [] } + ) + const allBucketItems = useTracker(() => { + const bucketAdLibPieces = BucketAdLibs.find({ + bucketId: props.bucket._id, + }).fetch() + const bucketActions = BucketAdLibActions.find({ + bucketId: props.bucket._id, + }) + .fetch() + .map((action) => actionToAdLibPieceUi(action, sourceLayers, outputLayers)) + return (bucketAdLibPieces as BucketAdLibItem[]) + .concat(bucketActions) + .sort((a, b) => a._rank - b._rank || a.name.localeCompare(b.name)) + }, [props.bucket._id, sourceLayers, outputLayers]) + + // Wait for data to load, it might take a tick + if (!studio || !showStyleBaseId || !showStyleVariantId) return null + + return ( + ) - const { nextAdLibIds, nextTags } = getNextPieceInstancesGrouped(props.playlist, props.showStyleBase) - const bucketAdLibPieces = BucketAdLibs.find({ - bucketId: props.bucket._id, - }).fetch() - const bucketActions = BucketAdLibActions.find({ - bucketId: props.bucket._id, - }) - .fetch() - .map((action) => actionToAdLibPieceUi(action, tSLayers, tOLayers)) - const allBucketItems = (bucketAdLibPieces as BucketAdLibItem[]) - .concat(bucketActions) - .sort((a, b) => a._rank - b._rank || a.name.localeCompare(b.name)) - - return literal({ - adLibPieces: allBucketItems, - studio, - unfinishedAdLibIds, - unfinishedTags, - showStyleBaseId, - showStyleVariantId, - nextAdLibIds, - nextTags, - outputLayers: tOLayers, - sourceLayers: tSLayers, - }) }, - (_data, props: IBucketPanelProps, nextProps: IBucketPanelProps) => { + (props: IBucketPanelProps, nextProps: IBucketPanelProps) => { return !_.isEqual(props, nextProps) } -)( +) + +const BucketPanelContent = withTranslation()( DropTarget([DragDropItemTypes.BUCKET, DragDropItemTypes.BUCKET_ADLIB_PIECE], bucketTarget, (connect) => ({ connectDropTarget: connect.dropTarget(), }))( @@ -361,7 +400,7 @@ export const BucketPanel = translateWithTracker, I connectDragPreview: connect.dragPreview(), isDragging: monitor.isDragging(), }))( - class BucketPanel extends MeteorReactComponent< + class BucketPanel extends React.Component< Translated & BucketSourceCollectedProps & BucketTargetCollectedProps, @@ -386,34 +425,6 @@ export const BucketPanel = translateWithTracker, I } componentDidMount(): void { - this.subscribe(MeteorPubSub.buckets, this.props.playlist.studioId, this.props.bucket._id) - this.subscribe(MeteorPubSub.uiBucketContentStatuses, this.props.playlist.studioId, this.props.bucket._id) - this.subscribe(MeteorPubSub.uiStudio, this.props.playlist.studioId) - this.autorun(() => { - const showStyles: Array<[ShowStyleBaseId, ShowStyleVariantId]> = - RundownPlaylistCollectionUtil.getRundownsUnordered(this.props.playlist).map((rundown) => [ - rundown.showStyleBaseId, - rundown.showStyleVariantId, - ]) - const showStyleBases = showStyles.map((showStyle) => showStyle[0]) - const showStyleVariants = showStyles.map((showStyle) => showStyle[1]) - this.subscribe( - CorelibPubSub.bucketAdLibPieces, - this.props.playlist.studioId, - this.props.bucket._id, - showStyleVariants - ) - this.subscribe( - CorelibPubSub.bucketAdLibActions, - this.props.playlist.studioId, - this.props.bucket._id, - showStyleVariants - ) - for (const showStyleBaseId of _.uniq(showStyleBases)) { - this.subscribe(MeteorPubSub.uiShowStyleBase, showStyleBaseId) - } - }) - window.addEventListener(MOSEvents.dragenter, this.onDragEnter) window.addEventListener(MOSEvents.dragleave, this.onDragLeave) @@ -431,8 +442,6 @@ export const BucketPanel = translateWithTracker, I } componentWillUnmount(): void { - this._cleanUp() - window.removeEventListener(MOSEvents.dragenter, this.onDragEnter) window.removeEventListener(MOSEvents.dragleave, this.onDragLeave) } From 98d8b3821513697baf10c1a90b12afc0e73984ba Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 15 Dec 2023 11:07:55 +0000 Subject: [PATCH 187/479] chore: refactor SegmentTimelineContainer to avoid `MeteorReactComponent` SOFIE-2751 (#1083) --- meteor/client/lib/lib.tsx | 22 ++- .../SegmentTimelineContainer.tsx | 186 +++++++----------- 2 files changed, 93 insertions(+), 115 deletions(-) diff --git a/meteor/client/lib/lib.tsx b/meteor/client/lib/lib.tsx index c3467e8d9f..7a03e0da5c 100644 --- a/meteor/client/lib/lib.tsx +++ b/meteor/client/lib/lib.tsx @@ -137,14 +137,30 @@ export function useInvalidateTimeout(func: () => [K, number], deps: any[]): K * @template K * @param {K} value value to be debounced * @param {number} delay how long to wait after an update before updating the state - * @return {*} debounced value + * @param shouldUpdate optional function to filter whether the value has changed + * @return {K} debounced value */ -export function useDebounce(value: K, delay: number): K { +export function useDebounce(value: K, delay: number, shouldUpdate?: (oldVal: K, newVal: K) => boolean): K { const [debouncedValue, setDebouncedValue] = useState(value) + // Store the function in a ref to avoid the debounce being reactive to the function changing + const shouldUpdateFn = useRef<(oldVal: K, newVal: K) => boolean>() + shouldUpdateFn.current = shouldUpdate + useEffect(() => { const handler = setTimeout(() => { - setDebouncedValue(value) + const shouldUpdate = shouldUpdateFn.current + if (typeof shouldUpdate === 'function') { + setDebouncedValue((oldVal) => { + if (shouldUpdate(oldVal, value)) { + return value + } else { + return oldVal + } + }) + } else { + setDebouncedValue(value) + } }, delay) return () => { diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx index c3620af664..09cd5ff342 100644 --- a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx +++ b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx @@ -1,19 +1,16 @@ -import * as React from 'react' +import React, { useMemo } from 'react' import * as PropTypes from 'prop-types' import * as _ from 'underscore' import { SegmentTimeline, SegmentTimelineClass } from './SegmentTimeline' import { computeSegmentDisplayDuration, RundownTiming, TimingEvent } from '../RundownView/RundownTiming/RundownTiming' import { UIStateStorage } from '../../lib/UIStateStorage' -import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { PartExtended } from '../../../lib/Rundown' import { MAGIC_TIME_SCALE_FACTOR } from '../RundownView' import { SpeechSynthesiser } from '../../lib/speechSynthesis' import { getElementWidth } from '../../utils/dimensions' import { isMaintainingFocus, scrollToSegment, getHeaderHeight } from '../../lib/viewPort' -import { meteorSubscribe } from '../../../lib/api/pubsub' -import { unprotectString, equalSets, equivalentArrays } from '../../../lib/lib' +import { equivalentArrays, unprotectString } from '../../../lib/lib' import { Settings } from '../../../lib/Settings' -import { Tracker } from 'meteor/tracker' import { Meteor } from 'meteor/meteor' import RundownViewEventBus, { RundownViewEvents, @@ -31,9 +28,10 @@ import { import { computeSegmentDuration, getPartInstanceTimingId, RundownTimingContext } from '../../lib/rundownTiming' import { RundownViewShelf } from '../RundownView/RundownViewShelf' import { PartInstanceId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { PartInstances, Parts, Segments } from '../../collections' -import { catchError } from '../../lib/lib' +import { PartInstances, Parts } from '../../collections' +import { catchError, useDebounce } from '../../lib/lib' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { useSubscription, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' // Kept for backwards compatibility export { SegmentUi, PartUi, PieceUi, ISourceLayerUi, IOutputLayerUi } from '../SegmentContainer/withResolvedSegment' @@ -79,8 +77,74 @@ interface IProps extends IResolvedSegmentProps { id: string } -export const SegmentTimelineContainer = withResolvedSegment( - class SegmentTimelineContainer extends MeteorReactComponent { +export function SegmentTimelineContainer(props: Readonly): JSX.Element { + const partIds = useTracker( + () => + Parts.find( + { + segmentId: props.segmentId, + }, + { + fields: { + _id: 1, + }, + } + ).map((part) => part._id), + [props.segmentId], + [] + ) + useSubscription(CorelibPubSub.pieces, [props.rundownId], partIds) + + const partInstanceIds = useTracker( + () => + PartInstances.find( + { + segmentId: props.segmentId, + reset: { + $ne: true, + }, + }, + { + fields: { + _id: 1, + part: 1, + }, + } + ).map((instance) => instance._id), + [props.segmentId] + ) + + const debouncedPartInstanceIds = useDebounce( + partInstanceIds, + 40, + (oldVal, newVal) => !oldVal || (!!newVal && !equivalentArrays(oldVal, newVal)) + ) + useSubscription(CorelibPubSub.pieceInstances, [props.rundownId], debouncedPartInstanceIds ?? [], {}) + + // Convert to an array and sort to allow the `useSubscription` to better detect them being unchanged + const sortedSegmentIds = useMemo(() => { + const segmentIds = Array.from(props.segmentsIdsBefore.values()) + + segmentIds.sort() + + return segmentIds + }, [props.segmentsIdsBefore]) + const sortedRundownIds = useMemo(() => { + const rundownIds = Array.from(props.rundownIdsBefore.values()) + + rundownIds.sort() + + return rundownIds + }, [props.rundownIdsBefore]) + + // past infinites subscription + useSubscription(CorelibPubSub.piecesInfiniteStartingBefore, props.rundownId, sortedSegmentIds, sortedRundownIds) + + return +} + +const SegmentTimelineContainerContent = withResolvedSegment( + class SegmentTimelineContainerContent extends React.Component { static contextTypes = { durations: PropTypes.object.isRequired, syncedDurations: PropTypes.object.isRequired, @@ -98,8 +162,6 @@ export const SegmentTimelineContainer = withResolvedSegment( syncedDurations: RundownTimingContext } - private pastInfinitesComp: Tracker.Computation | undefined - constructor(props: IProps & ITrackedResolvedSegmentProps) { super(props) @@ -133,53 +195,6 @@ export const SegmentTimelineContainer = withResolvedSegment( } componentDidMount(): void { - this.autorun(() => { - const partIds = Parts.find( - { - segmentId: this.props.segmentId, - }, - { - fields: { - _id: 1, - }, - } - ).map((part) => part._id) - - this.subscribe(CorelibPubSub.pieces, [this.props.rundownId], partIds ?? []) - }) - this.autorun(() => { - const partInstanceIds = PartInstances.find( - { - segmentId: this.props.segmentId, - reset: { - $ne: true, - }, - }, - { - fields: { - _id: 1, - part: 1, - }, - } - ).map((instance) => instance._id) - this.subscribeToPieceInstances(partInstanceIds) - }) - // past inifnites subscription - this.pastInfinitesComp = this.autorun(() => { - const segment = Segments.findOne(this.props.segmentId, { - fields: { - rundownId: 1, - _rank: 1, - }, - }) - segment && - this.subscribe( - CorelibPubSub.piecesInfiniteStartingBefore, - this.props.rundownId, - Array.from(this.props.segmentsIdsBefore.values()), - Array.from(this.props.rundownIdsBefore.values()) - ) - }) SpeechSynthesiser.init() this.rundownCurrentPartInstanceId = this.props.playlist.currentPartInfo?.partInstanceId ?? null @@ -320,14 +335,6 @@ export const SegmentTimelineContainer = withResolvedSegment( this.onFollowLiveLine(true) } - if ( - this.pastInfinitesComp && - (!equalSets(this.props.segmentsIdsBefore, prevProps.segmentsIdsBefore) || - !_.isEqual(this.props.rundownIdsBefore, prevProps.rundownIdsBefore)) - ) { - this.pastInfinitesComp.invalidate() - } - const budgetDuration = this.getSegmentBudgetDuration() if (!isLiveSegment && this.props.parts !== prevProps.parts) { @@ -349,16 +356,10 @@ export const SegmentTimelineContainer = withResolvedSegment( } componentWillUnmount(): void { - this._cleanUp() if (this.intersectionObserver && this.state.isLiveSegment && this.props.followLiveSegments) { if (typeof this.props.onSegmentScroll === 'function') this.props.onSegmentScroll() } - if (this.partInstanceSub !== undefined) { - const sub = this.partInstanceSub - setTimeout(() => { - sub.stop() - }, 500) - } + this.stopLive() RundownViewEventBus.off(RundownViewEvents.REWIND_SEGMENTS, this.onRewindSegment) RundownViewEventBus.off(RundownViewEvents.GO_TO_PART, this.onGoToPart) @@ -381,45 +382,6 @@ export const SegmentTimelineContainer = withResolvedSegment( return undefined } - private partInstanceSub: Meteor.SubscriptionHandle | undefined - private partInstanceSubPartInstanceIds: PartInstanceId[] | undefined - private subscribeToPieceInstancesInner = (partInstanceIds: PartInstanceId[]) => { - this.partInstanceSubDebounce = undefined - if ( - this.partInstanceSubPartInstanceIds && - equivalentArrays(this.partInstanceSubPartInstanceIds, partInstanceIds) - ) { - // old subscription is equivalent to the new one, don't do anything - return - } - // avoid having the subscription automatically scrapped by a re-run of the autorun - Tracker.nonreactive(() => { - if (this.partInstanceSub !== undefined) { - this.partInstanceSub.stop() - } - // we handle this subscription manually - this.partInstanceSub = meteorSubscribe( - CorelibPubSub.pieceInstances, - [this.props.rundownId], - partInstanceIds, - {} - ) - this.partInstanceSubPartInstanceIds = partInstanceIds - }) - } - private partInstanceSubDebounce: number | undefined - private subscribeToPieceInstances(partInstanceIds: PartInstanceId[]) { - // run the first subscribe immediately, to avoid unneccessary wait time during bootup - if (this.partInstanceSub === undefined) { - this.subscribeToPieceInstancesInner(partInstanceIds) - } else { - if (this.partInstanceSubDebounce !== undefined) { - clearTimeout(this.partInstanceSubDebounce) - } - this.partInstanceSubDebounce = setTimeout(this.subscribeToPieceInstancesInner, 40, partInstanceIds) - } - } - onWindowResize = _.throttle(() => { if (this.state.showingAllSegment) { this.updateMaxTimeScale() From a6ee4c26d02a690e6ba4eaacf8ae94e13612ea3e Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 15 Dec 2023 11:41:33 +0000 Subject: [PATCH 188/479] chore: refactor `RundownSystemStatus` to avoid `MeteorReactComponent` SOFIE-2751 (#1084) --- meteor/client/ui/RundownView.tsx | 5 +- .../ui/RundownView/RundownSystemStatus.tsx | 430 ++++++++---------- .../client/ui/Shelf/ShelfDashboardLayout.tsx | 4 +- meteor/client/ui/Shelf/SystemStatusPanel.tsx | 124 +++-- 4 files changed, 255 insertions(+), 308 deletions(-) diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx index d4fb3061d9..2c1814c7af 100644 --- a/meteor/client/ui/RundownView.tsx +++ b/meteor/client/ui/RundownView.tsx @@ -1096,9 +1096,8 @@ const RundownHeader = withTranslation()( layout={this.props.layout} /> diff --git a/meteor/client/ui/RundownView/RundownSystemStatus.tsx b/meteor/client/ui/RundownView/RundownSystemStatus.tsx index 2d5d048a90..aa0551dd93 100644 --- a/meteor/client/ui/RundownView/RundownSystemStatus.tsx +++ b/meteor/client/ui/RundownView/RundownSystemStatus.tsx @@ -1,76 +1,49 @@ -import { Meteor } from 'meteor/meteor' -import * as React from 'react' +import React, { useMemo } from 'react' import ClassNames from 'classnames' -import * as _ from 'underscore' -import { translateWithTracker, Translated } from '../../lib/ReactMeteorData/ReactMeteorData' +import { useSubscription, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { PeripheralDevice, PeripheralDeviceCategory, PeripheralDeviceType, } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { Time, getCurrentTime, unprotectString } from '../../../lib/lib' -import { withTranslation, WithTranslation } from 'react-i18next' -import { MeteorReactComponent } from '../../lib/MeteorReactComponent' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { Time, unprotectString } from '../../../lib/lib' +import { useTranslation } from 'react-i18next' import { StatusCode } from '@sofie-automation/blueprints-integration' -import { UIStudio } from '../../../lib/api/studios' -import { RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { RundownPlaylistId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PeripheralDevices } from '../../collections' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { useCurrentTime } from '../../lib/lib' interface IMOSStatusProps { lastUpdate: Time } -export const MOSLastUpdateStatus = withTranslation()( - class MOSLastUpdateStatus extends React.Component { - _interval: number +const MOSLastUpdateStatus = React.memo(function MOSLastUpdateStatus({ lastUpdate }: Readonly) { + const { t } = useTranslation() - componentDidMount(): void { - this._interval = Meteor.setInterval(() => { - this.tick() - }, 5000) - } - - componentWillUnmount(): void { - Meteor.clearInterval(this._interval) - } + const currentTime = useCurrentTime(5000) + const timeDiff = currentTime - lastUpdate - tick() { - this.forceUpdate() - } - - render(): JSX.Element { - const { t } = this.props - const timeDiff = getCurrentTime() - this.props.lastUpdate - return ( - - {timeDiff < 3000 && t('Just now')} - {timeDiff >= 3000 && timeDiff < 60 * 1000 && t('Less than a minute ago')} - {timeDiff >= 60 * 1000 && timeDiff < 5 * 60 * 1000 && t('Less than five minutes ago')} - {timeDiff >= 5 * 60 * 1000 && timeDiff < 10 * 60 * 1000 && t('Around 10 minutes ago')} - {timeDiff >= 10 * 60 * 1000 && timeDiff < 30 * 60 * 1000 && t('More than 10 minutes ago')} - {timeDiff >= 30 * 60 * 1000 && timeDiff < 2 * 60 * 60 * 1000 && t('More than 30 minutes ago')} - {timeDiff >= 2 * 60 * 60 * 1000 && timeDiff < 5 * 60 * 60 * 1000 && t('More than 2 hours ago')} - {timeDiff >= 5 * 60 * 60 * 1000 && timeDiff < 24 * 60 * 60 * 1000 && t('More than 5 hours ago')} - {timeDiff >= 24 * 60 * 60 * 1000 && t('More than a day ago')} - - ) - } - } -) + return ( + + {timeDiff < 3000 && t('Just now')} + {timeDiff >= 3000 && timeDiff < 60 * 1000 && t('Less than a minute ago')} + {timeDiff >= 60 * 1000 && timeDiff < 5 * 60 * 1000 && t('Less than five minutes ago')} + {timeDiff >= 5 * 60 * 1000 && timeDiff < 10 * 60 * 1000 && t('Around 10 minutes ago')} + {timeDiff >= 10 * 60 * 1000 && timeDiff < 30 * 60 * 1000 && t('More than 10 minutes ago')} + {timeDiff >= 30 * 60 * 1000 && timeDiff < 2 * 60 * 60 * 1000 && t('More than 30 minutes ago')} + {timeDiff >= 2 * 60 * 60 * 1000 && timeDiff < 5 * 60 * 60 * 1000 && t('More than 2 hours ago')} + {timeDiff >= 5 * 60 * 60 * 1000 && timeDiff < 24 * 60 * 60 * 1000 && t('More than 5 hours ago')} + {timeDiff >= 24 * 60 * 60 * 1000 && t('More than a day ago')} + + ) +}) interface IProps { - studio: UIStudio - playlist: DBRundownPlaylist - rundownIds: RundownId[] - firstRundown: Rundown | undefined -} - -interface IState { - mosDiff: OnLineOffLineList - playoutDiff: OnLineOffLineList + studioId: StudioId + playlistId: RundownPlaylistId + firstRundown: Pick | undefined } interface OnLineOffLineList { @@ -86,70 +59,72 @@ interface ITrackedProps { playoutDevices: OnLineOffLineList } -function diffOnLineOffLineList(prevList: OnLineOffLineList, list: OnLineOffLineList): OnLineOffLineList { - const diff: OnLineOffLineList = { - onLine: [], - offLine: [], +function calculateStatusForDevices(devices: PeripheralDevice[]) { + const status = devices + .filter((i) => !i.ignore) + .reduce((memo: StatusCode, device: PeripheralDevice) => { + if (device.connected && memo.valueOf() < device.status.statusCode.valueOf()) { + return device.status.statusCode + } else if (!device.connected) { + return StatusCode.FATAL + } else { + return memo + } + }, StatusCode.UNKNOWN) + + const onlineOffline: OnLineOffLineList = { + onLine: devices.filter((device) => device.connected && device.status.statusCode < StatusCode.WARNING_MINOR), + offLine: devices.filter((device) => !device.connected || device.status.statusCode >= StatusCode.WARNING_MINOR), + } + const lastUpdate = devices.reduce((memo, device) => Math.max(device.lastDataReceived || 0, memo), 0) + return { + status: status, + lastUpdate: lastUpdate, + onlineOffline: onlineOffline, } +} - list.onLine.forEach((i) => { - if (!prevList.onLine.find((j) => j._id === i._id)) { - diff.onLine.push(i) - } - }) - list.offLine.forEach((i) => { - if (!prevList.offLine.find((j) => j._id === i._id)) { - diff.offLine.push(i) - } - }) +export const RundownSystemStatus = React.memo( + function RundownSystemStatus(props: Readonly): JSX.Element { + useSubscription(CorelibPubSub.peripheralDevicesAndSubDevices, props.studioId) - return diff -} + const parentDevices = useTracker( + () => + PeripheralDevices.find({ + studioId: props.studioId, + }).fetch(), + [], + [] + ) + const parentDeviceIds = useMemo(() => parentDevices.map((pd) => pd._id), [parentDevices]) + const subDevices = useTracker( + () => + PeripheralDevices.find({ + parentDeviceId: { $in: parentDeviceIds }, + }).fetch(), + [parentDeviceIds], + [] + ) -export const RundownSystemStatus = translateWithTracker( - (props: IProps) => { - let attachedDevices: PeripheralDevice[] = [] + const ingest = useMemo(() => { + const attachedDevices = [...parentDevices, ...subDevices] - const parentDevices = PeripheralDevices.find({ - studioId: props.studio._id, - }).fetch() - attachedDevices = attachedDevices.concat(parentDevices) + const ingestDevices = attachedDevices.filter( + (i) => i.category === PeripheralDeviceCategory.INGEST || i.category === PeripheralDeviceCategory.MEDIA_MANAGER + ) - const subDevices = PeripheralDevices.find({ - parentDeviceId: { $in: _.pluck(parentDevices, '_id') }, - }).fetch() - attachedDevices = attachedDevices.concat(subDevices) + return calculateStatusForDevices(ingestDevices) + }, [parentDevices, subDevices]) - const ingestDevices = attachedDevices.filter( - (i) => i.category === PeripheralDeviceCategory.INGEST || i.category === PeripheralDeviceCategory.MEDIA_MANAGER - ) - const playoutDevices = attachedDevices.filter((i) => i.type === PeripheralDeviceType.PLAYOUT) + const playout = useMemo(() => { + const attachedDevices = [...parentDevices, ...subDevices] - const [ingest, playout] = [ingestDevices, playoutDevices].map((devices) => { - const status = devices - .filter((i) => !i.ignore) - .reduce((memo: StatusCode, device: PeripheralDevice) => { - if (device.connected && memo.valueOf() < device.status.statusCode.valueOf()) { - return device.status.statusCode - } else if (!device.connected) { - return StatusCode.FATAL - } else { - return memo - } - }, StatusCode.UNKNOWN) - const onlineOffline: OnLineOffLineList = { - onLine: devices.filter((device) => device.connected && device.status.statusCode < StatusCode.WARNING_MINOR), - offLine: devices.filter((device) => !device.connected || device.status.statusCode >= StatusCode.WARNING_MINOR), - } - const lastUpdate = devices.reduce((memo, device) => Math.max(device.lastDataReceived || 0, memo), 0) - return { - status: status, - lastUpdate: lastUpdate, - onlineOffline: onlineOffline, - } - }) + const playoutDevices = attachedDevices.filter((i) => i.type === PeripheralDeviceType.PLAYOUT) - return { + return calculateStatusForDevices(playoutDevices) + }, [parentDevices, subDevices]) + + const trackedProps: ITrackedProps = { mosStatus: ingest.status, mosDevices: ingest.onlineOffline, mosLastUpdate: ingest.lastUpdate, @@ -157,150 +132,125 @@ export const RundownSystemStatus = translateWithTracker( playoutStatus: playout.status, playoutDevices: playout.onlineOffline, } + + return }, - (_data, props: IProps, nextProps: IProps) => { - if (props.playlist._id === nextProps.playlist._id && props.studio._id === nextProps.studio._id) return false + (props: IProps, nextProps: IProps) => { + if (props.playlistId === nextProps.playlistId && props.studioId === nextProps.studioId) return false return true } -)( - class RundownSystemStatus extends MeteorReactComponent, IState> { - constructor(props: Translated) { - super(props) - - this.state = { - mosDiff: { - onLine: [], - offLine: props.mosDevices.offLine, - }, - playoutDiff: { - onLine: [], - offLine: props.playoutDevices.offLine, - }, - } - } - - componentDidMount(): void { - this.subscribe(CorelibPubSub.peripheralDevicesAndSubDevices, this.props.studio._id) - } +) - componentWillUnmount(): void { - super.componentWillUnmount() - } +function RundownSystemStatusContent({ + firstRundown, - componentDidUpdate(prevProps: IProps & ITrackedProps) { - if (prevProps !== this.props) { - const mosDiff = diffOnLineOffLineList(prevProps.mosDevices, this.props.mosDevices) - const playoutDiff = diffOnLineOffLineList(prevProps.playoutDevices, this.props.playoutDevices) + mosStatus, + mosLastUpdate, + mosDevices, + playoutStatus, + playoutDevices, +}: Readonly) { + const { t } = useTranslation() - this.setState({ - mosDiff, - playoutDiff, - }) - } - } - render(): JSX.Element { - const { t } = this.props - const playoutDevicesIssues = this.props.playoutDevices.offLine.filter((dev) => dev.connected) - const mosDevicesIssues = this.props.mosDevices.offLine.filter((dev) => dev.connected) - const mosDisconnected = this.props.mosDevices.offLine.filter((dev) => !dev.connected) - const playoutDisconnected = this.props.playoutDevices.offLine.filter((dev) => !dev.connected) + const playoutDevicesIssues = playoutDevices.offLine.filter((dev) => dev.connected) + const mosDevicesIssues = mosDevices.offLine.filter((dev) => dev.connected) + const mosDisconnected = mosDevices.offLine.filter((dev) => !dev.connected) + const playoutDisconnected = playoutDevices.offLine.filter((dev) => !dev.connected) - return ( -
-
-
+
+
+
+

+ {t('{{nrcsName}} Connection', { + nrcsName: firstRundown?.externalNRCSName || 'NRCS', })} - > -
-

- {t('{{nrcsName}} Connection', { - nrcsName: (this.props.firstRundown && this.props.firstRundown.externalNRCSName) || 'NRCS', - })} -

-
-
{t('Last update')}
- -
-
- {this.props.mosDevices.offLine.length > 0 ? ( +

+
+
{t('Last update')}
+ +
+
+ {mosDevices.offLine.length > 0 ? ( + + {mosDisconnected.length ? ( - {mosDisconnected.length ? ( - -
{t('Off-line devices')}
-
    - {mosDisconnected.map((device) => { - return
  • {device.name}
  • - })} -
-
- ) : null} - {mosDevicesIssues.length ? ( - -
{t('Devices with issues')}
-
    - {mosDevicesIssues.map((device) => { - return
  • {device.name}
  • - })} -
-
- ) : null} +
{t('Off-line devices')}
+
    + {mosDisconnected.map((device) => { + return
  • {device.name}
  • + })} +
- ) : ( - {t('All connections working correctly')} - )} -
-
+ ) : null} + {mosDevicesIssues.length ? ( + +
{t('Devices with issues')}
+
    + {mosDevicesIssues.map((device) => { + return
  • {device.name}
  • + })} +
+
+ ) : null} + + ) : ( + {t('All connections working correctly')} + )}
-
-
-

{t('Play-out')}

-
- {this.props.playoutDevices.offLine.length > 0 ? ( +
+
+
+
+

{t('Play-out')}

+
+ {playoutDevices.offLine.length > 0 ? ( + + {playoutDisconnected.length ? ( + +
{t('Off-line devices')}
+
    + {playoutDisconnected.map((device) => { + return
  • {device.name}
  • + })} +
+
+ ) : null} + {playoutDevicesIssues.length ? ( - {playoutDisconnected.length ? ( - -
{t('Off-line devices')}
-
    - {playoutDisconnected.map((device) => { - return
  • {device.name}
  • - })} -
-
- ) : null} - {playoutDevicesIssues.length ? ( - -
{t('Devices with issues')}
-
    - {playoutDevicesIssues.map((device) => { - return
  • {device.name}
  • - })} -
-
- ) : null} +
{t('Devices with issues')}
+
    + {playoutDevicesIssues.map((device) => { + return
  • {device.name}
  • + })} +
- ) : ( - {t('All devices working correctly')} - )} -
-
+ ) : null} + + ) : ( + {t('All devices working correctly')} + )}
- ) - } - } -) +
+
+ ) +} diff --git a/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx b/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx index 5536d31ddf..0d562644c4 100644 --- a/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx +++ b/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx @@ -203,10 +203,10 @@ export function ShelfDashboardLayout(props: Readonly return ( ) } else if (RundownLayoutsAPI.isShowStyleDisplay(panel)) { diff --git a/meteor/client/ui/Shelf/SystemStatusPanel.tsx b/meteor/client/ui/Shelf/SystemStatusPanel.tsx index b8d45b1227..c6b6bb7467 100644 --- a/meteor/client/ui/Shelf/SystemStatusPanel.tsx +++ b/meteor/client/ui/Shelf/SystemStatusPanel.tsx @@ -5,81 +5,79 @@ import { RundownLayoutBase, RundownLayoutSytemStatus, } from '../../../lib/collections/RundownLayouts' -import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { dashboardElementStyle } from './DashboardPanel' import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts' import { RundownSystemStatus } from '../RundownView/RundownSystemStatus' -import { DBRundown, Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { UIStudio } from '../../../lib/api/studios' -import { RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { Rundowns } from '../../collections' +import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { RundownId, RundownPlaylistId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { RundownPlaylists, Rundowns } from '../../collections' import { RundownPlaylistCollectionUtil } from '../../../lib/collections/rundownPlaylistUtil' +import { useTranslation } from 'react-i18next' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' interface ISystemStatusPanelProps { - studio: UIStudio - visible?: boolean + studioId: StudioId layout: RundownLayoutBase panel: RundownLayoutSytemStatus - playlist: DBRundownPlaylist -} - -interface IState {} - -interface ISystemStatusPanelTrackedProps { - firstRundown: Rundown | undefined - rundownIds: RundownId[] + playlistId: RundownPlaylistId } -class SystemStatusPanelInner extends React.Component< - Translated, - IState -> { - render(): JSX.Element { - const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout) - const { t, panel } = this.props +export function SystemStatusPanel({ + panel, + layout, + studioId, + playlistId, +}: Readonly): JSX.Element { + const { t } = useTranslation() - return ( -
- - {t('System Status')} - - -
- ) - } -} + const firstRundown = useTracker(() => { + const playlist = RundownPlaylists.findOne(playlistId, { + fields: { + rundownIdsInOrder: 1, + }, + }) as Pick | undefined + if (!playlist) return undefined -export const SystemStatusPanel = translateWithTracker( - (props: ISystemStatusPanelProps) => { - const rundownIds = RundownPlaylistCollectionUtil.getRundownOrderedIDs(props.playlist) - let firstRundown: DBRundown | undefined + let rundownId: RundownId | undefined = undefined - if (props.playlist.rundownIdsInOrder.length > 0) { - firstRundown = Rundowns.findOne({ - playlistId: props.playlist._id, - _id: props.playlist.rundownIdsInOrder[0], - }) - } else if (rundownIds.length > 0) { - firstRundown = Rundowns.findOne({ - playlistId: props.playlist._id, - _id: rundownIds[0], - }) + if (playlist.rundownIdsInOrder.length > 0) { + rundownId = playlist.rundownIdsInOrder[0] + } else { + rundownId = RundownPlaylistCollectionUtil.getRundownOrderedIDs(playlist)[0] } - return { - rundownIds, - firstRundown, + + if (rundownId) { + return Rundowns.findOne( + { + playlistId: playlistId, + _id: rundownId, + }, + { + fields: { + externalNRCSName: 1, + }, + } + ) as Pick | undefined + } else { + return undefined } - } -)(SystemStatusPanelInner) + }, [playlistId]) + + const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(layout) + + return ( +
+ + {t('System Status')} + + +
+ ) +} From 46c8adf02927bdac20277096b56c256d398991cc Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 15 Dec 2023 13:36:01 +0000 Subject: [PATCH 189/479] chore: refactor PresenterScreen and OverlayScreen to avoid MeteorReactComponent SOFIE-2751 (#1090) --- .../lib/ReactMeteorData/ReactMeteorData.tsx | 5 +- meteor/client/ui/ClockView/OverlayScreen.tsx | 194 ++++--- .../client/ui/ClockView/PresenterScreen.tsx | 541 +++++++++--------- 3 files changed, 385 insertions(+), 355 deletions(-) diff --git a/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx b/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx index 55eb4d9d72..b30f061638 100644 --- a/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx +++ b/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx @@ -8,6 +8,7 @@ import { withTranslation, WithTranslation } from 'react-i18next' import { MeteorReactComponent } from '../MeteorReactComponent' import { meteorSubscribe, AllPubSubTypes } from '../../../lib/api/pubsub' import { stringifyObjects } from '../../../lib/lib' +import _ from 'underscore' const globalTrackerQueue: Array = [] let globalTrackerTimestamp: number | undefined = undefined @@ -364,12 +365,12 @@ export function useSubscription( */ export function useSubscriptions( sub: K, - argsArray: Parameters[] + argsArray: Array | undefined | null | false> ): boolean { const [ready, setReady] = useState(false) useEffect(() => { - const subscriptions = Tracker.nonreactive(() => argsArray.map((args) => meteorSubscribe(sub, ...args))) + const subscriptions = Tracker.nonreactive(() => _.compact(argsArray).map((args) => meteorSubscribe(sub, ...args))) const isReadyComp = Tracker.nonreactive(() => Tracker.autorun(() => setReady(subscriptions.reduce((memo, subscription) => memo && subscription.ready(), true))) ) diff --git a/meteor/client/ui/ClockView/OverlayScreen.tsx b/meteor/client/ui/ClockView/OverlayScreen.tsx index cb0a02a5c6..c1b976bfa6 100644 --- a/meteor/client/ui/ClockView/OverlayScreen.tsx +++ b/meteor/client/ui/ClockView/OverlayScreen.tsx @@ -1,116 +1,138 @@ -import React from 'react' +import React, { useEffect } from 'react' import Moment from 'react-moment' -import { withTranslation, WithTranslation } from 'react-i18next' -import { withTiming } from '../RundownView/RundownTiming/withTiming' +import { useTranslation } from 'react-i18next' +import { WithTiming, withTiming } from '../RundownView/RundownTiming/withTiming' import { withTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { PieceIconContainer } from '../PieceIcons/PieceIcon' import { PieceNameContainer } from '../PieceIcons/PieceName' import { Timediff } from './Timediff' -import { getPresenterScreenReactive, PresenterScreenBase, RundownOverviewTrackedProps } from './PresenterScreen' +import { + getPresenterScreenReactive, + PresenterScreenTrackedProps, + usePresenterScreenSubscriptions, +} from './PresenterScreen' import { RundownPlaylistId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' interface TimeMap { [key: string]: number } -interface RundownOverviewProps { +interface OverlayScreenProps { studioId: StudioId playlistId: RundownPlaylistId segmentLiveDurations?: TimeMap } -interface RundownOverviewState {} +interface OverlayScreenState {} /** * This component renders a Countdown screen for a given playlist */ -export const OverlayScreen = withTranslation()( - withTracker( - getPresenterScreenReactive - )( - withTiming()( - class OverlayScreen extends PresenterScreenBase { - protected bodyClassList: string[] = ['transparent'] +export const OverlayScreen = withTracker( + getPresenterScreenReactive +)( + withTiming()(function OverlayScreen( + props: Readonly> + ) { + usePresenterScreenSubscriptions(props) - render(): JSX.Element { - const { playlist, segments, nextShowStyleBaseId, t, playlistId, currentPartInstance, nextPartInstance } = - this.props + useEffect(() => { + const bodyClassList: string[] = ['transparent'] - if (playlist && playlistId && segments) { - const currentPart = currentPartInstance + document.body.classList.add(...bodyClassList) - let currentPartCountdown: number | null = null - if (currentPart) { - currentPartCountdown = this.props.timingDurations.remainingTimeOnCurrentPart || 0 - } + return () => { + document.body.classList.remove(...bodyClassList) + } + }, []) + + return + }) +) + +function OverlayScreenContent({ + playlist, + segments, + nextShowStyleBaseId, + playlistId, + currentPartInstance, + nextPartInstance, + timingDurations, + rundownIds, +}: Readonly>) { + const { t } = useTranslation() + + if (playlist && playlistId && segments) { + const currentPart = currentPartInstance - const nextPart = nextPartInstance + let currentPartCountdown: number | null = null + if (currentPart) { + currentPartCountdown = timingDurations.remainingTimeOnCurrentPart || 0 + } - // The over-under counter is something we may want to introduce into the screen at some point, - // So I'm leaving these as a reference -- Jan Starzak, 2020/12/16 - // const overUnderClock = playlist.expectedDuration - // ? (this.props.timingDurations.asPlayedRundownDuration || 0) - playlist.expectedDuration - // : (this.props.timingDurations.asPlayedRundownDuration || 0) - - // (this.props.timingDurations.totalRundownDuration || 0) + const nextPart = nextPartInstance - const currentTime = this.props.timingDurations.currentTime || 0 + // The over-under counter is something we may want to introduce into the screen at some point, + // So I'm leaving these as a reference -- Jan Starzak, 2020/12/16 + // const overUnderClock = playlist.expectedDuration + // ? (this.props.timingDurations.asPlayedRundownDuration || 0) - playlist.expectedDuration + // : (this.props.timingDurations.asPlayedRundownDuration || 0) - + // (this.props.timingDurations.totalRundownDuration || 0) - return ( -
-
-
- {currentPartCountdown !== null ? ( - - ) : ( - {t('Next')} - )} -
- {/* - // An Auto-Next is something we may want to introduce in this view after we have - // some feedback from the users and they say it may be useful. - // -- Jan Starzak, 2020/12/16 + const currentTime = timingDurations.currentTime || 0 - {currentPart && currentPart.instance.part.autoNext ? ( -
- -
- ) : null} */} -
- {nextPart && nextShowStyleBaseId ? ( - - ) : null} -
-
- {nextPart && nextShowStyleBaseId && nextPart.instance.part.title ? ( - - ) : ( - '_' - )} -
- - - -
-
- ) - } - return ( -
-
+ return ( +
+
+
+ {currentPartCountdown !== null ? ( + + ) : ( + {t('Next')} + )} +
+ {/* + // An Auto-Next is something we may want to introduce in this view after we have + // some feedback from the users and they say it may be useful. + // -- Jan Starzak, 2020/12/16 + + {currentPart && currentPart.instance.part.autoNext ? ( +
+
- ) - } - } + ) : null} */} +
+ {nextPart && nextShowStyleBaseId ? ( + + ) : null} +
+
+ {nextPart && nextShowStyleBaseId && nextPart.instance.part.title ? ( + + ) : ( + '_' + )} +
+ + + +
+
) + } + return ( +
+
+
) -) +} diff --git a/meteor/client/ui/ClockView/PresenterScreen.tsx b/meteor/client/ui/ClockView/PresenterScreen.tsx index 2e8aa17347..1be7d134e9 100644 --- a/meteor/client/ui/ClockView/PresenterScreen.tsx +++ b/meteor/client/ui/ClockView/PresenterScreen.tsx @@ -1,15 +1,13 @@ -import * as React from 'react' +import React, { useEffect } from 'react' import ClassNames from 'classnames' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { PartUi } from '../SegmentTimeline/SegmentTimelineContainer' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { withTranslation, WithTranslation } from 'react-i18next' import { withTiming, WithTiming } from '../RundownView/RundownTiming/withTiming' -import { Translated, withTracker } from '../../lib/ReactMeteorData/ReactMeteorData' +import { useSubscription, useSubscriptions, useTracker, withTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { extendMandadory, getCurrentTime, protectString, unprotectString } from '../../../lib/lib' import { PartInstance } from '../../../lib/collections/PartInstances' -import { MeteorReactComponent } from '../../lib/MeteorReactComponent' import { MeteorPubSub } from '../../../lib/api/pubsub' import { PieceIconContainer } from '../PieceIcons/PieceIcon' import { PieceNameContainer } from '../PieceIcons/PieceName' @@ -19,7 +17,7 @@ import { PieceLifespan } from '@sofie-automation/blueprints-integration' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { PieceCountdownContainer } from '../PieceIcons/PieceCountdown' import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' -import { DashboardLayout, RundownLayoutBase, RundownLayoutPresenterView } from '../../../lib/collections/RundownLayouts' +import { DashboardLayout, RundownLayoutBase } from '../../../lib/collections/RundownLayouts' import { PartId, RundownId, @@ -51,17 +49,14 @@ interface TimeMap { [key: string]: number } -interface RundownOverviewProps { +interface PresenterScreenProps { studioId: StudioId playlistId: RundownPlaylistId segmentLiveDurations?: TimeMap } -interface RundownOverviewState { - presenterLayout: RundownLayoutPresenterView | undefined -} -export interface RundownOverviewTrackedProps { +export interface PresenterScreenTrackedProps { studio: UIStudio | undefined - playlist?: DBRundownPlaylist + playlist: DBRundownPlaylist | undefined rundowns: Rundown[] segments: Array pieces: Map @@ -163,7 +158,7 @@ function getShowStyleBaseIdSegmentPartUi( } } -export const getPresenterScreenReactive = (props: RundownOverviewProps): RundownOverviewTrackedProps => { +export const getPresenterScreenReactive = (props: PresenterScreenProps): PresenterScreenTrackedProps => { const studio = UIStudios.findOne(props.studioId) let playlist: DBRundownPlaylist | undefined @@ -286,296 +281,308 @@ export const getPresenterScreenReactive = (props: RundownOverviewProps): Rundown } } -export class PresenterScreenBase extends MeteorReactComponent< - WithTiming, - RundownOverviewState -> { - protected bodyClassList: string[] = ['dark', 'xdark'] +function PresenterScreenContent(props: WithTiming): JSX.Element { + usePresenterScreenSubscriptions(props) + + let selectedPresenterLayout: RundownLayoutBase | undefined = undefined + + if (props.rundownLayouts) { + // first try to use the one selected by the user + if (props.presenterLayoutId) { + selectedPresenterLayout = props.rundownLayouts.find((i) => i._id === props.presenterLayoutId) + } + + // if couldn't find based on id, try matching part of the name + if (props.presenterLayoutId && !selectedPresenterLayout) { + selectedPresenterLayout = props.rundownLayouts.find( + (i) => i.name.indexOf(unprotectString(props.presenterLayoutId!)) >= 0 + ) + } - constructor(props: WithTiming) { - super(props) - this.state = { - presenterLayout: undefined, + // if still not found, use the first one + if (!selectedPresenterLayout) { + selectedPresenterLayout = props.rundownLayouts.find((i) => RundownLayoutsAPI.isLayoutForPresenterView(i)) } } - componentDidMount(): void { - document.body.classList.add(...this.bodyClassList) - this.subscribeToData() + const presenterLayout = + selectedPresenterLayout && RundownLayoutsAPI.isLayoutForPresenterView(selectedPresenterLayout) + ? selectedPresenterLayout + : undefined + + useEffect(() => { + const bodyClassList: string[] = ['dark', 'xdark'] + + document.body.classList.add(...bodyClassList) + + return () => { + document.body.classList.remove(...bodyClassList) + } + }, []) + + if (presenterLayout && RundownLayoutsAPI.isDashboardLayout(presenterLayout)) { + return ( + + ) + } else { + return } +} - protected subscribeToData(): void { - this.autorun(() => { - this.subscribe(MeteorPubSub.uiStudio, this.props.studioId) +export function usePresenterScreenSubscriptions(props: PresenterScreenProps): void { + useSubscription(MeteorPubSub.uiStudio, props.studioId) - const playlist = RundownPlaylists.findOne(this.props.playlistId, { + const playlist = useTracker( + () => + RundownPlaylists.findOne(props.playlistId, { fields: { _id: 1, activationId: 1, }, - }) as Pick | undefined + }) as Pick | undefined, + [props.playlistId] + ) + + useSubscription(CorelibPubSub.rundownsInPlaylists, playlist ? [playlist._id] : []) + + const { rundownIds, showStyleBaseIds, showStyleVariantIds } = useTracker( + () => { if (playlist) { - this.subscribe(CorelibPubSub.rundownsInPlaylists, [playlist._id]) - - this.autorun(() => { - const rundowns = RundownPlaylistCollectionUtil.getRundownsUnordered(playlist, undefined, { - fields: { - _id: 1, - showStyleBaseId: 1, - showStyleVariantId: 1, - }, - }) as Array> - const rundownIds = rundowns.map((r) => r._id) - const showStyleBaseIds = rundowns.map((r) => r.showStyleBaseId) - const showStyleVariantIds = rundowns.map((r) => r.showStyleVariantId) - - this.subscribe(CorelibPubSub.segments, rundownIds, {}) - this.subscribe(CorelibPubSub.parts, rundownIds, null) - this.subscribe(CorelibPubSub.partInstances, rundownIds, playlist.activationId ?? null) - - for (const rundown of rundowns) { - this.subscribe(MeteorPubSub.uiShowStyleBase, rundown.showStyleBaseId) - } - - this.subscribe(CorelibPubSub.showStyleVariants, null, showStyleVariantIds) - this.subscribe(MeteorPubSub.rundownLayouts, showStyleBaseIds) - - this.autorun(() => { - const playlistR = RundownPlaylists.findOne(this.props.playlistId, { - fields: { - _id: 1, - currentPartInfo: 1, - nextPartInfo: 1, - previousPartInfo: 1, - }, - }) as Pick | undefined - if (playlistR) { - const { nextPartInstance, currentPartInstance } = - RundownPlaylistCollectionUtil.getSelectedPartInstances(playlistR) - if (currentPartInstance) { - this.subscribe( - CorelibPubSub.pieceInstances, - [currentPartInstance.rundownId], - [currentPartInstance._id], - {} - ) - } - if (nextPartInstance) { - this.subscribe(CorelibPubSub.pieceInstances, [nextPartInstance.rundownId], [nextPartInstance._id], {}) - } - } - }) - }) + const rundowns = RundownPlaylistCollectionUtil.getRundownsUnordered(playlist, undefined, { + fields: { + _id: 1, + showStyleBaseId: 1, + showStyleVariantId: 1, + }, + }) as Array> + const rundownIds = rundowns.map((r) => r._id) + const showStyleBaseIds = rundowns.map((r) => r.showStyleBaseId) + const showStyleVariantIds = rundowns.map((r) => r.showStyleVariantId) + + return { rundownIds, showStyleBaseIds, showStyleVariantIds } + } else { + return { rundownIds: [], showStyleBaseIds: [], showStyleVariantIds: [] } } - }) - } + }, + [playlist], + { rundownIds: [], showStyleBaseIds: [], showStyleVariantIds: [] } + ) - static getDerivedStateFromProps( - props: Translated - ): Partial { - let selectedPresenterLayout: RundownLayoutBase | undefined = undefined + useSubscription(CorelibPubSub.segments, rundownIds, {}) + useSubscription(CorelibPubSub.parts, rundownIds, null) + useSubscription(CorelibPubSub.partInstances, rundownIds, playlist?.activationId ?? null) + useSubscriptions( + MeteorPubSub.uiShowStyleBase, + showStyleBaseIds.map((id) => [id]) + ) + useSubscription(CorelibPubSub.showStyleVariants, null, showStyleVariantIds) + useSubscription(MeteorPubSub.rundownLayouts, showStyleBaseIds) - if (props.rundownLayouts) { - // first try to use the one selected by the user - if (props.presenterLayoutId) { - selectedPresenterLayout = props.rundownLayouts.find((i) => i._id === props.presenterLayoutId) - } + const { currentPartInstance, nextPartInstance } = useTracker( + () => { + const playlist = RundownPlaylists.findOne(props.playlistId, { + fields: { + _id: 1, + currentPartInfo: 1, + nextPartInfo: 1, + previousPartInfo: 1, + }, + }) as Pick | undefined - // if couldn't find based on id, try matching part of the name - if (props.presenterLayoutId && !selectedPresenterLayout) { - selectedPresenterLayout = props.rundownLayouts.find( - (i) => i.name.indexOf(unprotectString(props.presenterLayoutId!)) >= 0 - ) + if (playlist) { + return RundownPlaylistCollectionUtil.getSelectedPartInstances(playlist) + } else { + return { currentPartInstance: undefined, nextPartInstance: undefined, previousPartInstance: undefined } } + }, + [props.playlistId], + { currentPartInstance: undefined, nextPartInstance: undefined, previousPartInstance: undefined } + ) - // if still not found, use the first one - if (!selectedPresenterLayout) { - selectedPresenterLayout = props.rundownLayouts.find((i) => RundownLayoutsAPI.isLayoutForPresenterView(i)) - } - } + useSubscriptions(CorelibPubSub.pieceInstances, [ + currentPartInstance && [[currentPartInstance.rundownId], [currentPartInstance._id], {}], + nextPartInstance && [[nextPartInstance.rundownId], [nextPartInstance._id], {}], + ]) +} - return { - presenterLayout: - selectedPresenterLayout && RundownLayoutsAPI.isLayoutForPresenterView(selectedPresenterLayout) - ? selectedPresenterLayout - : undefined, - } - } +interface PresenterScreenContentDashboardLayoutProps { + studio: UIStudio | undefined + playlist: DBRundownPlaylist | undefined + currentShowStyleBase: UIShowStyleBase | undefined + currentShowStyleVariant: DBShowStyleVariant | undefined - componentWillUnmount(): void { - super.componentWillUnmount() - document.body.classList.remove(...this.bodyClassList) + layout: DashboardLayout +} +function PresenterScreenContentDashboardLayout({ + studio, + playlist, + currentShowStyleBase, + currentShowStyleVariant, + layout, +}: Readonly) { + if (studio && playlist && currentShowStyleBase && currentShowStyleVariant) { + return ( +
+ +
+ ) } + return null +} - render(): JSX.Element | null { - if (this.state.presenterLayout && RundownLayoutsAPI.isDashboardLayout(this.state.presenterLayout)) { - return this.renderDashboardLayout(this.state.presenterLayout) +function PresenterScreenContentDefaultLayout({ + playlist, + segments, + pieces, + currentShowStyleBaseId, + nextShowStyleBaseId, + playlistId, + currentPartInstance, + currentSegment, + timingDurations, + nextPartInstance, + nextSegment, + rundownIds, +}: Readonly>) { + if (playlist && playlistId && segments) { + let currentPartCountdown = 0 + if (currentPartInstance) { + currentPartCountdown = timingDurations.remainingTimeOnCurrentPart || 0 } - return this.renderDefaultLayout() - } - - private renderDefaultLayout() { - const { playlist, segments, pieces, currentShowStyleBaseId, nextShowStyleBaseId, playlistId } = this.props - if (playlist && playlistId && segments) { - const currentPart = this.props.currentPartInstance - const currentSegment = this.props.currentSegment - - let currentPartCountdown = 0 - if (currentPart) { - currentPartCountdown = this.props.timingDurations.remainingTimeOnCurrentPart || 0 - } - - const nextPart = this.props.nextPartInstance - const nextSegment = this.props.nextSegment - - const expectedStart = PlaylistTiming.getExpectedStart(playlist.timing) - const overUnderClock = getPlaylistTimingDiff(playlist, this.props.timingDurations) ?? 0 - - return ( -
-
-
- {currentSegment?.name} + const expectedStart = PlaylistTiming.getExpectedStart(playlist.timing) + const overUnderClock = getPlaylistTimingDiff(playlist, timingDurations) ?? 0 + + return ( +
+
+
+ {currentSegment?.name} +
+ {currentPartInstance && currentShowStyleBaseId ? ( + <> +
+ +
+
+ +
+
+ +
+
+ +
+ + ) : expectedStart ? ( +
+
- {currentPart && currentShowStyleBaseId ? ( - <> -
- +
+
+ {nextSegment?._id !== currentSegment?._id ? nextSegment?.name : undefined} +
+ {nextPartInstance && nextShowStyleBaseId ? ( + <> +
+ +
+
+ {currentPartInstance && currentPartInstance.instance.part.autoNext ? ( + Autonext -
-
+ ) : null} + {nextPartInstance && nextShowStyleBaseId && nextPartInstance.instance.part.title ? ( -
-
- -
-
- -
- - ) : expectedStart ? ( -
- -
- ) : null} -
-
-
- {nextSegment?._id !== currentSegment?._id ? nextSegment?.name : undefined} -
- {nextPart && nextShowStyleBaseId ? ( - <> -
- -
-
- {currentPart && currentPart.instance.part.autoNext ? ( - Autonext - ) : null} - {nextPart && nextShowStyleBaseId && nextPart.instance.part.title ? ( - - ) : ( - '_' - )} -
- - ) : null} + ) : ( + '_' + )} +
+ + ) : null} +
+
+
+ {playlist ? playlist.name : 'UNKNOWN'}
-
-
- {playlist ? playlist.name : 'UNKNOWN'} -
-
= 0, - })} - > - {RundownUtils.formatDiffToTimecode(overUnderClock, true, false, true, true, true, undefined, true, true)} -
+
= 0, + })} + > + {RundownUtils.formatDiffToTimecode(overUnderClock, true, false, true, true, true, undefined, true, true)}
- ) - } - return null - } - - private renderDashboardLayout(layout: DashboardLayout) { - const { studio, playlist, currentShowStyleBase, currentShowStyleVariant } = this.props - - if (studio && playlist && currentShowStyleBase && currentShowStyleVariant) { - return ( -
- -
- ) - } - return null +
+ ) } + return null } /** * This component renders a Countdown screen for a given playlist */ -export const PresenterScreen = withTranslation()( - withTracker( - getPresenterScreenReactive - )( - withTiming()( - PresenterScreenBase - ) - ) -) +export const PresenterScreen = withTracker( + getPresenterScreenReactive +)(withTiming()(PresenterScreenContent)) From 7396adbb13752edeb276eb5686960f253d3a35ba Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 15 Dec 2023 13:45:47 +0000 Subject: [PATCH 190/479] chore: enable typescript strict property initialisation (#1102) Co-authored-by: Johan Nyman --- .../lib/ConnectionStatusNotification.tsx | 4 +- meteor/client/lib/KeyboardFocusIndicator.tsx | 4 +- meteor/client/lib/LottieButton.tsx | 5 ++- meteor/client/lib/ModalDialog.tsx | 5 ++- meteor/client/lib/multiSelect.tsx | 10 +++-- .../client/lib/ui/containers/modals/Modal.tsx | 5 ++- .../ui/ClipTrimPanel/VideoEditMonitor.tsx | 9 +++-- .../NoraFloatingInspector.tsx | 8 ++-- meteor/client/ui/Prompter/PrompterView.tsx | 2 +- .../ui/Prompter/controller/joycon-device.ts | 6 ++- .../Prompter/controller/midi-pedal-device.ts | 5 ++- .../RundownTiming/RundownTimingProvider.tsx | 4 +- .../RundownView/RundownTiming/withTiming.tsx | 7 ++-- .../Parts/SegmentTimelinePart.tsx | 2 +- .../Renderers/DefaultLayerItemRenderer.tsx | 4 +- .../Renderers/L3rdSourceRenderer.tsx | 6 +-- .../Renderers/LocalLayerItemRenderer.tsx | 4 +- .../Renderers/MicSourceRenderer.tsx | 24 ++++++------ .../Renderers/SplitsSourceRenderer.tsx | 4 +- .../Renderers/TransitionSourceRenderer.tsx | 4 +- .../Renderers/VTSourceRenderer.tsx | 4 +- .../ui/SegmentTimeline/SegmentTimeline.tsx | 14 ++++--- .../SegmentTimelineContainer.tsx | 14 ++++--- .../SegmentTimelineZoomControls.tsx | 37 +++++++++++-------- .../ui/SegmentTimeline/SourceLayerItem.tsx | 6 +-- .../ui/SegmentTimeline/TimelineGrid.tsx | 28 +++++++------- meteor/client/ui/Settings/Migration.tsx | 3 +- meteor/client/ui/Shelf/DashboardPanel.tsx | 2 +- .../client/ui/Shelf/DashboardPieceButton.tsx | 4 +- meteor/client/ui/Shelf/ExternalFramePanel.tsx | 4 +- .../ItemRenderers/NoraItemEditor.tsx | 6 +-- .../client/ui/Shelf/OverflowingContainer.tsx | 2 +- meteor/client/ui/Shelf/Shelf.tsx | 2 +- .../ui/Shelf/TimelineDashboardPanel.tsx | 4 +- .../StudioScreenSaver/StudioScreenSaver.tsx | 2 +- meteor/lib/api/methods.ts | 7 ++-- .../deviceTriggers/RundownContentObserver.ts | 4 +- meteor/tsconfig-base.json | 7 ++-- 38 files changed, 151 insertions(+), 121 deletions(-) diff --git a/meteor/client/lib/ConnectionStatusNotification.tsx b/meteor/client/lib/ConnectionStatusNotification.tsx index 1f21207093..5d8e4ca3d0 100644 --- a/meteor/client/lib/ConnectionStatusNotification.tsx +++ b/meteor/client/lib/ConnectionStatusNotification.tsx @@ -240,7 +240,7 @@ interface IState { export const ConnectionStatusNotification = withTranslation()( class ConnectionStatusNotification extends React.Component, IState> { - private notifier: ConnectionStatusNotifier + private notifier: ConnectionStatusNotifier | undefined constructor(props: Translated) { super(props) @@ -251,7 +251,7 @@ export const ConnectionStatusNotification = withTranslation()( } componentWillUnmount(): void { - this.notifier.stop() + if (this.notifier) this.notifier.stop() } render(): JSX.Element { diff --git a/meteor/client/lib/KeyboardFocusIndicator.tsx b/meteor/client/lib/KeyboardFocusIndicator.tsx index 2d02e18bec..1f796f0b1a 100644 --- a/meteor/client/lib/KeyboardFocusIndicator.tsx +++ b/meteor/client/lib/KeyboardFocusIndicator.tsx @@ -18,7 +18,7 @@ export class KeyboardFocusIndicator extends React.Component< React.PropsWithChildren, IKeyboardFocusIndicatorState > { - private keyboardFocusInterval: number + private keyboardFocusInterval: number | undefined constructor(props: IKeyboardFocusIndicatorProps) { super(props) @@ -37,7 +37,7 @@ export class KeyboardFocusIndicator extends React.Component< } componentWillUnmount(): void { - Meteor.clearInterval(this.keyboardFocusInterval) + if (this.keyboardFocusInterval !== undefined) Meteor.clearInterval(this.keyboardFocusInterval) document.body.removeEventListener('focusin', this.checkFocus) document.body.removeEventListener('focus', this.checkFocus) document.body.removeEventListener('mousedown', this.checkFocus) diff --git a/meteor/client/lib/LottieButton.tsx b/meteor/client/lib/LottieButton.tsx index c6e9153321..80988e19c0 100644 --- a/meteor/client/lib/LottieButton.tsx +++ b/meteor/client/lib/LottieButton.tsx @@ -25,8 +25,9 @@ export class LottieButton extends React.Component void export type ModalInputResult = { [attribute: string]: any } export type SomeEvent = Event | React.SyntheticEvent export class ModalDialog extends React.Component> { - sorensen: Sorensen + sorensen: Sorensen | undefined private inputResult: ModalInputResult = {} @@ -65,6 +65,8 @@ export class ModalDialog extends React.Component { + if (!this.sorensen) return + if (this.props.show) { this.sorensen.bind(Settings.confirmKeyCode, this.preventDefault, { up: false, @@ -88,6 +90,7 @@ export class ModalDialog extends React.Component { + if (!this.sorensen) return this.sorensen.unbind(Settings.confirmKeyCode, this.preventDefault) this.sorensen.unbind(Settings.confirmKeyCode, this.handleKey) this.sorensen.unbind('Escape', this.preventDefault) diff --git a/meteor/client/lib/multiSelect.tsx b/meteor/client/lib/multiSelect.tsx index c7a0c7e4e6..ea3d515d92 100644 --- a/meteor/client/lib/multiSelect.tsx +++ b/meteor/client/lib/multiSelect.tsx @@ -31,9 +31,9 @@ interface IState { } export class MultiSelect extends React.Component { - private _titleRef: HTMLElement | null - private _popperRef: HTMLElement | null - private _popperUpdate: () => Promise + private _titleRef: HTMLElement | null = null + private _popperRef: HTMLElement | null = null + private _popperUpdate: (() => Promise) | undefined constructor(props: IProps) { super(props) @@ -153,7 +153,9 @@ export class MultiSelect extends React.Component { if (this.props.disabled) return - await this._popperUpdate() + if (typeof this._popperUpdate === 'function') { + await this._popperUpdate() + } this.setState({ expanded: !this.state.expanded, }) diff --git a/meteor/client/lib/ui/containers/modals/Modal.tsx b/meteor/client/lib/ui/containers/modals/Modal.tsx index f17829a36c..13454a5ab2 100644 --- a/meteor/client/lib/ui/containers/modals/Modal.tsx +++ b/meteor/client/lib/ui/containers/modals/Modal.tsx @@ -16,7 +16,7 @@ export type SomeEvent = Event | React.SyntheticEvent export class Modal extends React.Component> { boundKeys: Array = [] - sorensen: Sorensen + sorensen: Sorensen | undefined constructor(props: IModalAttributes) { super(props) @@ -36,6 +36,8 @@ export class Modal extends React.Component { + if (!this.sorensen) return + if (this.props.show) { this.sorensen.bind(Settings.confirmKeyCode, this.preventDefault, { up: false, @@ -59,6 +61,7 @@ export class Modal extends React.Component { + if (!this.sorensen) return this.sorensen.unbind(Settings.confirmKeyCode, this.preventDefault) this.sorensen.unbind(Settings.confirmKeyCode, this.handleKey) this.sorensen.unbind('Escape', this.preventDefault) diff --git a/meteor/client/ui/ClipTrimPanel/VideoEditMonitor.tsx b/meteor/client/ui/ClipTrimPanel/VideoEditMonitor.tsx index b45586f6fe..7c22395cfb 100644 --- a/meteor/client/ui/ClipTrimPanel/VideoEditMonitor.tsx +++ b/meteor/client/ui/ClipTrimPanel/VideoEditMonitor.tsx @@ -18,10 +18,10 @@ interface IState { export const VideoEditMonitor = withTranslation()( class VideoEditMonitor extends React.Component, IState> { - private videoEl: HTMLVideoElement + private videoEl: HTMLVideoElement | null = null private retryCount = 0 private internalTime = 0 - private lastPosition: number + private lastPosition = 0 constructor(props: Translated) { super(props) @@ -90,7 +90,7 @@ export const VideoEditMonitor = withTranslation()( } handleStepBack = (_e: React.MouseEvent) => { - if (this.props.fps) { + if (this.props.fps && this.videoEl) { this.internalTime = Math.max(0, this.internalTime - 1 / this.props.fps) this.videoEl.currentTime = this.internalTime if (this.props.onCurrentTimeChange) { @@ -104,11 +104,12 @@ export const VideoEditMonitor = withTranslation()( } handlePlay = (_e: React.MouseEvent) => { + if (!this.videoEl) return this.videoEl.play().catch(catchError('videoEl.play')) } handleStepForward = (_e: React.MouseEvent) => { - if (this.props.fps) { + if (this.props.fps && this.videoEl) { this.internalTime = Math.min( this.props.duration || this.videoEl.duration, this.internalTime + 1 / this.props.fps diff --git a/meteor/client/ui/FloatingInspectors/NoraFloatingInspector.tsx b/meteor/client/ui/FloatingInspectors/NoraFloatingInspector.tsx index 4576f2d970..b7168c050a 100644 --- a/meteor/client/ui/FloatingInspectors/NoraFloatingInspector.tsx +++ b/meteor/client/ui/FloatingInspectors/NoraFloatingInspector.tsx @@ -18,7 +18,7 @@ export const NoraFloatingInspector = React.forwardRef { - return NoraPreviewRenderer._singletonRef.rootElement + return NoraPreviewRenderer._singletonRef.rootElement as HTMLDivElement }, [] ) @@ -45,8 +45,8 @@ export const NoraFloatingInspector = React.forwardRef { static _singletonRef: NoraPreviewRenderer - iframeElement: HTMLIFrameElement - rootElement: HTMLDivElement + iframeElement: HTMLIFrameElement | null = null + rootElement: HTMLDivElement | null = null static update(noraContent: NoraContent, style: React.CSSProperties): void { NoraPreviewRenderer._singletonRef._update(noraContent, style) @@ -148,7 +148,7 @@ export class NoraPreviewRenderer extends React.Component<{}, IStateHeader> { } } - private _setRootElement = (e: HTMLDivElement) => { + private _setRootElement = (e: HTMLDivElement | null) => { this.rootElement = e } diff --git a/meteor/client/ui/Prompter/PrompterView.tsx b/meteor/client/ui/Prompter/PrompterView.tsx index 27a8720b4c..039c474d5c 100644 --- a/meteor/client/ui/Prompter/PrompterView.tsx +++ b/meteor/client/ui/Prompter/PrompterView.tsx @@ -586,7 +586,7 @@ export const Prompter = translateWithTracker, Translated & IPrompterTrackedProps>, {} > { - private _debounceUpdate: NodeJS.Timer + private _debounceUpdate: NodeJS.Timer | undefined constructor(props: Translated & IPrompterTrackedProps>) { super(props) diff --git a/meteor/client/ui/Prompter/controller/joycon-device.ts b/meteor/client/ui/Prompter/controller/joycon-device.ts index aa5d7465c3..eb025184a3 100644 --- a/meteor/client/ui/Prompter/controller/joycon-device.ts +++ b/meteor/client/ui/Prompter/controller/joycon-device.ts @@ -21,8 +21,8 @@ export class JoyConController extends ControllerAbstract { private reverseSpeedMap = [1, 2, 3, 4, 5, 8, 12, 30] private deadBand = 0.25 - private speedSpline: Spline - private reverseSpeedSpline: Spline + private speedSpline: Spline | undefined + private reverseSpeedSpline: Spline | undefined private updateSpeedHandle: number | null = null private timestampOfLastUsedJoyconInput = 0 @@ -307,6 +307,8 @@ export class JoyConController extends ControllerAbstract { } private calculateSpeed(inputs: JoyconWithData[]) { + if (!this.reverseSpeedSpline || !this.speedSpline) return 0 + const { rangeRevMin, rangeNeutralMin, rangeNeutralMax, rangeFwdMax } = this let inputValue = this.getActiveInputsOfJoycons(inputs) diff --git a/meteor/client/ui/Prompter/controller/midi-pedal-device.ts b/meteor/client/ui/Prompter/controller/midi-pedal-device.ts index c6376f51b8..9fc0edc182 100644 --- a/meteor/client/ui/Prompter/controller/midi-pedal-device.ts +++ b/meteor/client/ui/Prompter/controller/midi-pedal-device.ts @@ -19,8 +19,8 @@ export class MidiPedalController extends ControllerAbstract { private rangeFwdMax = 127 // pedal "all front" position where scrolling is maxed out private speedMap = [1, 2, 3, 4, 5, 7, 9, 12, 17, 19, 30] private reverseSpeedMap = [10, 30, 50] - private speedSpline: Spline - private reverseSpeedSpline: Spline + private speedSpline: Spline | undefined + private reverseSpeedSpline: Spline | undefined private updateSpeedHandle: number | null = null private lastSpeed = 0 @@ -135,6 +135,7 @@ export class MidiPedalController extends ControllerAbstract { } private onMidiInputCC = (e: InputEventControlchange) => { + if (!this.reverseSpeedSpline || !this.speedSpline) return const { rangeRevMin, rangeNeutralMin, rangeNeutralMax, rangeFwdMax } = this let inputValue = e.value || 0 diff --git a/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx b/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx index 2854707301..e638fca2cf 100644 --- a/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx +++ b/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx @@ -199,7 +199,7 @@ export const RundownTimingProvider = withTracker< syncedDurations: RundownTimingContext = { isLowResolution: true, } - refreshTimer: number + refreshTimer: number | undefined refreshTimerInterval: number refreshDecimator: number @@ -274,7 +274,7 @@ export const RundownTimingProvider = withTracker< componentWillUnmount(): void { delete (window as any)['rundownTimingContext'] - Meteor.clearInterval(this.refreshTimer) + if (this.refreshTimer !== undefined) Meteor.clearInterval(this.refreshTimer) } dispatchHREvent(now: number) { diff --git a/meteor/client/ui/RundownView/RundownTiming/withTiming.tsx b/meteor/client/ui/RundownView/RundownTiming/withTiming.tsx index 7b5f7e160a..a5cf4a0770 100644 --- a/meteor/client/ui/RundownView/RundownTiming/withTiming.tsx +++ b/meteor/client/ui/RundownView/RundownTiming/withTiming.tsx @@ -63,12 +63,13 @@ export function withTiming( syncedDurations: PropTypes.object.isRequired, } - context: { + // Setup by React.Component constructor + context!: { durations: RundownTimingContext syncedDurations: RundownTimingContext } - filterGetter: (o: any) => any + filterGetter: ((o: any) => any) | undefined previousValue: any = undefined isDirty = false @@ -151,7 +152,7 @@ export function withTiming( } function componentIsDirty( - filterGetter: (...args: any[]) => any | undefined, + filterGetter: ((...args: any[]) => any) | undefined, highResDurations: RundownTimingContext, dataResolution: TimingDataResolution ) { diff --git a/meteor/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx b/meteor/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx index 955aee4463..aa24b806aa 100644 --- a/meteor/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx +++ b/meteor/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx @@ -237,7 +237,7 @@ export class SegmentTimelinePartClass extends React.Component { if (e && e.partId === this.props.part.partId && !e.pieceId) { diff --git a/meteor/client/ui/SegmentTimeline/Renderers/DefaultLayerItemRenderer.tsx b/meteor/client/ui/SegmentTimeline/Renderers/DefaultLayerItemRenderer.tsx index bdab0c24cb..4b27975ead 100644 --- a/meteor/client/ui/SegmentTimeline/Renderers/DefaultLayerItemRenderer.tsx +++ b/meteor/client/ui/SegmentTimeline/Renderers/DefaultLayerItemRenderer.tsx @@ -7,8 +7,8 @@ type IProps = ICustomLayerItemProps interface IState {} export class DefaultLayerItemRenderer extends CustomLayerItemRenderer { - leftLabel: HTMLSpanElement | null - rightLabel: HTMLSpanElement | null + leftLabel: HTMLSpanElement | null = null + rightLabel: HTMLSpanElement | null = null private setLeftLabelRef = (e: HTMLSpanElement) => { this.leftLabel = e diff --git a/meteor/client/ui/SegmentTimeline/Renderers/L3rdSourceRenderer.tsx b/meteor/client/ui/SegmentTimeline/Renderers/L3rdSourceRenderer.tsx index 3e8d9647d8..cb678a70bd 100644 --- a/meteor/client/ui/SegmentTimeline/Renderers/L3rdSourceRenderer.tsx +++ b/meteor/client/ui/SegmentTimeline/Renderers/L3rdSourceRenderer.tsx @@ -11,9 +11,9 @@ import { PieceMultistepChevron } from '../../SegmentContainer/PieceMultistepChev type IProps = ICustomLayerItemProps interface IState {} export class L3rdSourceRenderer extends CustomLayerItemRenderer { - leftLabel: HTMLElement | null - rightLabel: HTMLElement | null - lastOverflowTime: boolean + leftLabel: HTMLElement | null = null + rightLabel: HTMLElement | null = null + lastOverflowTime: boolean | undefined private updateAnchoredElsWidths = () => { const leftLabelWidth = this.leftLabel ? getElementWidth(this.leftLabel) : 0 diff --git a/meteor/client/ui/SegmentTimeline/Renderers/LocalLayerItemRenderer.tsx b/meteor/client/ui/SegmentTimeline/Renderers/LocalLayerItemRenderer.tsx index 89d9810c2a..b6d6445006 100644 --- a/meteor/client/ui/SegmentTimeline/Renderers/LocalLayerItemRenderer.tsx +++ b/meteor/client/ui/SegmentTimeline/Renderers/LocalLayerItemRenderer.tsx @@ -7,8 +7,8 @@ type IProps = ICustomLayerItemProps interface IState {} export class LocalLayerItemRenderer extends CustomLayerItemRenderer { - leftLabel: HTMLSpanElement | null - rightLabel: HTMLSpanElement | null + leftLabel: HTMLSpanElement | null = null + rightLabel: HTMLSpanElement | null = null private setLeftLabelRef = (e: HTMLSpanElement) => { this.leftLabel = e diff --git a/meteor/client/ui/SegmentTimeline/Renderers/MicSourceRenderer.tsx b/meteor/client/ui/SegmentTimeline/Renderers/MicSourceRenderer.tsx index 98acfb23b7..9ae04bdfac 100644 --- a/meteor/client/ui/SegmentTimeline/Renderers/MicSourceRenderer.tsx +++ b/meteor/client/ui/SegmentTimeline/Renderers/MicSourceRenderer.tsx @@ -18,16 +18,15 @@ interface IState {} export const MicSourceRenderer = withTranslation()( class MicSourceRenderer extends CustomLayerItemRenderer { - itemPosition: number - itemWidth: number - itemElement: HTMLElement | null - lineItem: HTMLElement - linePosition: number - leftLabel: HTMLSpanElement | null - rightLabel: HTMLSpanElement | null + itemPosition = 0 + itemElement: HTMLElement | null = null + lineItem: HTMLElement | null = null + linePosition: number | undefined + leftLabel: HTMLSpanElement | null = null + rightLabel: HTMLSpanElement | null = null - readTime: number - lastPartDuration: number + readTime: number | undefined + lastPartDuration: number | undefined private _lineAtEnd = false @@ -36,14 +35,17 @@ export const MicSourceRenderer = withTranslation()( } repositionLine = () => { + if (!this.lineItem) return this.lineItem.style.left = this.linePosition + 'px' } addClassToLine = (className: string) => { + if (!this.lineItem) return this.lineItem.classList.add(className) } removeClassFromLine = (className: string) => { + if (!this.lineItem) return this.lineItem.classList.remove(className) } @@ -150,7 +152,7 @@ export const MicSourceRenderer = withTranslation()( // Move the line element if (this.itemElement !== this.props.itemElement) { - if (this.itemElement) { + if (this.itemElement && this.lineItem) { try { this.lineItem.remove() } catch (err) { @@ -158,7 +160,7 @@ export const MicSourceRenderer = withTranslation()( } } this.itemElement = this.props.itemElement - if (this.itemElement) { + if (this.itemElement && this.lineItem) { this.itemElement.parentElement?.parentElement?.parentElement?.appendChild(this.lineItem) _forceSizingRecheck = true } diff --git a/meteor/client/ui/SegmentTimeline/Renderers/SplitsSourceRenderer.tsx b/meteor/client/ui/SegmentTimeline/Renderers/SplitsSourceRenderer.tsx index 512cfe4e9e..943783c0b1 100644 --- a/meteor/client/ui/SegmentTimeline/Renderers/SplitsSourceRenderer.tsx +++ b/meteor/client/ui/SegmentTimeline/Renderers/SplitsSourceRenderer.tsx @@ -16,8 +16,8 @@ interface IState { } export class SplitsSourceRenderer extends CustomLayerItemRenderer { - leftLabel: HTMLSpanElement | null - rightLabel: HTMLSpanElement | null + leftLabel: HTMLSpanElement | null = null + rightLabel: HTMLSpanElement | null = null constructor(props: IProps) { super(props) diff --git a/meteor/client/ui/SegmentTimeline/Renderers/TransitionSourceRenderer.tsx b/meteor/client/ui/SegmentTimeline/Renderers/TransitionSourceRenderer.tsx index 14d458d02e..d2fb76c5a1 100644 --- a/meteor/client/ui/SegmentTimeline/Renderers/TransitionSourceRenderer.tsx +++ b/meteor/client/ui/SegmentTimeline/Renderers/TransitionSourceRenderer.tsx @@ -12,8 +12,8 @@ interface IState { iconFailed: boolean } export class TransitionSourceRenderer extends CustomLayerItemRenderer { - leftLabel: HTMLElement - rightLabel: HTMLElement + leftLabel: HTMLElement | null = null + rightLabel: HTMLElement | null = null constructor(props: IProps) { super(props) diff --git a/meteor/client/ui/SegmentTimeline/Renderers/VTSourceRenderer.tsx b/meteor/client/ui/SegmentTimeline/Renderers/VTSourceRenderer.tsx index d900cec9f3..09b8d5bbe6 100644 --- a/meteor/client/ui/SegmentTimeline/Renderers/VTSourceRenderer.tsx +++ b/meteor/client/ui/SegmentTimeline/Renderers/VTSourceRenderer.tsx @@ -33,8 +33,8 @@ interface IState { sourceEndCountdownAppendage?: boolean } export class VTSourceRendererBase extends CustomLayerItemRenderer { - private leftLabel: HTMLSpanElement - private rightLabel: HTMLSpanElement + private leftLabel: HTMLSpanElement | null = null + private rightLabel: HTMLSpanElement | null = null private leftLabelNodes: JSX.Element | null = null private rightLabelNodes: JSX.Element | null = null diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx index 7227d53385..ed8fc2c884 100644 --- a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx +++ b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx @@ -131,9 +131,11 @@ const SegmentTimelineZoom = class SegmentTimelineZoom extends React.Component< durations: PropTypes.object.isRequired, } - context: { - durations: RundownTimingContext - } + context: + | { + durations: RundownTimingContext + } + | undefined constructor(props: IProps & IZoomPropsHeader, context: any) { super(props, context) @@ -240,8 +242,8 @@ export const BUDGET_GAP_PART = { export class SegmentTimelineClass extends React.Component>, IStateHeader> { static whyDidYouRender = true - timeline: HTMLDivElement - segmentBlock: HTMLDivElement + timeline: HTMLDivElement | null = null + segmentBlock: HTMLDivElement | null = null private _touchSize = 0 private _touchAttached = false @@ -296,7 +298,7 @@ export class SegmentTimelineClass extends React.Component { if (e.segmentId === this.props.segment._id && !e.partId && !e.pieceId) { diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx index 09cd5ff342..253fa8b350 100644 --- a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx +++ b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx @@ -151,13 +151,14 @@ const SegmentTimelineContainerContent = withResolvedSegment( } isVisible: boolean - rundownCurrentPartInstanceId: PartInstanceId | null - timelineDiv: HTMLDivElement + rundownCurrentPartInstanceId: PartInstanceId | null = null + timelineDiv: HTMLDivElement | null = null intersectionObserver: IntersectionObserver | undefined - mountedTime: number - nextPartOffset: number + mountedTime = 0 + nextPartOffset = 0 - context: { + // Setup by React.Component constructor + context!: { durations: RundownTimingContext syncedDurations: RundownTimingContext } @@ -576,7 +577,8 @@ const SegmentTimelineContainerContent = withResolvedSegment( rootMargin: `-${getHeaderHeight() * zoomFactor}px 0px -${20 * zoomFactor}px 0px`, threshold: [0, 0.25, 0.5, 0.75, 0.98], }) - this.intersectionObserver.observe(this.timelineDiv.parentElement!) + if (!this.timelineDiv?.parentElement) return + this.intersectionObserver.observe(this.timelineDiv.parentElement) } stopLive = () => { diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimelineZoomControls.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimelineZoomControls.tsx index dfc33fe26e..c94fafad86 100644 --- a/meteor/client/ui/SegmentTimeline/SegmentTimelineZoomControls.tsx +++ b/meteor/client/ui/SegmentTimeline/SegmentTimelineZoomControls.tsx @@ -28,15 +28,15 @@ export const SegmentTimelineZoomControls = class SegmentTimelineZoomControls ext IPropsHeader, IStateHeader > { - parentElement: HTMLDivElement - selAreaElement: HTMLDivElement - parentOffsetX: number - parentOffsetY: number - clickOffsetX: number - clickOffsetY: number + parentElement: HTMLDivElement | null = null + selAreaElement: HTMLDivElement | null = null + parentOffsetX = 0 + parentOffsetY = 0 + clickOffsetX = 0 + clickOffsetY = 0 private _isTouch = false - private resizeObserver: ResizeObserver + private resizeObserver: ResizeObserver | undefined SMALL_WIDTH_BREAKPOINT = 25 @@ -53,6 +53,7 @@ export const SegmentTimelineZoomControls = class SegmentTimelineZoomControls ext } private checkSmallMode = () => { + if (!this.selAreaElement) return const selAreaElementWidth = getElementWidth(this.selAreaElement) if (selAreaElementWidth && selAreaElementWidth < this.SMALL_WIDTH_BREAKPOINT) { @@ -67,10 +68,10 @@ export const SegmentTimelineZoomControls = class SegmentTimelineZoomControls ext } private onElementResize = (entries: ResizeObserverEntry[]) => { - let width: number + let width = 0 if (entries && entries[0] && entries[0].contentRect) { width = entries[0].contentRect.width - } else { + } else if (this.parentElement) { width = getElementWidth(this.parentElement) } @@ -163,23 +164,27 @@ export const SegmentTimelineZoomControls = class SegmentTimelineZoomControls ext }) } - private setParentRef = (element: HTMLDivElement) => { + private setParentRef = (element: HTMLDivElement | null) => { this.parentElement = element } - private setSelAreaRef = (element: HTMLDivElement) => { + private setSelAreaRef = (element: HTMLDivElement | null) => { this.selAreaElement = element } componentDidMount(): void { - this.resizeObserver = onElementResize(this.parentElement, this.onElementResize) - this.setState({ - width: getElementWidth(this.parentElement) || 1, - }) + if (this.parentElement) { + this.resizeObserver = onElementResize(this.parentElement, this.onElementResize) + this.setState({ + width: getElementWidth(this.parentElement) || 1, + }) + } } componentWillUnmount(): void { - offElementResize(this.resizeObserver, this.parentElement) + if (this.resizeObserver && this.parentElement) { + offElementResize(this.resizeObserver, this.parentElement) + } } render(): JSX.Element { diff --git a/meteor/client/ui/SegmentTimeline/SourceLayerItem.tsx b/meteor/client/ui/SegmentTimeline/SourceLayerItem.tsx index 06a771a252..e3a89d5efc 100644 --- a/meteor/client/ui/SegmentTimeline/SourceLayerItem.tsx +++ b/meteor/client/ui/SegmentTimeline/SourceLayerItem.tsx @@ -101,7 +101,7 @@ interface ISourceLayerItemState { } export const SourceLayerItem = withTranslation()( class SourceLayerItem extends React.Component { - animFrameHandle: number + animFrameHandle: number | undefined constructor(props: ISourceLayerItemProps & WithTranslation) { super(props) @@ -408,7 +408,7 @@ export const SourceLayerItem = withTranslation()( // } // } - private highlightTimeout: NodeJS.Timer + private highlightTimeout: NodeJS.Timer | undefined private onHighlight = (e: HighlightEvent) => { if ( @@ -512,7 +512,7 @@ export const SourceLayerItem = withTranslation()( this.animFrameHandle = requestAnimationFrame(updatePos) } this.animFrameHandle = requestAnimationFrame(updatePos) - } else { + } else if (this.animFrameHandle !== undefined) { cancelAnimationFrame(this.animFrameHandle) } } diff --git a/meteor/client/ui/SegmentTimeline/TimelineGrid.tsx b/meteor/client/ui/SegmentTimeline/TimelineGrid.tsx index e5663d7faa..0121388b30 100644 --- a/meteor/client/ui/SegmentTimeline/TimelineGrid.tsx +++ b/meteor/client/ui/SegmentTimeline/TimelineGrid.tsx @@ -54,20 +54,22 @@ export class TimelineGrid extends React.Component { durations: PropTypes.object.isRequired, } - context: { - durations: RundownTimingContext - } - - canvasElement: HTMLCanvasElement | null - parentElement: HTMLDivElement | null - ctx: CanvasRenderingContext2D | null - - width: number - height: number - pixelRatio: number + context: + | { + durations: RundownTimingContext + } + | undefined + + canvasElement: HTMLCanvasElement | null = null + parentElement: HTMLDivElement | null = null + ctx: CanvasRenderingContext2D | null = null + + width = 1 + height = 1 + pixelRatio = 1 scheduledRepaint?: number | null - private _resizeObserver: ResizeObserver + private _resizeObserver: ResizeObserver | undefined private fontSize: number = FONT_SIZE private labelTop: number = LABEL_TOP @@ -444,7 +446,7 @@ export class TimelineGrid extends React.Component { } componentWillUnmount(): void { - this._resizeObserver.disconnect() + if (this._resizeObserver) this._resizeObserver.disconnect() window.removeEventListener(RundownTiming.Events.timeupdateLowResolution, this.onTimeupdate) window.removeEventListener(RundownTiming.Events.timeupdateHighResolution, this.onTimeupdate) } diff --git a/meteor/client/ui/Settings/Migration.tsx b/meteor/client/ui/Settings/Migration.tsx index e396d04302..d00b7d3c53 100644 --- a/meteor/client/ui/Settings/Migration.tsx +++ b/meteor/client/ui/Settings/Migration.tsx @@ -47,7 +47,8 @@ export const MigrationView = translateWithTracker return {} })( class MigrationView extends React.Component, IState> { - private cancelRequests: boolean + private cancelRequests = false + constructor(props: Translated) { super(props) diff --git a/meteor/client/ui/Shelf/DashboardPanel.tsx b/meteor/client/ui/Shelf/DashboardPanel.tsx index eab5ca5215..89f6589070 100644 --- a/meteor/client/ui/Shelf/DashboardPanel.tsx +++ b/meteor/client/ui/Shelf/DashboardPanel.tsx @@ -414,7 +414,7 @@ export class DashboardPanelInner extends React.Component { + protected setRef = (ref: HTMLDivElement | null): void => { const _panel = ref if (_panel) { const style = window.getComputedStyle(_panel) diff --git a/meteor/client/ui/Shelf/DashboardPieceButton.tsx b/meteor/client/ui/Shelf/DashboardPieceButton.tsx index ec4e87b4f5..fcf8776159 100644 --- a/meteor/client/ui/Shelf/DashboardPieceButton.tsx +++ b/meteor/client/ui/Shelf/DashboardPieceButton.tsx @@ -73,7 +73,7 @@ export class DashboardPieceButtonBase extends React.Component< width: number height: number } | null = null - private _labelEl: HTMLTextAreaElement + private _labelEl: HTMLTextAreaElement | null = null private pointerId: number | null = null private hoverTimeout: number | null = null protected inBucket = false @@ -323,7 +323,7 @@ export class DashboardPieceButtonBase extends React.Component< input.setSelectionRange(0, input.value.length) } - private onRenameTextBoxShow = (ref: HTMLTextAreaElement) => { + private onRenameTextBoxShow = (ref: HTMLTextAreaElement | null) => { if (ref && !this._labelEl) { ref.addEventListener('keyup', this.onRenameTextBoxKeyUp) this.renameTextBoxFocus(ref) diff --git a/meteor/client/ui/Shelf/ExternalFramePanel.tsx b/meteor/client/ui/Shelf/ExternalFramePanel.tsx index e99200b98c..802ba5cd40 100644 --- a/meteor/client/ui/Shelf/ExternalFramePanel.tsx +++ b/meteor/client/ui/Shelf/ExternalFramePanel.tsx @@ -96,7 +96,7 @@ interface CurrentNextPartChangedSofieExternalMessage extends SofieExternalMessag export const ExternalFramePanel = withTranslation()( class ExternalFramePanel extends React.Component> { - frame: HTMLIFrameElement + frame: HTMLIFrameElement | null = null mounted = false initialized = false failedDragTimeout: number | undefined @@ -108,7 +108,7 @@ export const ExternalFramePanel = withTranslation()( } } = {} - setElement = (frame: HTMLIFrameElement) => { + setElement = (frame: HTMLIFrameElement | null) => { this.frame = frame if (this.frame && !this.mounted) { this.registerHandlers() diff --git a/meteor/client/ui/Shelf/Inspector/ItemRenderers/NoraItemEditor.tsx b/meteor/client/ui/Shelf/Inspector/ItemRenderers/NoraItemEditor.tsx index b206807e31..f49d55e97d 100644 --- a/meteor/client/ui/Shelf/Inspector/ItemRenderers/NoraItemEditor.tsx +++ b/meteor/client/ui/Shelf/Inspector/ItemRenderers/NoraItemEditor.tsx @@ -18,7 +18,7 @@ interface INoraEditorProps { } class NoraItemEditor extends React.Component { - iframe: HTMLIFrameElement + iframe: HTMLIFrameElement | null = null componentDidMount(): void { this.setUpEventListeners(window) @@ -68,12 +68,12 @@ class NoraItemEditor extends React.Component { } private handleMosMessage(mos: MosPluginMessage) { - if (mos.ncsReqAppInfo) { + if (mos.ncsReqAppInfo && this.iframe) { this.sendAppInfo(this.iframe.contentWindow) // delay to send in order setTimeout(() => { - this.postPayload(this.iframe.contentWindow) + if (this.iframe) this.postPayload(this.iframe.contentWindow) }, 1) } } diff --git a/meteor/client/ui/Shelf/OverflowingContainer.tsx b/meteor/client/ui/Shelf/OverflowingContainer.tsx index 3a8845020b..f4088922d0 100644 --- a/meteor/client/ui/Shelf/OverflowingContainer.tsx +++ b/meteor/client/ui/Shelf/OverflowingContainer.tsx @@ -16,7 +16,7 @@ interface IState { } export class OverflowingContainer extends React.Component, IState> { - _element: HTMLDivElement | null + _element: HTMLDivElement | null = null _scrollFactor = 0 constructor(props: IProps) { diff --git a/meteor/client/ui/Shelf/Shelf.tsx b/meteor/client/ui/Shelf/Shelf.tsx index a5d8a3c859..d5787f8e52 100644 --- a/meteor/client/ui/Shelf/Shelf.tsx +++ b/meteor/client/ui/Shelf/Shelf.tsx @@ -96,7 +96,7 @@ export class ShelfBase extends React.Component, IState> x: 0, y: 0, } - private _mouseDown: number + private _mouseDown = 0 constructor(props: Translated) { super(props) diff --git a/meteor/client/ui/Shelf/TimelineDashboardPanel.tsx b/meteor/client/ui/Shelf/TimelineDashboardPanel.tsx index dcc976d9bb..d4441b29a9 100644 --- a/meteor/client/ui/Shelf/TimelineDashboardPanel.tsx +++ b/meteor/client/ui/Shelf/TimelineDashboardPanel.tsx @@ -38,14 +38,14 @@ export const TimelineDashboardPanel = React.memo( const TimelineDashboardPanelContent = withTranslation()( class TimelineDashboardPanelContent extends DashboardPanelInner { - liveLine: HTMLDivElement + liveLine: HTMLDivElement | null = null scrollIntoViewTimeout: NodeJS.Timer | undefined = undefined constructor(props: Translated) { super(props) } - private setRefExt = (ref: HTMLDivElement) => { + private setRefExt = (ref: HTMLDivElement | null) => { this.liveLine = ref this.ensureLiveLineVisible() diff --git a/meteor/client/ui/StudioScreenSaver/StudioScreenSaver.tsx b/meteor/client/ui/StudioScreenSaver/StudioScreenSaver.tsx index 45cbfe868f..4461719f50 100644 --- a/meteor/client/ui/StudioScreenSaver/StudioScreenSaver.tsx +++ b/meteor/client/ui/StudioScreenSaver/StudioScreenSaver.tsx @@ -114,7 +114,7 @@ const StudioScreenSaverContent = withTranslation()( private _nextAnimationFrameRequest: number | undefined private readonly SPEED = 0.5 // non-unit value private readonly FRAME_MARGIN: [number, number, number, number] = [10, 28, 10, 10] // margin, specified in vmin CSS units - private FRAME_PIXEL_MARGIN: [number, number, number, number] // margin, calculated to pixels on resize + private FRAME_PIXEL_MARGIN: [number, number, number, number] = [0, 0, 0, 0] // margin, calculated to pixels on resize private PIXEL_SPEED = 0.5 private position: { diff --git a/meteor/lib/api/methods.ts b/meteor/lib/api/methods.ts index e08b92f630..40c4a513d0 100644 --- a/meteor/lib/api/methods.ts +++ b/meteor/lib/api/methods.ts @@ -89,8 +89,9 @@ export interface MethodContext extends Omit { /** Abstarct class to be used when defining Mehod-classes */ export abstract class MethodContextAPI implements MethodContext { - public userId: UserId | null - public isSimulation: boolean + // These properties are added by Meteor to the `this` context when calling methods + public userId!: UserId | null + public isSimulation!: boolean public setUserId(_userId: string | null): void { throw new Meteor.Error( 500, @@ -103,5 +104,5 @@ export abstract class MethodContextAPI implements MethodContext { `This shoulc never be called, there's something wrong in with 'this' in the calling method` ) } - public connection: Meteor.Connection | null + public connection!: Meteor.Connection | null } diff --git a/meteor/server/api/deviceTriggers/RundownContentObserver.ts b/meteor/server/api/deviceTriggers/RundownContentObserver.ts index 270973bf9f..ee7e859d1a 100644 --- a/meteor/server/api/deviceTriggers/RundownContentObserver.ts +++ b/meteor/server/api/deviceTriggers/RundownContentObserver.ts @@ -37,7 +37,9 @@ export class RundownContentObserver { #observers: Meteor.LiveQueryHandle[] = [] #cache: ContentCache #cancelCache: () => void - #cleanup: () => void + #cleanup: () => void = () => { + throw new Error('RundownContentObserver.#cleanup has not been set!') + } constructor( rundownPlaylistId: RundownPlaylistId, diff --git a/meteor/tsconfig-base.json b/meteor/tsconfig-base.json index 990b0c253e..d57507f4db 100644 --- a/meteor/tsconfig-base.json +++ b/meteor/tsconfig-base.json @@ -3,7 +3,6 @@ "compilerOptions": { /* At the time of writing we are not ready for stricter rules */ "strict": true, - "strictPropertyInitialization": false, "skipLibCheck": true, "sourceMap": true, @@ -12,10 +11,10 @@ "paths": { "meteor/*": [ - // "./node_modules/@types/meteor/*", - "./.meteor/local/types/packages.d.ts" + // "./node_modules/@types/meteor/*", + "./.meteor/local/types/packages.d.ts" ] - } + } }, "include": ["client/**/*", "server/**/*", "lib/**/*", "__mocks__/**/*", "tslint-rules/**/*"], "exclude": ["node_modules", "**/.coverage/**/*"] From ff1f730dacf82154e809e01766974e6955c29cf7 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 18 Dec 2023 13:24:24 +0000 Subject: [PATCH 191/479] feat: remove some usages of `DbCacheWriteCollection` SOFIE-2672 (#1106) --- packages/job-worker/src/cache/lib.ts | 43 +++++++ packages/job-worker/src/ingest/ingestCache.ts | 109 ++++++++-------- .../playout/timings/timelineTriggerTime.ts | 116 ++++++++++++------ 3 files changed, 178 insertions(+), 90 deletions(-) diff --git a/packages/job-worker/src/cache/lib.ts b/packages/job-worker/src/cache/lib.ts index 712b3432ab..fc9cbaee9f 100644 --- a/packages/job-worker/src/cache/lib.ts +++ b/packages/job-worker/src/cache/lib.ts @@ -4,6 +4,8 @@ import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' import { logger } from '../logging' import { ChangedIds, SaveIntoDbHooks, saveIntoBase } from '../db/changes' import { JobContext } from '../jobs' +import { normalizeArrayToMap } from '@sofie-automation/corelib/dist/lib' +import _ = require('underscore') /** * Check if an object is a writable db object. (DbCacheWriteCollection, DbCacheWriteObject or DbCacheWriteOptionalObject) @@ -49,3 +51,44 @@ export function logChanges(collection: string, changes: ChangedIds }>( + changedIds: Set, + oldObjects: T[], + newObjects: T[], + mergeFn?: (oldValue: T, newValue: T) => T +): T[] { + const oldObjectMap = normalizeArrayToMap(oldObjects, '_id') + + const result: T[] = [] + + // Compare each newObject + for (const newObject of newObjects) { + const oldObject = oldObjectMap.get(newObject._id) + oldObjectMap.delete(newObject._id) + + const mergedObject = mergeFn && oldObject ? mergeFn(oldObject, newObject) : newObject + + if (!oldObject || changedIds.has(mergedObject._id) || !_.isEqual(oldObject, mergedObject)) { + result.push(mergedObject) + changedIds.add(mergedObject._id) + } else { + result.push(oldObject) + } + } + + // Anything left in the map is missing in the newObjects array + for (const oldObjectId of oldObjectMap.keys()) { + changedIds.add(oldObjectId) + } + + return result +} diff --git a/packages/job-worker/src/ingest/ingestCache.ts b/packages/job-worker/src/ingest/ingestCache.ts index 621246c4c7..8ef7ea32cb 100644 --- a/packages/job-worker/src/ingest/ingestCache.ts +++ b/packages/job-worker/src/ingest/ingestCache.ts @@ -7,17 +7,15 @@ import { IngestDataCacheObjPart, } from '@sofie-automation/corelib/dist/dataModel/IngestDataCache' import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' -import { DbCacheWriteCollection } from '../cache/CacheCollection' -import { saveIntoCache } from '../cache/lib' -import { Changes } from '../db/changes' +import { diffAndReturnLatestObjects } from '../cache/lib' import { getCurrentTime } from '../lib' -import { logger } from '../logging' import _ = require('underscore') import { IngestRundown, IngestSegment, IngestPart } from '@sofie-automation/blueprints-integration' import { JobContext } from '../jobs' import { getPartId, getSegmentId } from './lib' import { SetOptional } from 'type-fest' -import { groupByToMap } from '@sofie-automation/corelib/dist/lib' +import { groupByToMap, normalizeArrayToMap } from '@sofie-automation/corelib/dist/lib' +import { AnyBulkWriteOperation } from 'mongodb' interface LocalIngestBase { modified: number @@ -51,28 +49,24 @@ export function makeNewIngestPart(ingestPart: IngestPart): LocalIngestPart { } export class RundownIngestDataCache { + readonly #changedDocumentIds = new Set() + private constructor( private readonly context: JobContext, private readonly rundownId: RundownId, - private readonly collection: DbCacheWriteCollection + private documents: IngestDataCacheObj[] ) {} static async create(context: JobContext, rundownId: RundownId): Promise { - const col = await DbCacheWriteCollection.createFromDatabase( - context, - context.directCollections.IngestDataCache, - { rundownId } - ) + const docs = await context.directCollections.IngestDataCache.findFetch({ rundownId }) - return new RundownIngestDataCache(context, rundownId, col) + return new RundownIngestDataCache(context, rundownId, docs) } fetchRundown(): LocalIngestRundown | undefined { const span = this.context.startSpan('ingest.ingestCache.loadCachedRundownData') - const cacheEntries = this.collection.findAll(null) - - const cachedRundown = cacheEntries.find((e) => e.type === IngestCacheType.RUNDOWN) + const cachedRundown = this.documents.find((e) => e.type === IngestCacheType.RUNDOWN) if (!cachedRundown) { span?.end() return undefined @@ -85,7 +79,7 @@ export class RundownIngestDataCache { return !!obj.segmentId } - const segmentMap = groupByToMap(cacheEntries.filter(hasSegmentId), 'segmentId') + const segmentMap = groupByToMap(this.documents.filter(hasSegmentId), 'segmentId') for (const objs of segmentMap.values()) { const segmentEntry = objs.find((e) => e.type === IngestCacheType.SEGMENT) if (segmentEntry) { @@ -112,48 +106,53 @@ export class RundownIngestDataCache { return ingestRundown } - fetchSegment(segmentId: SegmentId): LocalIngestSegment | undefined { - const cacheEntries = this.collection.findAll((d) => d.segmentId === segmentId) - - const segmentEntries = cacheEntries.filter((e) => e.type === IngestCacheType.SEGMENT) - if (segmentEntries.length > 1) - logger.warn( - `There are multiple segments (${cacheEntries.length}) in IngestDataCache for rundownId: "${this.rundownId}", segmentId: "${segmentId}"` - ) - - const segmentEntry = segmentEntries[0] - if (!segmentEntry) return undefined - if (segmentEntry.type !== IngestCacheType.SEGMENT) throw new Error('Wrong type on cached segment') - - const ingestSegment = segmentEntry.data as LocalIngestSegment - ingestSegment.modified = segmentEntry.modified + update(ingestRundown: LocalIngestRundown): void { + const cacheEntries: IngestDataCacheObj[] = generateCacheForRundown(this.rundownId, ingestRundown) - for (const entry of cacheEntries) { - if (entry.type === IngestCacheType.PART) { - const ingestPart = entry.data as LocalIngestPart - ingestPart.modified = entry.modified + this.documents = diffAndReturnLatestObjects(this.#changedDocumentIds, this.documents, cacheEntries) + } - ingestSegment.parts.push(ingestPart) - } + delete(): void { + // Mark each document for deletion + for (const doc of this.documents) { + this.#changedDocumentIds.add(doc._id) } - ingestSegment.parts = _.sortBy(ingestSegment.parts, (s) => s.rank) - - return ingestSegment + this.documents = [] } - update(ingestRundown: LocalIngestRundown): void { - // cache the Data: - const cacheEntries: IngestDataCacheObj[] = generateCacheForRundown(this.rundownId, ingestRundown) - saveIntoCache(this.context, this.collection, null, cacheEntries) - } + async saveToDatabase(): Promise { + const documentsMap = normalizeArrayToMap(this.documents, '_id') + + const updates: AnyBulkWriteOperation[] = [] + const removedIds: IngestDataCacheObjId[] = [] + for (const changedId of this.#changedDocumentIds) { + const newDoc = documentsMap.get(changedId) + if (!newDoc) { + removedIds.push(changedId) + } else { + updates.push({ + replaceOne: { + filter: { + _id: changedId, + }, + replacement: newDoc, + }, + }) + } + } - delete(): void { - this.collection.remove(null) - } + if (removedIds.length) { + updates.push({ + deleteMany: { + filter: { + _id: { $in: removedIds as any }, + }, + }, + }) + } - async saveToDatabase(): Promise { - return this.collection.updateDatabaseWithData() + await this.context.directCollections.IngestDataCache.bulkWrite(updates) } } @@ -171,7 +170,11 @@ function generateCacheForRundown(rundownId: RundownId, ingestRundown: LocalInges }, } cacheEntries.push(rundown) - _.each(ingestRundown.segments, (segment) => cacheEntries.push(...generateCacheForSegment(rundownId, segment))) + + for (const segment of ingestRundown.segments) { + cacheEntries.push(...generateCacheForSegment(rundownId, segment)) + } + return cacheEntries } function generateCacheForSegment(rundownId: RundownId, ingestSegment: LocalIngestSegment): IngestDataCacheObj[] { @@ -191,9 +194,9 @@ function generateCacheForSegment(rundownId: RundownId, ingestSegment: LocalInges } cacheEntries.push(segment) - _.each(ingestSegment.parts, (part) => { + for (const part of ingestSegment.parts) { cacheEntries.push(generateCacheForPart(rundownId, segmentId, part)) - }) + } return cacheEntries } diff --git a/packages/job-worker/src/playout/timings/timelineTriggerTime.ts b/packages/job-worker/src/playout/timings/timelineTriggerTime.ts index a2551f7fcd..e3c585f0ca 100644 --- a/packages/job-worker/src/playout/timings/timelineTriggerTime.ts +++ b/packages/job-worker/src/playout/timings/timelineTriggerTime.ts @@ -1,19 +1,18 @@ -import { PartInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PartInstanceId, PieceInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { OnTimelineTriggerTimeProps } from '@sofie-automation/corelib/dist/worker/studio' import { logger } from '../../logging' -import _ = require('underscore') import { JobContext } from '../../jobs' import { runJobWithPlaylistLock } from '../lock' import { saveTimeline } from '../timeline/generate' -import { applyToArray } from '@sofie-automation/corelib/dist/lib' +import { applyToArray, normalizeArrayToMap } from '@sofie-automation/corelib/dist/lib' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { runJobWithStudioPlayoutModel } from '../../studio/lock' import { StudioPlayoutModel } from '../../studio/model/StudioPlayoutModel' -import { DbCacheWriteCollection } from '../../cache/CacheCollection' import { PieceTimelineMetadata } from '../timeline/pieceGroup' import { deserializeTimelineBlob } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { ReadonlyDeep } from 'type-fest' +import { AnyBulkWriteOperation } from 'mongodb' /** * Called from Playout-gateway when the trigger-time of a timeline object has updated @@ -42,21 +41,24 @@ export async function handleTimelineTriggerTime(context: JobContext, data: OnTim ) // We only need the PieceInstances, so load just them - const pieceInstanceCache = await DbCacheWriteCollection.createFromDatabase( - context, - context.directCollections.PieceInstances, - { - rundownId: { $in: rundownIDs }, - partInstanceId: { - $in: partInstanceIDs, - }, - } - ) + const pieceInstances = await context.directCollections.PieceInstances.findFetch({ + rundownId: { $in: rundownIDs }, + partInstanceId: { + $in: partInstanceIDs, + }, + }) + const pieceInstancesMap = normalizeArrayToMap(pieceInstances, '_id') // Take ownership of the playlist in the db, so that we can mutate the timeline and piece instances - timelineTriggerTimeInner(context, studioCache, data.results, pieceInstanceCache, activePlaylist) + const changes = timelineTriggerTimeInner( + context, + studioCache, + data.results, + pieceInstancesMap, + activePlaylist + ) - await pieceInstanceCache.updateDatabaseWithData() + await writePieceInstanceChangesToMongo(context, changes) }) } else { // No playlist is active. no extra lock needed @@ -66,22 +68,62 @@ export async function handleTimelineTriggerTime(context: JobContext, data: OnTim } } +async function writePieceInstanceChangesToMongo(context: JobContext, changes: PieceInstancesChanges): Promise { + const updates: AnyBulkWriteOperation[] = [] + for (const [pieceInstanceId, newTime] of changes.setStartTime.entries()) { + updates.push({ + updateOne: { + filter: { + _id: pieceInstanceId, + }, + update: { + $set: { + 'piece.enable.start': newTime, + }, + }, + }, + }) + } + + if (changes.removeIds.length) { + updates.push({ + deleteMany: { + filter: { + _id: { $in: changes.removeIds as any }, + }, + }, + }) + } + + await context.directCollections.PieceInstances.bulkWrite(updates) +} + +interface PieceInstancesChanges { + removeIds: PieceInstanceId[] + setStartTime: Map +} + function timelineTriggerTimeInner( context: JobContext, studioPlayoutModel: StudioPlayoutModel, results: OnTimelineTriggerTimeProps['results'], - pieceInstanceCache: DbCacheWriteCollection | undefined, + pieceInstances: Map | undefined, activePlaylist: ReadonlyDeep | undefined ) { let lastTakeTime: number | undefined + const changes: PieceInstancesChanges = { + removeIds: [], + setStartTime: new Map(), + } + // ------------------------------ const timeline = studioPlayoutModel.timeline if (timeline) { const timelineObjs = deserializeTimelineBlob(timeline.timelineBlob) let tlChanged = false - _.each(results, (o) => { + for (const o of results) { logger.debug(`Timeline: Setting time: "${o.id}": ${o.time}`) const obj = timelineObjs.find((tlo) => tlo.id === o.id) @@ -100,43 +142,41 @@ function timelineTriggerTimeInner( const objPieceInstanceId = (obj.metaData as Partial | undefined) ?.triggerPieceInstanceId - if (objPieceInstanceId && activePlaylist && pieceInstanceCache) { + if (objPieceInstanceId && activePlaylist && pieceInstances) { logger.debug('Update PieceInstance: ', { pieceId: objPieceInstanceId, time: new Date(o.time).toTimeString(), }) - const pieceInstance = pieceInstanceCache.findOne(objPieceInstanceId) + const pieceInstance = pieceInstances.get(objPieceInstanceId) if ( pieceInstance && - pieceInstance.dynamicallyInserted && + pieceInstance.dynamicallyInserted !== undefined && pieceInstance.piece.enable.start === 'now' ) { - pieceInstanceCache.updateOne(pieceInstance._id, (p) => { - p.piece.enable.start = o.time - return p - }) + pieceInstance.piece.enable.start = o.time + changes.setStartTime.set(pieceInstance._id, o.time) const takeTime = pieceInstance.dynamicallyInserted lastTakeTime = lastTakeTime === undefined ? takeTime : Math.max(lastTakeTime, takeTime) } } } - }) + } - if (lastTakeTime !== undefined && activePlaylist?.currentPartInfo && pieceInstanceCache) { + if (lastTakeTime !== undefined && activePlaylist?.currentPartInfo && pieceInstances) { // We updated some pieceInstance from now, so lets ensure any earlier adlibs do not still have a now - const remainingNowPieces = pieceInstanceCache.findAll( - (p) => - p.partInstanceId === activePlaylist.currentPartInfo?.partInstanceId && - p.dynamicallyInserted !== undefined && - !p.disabled - ) - for (const piece of remainingNowPieces) { - const pieceTakeTime = piece.dynamicallyInserted - if (pieceTakeTime && pieceTakeTime <= lastTakeTime && piece.piece.enable.start === 'now') { + for (const piece of pieceInstances.values()) { + if ( + piece.partInstanceId === activePlaylist.currentPartInfo.partInstanceId && + !piece.disabled && + piece.dynamicallyInserted !== undefined && + piece.dynamicallyInserted <= lastTakeTime && + piece.piece.enable.start === 'now' + ) { // Delete the instance which has no duration - pieceInstanceCache.remove(piece._id) + changes.removeIds.push(piece._id) + pieceInstances.delete(piece._id) } } } @@ -144,4 +184,6 @@ function timelineTriggerTimeInner( saveTimeline(context, studioPlayoutModel, timelineObjs, timeline.generationVersions) } } + + return changes } From 2dc6b4245fc416d87147b7b49d7dd5e52908af6c Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 20 Dec 2023 10:14:26 +0000 Subject: [PATCH 192/479] feat: Dynamic message in Evaluation form SOFIE-2766 (#1101) --- meteor/client/styles/afterBroadcastForm.scss | 40 +- meteor/client/ui/AfterBroadcastForm.tsx | 57 +- .../client/ui/Settings/SystemManagement.tsx | 674 ++++++++++-------- meteor/lib/collections/CoreSystem.ts | 6 + meteor/server/collections/index.ts | 12 +- meteor/server/publications/system.ts | 1 + meteor/server/security/lib/lib.ts | 25 +- 7 files changed, 504 insertions(+), 311 deletions(-) diff --git a/meteor/client/styles/afterBroadcastForm.scss b/meteor/client/styles/afterBroadcastForm.scss index d7e04788fd..a1d62d79b3 100644 --- a/meteor/client/styles/afterBroadcastForm.scss +++ b/meteor/client/styles/afterBroadcastForm.scss @@ -6,6 +6,8 @@ margin-right: auto; margin-left: auto; display: inline-block; + max-width: 50em; + h2 { margin-left: 0em; } @@ -45,7 +47,43 @@ display: inline-block; vertical-align: top; margin-top: 2.6em; - margin-left: 0.5em; + margin-left: 0.5em; + } + + .afterbroadcastform-bubble-container { + display: block; + margin-top: 2rem; + + .afterbroadcastform-bubble { + background-color: #fff; + width: 70%; + margin-left: auto; + padding: 1.2rem 1.5rem; + + border-radius: 1.5rem; + box-shadow: 5px 5px 20px rgba(0, 0, 0, 0.3); + + h5 { + color: #000; + margin: 0; + margin-bottom: 0.5rem; + font-size: 1rem; + font-weight: 500 !important; + letter-spacing: -0.5px; + } + + p { + color: #000; + letter-spacing: -0.5px; + } + } + + svg { + position: relative; + float: right; + margin-top: -1px; + right: 50%; + } } } } diff --git a/meteor/client/ui/AfterBroadcastForm.tsx b/meteor/client/ui/AfterBroadcastForm.tsx index 5e14043a42..98303f8ed8 100644 --- a/meteor/client/ui/AfterBroadcastForm.tsx +++ b/meteor/client/ui/AfterBroadcastForm.tsx @@ -13,6 +13,8 @@ import { MultiLineTextInputControl } from '../lib/Components/MultiLineTextInput' import { TextInputControl } from '../lib/Components/TextInput' import { Spinner } from '../lib/Spinner' import { NotificationCenter, Notification, NoticeLevel } from '../../lib/notifications/notifications' +import { useTracker } from '../lib/ReactMeteorData/ReactMeteorData' +import { CoreSystem } from '../collections' type ProblemType = 'nothing' | 'minor' | 'major' @@ -116,18 +118,17 @@ export function AfterBroadcastForm({ playlist }: Readonly<{ playlist: DBRundownP return (
-

{t('Evaluation')}

+ -

- {t('Please take a minute to fill in this form.')} -

-

- {t('Be aware that while filling out the form keyboard and streamdeck commands will not be executed!')} -

+

{t('How did the show go?')}

+ +

{t('Keyboard shortcuts and Stream Deck buttons will not work while filling out the form!')}

-

{t('Did you have any problems with the broadcast?')}

+

+ {t('Did you have any problems with the broadcast?')} +

+ {t('Please explain the problems you experienced')} +
{t( - 'Please explain the problems you experienced (what happened and when, what should have happened, what could have triggered the problems, etcetera...)' + '(what happened and when, what should have happened, what could have triggered the problems, etcetera...)' )}

@@ -148,7 +151,9 @@ export function AfterBroadcastForm({ playlist }: Readonly<{ playlist: DBRundownP
-

{t('Your name')}

+

+ {t('Your name')} +

@@ -156,7 +161,7 @@ export function AfterBroadcastForm({ playlist }: Readonly<{ playlist: DBRundownP
{busy ? : null}
@@ -166,6 +171,36 @@ export function AfterBroadcastForm({ playlist }: Readonly<{ playlist: DBRundownP ) } +const EvaluationInfoBubble = React.memo(function EvaluationInfoBubble() { + const coreSystem = useTracker(() => CoreSystem.findOne(), []) + + const message = coreSystem?.evaluations?.enabled ? coreSystem.evaluations : undefined + if (!message) return null + + return ( +
+
+
{message.heading ?? ''}
+

{message.message ?? ''}

+
+ +
+ ) +}) + +function EvaluationBubbleStem() { + return ( + + + + ) +} + export function getQuestionOptions(t: TFunction): Omit, 'i'>[] { return [ { value: 'nothing', name: t('No problems') }, diff --git a/meteor/client/ui/Settings/SystemManagement.tsx b/meteor/client/ui/Settings/SystemManagement.tsx index 434ba7d96b..9870cf9648 100644 --- a/meteor/client/ui/Settings/SystemManagement.tsx +++ b/meteor/client/ui/Settings/SystemManagement.tsx @@ -1,14 +1,13 @@ -import * as React from 'react' -import { translateWithTracker, Translated } from '../../lib/ReactMeteorData/ReactMeteorData' +import React, { useCallback, useMemo } from 'react' +import { useTracker, useSubscription } from '../../lib/ReactMeteorData/ReactMeteorData' import { ICoreSystem, SofieLogo } from '../../../lib/collections/CoreSystem' -import { meteorSubscribe, MeteorPubSub } from '../../../lib/api/pubsub' +import { MeteorPubSub } from '../../../lib/api/pubsub' import { EditAttribute } from '../../lib/EditAttribute' import { doModalDialog } from '../../lib/ModalDialog' import { MeteorCall } from '../../../lib/api/methods' -import * as _ from 'underscore' import { languageAnd } from '../../lib/language' import { TriggeredActionsEditor } from './components/triggeredActions/TriggeredActionsEditor' -import { TFunction } from 'react-i18next' +import { TFunction, useTranslation } from 'react-i18next' import { Meteor } from 'meteor/meteor' import { LogLevel } from '../../../lib/lib' import { CoreSystem } from '../../collections' @@ -16,296 +15,405 @@ import { CollectionCleanupResult } from '../../../lib/api/system' import { LabelActual } from '../../lib/Components/LabelAndOverrides' import { catchError } from '../../lib/lib' -interface IProps {} - -interface ITrackedProps { +interface WithCoreSystemProps { coreSystem: ICoreSystem | undefined } -export default translateWithTracker((_props: IProps) => { - return { - coreSystem: CoreSystem.findOne(), - } -})( - class SystemManagement extends React.Component> { - componentDidMount(): void { - meteorSubscribe(MeteorPubSub.coreSystem) - } - cleanUpOldDatabaseIndexes(): void { - const { t } = this.props - MeteorCall.system - .cleanupIndexes(false) - .then((indexesToRemove) => { - console.log(indexesToRemove) - doModalDialog({ - title: t('Remove indexes'), - message: t('This will remove {{indexCount}} old indexes, do you want to continue?', { - indexCount: indexesToRemove.length, - }), - yes: t('Yes'), - no: t('No'), - onAccept: () => { - MeteorCall.system - .cleanupIndexes(true) - .then((indexesRemoved) => { - doModalDialog({ - title: t('Remove indexes'), - message: t('{{indexCount}} indexes was removed.', { - indexCount: indexesRemoved.length, - }), - acceptOnly: true, - onAccept: () => { - // nothing - }, - }) - }) - .catch(catchError('system.cleanupIndexes')) - }, - }) - }) - .catch(catchError('system.cleanupIndexes')) - } - render(): JSX.Element | null { - const { t } = this.props - - return this.props.coreSystem ? ( -
-
-
- - - -
- -

{t('System-wide Notification Message')}

-
- - -
- -
- -
+export default function SystemManagement(): JSX.Element | null { + useSubscription(MeteorPubSub.coreSystem) + + const coreSystem = useTracker(() => CoreSystem.findOne(), []) + const emptyObject = useMemo(() => ({}), []) + + if (!coreSystem) return null + return ( +
+ + + + + + + + +
+
+ +
+
+ + + + + + +
+ ) +} + +function SystemManagementGeneral({ coreSystem }: Readonly) { + const { t } = useTranslation() + + return ( + <> +

{t('General')}

+
+ + + +
+ + ) +} + +function SystemManagementNotificationMessage({ coreSystem }: Readonly) { + const { t } = useTranslation() + + return ( + <> +

{t('System-wide Notification Message')}

+
+ + +
+ + ) +} + +function SystemManagementSupportPanel({ coreSystem }: Readonly) { + const { t } = useTranslation() -
-
- -
+ return ( + <> +

{t('Support Panel')}

+
+ +
+ + ) +} + +function SystemManagementEvaluationsMessage({ coreSystem }: Readonly) { + const { t } = useTranslation() -

{t('Application Performance Monitoring')}

-
- - + return ( + <> +

{t('Evaluations')}

+
+ + + +
+ + ) +} -
- +function SystemManagementMonitoring({ coreSystem }: Readonly) { + const { t } = useTranslation() + + return ( + <> +

{t('Application Performance Monitoring')}

+
+ + +
-
{t('Note: Core needs to be restarted to apply these settings')}
- -

{t('Cron jobs')}

-
- - - +
+ +
+ +
{t('Note: Core needs to be restarted to apply these settings')}
+ + ) +} + +function SystemManagementCronJobs({ coreSystem }: Readonly) { + const { t } = useTranslation() -

{t('Cleanup')}

-
- + return ( + <> +

{t('Cron jobs')}

+
+ + +
+ + ) +} + +function SystemManagementCleanup() { + const { t } = useTranslation() + + const localCheckForOldDataAndCleanUp = useCallback(() => { + checkForOldDataAndCleanUp(t) + }, [t]) + + const cleanUpOldDatabaseIndexes = useCallback(() => { + MeteorCall.system + .cleanupIndexes(false) + .then((indexesToRemove) => { + console.log(indexesToRemove) + doModalDialog({ + title: t('Remove indexes'), + message: t('This will remove {{indexCount}} old indexes, do you want to continue?', { + indexCount: indexesToRemove.length, + }), + yes: t('Yes'), + no: t('No'), + onAccept: () => { + MeteorCall.system + .cleanupIndexes(true) + .then((indexesRemoved) => { + doModalDialog({ + title: t('Remove indexes'), + message: t('{{indexCount}} indexes was removed.', { + indexCount: indexesRemoved.length, + }), + acceptOnly: true, + onAccept: () => { + // nothing + }, + }) + }) + .catch(catchError('system.cleanupIndexes')) + }, + }) + }) + .catch(catchError('system.cleanupIndexes')) + }, [t]) + + return ( + <> +

{t('Cleanup')}

+
+ +
+
+ +
+ + ) +} export function checkForOldDataAndCleanUp(t: TFunction, retriesLeft = 0): void { MeteorCall.system @@ -335,12 +443,12 @@ export function checkForOldDataAndCleanUp(t: TFunction, retriesLeft = 0): void { let totalCount = 0 const affectedCollections: string[] = [] - _.each(results, (result) => { + for (const result of Object.values(results)) { totalCount += result.docsToRemove if (result.docsToRemove > 0) { affectedCollections.push(result.collectionName) } - }) + } if (totalCount) { doModalDialog({ title: t('Remove old data from database'), @@ -375,7 +483,7 @@ export function checkForOldDataAndCleanUp(t: TFunction, retriesLeft = 0): void { .then((results) => { console.log(results) - if (_.isString(results)) { + if (typeof results === 'string') { doModalDialog({ title: t('Error'), message: results, diff --git a/meteor/lib/collections/CoreSystem.ts b/meteor/lib/collections/CoreSystem.ts index f62304a5cb..4e885f720f 100644 --- a/meteor/lib/collections/CoreSystem.ts +++ b/meteor/lib/collections/CoreSystem.ts @@ -67,6 +67,12 @@ export interface ICoreSystem { enabled: boolean } + evaluations?: { + enabled: boolean + heading: string + message: string + } + /** A user-defined name for the installation */ name?: string diff --git a/meteor/server/collections/index.ts b/meteor/server/collections/index.ts index 9aeb6ad048..e2865e7bb9 100644 --- a/meteor/server/collections/index.ts +++ b/meteor/server/collections/index.ts @@ -64,7 +64,17 @@ export const CoreSystem = createAsyncOnlyMongoCollection(Collection const cred = await resolveCredentials({ userId: userId }) const access = await allowAccessToCoreSystem(cred) if (!access.update) return logNotAllowed('CoreSystem', access.reason) - return allowOnlyFields(doc, fields, ['support', 'systemInfo', 'name', 'logLevel', 'apm', 'cron', 'logo']) + + return allowOnlyFields(doc, fields, [ + 'support', + 'systemInfo', + 'name', + 'logLevel', + 'apm', + 'cron', + 'logo', + 'evaluations', + ]) }, }) diff --git a/meteor/server/publications/system.ts b/meteor/server/publications/system.ts index 6870d772d1..215e0b3b58 100644 --- a/meteor/server/publications/system.ts +++ b/meteor/server/publications/system.ts @@ -22,6 +22,7 @@ meteorPublish(MeteorPubSub.coreSystem, async function (token: string | undefined blueprintId: 1, cron: 1, logo: 1, + evaluations: 1, }, }) } diff --git a/meteor/server/security/lib/lib.ts b/meteor/server/security/lib/lib.ts index bc7a359ad3..b4ee3df75d 100644 --- a/meteor/server/security/lib/lib.ts +++ b/meteor/server/security/lib/lib.ts @@ -1,4 +1,3 @@ -import * as _ from 'underscore' import { FieldNames } from '../../../lib/collections/lib' import { logger } from '../../logging' /** @@ -9,15 +8,13 @@ import { logger } from '../../logging' */ export function allowOnlyFields(_doc: T, fieldNames: FieldNames, allowFields: FieldNames): boolean { // Note: _doc is only included to set the type T in this generic function - let allow = true - _.find(fieldNames, (field) => { + for (const field of fieldNames) { if (allowFields.indexOf(field) === -1) { - allow = false - return true + return false } - return false - }) - return allow + } + + return true } /** * Don't allow edits to the fields specified. All other edits are approved @@ -27,15 +24,13 @@ export function allowOnlyFields(_doc: T, fieldNames: FieldNames, allowFiel */ export function rejectFields(_doc: T, fieldNames: FieldNames, rejectFields: FieldNames): boolean { // Note: _doc is only included to set the type T in this generic function - let allow = true - _.find(fieldNames, (field) => { + for (const field of fieldNames) { if (rejectFields.indexOf(field) !== -1) { - allow = false - return true + return false } - return false - }) - return allow + } + + return true } export function logNotAllowed(area: string, reason: string): false { From ada291e4496563c5d53122d5073f28ea91cb7c5e Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 20 Dec 2023 10:37:22 +0000 Subject: [PATCH 193/479] chore: refactor PrompterView to avoid MeteorReactComponent SOFIE-2751 (#1085) --- .../lib/ReactMeteorData/ReactMeteorData.tsx | 44 +++- meteor/client/ui/Prompter/PrompterView.tsx | 229 +++++++++--------- .../ui/Prompter/controller/joycon-device.ts | 6 +- .../ui/Prompter/controller/keyboard-device.ts | 6 +- .../client/ui/Prompter/controller/manager.ts | 6 +- .../Prompter/controller/midi-pedal-device.ts | 6 +- .../Prompter/controller/mouse-ish-device.ts | 6 +- .../controller/shuttle-keyboard-device.ts | 6 +- 8 files changed, 181 insertions(+), 128 deletions(-) diff --git a/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx b/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx index b30f061638..22135d7d92 100644 --- a/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx +++ b/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx @@ -1,6 +1,6 @@ /* eslint-disable react/prefer-stateless-function */ -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useRef } from 'react' import { Meteor } from 'meteor/meteor' import { Mongo } from 'meteor/mongo' import { Tracker } from 'meteor/tracker' @@ -14,6 +14,48 @@ const globalTrackerQueue: Array = [] let globalTrackerTimestamp: number | undefined = undefined let globalTrackerTimeout: number | undefined = undefined +/** + * Delay an update to be batched with the global tracker invalidation queue + */ +export function useGlobalDelayedTrackerUpdateState(newValue: T): T { + const [delayedValue, setDelayedValue] = useState(newValue) + + useGlobalDelayedTrackerUpdate(setDelayedValue, newValue) + + return delayedValue +} + +/** + * Delay an update to be batched with the global tracker invalidation queue + * @param performUpdate Function to call to apply the update + * @param newValue New value to apply with the tracker + */ +export function useGlobalDelayedTrackerUpdate(performUpdate: (value: T) => void, newValue: T): void { + const isPending = useRef(false) + const updateRef = useRef<() => void>() + + useEffect(() => { + // Store the new callback + updateRef.current = () => performUpdate(newValue) + + return () => { + // Invalidated, discard current callback + updateRef.current = undefined + } + }, [performUpdate, newValue]) + + // If a call isn't pending, enqueue the callback + if (!isPending.current) { + MeteorDataManager.enqueueUpdate(() => { + isPending.current = false + if (updateRef.current) { + updateRef.current() + } + }) + isPending.current = true + } +} + const METEOR_DATA_DEBOUNCE = 120 const METEOR_DATA_DEBOUNCE_STALE = 200 diff --git a/meteor/client/ui/Prompter/PrompterView.tsx b/meteor/client/ui/Prompter/PrompterView.tsx index 039c474d5c..50a6cbee4a 100644 --- a/meteor/client/ui/Prompter/PrompterView.tsx +++ b/meteor/client/ui/Prompter/PrompterView.tsx @@ -1,17 +1,22 @@ -import React, { PropsWithChildren } from 'react' +import React, { PropsWithChildren, useEffect, useState } from 'react' import _ from 'underscore' // @ts-expect-error No types available import Velocity from 'velocity-animate' import ClassNames from 'classnames' import { Meteor } from 'meteor/meteor' import { Route } from 'react-router-dom' -import { translateWithTracker, Translated } from '../../lib/ReactMeteorData/ReactMeteorData' +import { + Translated, + useTracker, + useGlobalDelayedTrackerUpdateState, + useSubscription, + useSubscriptions, +} from '../../lib/ReactMeteorData/ReactMeteorData' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { parse as queryStringParse } from 'query-string' import { Spinner } from '../../lib/Spinner' -import { MeteorReactComponent } from '../../lib/MeteorReactComponent' -import { firstIfArray, literal, protectString } from '../../../lib/lib' +import { firstIfArray, protectString } from '../../../lib/lib' import { PrompterData, PrompterAPI, PrompterDataPart } from './prompter' import { PrompterControlManager } from './controller/manager' import { MeteorPubSub } from '../../../lib/api/pubsub' @@ -27,6 +32,7 @@ import { RundownPlaylists, Rundowns } from '../../collections' import { RundownPlaylistCollectionUtil } from '../../../lib/collections/rundownPlaylistUtil' import { logger } from '../../../lib/logging' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { withTranslation } from 'react-i18next' const DEFAULT_UPDATE_THROTTLE = 250 //ms const PIECE_MISSING_UPDATE_THROTTLE = 2000 //ms @@ -82,9 +88,6 @@ interface IProps { interface ITrackedProps { rundownPlaylist?: DBRundownPlaylist studio?: UIStudio -} - -interface IState { subsReady: boolean } @@ -98,9 +101,7 @@ function asArray(value: T | T[] | null): T[] { } } -export class PrompterViewInner extends MeteorReactComponent, IState> { - usedHotkeys: Array = [] - +export class PrompterViewContent extends React.Component> { autoScrollPreviousPartInstanceId: PartInstanceId | null = null configOptions: PrompterConfig @@ -203,38 +204,6 @@ export class PrompterViewInner extends MeteorReactComponent { - const playlist = RundownPlaylists.findOne( - { - studioId: this.props.studioId, - activationId: { $exists: true }, - }, - { - fields: { - _id: 1, - }, - } - ) as Pick | undefined - if (playlist?._id) { - this.subscribe(CorelibPubSub.rundownsInPlaylists, [playlist._id]) - } - }) - - this.autorun(() => { - const subsReady = this.subscriptionsReady() - if (subsReady !== this.state.subsReady) { - this.setState({ - subsReady: subsReady, - }) - } - }) - const themeColor = document.head.querySelector('meta[name="theme-color"]') if (themeColor) { themeColor.setAttribute('data-content', themeColor.getAttribute('content') || '') @@ -256,8 +225,6 @@ export class PrompterViewInner extends MeteorReactComponent - {!this.state.subsReady ? ( + {!this.props.subsReady ? (
@@ -543,27 +510,63 @@ export class PrompterViewInner extends MeteorReactComponent(({ studioId }: IProps) => { - const studio = UIStudios.findOne(studioId) - - const rundownPlaylist = RundownPlaylists.findOne( - { - activationId: { $exists: true }, - studioId: studioId, - }, - { - projection: { - trackedAbSessions: 0, - lastIncorrectPartPlaybackReported: 0, - }, - } - ) as Omit | undefined - return literal({ - rundownPlaylist, - studio, - }) -})(PrompterViewInner) +const PrompterViewContentWithTranslation = withTranslation()(PrompterViewContent) + +export function PrompterView(props: Readonly): JSX.Element { + const studioIsReady = useSubscription(MeteorPubSub.uiStudio, props.studioId) + const playlistIsReady = useSubscription(MeteorPubSub.rundownPlaylistForStudio, props.studioId, true) + + const playlist = useTracker( + () => + RundownPlaylists.findOne( + { + studioId: props.studioId, + activationId: { $exists: true }, + }, + { + fields: { + _id: 1, + }, + } + ) as Pick | undefined, + [props.studioId] + ) + const rundownsIsReady = useSubscription(CorelibPubSub.rundownsInPlaylists, playlist ? [playlist._id] : []) + + const [subsReady, setSubsReady] = useState(false) + useEffect(() => { + setSubsReady(studioIsReady && playlistIsReady && rundownsIsReady) + }, [studioIsReady, playlistIsReady, rundownsIsReady]) + + const studio = useTracker(() => UIStudios.findOne(props.studioId), [props.studioId]) + + const rundownPlaylist = useTracker( + () => + RundownPlaylists.findOne( + { + activationId: { $exists: true }, + studioId: props.studioId, + }, + { + projection: { + trackedAbSessions: 0, + lastIncorrectPartPlaybackReported: 0, + }, + } + ) as Omit | undefined, + [props.studioId] + ) + + return ( + + ) +} interface IPrompterProps { rundownPlaylistId: RundownPlaylistId @@ -575,14 +578,58 @@ interface IPrompterTrackedProps { type ScrollAnchor = [number, string] | null -export const Prompter = translateWithTracker, {}, IPrompterTrackedProps>( - (props: IPrompterProps) => ({ - prompterData: PrompterAPI.getPrompterData(props.rundownPlaylistId), - }), - undefined, - true -)( - class Prompter extends MeteorReactComponent< +function Prompter(props: Readonly>): JSX.Element { + useSubscription(CorelibPubSub.rundownsInPlaylists, [props.rundownPlaylistId]) + + const playlist = useTracker( + () => + RundownPlaylists.findOne(props.rundownPlaylistId, { + fields: { + _id: 1, + activationId: 1, + }, + }) as Pick | undefined, + [props.rundownPlaylistId] + ) + const rundownIDs = playlist ? RundownPlaylistCollectionUtil.getRundownUnorderedIDs(playlist) : [] + useSubscription(CorelibPubSub.segments, rundownIDs, {}) + useSubscription(CorelibPubSub.parts, rundownIDs, null) + useSubscription(CorelibPubSub.partInstances, rundownIDs, playlist?.activationId ?? null) + useSubscription(CorelibPubSub.pieces, rundownIDs, null) + useSubscription(CorelibPubSub.pieceInstancesSimple, rundownIDs, null) + + const rundowns = useTracker( + () => + Rundowns.find( + { playlistId: props.rundownPlaylistId }, + { + fields: { + _id: 1, + showStyleBaseId: 1, + }, + } + ).fetch() as Pick[], + [props.rundownPlaylistId], + [] + ) + useSubscriptions( + MeteorPubSub.uiShowStyleBase, + rundowns.map((rundown) => [rundown.showStyleBaseId]) + ) + + const nextTrackedProps = useTracker( + () => PrompterAPI.getPrompterData(props.rundownPlaylistId), + [props.rundownPlaylistId], + null + ) + + const delayedTrackedProps = useGlobalDelayedTrackerUpdateState(nextTrackedProps) + + return +} + +const PrompterContent = withTranslation()( + class PrompterContent extends React.Component< Translated & IPrompterTrackedProps>, {} > { @@ -595,42 +642,6 @@ export const Prompter = translateWithTracker, } } - componentDidMount(): void { - this.subscribe(CorelibPubSub.rundownsInPlaylists, [this.props.rundownPlaylistId]) - - this.autorun(() => { - const playlist = RundownPlaylists.findOne(this.props.rundownPlaylistId, { - fields: { - _id: 1, - activationId: 1, - }, - }) as Pick | undefined - if (playlist) { - const rundownIDs = RundownPlaylistCollectionUtil.getRundownUnorderedIDs(playlist) - this.subscribe(CorelibPubSub.segments, rundownIDs, {}) - this.subscribe(CorelibPubSub.parts, rundownIDs, null) - this.subscribe(CorelibPubSub.partInstances, rundownIDs, playlist.activationId ?? null) - this.subscribe(CorelibPubSub.pieces, rundownIDs, null) - this.subscribe(CorelibPubSub.pieceInstancesSimple, rundownIDs, null) - } - }) - - this.autorun(() => { - const rundowns = Rundowns.find( - { playlistId: this.props.rundownPlaylistId }, - { - fields: { - _id: 1, - showStyleBaseId: 1, - }, - } - ).fetch() as Pick[] - for (const rundown of rundowns) { - this.subscribe(MeteorPubSub.uiShowStyleBase, rundown.showStyleBaseId) - } - }) - } - getScrollAnchor = () => { let readPosition = ((this.props.config.margin || 0) / 100) * window.innerHeight switch (this.props.config.marker) { diff --git a/meteor/client/ui/Prompter/controller/joycon-device.ts b/meteor/client/ui/Prompter/controller/joycon-device.ts index eb025184a3..e2690712a7 100644 --- a/meteor/client/ui/Prompter/controller/joycon-device.ts +++ b/meteor/client/ui/Prompter/controller/joycon-device.ts @@ -1,5 +1,5 @@ import { ControllerAbstract } from './lib' -import { PrompterConfigMode, PrompterViewInner } from '../PrompterView' +import { PrompterConfigMode, PrompterViewContent } from '../PrompterView' import Spline from 'cubic-spline' import { logger } from '../../../../lib/logging' @@ -10,7 +10,7 @@ type JoyconMode = 'L' | 'R' | 'LR' | null * This class handles control of the prompter using */ export class JoyConController extends ControllerAbstract { - private prompterView: PrompterViewInner + private prompterView: PrompterViewContent private invertJoystick = false // change scrolling direction for joystick private rangeRevMin = -1 // pedal "all back" position, the max-reverse-position @@ -30,7 +30,7 @@ export class JoyConController extends ControllerAbstract { private lastInputValue = '' private lastButtonInputs: { [index: number]: { mode: JoyconMode; buttons: number[] } } = {} - constructor(view: PrompterViewInner) { + constructor(view: PrompterViewContent) { super() this.prompterView = view diff --git a/meteor/client/ui/Prompter/controller/keyboard-device.ts b/meteor/client/ui/Prompter/controller/keyboard-device.ts index 564a11a2b9..03b7f1e157 100644 --- a/meteor/client/ui/Prompter/controller/keyboard-device.ts +++ b/meteor/client/ui/Prompter/controller/keyboard-device.ts @@ -1,5 +1,5 @@ import { ControllerAbstract, LONGPRESS_TIME } from './lib' -import { PrompterViewInner, PrompterConfigMode } from '../PrompterView' +import { PrompterViewContent, PrompterConfigMode } from '../PrompterView' const LOCALSTORAGE_MODE = 'prompter-controller-arrowkeys' @@ -15,7 +15,7 @@ export class KeyboardController extends ControllerAbstract { private _keyDown: { [button: string]: number } = {} - private _prompterView: PrompterViewInner + private _prompterView: PrompterViewContent /** Scroll speed, in pixels per frame */ private _maxSpeed = 100 @@ -29,7 +29,7 @@ export class KeyboardController extends ControllerAbstract { private _updateSpeedHandle: number | null = null - constructor(view: PrompterViewInner) { + constructor(view: PrompterViewContent) { super() this._prompterView = view diff --git a/meteor/client/ui/Prompter/controller/manager.ts b/meteor/client/ui/Prompter/controller/manager.ts index aa93849f92..6c1b5e2b7b 100644 --- a/meteor/client/ui/Prompter/controller/manager.ts +++ b/meteor/client/ui/Prompter/controller/manager.ts @@ -1,4 +1,4 @@ -import { PrompterViewInner, PrompterConfigMode } from '../PrompterView' +import { PrompterViewContent, PrompterConfigMode } from '../PrompterView' import { MouseIshController } from './mouse-ish-device' import { MidiPedalController } from './midi-pedal-device' import { ControllerAbstract } from './lib' @@ -7,10 +7,10 @@ import { KeyboardController } from './keyboard-device' import { ShuttleKeyboardController } from './shuttle-keyboard-device' export class PrompterControlManager { - private _view: PrompterViewInner + private _view: PrompterViewContent private _controllers: Array = [] - constructor(view: PrompterViewInner) { + constructor(view: PrompterViewContent) { this._view = view window.addEventListener('keydown', this._onKeyDown) diff --git a/meteor/client/ui/Prompter/controller/midi-pedal-device.ts b/meteor/client/ui/Prompter/controller/midi-pedal-device.ts index 9fc0edc182..9946d63bc8 100644 --- a/meteor/client/ui/Prompter/controller/midi-pedal-device.ts +++ b/meteor/client/ui/Prompter/controller/midi-pedal-device.ts @@ -1,5 +1,5 @@ import { ControllerAbstract } from './lib' -import { PrompterViewInner, PrompterConfigMode } from '../PrompterView' +import { PrompterViewContent, PrompterConfigMode } from '../PrompterView' import Spline from 'cubic-spline' import webmidi, { Input, InputEventControlchange } from 'webmidi' @@ -9,7 +9,7 @@ import { logger } from '../../../../lib/logging' * This class handles control of the prompter using */ export class MidiPedalController extends ControllerAbstract { - private prompterView: PrompterViewInner + private prompterView: PrompterViewContent private midiInputs: Input[] = [] private idleMidiInputs: { [midiId: string]: boolean } = {} @@ -26,7 +26,7 @@ export class MidiPedalController extends ControllerAbstract { private lastSpeed = 0 private currentPosition = 0 - constructor(view: PrompterViewInner) { + constructor(view: PrompterViewContent) { super() this.prompterView = view diff --git a/meteor/client/ui/Prompter/controller/mouse-ish-device.ts b/meteor/client/ui/Prompter/controller/mouse-ish-device.ts index 879c968c91..2ebd9d5e07 100644 --- a/meteor/client/ui/Prompter/controller/mouse-ish-device.ts +++ b/meteor/client/ui/Prompter/controller/mouse-ish-device.ts @@ -1,5 +1,5 @@ import { ControllerAbstract, LONGPRESS_TIME } from './lib' -import { PrompterViewInner, PrompterConfigMode } from '../PrompterView' +import { PrompterViewContent, PrompterConfigMode } from '../PrompterView' import { NotificationCenter, Notification, NoticeLevel } from '../../../../lib/notifications/notifications' const LOCALSTORAGE_MODE = 'prompter-controller-mouseish' @@ -25,7 +25,7 @@ export class MouseIshController extends ControllerAbstract { private _mouseKeyDown: { [button: string]: number } = {} - private _prompterView: PrompterViewInner + private _prompterView: PrompterViewContent /** scroll speed, in pixels per frame */ private _scrollSpeedTarget = 4 @@ -44,7 +44,7 @@ export class MouseIshController extends ControllerAbstract { private _nextPausePosition: number | null = null private _lastWheelTime = 0 - constructor(view: PrompterViewInner) { + constructor(view: PrompterViewContent) { super() this._prompterView = view diff --git a/meteor/client/ui/Prompter/controller/shuttle-keyboard-device.ts b/meteor/client/ui/Prompter/controller/shuttle-keyboard-device.ts index 2fc897cb29..89b9e482ba 100644 --- a/meteor/client/ui/Prompter/controller/shuttle-keyboard-device.ts +++ b/meteor/client/ui/Prompter/controller/shuttle-keyboard-device.ts @@ -1,5 +1,5 @@ import { ControllerAbstract } from './lib' -import { PrompterViewInner, PrompterConfigMode } from '../PrompterView' +import { PrompterViewContent, PrompterConfigMode } from '../PrompterView' /** * This class handles control of the prompter using Keyboard keys sent from an xkeys @@ -8,7 +8,7 @@ import { PrompterViewInner, PrompterConfigMode } from '../PrompterView' * Supports Page-up / Page-down keys for previous/next story */ export class ShuttleKeyboardController extends ControllerAbstract { - private prompterView: PrompterViewInner + private prompterView: PrompterViewContent private speedMap = [0, 1, 2, 3, 5, 7, 9, 30] private speedStepMap = ShuttleKeyboardController.makeSpeedStepMap(this.speedMap) @@ -19,7 +19,7 @@ export class ShuttleKeyboardController extends ControllerAbstract { private lastSpeedMapPosition = ShuttleKeyboardController.SPEEDMAPNEUTRALPOSITION private currentPosition = 0 - constructor(view: PrompterViewInner) { + constructor(view: PrompterViewContent) { super() this.prompterView = view From 26ae99ed00b5f2310439697a1a3b59d44be42f46 Mon Sep 17 00:00:00 2001 From: ianshade Date: Wed, 20 Dec 2023 11:56:08 +0100 Subject: [PATCH 194/479] fix: bucket selectors and unhandled promises --- meteor/server/api/rest/v1/buckets.ts | 2 +- meteor/server/api/rest/v1/index.ts | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/meteor/server/api/rest/v1/buckets.ts b/meteor/server/api/rest/v1/buckets.ts index d336654ee7..2f5001cf22 100644 --- a/meteor/server/api/rest/v1/buckets.ts +++ b/meteor/server/api/rest/v1/buckets.ts @@ -31,7 +31,7 @@ export class BucketsServerAPI implements BucketsRestAPI { _event: string, bucketId: BucketId ): Promise> { - const bucket = await Buckets.findOneAsync({}, { projection: { _id: 1, name: 1, studioId: 1 } }) + const bucket = await Buckets.findOneAsync(bucketId, { projection: { _id: 1, name: 1, studioId: 1 } }) if (!bucket) { return ClientAPI.responseError( UserError.from(new Error(`Bucket ${bucketId} not found`), UserErrorMessage.BucketNotFound), diff --git a/meteor/server/api/rest/v1/index.ts b/meteor/server/api/rest/v1/index.ts index 86b8dfa257..9d73f49cb0 100644 --- a/meteor/server/api/rest/v1/index.ts +++ b/meteor/server/api/rest/v1/index.ts @@ -318,7 +318,7 @@ export class ServerRestAPI implements RestAPI { externalId: string, triggerMode?: string | null ): Promise> { - const bucketPromise = Buckets.findOneAsync({}, { projection: { _id: 1, name: 1, studioId: 1 } }) + const bucketPromise = Buckets.findOneAsync(bucketId, { projection: { _id: 1 } }) const bucketAdlibPromise = BucketAdLibs.findOneAsync({ bucketId, externalId }, { projection: { _id: 1 } }) const bucketAdlibActionPromise = BucketAdLibActions.findOneAsync( { bucketId, externalId }, @@ -326,13 +326,18 @@ export class ServerRestAPI implements RestAPI { projection: { _id: 1 }, } ) - if (!(await bucketPromise)) { + const [bucket, bucketAdlib, bucketAdlibAction] = await Promise.all([ + bucketPromise, + bucketAdlibPromise, + bucketAdlibActionPromise, + ]) + if (!bucket) { return ClientAPI.responseError( UserError.from(new Error(`Bucket ${bucketId} not found`), UserErrorMessage.BucketNotFound), 412 ) } - if (!((await bucketAdlibPromise) ?? (await bucketAdlibActionPromise))) { + if (!bucketAdlib && !bucketAdlibAction) { return ClientAPI.responseError( UserError.from( new Error(`No adLib with Id ${externalId}, in bucket ${bucketId}`), From 6c2a1bf3c010fe882f0ef91f829ede183d43375a Mon Sep 17 00:00:00 2001 From: ianshade Date: Wed, 20 Dec 2023 12:02:27 +0100 Subject: [PATCH 195/479] fix: save to database when starting a piece --- packages/job-worker/src/playout/adlibAction.ts | 4 ++-- packages/job-worker/src/playout/bucketAdlibJobs.ts | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/job-worker/src/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index 23488e8000..24cdaf4057 100644 --- a/packages/job-worker/src/playout/adlibAction.ts +++ b/packages/job-worker/src/playout/adlibAction.ts @@ -37,11 +37,11 @@ export async function handleExecuteAdlibAction( const initCache = await CacheForPlayoutPreInit.createPreInit(context, lock, playlist, false) - return executeAdlibAction(context, playlist, initCache, data) + return executeAdlibActionAndSaveModel(context, playlist, initCache, data) }) } -export async function executeAdlibAction( +export async function executeAdlibActionAndSaveModel( context: JobContext, playlist: DBRundownPlaylist, initCache: ReadOnlyCache, diff --git a/packages/job-worker/src/playout/bucketAdlibJobs.ts b/packages/job-worker/src/playout/bucketAdlibJobs.ts index f58cad59de..5604ed702c 100644 --- a/packages/job-worker/src/playout/bucketAdlibJobs.ts +++ b/packages/job-worker/src/playout/bucketAdlibJobs.ts @@ -5,7 +5,7 @@ import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/erro import { BucketId, ShowStyleBaseId, ShowStyleVariantId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { CacheForPlayout, CacheForPlayoutPreInit } from './cache' import { innerStartOrQueueAdLibPiece } from './adlibUtils' -import { executeAdlibAction } from './adlibAction' +import { executeAdlibActionAndSaveModel } from './adlibAction' import { RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' /** @@ -50,9 +50,10 @@ export async function handleExecuteBucketAdLibOrAction( partInstance, bucketAdLib ) + await fullCache.saveAllToDatabase() return {} } else if (bucketAdLibAction) { - return await executeAdlibAction(context, playlist, initCache, { + return await executeAdlibActionAndSaveModel(context, playlist, initCache, { actionDocId: bucketAdLibAction._id, actionId: bucketAdLibAction.actionId, playlistId: playlist._id, From 748f0e1b42a828c89514a87157d943368a678034 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 20 Dec 2023 13:01:41 +0000 Subject: [PATCH 196/479] chore: refactor RundownView to avoid MeteorReactComponent SOFIE-2751 (#1091) --- .../lib/ReactMeteorData/ReactMeteorData.tsx | 37 +++ meteor/client/ui/RundownView.tsx | 279 ++++++++++-------- 2 files changed, 192 insertions(+), 124 deletions(-) diff --git a/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx b/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx index 22135d7d92..def3170d61 100644 --- a/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx +++ b/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx @@ -402,6 +402,43 @@ export function useSubscription( return ready } + +/** + * A Meteor Subscription hook that allows using React Functional Components and the Hooks API with Meteor subscriptions. + * Subscriptions will be torn down 1000ms after unmounting the component. + * + * @export + * @param {PubSub} sub The subscription to be subscribed to + * @param {boolean} enable Whether the subscription is enabled + * @param {...any[]} args A list of arugments for the subscription. This is used for optimizing the subscription across + * renders so that it isn't torn down and created for every render. + */ +export function useSubscriptionIfEnabled( + sub: K, + enable: boolean, + ...args: Parameters +): boolean { + const [ready, setReady] = useState(false) + + useEffect(() => { + if (!enable) { + setReady(false) + return + } + + const subscription = Tracker.nonreactive(() => meteorSubscribe(sub, ...args)) + const isReadyComp = Tracker.nonreactive(() => Tracker.autorun(() => setReady(subscription.ready()))) + return () => { + isReadyComp.stop() + setTimeout(() => { + subscription.stop() + }, 1000) + } + }, [sub, enable, stringifyObjects(args)]) + + return ready +} + /** * Sets up multiple subscriptions of the same type, but with different arguments */ diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx index 2c1814c7af..9d64d91117 100644 --- a/meteor/client/ui/RundownView.tsx +++ b/meteor/client/ui/RundownView.tsx @@ -3,7 +3,13 @@ import * as React from 'react' import { parse as queryStringParse } from 'query-string' // @ts-expect-error No types available import * as VelocityReact from 'velocity-react' -import { Translated, translateWithTracker } from '../lib/ReactMeteorData/react-meteor-data' +import { + Translated, + translateWithTracker, + useSubscriptionIfEnabled, + useSubscriptions, + useTracker, +} from '../lib/ReactMeteorData/react-meteor-data' import { VTContent, TSR, NoteSeverity, ISourceLayer } from '@sofie-automation/blueprints-integration' import { withTranslation, WithTranslation } from 'react-i18next' import timer from 'react-timer-hoc' @@ -34,7 +40,6 @@ import { getCurrentTime, unprotectString, protectString } from '../../lib/lib' import { RundownUtils } from '../lib/rundown' import { ErrorBoundary } from '../lib/ErrorBoundary' import { ModalDialog, doModalDialog, isModalShowing } from '../lib/ModalDialog' -import { MeteorReactComponent } from '../lib/MeteorReactComponent' import { getAllowStudio, getAllowDeveloper, getHelpMode } from '../lib/localStorage' import { ClientAPI } from '../../lib/api/client' import { @@ -63,7 +68,7 @@ import { PeripheralDevice, PeripheralDeviceType } from '@sofie-automation/coreli import { doUserAction, UserAction } from '../../lib/clientUserAction' import { hashSingleUseToken, ReloadRundownPlaylistResponse, TriggerReloadDataResponse } from '../../lib/api/userActions' import { ClipTrimDialog } from './ClipTrimPanel/ClipTrimDialog' -import { meteorSubscribe, MeteorPubSub } from '../../lib/api/pubsub' +import { MeteorPubSub, meteorSubscribe } from '../../lib/api/pubsub' import { RundownLayoutType, RundownLayoutBase, @@ -1150,7 +1155,6 @@ interface IState { bottomMargin: string followLiveSegments: boolean manualSetAsNext: boolean - subsReady: boolean isNotificationsCenterOpen: NoticeLevel | undefined isSupportPanelOpen: boolean isInspectorShelfExpanded: boolean @@ -1204,7 +1208,141 @@ interface ITrackedProps { currentSegmentPartIds: PartId[] nextSegmentPartIds: PartId[] } -export const RundownView = translateWithTracker((props: Translated) => { +export function RundownView(props: Readonly): JSX.Element { + const playlistId = props.playlistId + + const subsReady: boolean[] = [] + subsReady.push(useSubscriptionIfEnabled(CorelibPubSub.rundownPlaylists, true, [playlistId], null)) + subsReady.push(useSubscriptionIfEnabled(CorelibPubSub.rundownsInPlaylists, true, [playlistId])) + + const playlistStudioId = useTracker(() => { + const playlist = RundownPlaylists.findOne(playlistId, { + fields: { + _id: 1, + studioId: 1, + }, + }) as Pick | undefined + + return playlist?.studioId + }, [playlistId]) + // Load once the playlist is confirmed to exist + subsReady.push(useSubscriptionIfEnabled(MeteorPubSub.uiSegmentPartNotes, !!playlistStudioId, playlistId)) + subsReady.push(useSubscriptionIfEnabled(MeteorPubSub.uiPieceContentStatuses, !!playlistStudioId, playlistId)) + // Load only when the studio is known + subsReady.push( + useSubscriptionIfEnabled(MeteorPubSub.uiStudio, !!playlistStudioId, playlistStudioId ?? protectString('')) + ) + subsReady.push( + useSubscriptionIfEnabled(MeteorPubSub.buckets, !!playlistStudioId, playlistStudioId ?? protectString(''), null) + ) + + const { rundownIds, showStyleBaseIds, showStyleVariantIds, playlistActivationId } = useTracker( + () => { + const playlist = RundownPlaylists.findOne(playlistId, { + fields: { + _id: 1, + activationId: 1, + }, + }) as Pick | undefined + if (playlist) { + const rundowns = RundownPlaylistCollectionUtil.getRundownsUnordered(playlist, undefined, { + fields: { + _id: 1, + showStyleBaseId: 1, + showStyleVariantId: 1, + }, + }) as Pick[] + + const rundownIds = rundowns.map((rundown) => rundown._id) + const showStyleBaseIds = rundowns.map((rundown) => rundown.showStyleBaseId) + const showStyleVariantIds = rundowns.map((rundown) => rundown.showStyleVariantId) + return { rundownIds, showStyleBaseIds, showStyleVariantIds, playlistActivationId: playlist.activationId } + } + + return { rundownIds: [], showStyleBaseIds: [], showStyleVariantIds: [], playlistActivationId: undefined } + }, + [], + { rundownIds: [], showStyleBaseIds: [], showStyleVariantIds: [], playlistActivationId: undefined } + ) + + subsReady.push( + useSubscriptions( + MeteorPubSub.uiShowStyleBase, + showStyleBaseIds.map((id) => [id]) + ) + ) + subsReady.push( + useSubscriptionIfEnabled(CorelibPubSub.showStyleVariants, showStyleVariantIds.length > 0, null, showStyleVariantIds) + ) + subsReady.push(useSubscriptionIfEnabled(MeteorPubSub.rundownLayouts, showStyleBaseIds.length > 0, showStyleBaseIds)) + + subsReady.push(useSubscriptionIfEnabled(CorelibPubSub.segments, rundownIds.length > 0, rundownIds, {})) + subsReady.push(useSubscriptionIfEnabled(CorelibPubSub.adLibPieces, rundownIds.length > 0, rundownIds)) + subsReady.push(useSubscriptionIfEnabled(CorelibPubSub.rundownBaselineAdLibPieces, rundownIds.length > 0, rundownIds)) + subsReady.push(useSubscriptionIfEnabled(CorelibPubSub.adLibActions, rundownIds.length > 0, rundownIds)) + subsReady.push(useSubscriptionIfEnabled(CorelibPubSub.rundownBaselineAdLibActions, rundownIds.length > 0, rundownIds)) + subsReady.push(useSubscriptionIfEnabled(CorelibPubSub.parts, rundownIds.length > 0, rundownIds, null)) + subsReady.push( + useSubscriptionIfEnabled( + CorelibPubSub.partInstances, + rundownIds.length > 0, + rundownIds, + playlistActivationId ?? null + ) + ) + + useTracker(() => { + const playlist = RundownPlaylists.findOne(playlistId, { + fields: { + currentPartInfo: 1, + nextPartInfo: 1, + previousPartInfo: 1, + }, + }) as Pick | undefined + if (playlist) { + const rundownIds = RundownPlaylistCollectionUtil.getRundownUnorderedIDs(playlist) + // Use meteorSubscribe so that this subscription doesn't mess with this.subscriptionsReady() + // it's run in useTracker, so the subscription will be stopped along with the autorun, + // so we don't have to manually clean up after ourselves. + meteorSubscribe( + CorelibPubSub.pieceInstances, + rundownIds, + [ + playlist.currentPartInfo?.partInstanceId, + playlist.nextPartInfo?.partInstanceId, + playlist.previousPartInfo?.partInstanceId, + ].filter((p): p is PartInstanceId => p !== null), + {} + ) + const { previousPartInstance, currentPartInstance } = + RundownPlaylistCollectionUtil.getSelectedPartInstances(playlist) + + if (previousPartInstance) { + meteorSubscribe( + CorelibPubSub.partInstancesForSegmentPlayout, + previousPartInstance.rundownId, + previousPartInstance.segmentPlayoutId + ) + } + if (currentPartInstance) { + meteorSubscribe( + CorelibPubSub.partInstancesForSegmentPlayout, + currentPartInstance.rundownId, + currentPartInstance.segmentPlayoutId + ) + } + } + }, [playlistId]) + + const allSubsReady = !subsReady.find((ready) => !ready) + return +} + +interface IPropsWithReady extends IProps { + subsReady: boolean +} + +const RundownViewContent = translateWithTracker((props: Translated) => { const playlistId = props.playlistId const playlist = RundownPlaylists.findOne(playlistId) @@ -1336,7 +1474,7 @@ export const RundownView = translateWithTracker(( : [], } })( - class RundownView extends MeteorReactComponent, IState> { + class RundownViewContent extends React.Component, IState> { private _hideNotificationsAfterMount: number | undefined /** MiniShelf data */ private keyboardQueuedPiece: AdLibPieceUi | undefined = undefined @@ -1344,7 +1482,7 @@ export const RundownView = translateWithTracker(( private shouldKeyboardRequeue = false private isKeyboardQueuePending = false - constructor(props: Translated) { + constructor(props: Translated) { super(props) const shelfLayout = this.props.rundownLayouts?.find((layout) => layout._id === this.props.shelfLayoutId) @@ -1361,7 +1499,6 @@ export const RundownView = translateWithTracker(( bottomMargin: '', followLiveSegments: true, manualSetAsNext: false, - subsReady: false, isNotificationsCenterOpen: undefined, isSupportPanelOpen: false, isInspectorShelfExpanded, @@ -1573,115 +1710,6 @@ export const RundownView = translateWithTracker(( } componentDidMount(): void { - const playlistId = this.props.rundownPlaylistId - - this.subscribe(CorelibPubSub.rundownPlaylists, [playlistId], null) - this.subscribe(CorelibPubSub.rundownsInPlaylists, [playlistId]) - this.autorun(() => { - const playlist = RundownPlaylists.findOne(playlistId, { - fields: { - _id: 1, - studioId: 1, - }, - }) as Pick | undefined - if (!playlist) return - - this.subscribe(MeteorPubSub.uiSegmentPartNotes, playlistId) - this.subscribe(MeteorPubSub.uiPieceContentStatuses, playlistId) - this.subscribe(MeteorPubSub.uiStudio, playlist.studioId) - this.subscribe(MeteorPubSub.buckets, playlist.studioId, null) - }) - - this.autorun(() => { - const playlist = RundownPlaylists.findOne(playlistId, { - fields: { - _id: 1, - activationId: 1, - }, - }) as Pick | undefined - if (!playlist) return - - const rundowns = RundownPlaylistCollectionUtil.getRundownsUnordered(playlist, undefined, { - fields: { - _id: 1, - showStyleBaseId: 1, - showStyleVariantId: 1, - }, - }) as Pick[] - - for (const rundown of rundowns) { - this.subscribe(MeteorPubSub.uiShowStyleBase, rundown.showStyleBaseId) - } - - this.subscribe( - CorelibPubSub.showStyleVariants, - null, - rundowns.map((i) => i.showStyleVariantId) - ) - this.subscribe( - MeteorPubSub.rundownLayouts, - rundowns.map((i) => i.showStyleBaseId) - ) - const rundownIDs = rundowns.map((i) => i._id) - this.subscribe(CorelibPubSub.segments, rundownIDs, {}) - this.subscribe(CorelibPubSub.adLibPieces, rundownIDs) - this.subscribe(CorelibPubSub.rundownBaselineAdLibPieces, rundownIDs) - this.subscribe(CorelibPubSub.adLibActions, rundownIDs) - this.subscribe(CorelibPubSub.rundownBaselineAdLibActions, rundownIDs) - this.subscribe(CorelibPubSub.parts, rundownIDs, null) - this.subscribe(CorelibPubSub.partInstances, rundownIDs, playlist.activationId ?? null) - }) - this.autorun(() => { - const playlist = RundownPlaylists.findOne(playlistId, { - fields: { - currentPartInfo: 1, - nextPartInfo: 1, - previousPartInfo: 1, - }, - }) as Pick | undefined - if (playlist) { - const rundownIds = RundownPlaylistCollectionUtil.getRundownUnorderedIDs(playlist) - // Use meteorSubscribe so that this subscription doesn't mess with this.subscriptionsReady() - // it's run in this.autorun, so the subscription will be stopped along with the autorun, - // so we don't have to manually clean up after ourselves. - meteorSubscribe( - CorelibPubSub.pieceInstances, - rundownIds, - [ - playlist.currentPartInfo?.partInstanceId, - playlist.nextPartInfo?.partInstanceId, - playlist.previousPartInfo?.partInstanceId, - ].filter((p): p is PartInstanceId => p !== null), - {} - ) - const { previousPartInstance, currentPartInstance } = - RundownPlaylistCollectionUtil.getSelectedPartInstances(playlist) - - if (previousPartInstance) { - meteorSubscribe( - CorelibPubSub.partInstancesForSegmentPlayout, - previousPartInstance.rundownId, - previousPartInstance.segmentPlayoutId - ) - } - if (currentPartInstance) { - meteorSubscribe( - CorelibPubSub.partInstancesForSegmentPlayout, - currentPartInstance.rundownId, - currentPartInstance.segmentPlayoutId - ) - } - } - }) - this.autorun(() => { - const subsReady = this.subscriptionsReady() - if (subsReady !== this.state.subsReady) { - this.setState({ - subsReady: subsReady, - }) - } - }) - document.body.classList.add('dark', 'vertical-overflow-only') rundownNotificationHandler.set(this.onRONotificationClick) @@ -1711,7 +1739,7 @@ export const RundownView = translateWithTracker(( NotificationCenter.isConcentrationMode = true } - componentDidUpdate(prevProps: IProps & ITrackedProps, prevState: IState) { + componentDidUpdate(prevProps: IPropsWithReady & ITrackedProps, prevState: IState) { if (!this.props.onlyShelf) { if ( this.props.playlist && @@ -1776,8 +1804,8 @@ export const RundownView = translateWithTracker(( // initial Rundown open this.props.playlist && this.props.playlist.currentPartInfo && - this.state.subsReady && - !prevState.subsReady + this.props.subsReady && + !prevProps.subsReady ) { // allow for some time for the Rundown to render maintainFocusOnPartInstance(this.props.playlist.currentPartInfo.partInstanceId, 7000, true, true) @@ -1887,7 +1915,6 @@ export const RundownView = translateWithTracker(( } componentWillUnmount(): void { - this._cleanUp() document.body.classList.remove('dark', 'vertical-overflow-only') window.removeEventListener('beforeunload', this.onBeforeUnload) @@ -2669,7 +2696,9 @@ export const RundownView = translateWithTracker(( new Notification( undefined, NoticeLevel.NOTIFICATION, - t('Playout\xa0Gateway "{{playoutDeviceName}}" is now restarting.', { playoutDeviceName: item.name }), + t('Playout\xa0Gateway "{{playoutDeviceName}}" is now restarting.', { + playoutDeviceName: item.name, + }), 'RundownView' ) ) @@ -2679,7 +2708,9 @@ export const RundownView = translateWithTracker(( new Notification( undefined, NoticeLevel.CRITICAL, - t('Could not restart Playout\xa0Gateway "{{playoutDeviceName}}".', { playoutDeviceName: item.name }), + t('Could not restart Playout\xa0Gateway "{{playoutDeviceName}}".', { + playoutDeviceName: item.name, + }), 'RundownView' ) ) @@ -3170,7 +3201,7 @@ export const RundownView = translateWithTracker(( } render(): JSX.Element { - if (!this.state.subsReady) { + if (!this.props.subsReady) { return (
From 95e160736c73a13e348f0f501217e5f3b5a3344d Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 8 Dec 2023 16:09:44 +0000 Subject: [PATCH 197/479] chore: remove `MeteorReactComponent` --- meteor/client/lib/MeteorReactComponent.ts | 78 ------ .../lib/ReactMeteorData/ReactMeteorData.tsx | 4 - meteor/client/ui/examples.tsx | 227 +++--------------- 3 files changed, 40 insertions(+), 269 deletions(-) delete mode 100644 meteor/client/lib/MeteorReactComponent.ts diff --git a/meteor/client/lib/MeteorReactComponent.ts b/meteor/client/lib/MeteorReactComponent.ts deleted file mode 100644 index 82c2a0fb5b..0000000000 --- a/meteor/client/lib/MeteorReactComponent.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Tracker } from 'meteor/tracker' -import * as React from 'react' -import { stringifyObjects } from '../../lib/lib' -import { Meteor } from 'meteor/meteor' -import { AllPubSubTypes } from '../../lib/api/pubsub' -import { catchError } from './lib' -export class MeteorReactComponent extends React.Component { - private _subscriptions: { [id: string]: Meteor.SubscriptionHandle } = {} - private _computations: Array = [] - constructor(props: IProps, context?: never) { - super(props, context) - } - - componentWillUnmount(): void { - this._cleanUp() - } - subscribe( - name: K, - ...args: Parameters - ): Meteor.SubscriptionHandle { - return Tracker.nonreactive(() => { - // let id = name + '_' + JSON.stringify(args.join()) - const id = name + '_' + stringifyObjects(args) - - const callbacks = { - onError: catchError(`subscribe(${name}, ${JSON.stringify(args)})`), - } - if (Tracker.active) { - // if in a reactive context, Meteor will keep track of duplicates of subscriptions - - const sub = Meteor.subscribe(name, ...args, callbacks) - this._subscriptions[id] = sub - return sub - } else { - if (this._subscriptions[id]) { - // already subscribed to that - return this._subscriptions[id] - } else { - const sub = Meteor.subscribe(name, ...args, callbacks) - this._subscriptions[id] = sub - return sub - } - } - }) - } - autorun(...args: Parameters): Tracker.Computation { - const computation = Tracker.nonreactive(() => { - return Tracker.autorun(...args) - }) - this._computations.push(computation) - return computation - } - subscriptionsReady(): boolean { - const values = Object.values(this._subscriptions) - for (let i = 0; i < values.length; i++) { - if (!values[i].ready()) { - return false - } - } - return true - } - subscriptions(): Array { - return Object.values(this._subscriptions) - } - protected _cleanUp(): void { - const subscriptions = Object.values(this._subscriptions) - for (let i = 0; i < subscriptions.length; i++) { - // Wait a little bit with unsubscribing, maybe the next view is going to subscribe to the same data as well? - // In that case, by unsubscribing directly, we'll get a flicker in the view because of the unloading+loading - Meteor.setTimeout(() => { - subscriptions[i].stop() - }, 100) - } - for (let i = 0; i < this._computations.length; i++) { - this._computations[i].stop() - } - } -} diff --git a/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx b/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx index def3170d61..133178f2b8 100644 --- a/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx +++ b/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx @@ -5,7 +5,6 @@ import { Meteor } from 'meteor/meteor' import { Mongo } from 'meteor/mongo' import { Tracker } from 'meteor/tracker' import { withTranslation, WithTranslation } from 'react-i18next' -import { MeteorReactComponent } from '../MeteorReactComponent' import { meteorSubscribe, AllPubSubTypes } from '../../../lib/api/pubsub' import { stringifyObjects } from '../../../lib/lib' import _ from 'underscore' @@ -254,9 +253,6 @@ export const ReactMeteorData = { componentWillUnmount(this: any): void { this._meteorDataManager.dispose() }, - // pick the MeteorReactComponent member functions, so they will be available in withTracker(() => { >here< }) - autorun: MeteorReactComponent.prototype.autorun, - subscribe: MeteorReactComponent.prototype.subscribe, } class ReactMeteorComponentWrapper extends React.Component { diff --git a/meteor/client/ui/examples.tsx b/meteor/client/ui/examples.tsx index ad6ec00fe9..8e1b0f81cc 100644 --- a/meteor/client/ui/examples.tsx +++ b/meteor/client/ui/examples.tsx @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { withTracker, translateWithTracker, Translated } from '../lib/ReactMeteorData/ReactMeteorData' +import { useTracker } from '../lib/ReactMeteorData/ReactMeteorData' import * as React from 'react' import { TimingDataResolution, @@ -7,8 +7,7 @@ import { withTiming, WithTiming, } from './RundownView/RundownTiming/withTiming' -import { withTranslation } from 'react-i18next' -import { MeteorReactComponent } from '../lib/MeteorReactComponent' +import { useTranslation } from 'react-i18next' import { Meteor } from 'meteor/meteor' // These are examples of how to write different types of components @@ -18,170 +17,41 @@ import { Meteor } from 'meteor/meteor' interface SimpleComponentProps { myProp0: string } -interface SimpleComponentState { - myState0: string -} -class SimpleComponent extends React.Component { - constructor(props: SimpleComponentProps) { - super(props) - this.state = { - myState0: '', - } - } - render(): JSX.Element { - return ( -
- {this.props.myProp0} - {this.state.myState0} - {/* {this.props.asdf} invalid argument */} - {/* {this.state.asdf} invalid argument */} -
- ) - } -} -export function testSimpleComponent(): SimpleComponent { - return new SimpleComponent({ - myProp0: '', - // asdf: 123, // invalid argument - }) -} -// Translated Simple component ------------------------------ -interface TranslatedSimpleComponentProps { - myProp0: string -} -interface TranslatedSimpleComponentState { - myState0: string +export function SimpleComponent({ myProp0 }: Readonly): JSX.Element { + const { t } = useTranslation() + + const [myState0, _setMyState0] = React.useState('default') + return ( +
+ {t('Test test')} + {myProp0} + {myState0} + {/* {this.props.asdf} invalid argument */} + {/* {this.state.asdf} invalid argument */} +
+ ) } -export const TranslatedSimpleComponent = withTranslation()( - class TranslatedSimpleComponent extends React.Component< - Translated, - TranslatedSimpleComponentState - > { - constructor(props: Translated) { - super(props) - this.state = { - myState0: '', - } - } - render(): JSX.Element { - const t = this.props.t - return ( -
- {t('Test test')} - {this.props.myProp0} - {this.state.myState0} - {/* {this.props.asdf} invalid argument */} - {/* {this.state.asdf} invalid argument */} -
- ) - } - } -) -// function testTranslatedSimpleComponent () { -// let a = new TranslatedSimpleComponent({ -// myProp0: '', -// // asdf: 123, // invalid argument -// }) -// } + // Reactive Component ---------------------------- interface ReactiveComponentProps { myProp0: string } -interface ReactiveComponentState { - myState0: string -} -interface ReactiveComponentTrackedProps { - myReactiveProp0: string -} -const ReactiveComponent = withTracker( - () => { - return { - myReactiveProp0: Meteor.status().status, - } - } -)( - class ReactiveComponent extends MeteorReactComponent< - ReactiveComponentProps & ReactiveComponentTrackedProps, - ReactiveComponentState - > { - constructor(props: ReactiveComponentProps & ReactiveComponentTrackedProps) { - super(props) - this.state = { - myState0: '', - } - } - render(): JSX.Element { - return ( -
- {this.props.myProp0} - {this.state.myState0} - {this.props.myReactiveProp0} - {/* {this.props.asdf} invalid argument */} - {/* {this.state.asdf} invalid argument */} -
- ) - } - } -) -export function testReactiveComponent(): React.Component { - return new ReactiveComponent({ - myProp0: '', - // myReactiveProp0: '', // invalid argument - // asdf: 123, // invalid argument - }) -} -// Translated Reactive Component ------------------------------ -interface TranslatedReactiveComponentProps { - myProp0: string -} -interface TranslatedReactiveComponentState { - myState0: string -} -interface TranslatedReactiveComponentTrackedProps { - myReactiveProp0: string -} +export function ReactiveComponent(props: Readonly): JSX.Element { + const { t } = useTranslation() -export const TranslatedReactiveComponent = translateWithTracker< - TranslatedReactiveComponentProps, - TranslatedReactiveComponentState, - TranslatedReactiveComponentTrackedProps ->(() => { - return { - myReactiveProp0: Meteor.status().status, - } -})( - class TranslatedReactiveComponent extends MeteorReactComponent< - Translated, - TranslatedReactiveComponentState - > { - constructor(props: Translated) { - super(props) - this.state = { - myState0: '', - } - } - render(): JSX.Element { - const t = this.props.t - return ( -
- {t('Test test')} - {this.props.myProp0} - {this.state.myState0} - {this.props.myReactiveProp0} - {/* {this.props.asdf} invalid argument */} - {/* {this.state.asdf} invalid argument */} -
- ) - } - } -) -// function testTranslatedReactiveComponent () { -// let a = new TranslatedReactiveComponent({ -// myProp0: '', -// // asdf: 123, // invalid argument -// }) -// } + const myReactiveProp0 = useTracker(() => Meteor.status().status, []) + + return ( +
+ {t('Test test')} + {props.myProp0} + {myReactiveProp0} + {/* {this.props.asdf} invalid argument */} + {/* {this.state.asdf} invalid argument */} +
+ ) +} // withTiming ---------------------- interface WithTimingComponentProps { @@ -193,30 +63,13 @@ interface WithTimingComponentState { export const WithTimingComponent = withTiming({ dataResolution: TimingDataResolution.Synced, tickResolution: TimingTickResolution.Synced, -})( - class WithTimingComponent extends React.Component, WithTimingComponentState> { - _refreshTimer: number | undefined - - constructor(props: WithTiming) { - super(props) - - const a = this.props.myProp0 - - this.state = { - myState0: a, - // asdf: '' // invalid state attr - } - } - - render(): JSX.Element { - return ( -
- {this.props.myProp0} - {this.state.myState0} - {/* {this.props.asdf} invalid argument */} - {/* {this.state.asdf} invalid argument */} -
- ) - } - } -) +})(function WithTimingComponent({ myProp0, timingDurations }: Readonly>) { + return ( +
+ {myProp0} + {timingDurations.currentTime} + {/* {this.props.asdf} invalid argument */} + {/* {this.state.asdf} invalid argument */} +
+ ) +}) From d67fc7389607d471b2c4c5a51054a1371311b4d6 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 8 Dec 2023 16:13:42 +0000 Subject: [PATCH 198/479] chore: add `useSetDocumentClass` helper --- meteor/client/ui/ActiveRundownView.tsx | 11 +++-------- meteor/client/ui/ClockView/CameraScreen/index.tsx | 5 ++--- meteor/client/ui/ClockView/OverlayScreenSaver.tsx | 8 ++------ meteor/client/ui/ClockView/PresenterScreen.tsx | 13 +++---------- meteor/client/ui/util/useSetDocumentClass.ts | 15 +++++++++++++++ 5 files changed, 25 insertions(+), 27 deletions(-) create mode 100644 meteor/client/ui/util/useSetDocumentClass.ts diff --git a/meteor/client/ui/ActiveRundownView.tsx b/meteor/client/ui/ActiveRundownView.tsx index 907cb16cfe..04b1ed05ee 100644 --- a/meteor/client/ui/ActiveRundownView.tsx +++ b/meteor/client/ui/ActiveRundownView.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react' +import React from 'react' import { NavLink, Route, Switch, useRouteMatch } from 'react-router-dom' import { useSubscription, useTracker } from '../lib/ReactMeteorData/ReactMeteorData' @@ -9,6 +9,7 @@ import { UIStudios } from './Collections' import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { RundownPlaylists } from '../collections' import { useTranslation } from 'react-i18next' +import { useSetDocumentClass } from './util/useSetDocumentClass' export function ActiveRundownView({ studioId }: Readonly<{ studioId: StudioId }>): JSX.Element | null { const { t } = useTranslation() @@ -30,13 +31,7 @@ export function ActiveRundownView({ studioId }: Readonly<{ studioId: StudioId }> [studioId] ) - useEffect(() => { - document.body.classList.add('dark', 'vertical-overflow-only') - - return () => { - document.body.classList.remove('dark', 'vertical-overflow-only') - } - }, [playlist]) + useSetDocumentClass('dark', 'vertical-overflow-only') if (!subsReady) { return ( diff --git a/meteor/client/ui/ClockView/CameraScreen/index.tsx b/meteor/client/ui/ClockView/CameraScreen/index.tsx index e2c6fdc800..029395b67e 100644 --- a/meteor/client/ui/ClockView/CameraScreen/index.tsx +++ b/meteor/client/ui/ClockView/CameraScreen/index.tsx @@ -22,6 +22,7 @@ import { useBlackBrowserTheme } from '../../../lib/useBlackBrowserTheme' import { useWakeLock } from './useWakeLock' import { catchError, useDebounce } from '../../../lib/lib' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { useSetDocumentClass } from '../../util/useSetDocumentClass' interface IProps { playlist: DBRundownPlaylist | undefined @@ -140,14 +141,12 @@ export function CameraScreen({ playlist, studioId }: Readonly): JSX.Elem [currentPartInstance, nextPartInstance] ) + useSetDocumentClass('dark', 'xdark', 'vertical-overflow-only') useEffect(() => { - document.body.classList.add('dark', 'xdark', 'vertical-overflow-only') - const containerEl = document.querySelector('#render-target > .container-fluid.header-clear') if (containerEl) containerEl.classList.remove('header-clear') return () => { - document.body.classList.remove('dark', 'xdark', 'vertical-overflow-only') if (containerEl) containerEl.classList.add('header-clear') } }, []) diff --git a/meteor/client/ui/ClockView/OverlayScreenSaver.tsx b/meteor/client/ui/ClockView/OverlayScreenSaver.tsx index 55d0c7db67..473f21a37b 100644 --- a/meteor/client/ui/ClockView/OverlayScreenSaver.tsx +++ b/meteor/client/ui/ClockView/OverlayScreenSaver.tsx @@ -6,16 +6,12 @@ import { findNextPlaylist } from '../StudioScreenSaver/StudioScreenSaver' // @ts-expect-error No types available import Velocity from 'velocity-animate' import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { useSetDocumentClass } from '../util/useSetDocumentClass' export function OverlayScreenSaver({ studioId }: Readonly<{ studioId: StudioId }>): JSX.Element { const studioNameRef = useRef(null) - useEffect(() => { - document.body.classList.add('transparent') - return () => { - document.body.classList.remove('transparent') - } - }) + useSetDocumentClass('transparent') useSubscription(MeteorPubSub.uiStudio, studioId) useSubscription(MeteorPubSub.rundownPlaylistForStudio, studioId, false) diff --git a/meteor/client/ui/ClockView/PresenterScreen.tsx b/meteor/client/ui/ClockView/PresenterScreen.tsx index 1be7d134e9..ea1d390981 100644 --- a/meteor/client/ui/ClockView/PresenterScreen.tsx +++ b/meteor/client/ui/ClockView/PresenterScreen.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react' +import React from 'react' import ClassNames from 'classnames' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { PartUi } from '../SegmentTimeline/SegmentTimelineContainer' @@ -40,6 +40,7 @@ import { PieceInstances, RundownLayouts, RundownPlaylists, Rundowns, ShowStyleVa import { RundownPlaylistCollectionUtil } from '../../../lib/collections/rundownPlaylistUtil' import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { useSetDocumentClass } from '../util/useSetDocumentClass' interface SegmentUi extends DBSegment { items: Array @@ -310,15 +311,7 @@ function PresenterScreenContent(props: WithTiming { - const bodyClassList: string[] = ['dark', 'xdark'] - - document.body.classList.add(...bodyClassList) - - return () => { - document.body.classList.remove(...bodyClassList) - } - }, []) + useSetDocumentClass('dark', 'xdark') if (presenterLayout && RundownLayoutsAPI.isDashboardLayout(presenterLayout)) { return ( diff --git a/meteor/client/ui/util/useSetDocumentClass.ts b/meteor/client/ui/util/useSetDocumentClass.ts new file mode 100644 index 0000000000..e63ba88b88 --- /dev/null +++ b/meteor/client/ui/util/useSetDocumentClass.ts @@ -0,0 +1,15 @@ +import { useEffect } from 'react' + +/** + * Adds the provided classes to `document.body` upon mount, and removes them when unmounted + * @param classNames Classnames to add + */ +export function useSetDocumentClass(...classNames: string[]): void { + useEffect(() => { + document.body.classList.add(...classNames) + + return () => { + document.body.classList.remove(...classNames) + } + }, [JSON.stringify(classNames)]) +} From 3eb75296de8600cdf676fa03ee02c54b689e9ca1 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 8 Dec 2023 16:21:32 +0000 Subject: [PATCH 199/479] chore: add `useRundownAndShowStyleIdsForPlaylist` helper --- .../client/ui/ClockView/PresenterScreen.tsx | 24 +---------- meteor/client/ui/RundownView.tsx | 37 +++++----------- meteor/client/ui/Shelf/BucketPanel.tsx | 22 +++------- .../useRundownAndShowStyleIdsForPlaylist.ts | 43 +++++++++++++++++++ meteor/lib/collections/rundownPlaylistUtil.ts | 8 ++-- 5 files changed, 66 insertions(+), 68 deletions(-) create mode 100644 meteor/client/ui/util/useRundownAndShowStyleIdsForPlaylist.ts diff --git a/meteor/client/ui/ClockView/PresenterScreen.tsx b/meteor/client/ui/ClockView/PresenterScreen.tsx index ea1d390981..5a3529083e 100644 --- a/meteor/client/ui/ClockView/PresenterScreen.tsx +++ b/meteor/client/ui/ClockView/PresenterScreen.tsx @@ -41,6 +41,7 @@ import { RundownPlaylistCollectionUtil } from '../../../lib/collections/rundownP import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { useSetDocumentClass } from '../util/useSetDocumentClass' +import { useRundownAndShowStyleIdsForPlaylist } from '../util/useRundownAndShowStyleIdsForPlaylist' interface SegmentUi extends DBSegment { items: Array @@ -344,28 +345,7 @@ export function usePresenterScreenSubscriptions(props: PresenterScreenProps): vo useSubscription(CorelibPubSub.rundownsInPlaylists, playlist ? [playlist._id] : []) - const { rundownIds, showStyleBaseIds, showStyleVariantIds } = useTracker( - () => { - if (playlist) { - const rundowns = RundownPlaylistCollectionUtil.getRundownsUnordered(playlist, undefined, { - fields: { - _id: 1, - showStyleBaseId: 1, - showStyleVariantId: 1, - }, - }) as Array> - const rundownIds = rundowns.map((r) => r._id) - const showStyleBaseIds = rundowns.map((r) => r.showStyleBaseId) - const showStyleVariantIds = rundowns.map((r) => r.showStyleVariantId) - - return { rundownIds, showStyleBaseIds, showStyleVariantIds } - } else { - return { rundownIds: [], showStyleBaseIds: [], showStyleVariantIds: [] } - } - }, - [playlist], - { rundownIds: [], showStyleBaseIds: [], showStyleVariantIds: [] } - ) + const { rundownIds, showStyleBaseIds, showStyleVariantIds } = useRundownAndShowStyleIdsForPlaylist(playlist?._id) useSubscription(CorelibPubSub.segments, rundownIds, {}) useSubscription(CorelibPubSub.parts, rundownIds, null) diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx index 9d64d91117..b0108bc059 100644 --- a/meteor/client/ui/RundownView.tsx +++ b/meteor/client/ui/RundownView.tsx @@ -153,6 +153,7 @@ import { logger } from '../../lib/logging' import { isTranslatableMessage, translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import { i18nTranslator } from './i18n' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { useRundownAndShowStyleIdsForPlaylist } from './util/useRundownAndShowStyleIdsForPlaylist' export const MAGIC_TIME_SCALE_FACTOR = 0.03 @@ -1236,34 +1237,18 @@ export function RundownView(props: Readonly): JSX.Element { useSubscriptionIfEnabled(MeteorPubSub.buckets, !!playlistStudioId, playlistStudioId ?? protectString(''), null) ) - const { rundownIds, showStyleBaseIds, showStyleVariantIds, playlistActivationId } = useTracker( - () => { - const playlist = RundownPlaylists.findOne(playlistId, { - fields: { - _id: 1, - activationId: 1, - }, - }) as Pick | undefined - if (playlist) { - const rundowns = RundownPlaylistCollectionUtil.getRundownsUnordered(playlist, undefined, { - fields: { - _id: 1, - showStyleBaseId: 1, - showStyleVariantId: 1, - }, - }) as Pick[] + const playlistActivationId = useTracker(() => { + const playlist = RundownPlaylists.findOne(playlistId, { + fields: { + _id: 1, + activationId: 1, + }, + }) as Pick | undefined - const rundownIds = rundowns.map((rundown) => rundown._id) - const showStyleBaseIds = rundowns.map((rundown) => rundown.showStyleBaseId) - const showStyleVariantIds = rundowns.map((rundown) => rundown.showStyleVariantId) - return { rundownIds, showStyleBaseIds, showStyleVariantIds, playlistActivationId: playlist.activationId } - } + return playlist?.activationId + }, [playlistId]) - return { rundownIds: [], showStyleBaseIds: [], showStyleVariantIds: [], playlistActivationId: undefined } - }, - [], - { rundownIds: [], showStyleBaseIds: [], showStyleVariantIds: [], playlistActivationId: undefined } - ) + const { rundownIds, showStyleBaseIds, showStyleVariantIds } = useRundownAndShowStyleIdsForPlaylist(playlistId) subsReady.push( useSubscriptions( diff --git a/meteor/client/ui/Shelf/BucketPanel.tsx b/meteor/client/ui/Shelf/BucketPanel.tsx index 3ebbeaceb0..8233bcc112 100644 --- a/meteor/client/ui/Shelf/BucketPanel.tsx +++ b/meteor/client/ui/Shelf/BucketPanel.tsx @@ -1,6 +1,5 @@ import { Meteor } from 'meteor/meteor' import * as React from 'react' -import * as _ from 'underscore' import { Translated, useSubscription, useSubscriptions, useTracker } from '../../lib/ReactMeteorData/react-meteor-data' import { IAdLibListItem } from './AdLibListItem' import ClassNames from 'classnames' @@ -73,6 +72,8 @@ import { import { RundownPlaylistCollectionUtil } from '../../../lib/collections/rundownPlaylistUtil' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { withTranslation } from 'react-i18next' +import { useRundownAndShowStyleIdsForPlaylist } from '../util/useRundownAndShowStyleIdsForPlaylist' +import _ from 'underscore' interface IBucketPanelDragObject { id: BucketId @@ -273,25 +274,14 @@ export const BucketPanel = React.memo( useSubscription(MeteorPubSub.uiBucketContentStatuses, props.playlist.studioId, props.bucket._id) useSubscription(MeteorPubSub.uiStudio, props.playlist.studioId) - const { showStyleBases, showStyleVariants } = useTracker( - () => { - const rundowns = RundownPlaylistCollectionUtil.getRundownsUnordered(props.playlist) - - const showStyleBases = _.uniq(rundowns.map((rundown) => rundown.showStyleBaseId)) - const showStyleVariants = _.uniq(rundowns.map((rundown) => rundown.showStyleVariantId)) - - return { showStyleBases, showStyleVariants } - }, - [props.playlist], - { showStyleBases: [], showStyleVariants: [] } - ) + const { showStyleBaseIds, showStyleVariantIds } = useRundownAndShowStyleIdsForPlaylist(props.playlist._id) - useSubscription(CorelibPubSub.bucketAdLibPieces, props.playlist.studioId, props.bucket._id, showStyleVariants) - useSubscription(CorelibPubSub.bucketAdLibActions, props.playlist.studioId, props.bucket._id, showStyleVariants) + useSubscription(CorelibPubSub.bucketAdLibPieces, props.playlist.studioId, props.bucket._id, showStyleVariantIds) + useSubscription(CorelibPubSub.bucketAdLibActions, props.playlist.studioId, props.bucket._id, showStyleVariantIds) useSubscriptions( MeteorPubSub.uiShowStyleBase, - showStyleBases.map((id) => [id]) + showStyleBaseIds.map((id) => [id]) ) // Data processing: diff --git a/meteor/client/ui/util/useRundownAndShowStyleIdsForPlaylist.ts b/meteor/client/ui/util/useRundownAndShowStyleIdsForPlaylist.ts new file mode 100644 index 0000000000..5e5f3e5efb --- /dev/null +++ b/meteor/client/ui/util/useRundownAndShowStyleIdsForPlaylist.ts @@ -0,0 +1,43 @@ +import { + RundownId, + RundownPlaylistId, + ShowStyleBaseId, + ShowStyleVariantId, +} from '@sofie-automation/corelib/dist/dataModel/Ids' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { RundownPlaylistCollectionUtil } from '../../../lib/collections/rundownPlaylistUtil' +import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' +import _ from 'underscore' + +interface RundownAndShowStyleIds { + rundownIds: RundownId[] + showStyleBaseIds: ShowStyleBaseId[] + showStyleVariantIds: ShowStyleVariantId[] +} + +export function useRundownAndShowStyleIdsForPlaylist( + playlistId: RundownPlaylistId | undefined +): RundownAndShowStyleIds { + return useTracker( + () => { + if (playlistId) { + const rundowns = RundownPlaylistCollectionUtil.getRundownsUnordered(playlistId, undefined, { + fields: { + _id: 1, + showStyleBaseId: 1, + showStyleVariantId: 1, + }, + }) as Array> + const rundownIds = rundowns.map((r) => r._id) + const showStyleBaseIds = _.uniq(rundowns.map((r) => r.showStyleBaseId)) + const showStyleVariantIds = _.uniq(rundowns.map((r) => r.showStyleVariantId)) + + return { rundownIds, showStyleBaseIds, showStyleVariantIds } + } else { + return { rundownIds: [], showStyleBaseIds: [], showStyleVariantIds: [] } + } + }, + [playlistId], + { rundownIds: [], showStyleBaseIds: [], showStyleVariantIds: [] } + ) +} diff --git a/meteor/lib/collections/rundownPlaylistUtil.ts b/meteor/lib/collections/rundownPlaylistUtil.ts index 7edb062387..114bc45ebb 100644 --- a/meteor/lib/collections/rundownPlaylistUtil.ts +++ b/meteor/lib/collections/rundownPlaylistUtil.ts @@ -1,4 +1,4 @@ -import { PartId, RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PartId, RundownId, RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { Rundown, DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' @@ -29,7 +29,7 @@ export class RundownPlaylistCollectionUtil { selector?: MongoQuery, options?: FindOptions ): Rundown[] { - const allRundowns = RundownPlaylistCollectionUtil.getRundownsUnordered(playlist, selector, options) + const allRundowns = RundownPlaylistCollectionUtil.getRundownsUnordered(playlist._id, selector, options) const rundownsMap = normalizeArrayToMap(allRundowns, '_id') @@ -39,13 +39,13 @@ export class RundownPlaylistCollectionUtil { } /** Returns an array of all Rundowns in the RundownPlaylist, in no predictable order */ static getRundownsUnordered( - playlist: Pick, + playlistId: RundownPlaylistId, selector?: MongoQuery, options?: FindOptions ): Rundown[] { return Rundowns.find( { - playlistId: playlist._id, + playlistId: playlistId, ...selector, }, { From 5d94d8a0f3d0ef09894ccb03e1987638d1ad3573 Mon Sep 17 00:00:00 2001 From: Krzysztof Zegzula Date: Wed, 20 Dec 2023 16:39:12 +0100 Subject: [PATCH 200/479] feat: Buckets in the HTTP API (SOFIE-2795) (#1070) --- meteor/lib/api/client.ts | 2 +- meteor/lib/api/rest/v1/buckets.ts | 120 ++++++++ meteor/lib/api/rest/v1/index.ts | 1 + meteor/lib/api/rest/v1/playlists.ts | 23 ++ meteor/server/api/rest/v1/buckets.ts | 281 +++++++++++++++++ meteor/server/api/rest/v1/index.ts | 2 + meteor/server/api/rest/v1/playlists.ts | 102 +++++- meteor/server/api/rest/v1/typeConversion.ts | 25 +- packages/corelib/src/error.ts | 2 + packages/corelib/src/worker/studio.ts | 11 + .../job-worker/src/playout/adlibAction.ts | 132 ++++---- .../job-worker/src/playout/bucketAdlibJobs.ts | 95 ++++++ .../job-worker/src/workers/studio/jobs.ts | 2 + packages/openapi/api/actions.yaml | 11 + .../openapi/api/definitions/blueprints.yaml | 4 +- packages/openapi/api/definitions/buckets.yaml | 291 ++++++++++++++++++ .../openapi/api/definitions/playlists.yaml | 56 ++++ .../openapi/src/__tests__/buckets.spec.ts | 101 ++++++ .../openapi/src/__tests__/playlists.spec.ts | 12 + 19 files changed, 1208 insertions(+), 65 deletions(-) create mode 100644 meteor/lib/api/rest/v1/buckets.ts create mode 100644 meteor/server/api/rest/v1/buckets.ts create mode 100644 packages/job-worker/src/playout/bucketAdlibJobs.ts create mode 100644 packages/openapi/api/definitions/buckets.yaml create mode 100644 packages/openapi/src/__tests__/buckets.spec.ts diff --git a/meteor/lib/api/client.ts b/meteor/lib/api/client.ts index 2033ae4001..34910deb34 100644 --- a/meteor/lib/api/client.ts +++ b/meteor/lib/api/client.ts @@ -63,7 +63,7 @@ export namespace ClientAPI { /** On success, return success code (by default, use 200) */ success: number /** Optionally, provide method result */ - result?: Result + result: Result } export function responseSuccess(result: Result, code?: number): ClientResponseSuccess { if (isClientResponseSuccess(result)) result = result.result diff --git a/meteor/lib/api/rest/v1/buckets.ts b/meteor/lib/api/rest/v1/buckets.ts new file mode 100644 index 0000000000..6df295761d --- /dev/null +++ b/meteor/lib/api/rest/v1/buckets.ts @@ -0,0 +1,120 @@ +import { Meteor } from 'meteor/meteor' +import { ClientAPI } from '../../client' +import { BucketId, ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { IngestAdlib } from '@sofie-automation/blueprints-integration' + +export interface BucketsRestAPI { + /** + * Get all available Buckets. + * + * @param connection Connection data including client and header details + * @param event User event string + * @param inputs Migration data to apply + */ + getAllBuckets( + connection: Meteor.Connection, + event: string + ): Promise>> + + /** + * Get a Bucket. + * + * @param connection Connection data including client and header details + * @param event User event string + * @param inputs Migration data to apply + */ + getBucket( + connection: Meteor.Connection, + event: string, + bucketId: BucketId + ): Promise> + + /** + * Adds a new Bucket, returns the Id of the newly created Bucket. + * + * @param connection Connection data including client and header details + * @param event User event string + * @param bucket Bucket to add + */ + addBucket( + connection: Meteor.Connection, + event: string, + bucket: APIBucket + ): Promise> + + /** + * Deletes a Bucket. + * + * @param connection Connection data including client and header details + * @param event User event string + * @param bucketId Id of the bucket to delete + */ + deleteBucket( + connection: Meteor.Connection, + event: string, + bucketId: BucketId + ): Promise> + + /** + * Empties a Bucket. + * + * @param connection Connection data including client and header details + * @param event User event string + * @param bucketId Id of the bucket to empty + */ + emptyBucket( + connection: Meteor.Connection, + event: string, + bucketId: BucketId + ): Promise> + + /** + * Deletes a Bucket AdLib. + * + * @param connection Connection data including client and header details + * @param event User event string + * @param adLibId Id of the bucket adlib to delete + */ + deleteBucketAdLib( + connection: Meteor.Connection, + event: string, + externalId: string + ): Promise> + + /** + * Imports a Bucket AdLib. + * If adlibs with the same `ingestItem.externalId` already exist in the bucket, they will be replaced. + * + * @param connection Connection data including client and header details + * @param event User event string + * @param bucketId Id of the bucket where to import the adlib + * @param showStyleBaseId Id of the showStyle to use when importing the adlib + * @param ingestItem Adlib to be imported + */ + importAdLibToBucket( + connection: Meteor.Connection, + event: string, + bucketId: BucketId, + showStyleBaseId: ShowStyleBaseId, + ingestItem: IngestAdlib + ): Promise> +} + +export interface APIBucket { + name: string + studioId: string +} + +export interface APIBucketComplete extends APIBucket { + id: string +} + +// Based on the IngestAdlib interface +export interface APIImportAdlib { + externalId: string + name: string + payloadType: string + payload?: unknown + + showStyleBaseId: string +} diff --git a/meteor/lib/api/rest/v1/index.ts b/meteor/lib/api/rest/v1/index.ts index 0c28e0a060..b539ccde63 100644 --- a/meteor/lib/api/rest/v1/index.ts +++ b/meteor/lib/api/rest/v1/index.ts @@ -1,4 +1,5 @@ export * from './blueprints' +export * from './buckets' export * from './devices' export * from './playlists' export * from './showstyles' diff --git a/meteor/lib/api/rest/v1/playlists.ts b/meteor/lib/api/rest/v1/playlists.ts index e7ff208be2..d3dcbb7adf 100644 --- a/meteor/lib/api/rest/v1/playlists.ts +++ b/meteor/lib/api/rest/v1/playlists.ts @@ -2,6 +2,7 @@ import { ClientAPI } from '../../client' import { AdLibActionId, BucketAdLibId, + BucketId, PartId, PartInstanceId, PieceId, @@ -75,6 +76,28 @@ export interface PlaylistsRestAPI { adLibId: AdLibActionId | RundownBaselineAdLibActionId | PieceId | BucketAdLibId, triggerMode?: string ): Promise> + /** + * Executes the requested Bucket AdLib/AdLib Action. This is a Bucket AdLib (Action) that has been previously inserted into a Bucket. + * It will automatically find the variation matching the showStyleBaseId and showStyleVariantId of the current Rundown. + * + * Throws if the target Playlist is not active. + * Throws if there is not an on-air part instance. + * @returns a `ClientResponseError` if a bucket or adlib for the provided ids cannot be found. + * @param connection Connection data including client and header details + * @param event User event string + * @param rundownPlaylistId Playlist to execute adLib in. + * @param bucketId Bucket to execute the adlib from + * @param externalId External Id of the Bucket AdLib to execute. + * @param triggerMode A string to specify a particular variation for the AdLibAction, valid actionType strings are to be read from the status API. + */ + executeBucketAdLib( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + bucketId: BucketId, + externalId: string, + triggerMode?: string + ): Promise> /** * Moves the next point by `delta` places. Negative values are allowed to move "backwards" in the script. * diff --git a/meteor/server/api/rest/v1/buckets.ts b/meteor/server/api/rest/v1/buckets.ts new file mode 100644 index 0000000000..36aaaa404d --- /dev/null +++ b/meteor/server/api/rest/v1/buckets.ts @@ -0,0 +1,281 @@ +import { Meteor } from 'meteor/meteor' +import { APIBucket, APIBucketComplete, APIImportAdlib, BucketsRestAPI } from '../../../../lib/api/rest/v1/buckets' +import { BucketAdLibActions, BucketAdLibs, Buckets } from '../../../collections' +import { APIBucketFrom } from './typeConversion' +import { ClientAPI } from '../../../../lib/api/client' +import { BucketId, ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { ServerClientAPI } from '../../client' +import { getCurrentTime, protectString } from '../../../../lib/lib' +import { check } from 'meteor/check' +import { StudioContentWriteAccess } from '../../../security/studio' +import { BucketsAPI } from '../../buckets' +import { BucketSecurity } from '../../../security/buckets' +import { APIFactory, APIRegisterHook, ServerAPIContext } from './types' +import { logger } from '../../../logging' +import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/error' +import { IngestAdlib } from '@sofie-automation/blueprints-integration' + +export class BucketsServerAPI implements BucketsRestAPI { + constructor(private context: ServerAPIContext) {} + + async getAllBuckets( + _connection: Meteor.Connection, + _event: string + ): Promise>> { + const buckets = await Buckets.findFetchAsync({}, { projection: { _id: 1, name: 1, studioId: 1 } }) + return ClientAPI.responseSuccess(buckets.map(APIBucketFrom)) + } + + async getBucket( + _connection: Meteor.Connection, + _event: string, + bucketId: BucketId + ): Promise> { + const bucket = await Buckets.findOneAsync(bucketId, { projection: { _id: 1, name: 1, studioId: 1 } }) + if (!bucket) { + return ClientAPI.responseError( + UserError.from(new Error(`Bucket ${bucketId} not found`), UserErrorMessage.BucketNotFound), + 404 + ) + } + return ClientAPI.responseSuccess(APIBucketFrom(bucket)) + } + + async addBucket( + connection: Meteor.Connection, + event: string, + bucket: APIBucket + ): Promise> { + const createdBucketResponse = await ServerClientAPI.runUserActionInLog( + this.context.getMethodContext(connection), + event, + getCurrentTime(), + 'bucketsCreateNewBucket', + [bucket], + async () => { + check(bucket.studioId, String) + check(bucket.name, String) + + const access = await StudioContentWriteAccess.bucket( + this.context.getCredentials(), + protectString(bucket.studioId) + ) + return BucketsAPI.createNewBucket(access, bucket.name) + } + ) + if (ClientAPI.isClientResponseSuccess(createdBucketResponse)) { + return ClientAPI.responseSuccess(createdBucketResponse.result._id) + } + return createdBucketResponse + } + + async deleteBucket( + connection: Meteor.Connection, + event: string, + bucketId: BucketId + ): Promise> { + return ServerClientAPI.runUserActionInLog( + this.context.getMethodContext(connection), + event, + getCurrentTime(), + 'bucketsRemoveBucket', + [bucketId], + async () => { + check(bucketId, String) + + const access = await BucketSecurity.allowWriteAccess(this.context.getCredentials(), bucketId) + return BucketsAPI.removeBucket(access) + } + ) + } + + async emptyBucket( + connection: Meteor.Connection, + event: string, + bucketId: BucketId + ): Promise> { + return ServerClientAPI.runUserActionInLog( + this.context.getMethodContext(connection), + event, + getCurrentTime(), + 'bucketsEmptyBucket', + [bucketId], + async () => { + check(bucketId, String) + + const access = await BucketSecurity.allowWriteAccess(this.context.getCredentials(), bucketId) + return BucketsAPI.emptyBucket(access) + } + ) + } + + async deleteBucketAdLib( + connection: Meteor.Connection, + event: string, + externalId: string + ): Promise> { + return ServerClientAPI.runUserActionInLog( + this.context.getMethodContext(connection), + event, + getCurrentTime(), + 'bucketsRemoveBucketAdLib', + [externalId], + async () => { + const bucketAdLibPiecePromise = BucketAdLibs.findOneAsync( + { externalId }, + { + projection: { _id: 1 }, + } + ) + const bucketAdLibActionPromise = BucketAdLibActions.findOneAsync( + { externalId }, + { + projection: { _id: 1 }, + } + ) + const [bucketAdLibPiece, bucketAdLibAction] = await Promise.all([ + bucketAdLibPiecePromise, + bucketAdLibActionPromise, + ]) + if (bucketAdLibPiece) { + const access = await BucketSecurity.allowWriteAccessPiece( + this.context.getCredentials(), + bucketAdLibPiece._id + ) + return BucketsAPI.removeBucketAdLib(access) + } else if (bucketAdLibAction) { + const access = await BucketSecurity.allowWriteAccessAction( + this.context.getCredentials(), + bucketAdLibAction._id + ) + return BucketsAPI.removeBucketAdLibAction(access) + } + } + ) + } + + async importAdLibToBucket( + connection: Meteor.Connection, + event: string, + bucketId: BucketId, + showStyleBaseId: ShowStyleBaseId, + ingestItem: IngestAdlib + ): Promise> { + return ServerClientAPI.runUserActionInLog( + this.context.getMethodContext(connection), + event, + getCurrentTime(), + 'bucketAdlibImport', + [bucketId, showStyleBaseId, ingestItem], + async () => { + check(bucketId, String) + check(showStyleBaseId, String) + check(ingestItem, Object) + + const access = await BucketSecurity.allowWriteAccess(this.context.getCredentials(), bucketId) + return BucketsAPI.importAdlibToBucket(access, showStyleBaseId, undefined, ingestItem) + } + ) + } +} + +class BucketsAPIFactory implements APIFactory { + createServerAPI(context: ServerAPIContext): BucketsRestAPI { + return new BucketsServerAPI(context) + } +} + +export function registerRoutes(registerRoute: APIRegisterHook): void { + const bucketsApiFactory = new BucketsAPIFactory() + + registerRoute>( + 'get', + '/buckets', + new Map(), + bucketsApiFactory, + async (serverAPI, connection, event, _params, _body) => { + logger.info(`API GET: Buckets`) + return await serverAPI.getAllBuckets(connection, event) + } + ) + + registerRoute<{ bucketId: string }, never, APIBucket>( + 'get', + '/buckets/:bucketId', + new Map(), + bucketsApiFactory, + async (serverAPI, connection, event, params, _body) => { + logger.info(`API GET: Bucket`) + const bucketId = protectString(params.bucketId) + check(bucketId, String) + return await serverAPI.getBucket(connection, event, bucketId) + } + ) + + registerRoute( + 'post', + '/buckets', + new Map([[404, [UserErrorMessage.StudioNotFound]]]), + bucketsApiFactory, + async (serverAPI, connection, event, _params, body) => { + logger.info(`API POST: Add Bucket`) + return await serverAPI.addBucket(connection, event, body) + } + ) + + registerRoute<{ bucketId: string }, never, void>( + 'delete', + '/buckets/:bucketId', + new Map([[404, [UserErrorMessage.BucketNotFound]]]), + bucketsApiFactory, + async (serverAPI, connection, event, params, _body) => { + logger.info(`API DELETE: Bucket`) + const bucketId = protectString(params.bucketId) + return await serverAPI.deleteBucket(connection, event, bucketId) + } + ) + + registerRoute<{ bucketId: string }, never, void>( + 'delete', + '/buckets/:bucketId/adlibs', + new Map([[404, [UserErrorMessage.BucketNotFound]]]), + bucketsApiFactory, + async (serverAPI, connection, event, params, _body) => { + logger.info(`API DELETE: Empty Bucket`) + const bucketId = protectString(params.bucketId) + check(bucketId, String) + return await serverAPI.emptyBucket(connection, event, bucketId) + } + ) + + registerRoute<{ externalId: string }, never, void>( + 'delete', + '/buckets/:bucketId/adlibs/:externalId', + new Map([[404, [UserErrorMessage.BucketNotFound]]]), + bucketsApiFactory, + async (serverAPI, connection, event, params, _body) => { + logger.info(`API DELETE: Remove Bucket AdLib`) + const adLibId = protectString(params.externalId) + check(adLibId, String) + return await serverAPI.deleteBucketAdLib(connection, event, adLibId) + } + ) + + registerRoute<{ bucketId: string }, APIImportAdlib, void>( + 'put', + '/buckets/:bucketId/adlibs', + new Map([[404, [UserErrorMessage.BucketNotFound]]]), + bucketsApiFactory, + async (serverAPI, connection, event, params, body) => { + logger.info(`API POST: Add AdLib to Bucket`) + const bucketId = protectString(params.bucketId) + check(bucketId, String) + check(body.externalId, String) + check(body.name, String) + check(body.payloadType, String) + check(body.showStyleBaseId, String) + const showStyleBaseId = protectString(body.showStyleBaseId) + return await serverAPI.importAdLibToBucket(connection, event, bucketId, showStyleBaseId, body) + } + ) +} diff --git a/meteor/server/api/rest/v1/index.ts b/meteor/server/api/rest/v1/index.ts index c1f34be5ca..594bee5667 100644 --- a/meteor/server/api/rest/v1/index.ts +++ b/meteor/server/api/rest/v1/index.ts @@ -17,6 +17,7 @@ import { registerRoutes as registerPlaylistsRoutes } from './playlists' import { registerRoutes as registerShowStylesRoutes } from './showstyles' import { registerRoutes as registerStudiosRoutes } from './studios' import { registerRoutes as registerSystemRoutes } from './system' +import { registerRoutes as registerBucketsRoutes } from './buckets' import { APIFactory, ServerAPIContext } from './types' function restAPIUserEvent( @@ -181,3 +182,4 @@ registerPlaylistsRoutes(sofieAPIRequest) registerShowStylesRoutes(sofieAPIRequest) registerStudiosRoutes(sofieAPIRequest) registerSystemRoutes(sofieAPIRequest) +registerBucketsRoutes(sofieAPIRequest) diff --git a/meteor/server/api/rest/v1/playlists.ts b/meteor/server/api/rest/v1/playlists.ts index 29dd6a6429..7fa1dcfb47 100644 --- a/meteor/server/api/rest/v1/playlists.ts +++ b/meteor/server/api/rest/v1/playlists.ts @@ -5,6 +5,7 @@ import { protectString, unprotectString } from '@sofie-automation/corelib/dist/p import { AdLibActionId, BucketAdLibId, + BucketId, PartId, PartInstanceId, PieceId, @@ -19,7 +20,9 @@ import { ClientAPI } from '../../../../lib/api/client' import { AdLibActions, AdLibPieces, + BucketAdLibActions, BucketAdLibs, + Buckets, RundownBaselineAdLibActions, RundownBaselineAdLibPieces, RundownPlaylists, @@ -200,7 +203,7 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { actionDocId: adLibActionDoc._id, actionId: adLibActionDoc.actionId, userData: adLibActionDoc.userData, - triggerMode: triggerMode ? triggerMode : undefined, + triggerMode: triggerMode ?? undefined, } ) } else { @@ -210,6 +213,62 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { ) } } + async executeBucketAdLib( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + bucketId: BucketId, + externalId: string, + triggerMode?: string | null + ): Promise> { + const bucketPromise = Buckets.findOneAsync(bucketId, { projection: { _id: 1 } }) + const bucketAdlibPromise = BucketAdLibs.findOneAsync({ bucketId, externalId }, { projection: { _id: 1 } }) + const bucketAdlibActionPromise = BucketAdLibActions.findOneAsync( + { bucketId, externalId }, + { + projection: { _id: 1 }, + } + ) + const [bucket, bucketAdlib, bucketAdlibAction] = await Promise.all([ + bucketPromise, + bucketAdlibPromise, + bucketAdlibActionPromise, + ]) + if (!bucket) { + return ClientAPI.responseError( + UserError.from(new Error(`Bucket ${bucketId} not found`), UserErrorMessage.BucketNotFound), + 412 + ) + } + if (!bucketAdlib && !bucketAdlibAction) { + return ClientAPI.responseError( + UserError.from( + new Error(`No adLib with Id ${externalId}, in bucket ${bucketId}`), + UserErrorMessage.AdlibNotFound + ), + 412 + ) + } + + return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( + this.context.getMethodContext(connection), + event, + getCurrentTime(), + rundownPlaylistId, + () => { + check(rundownPlaylistId, String) + check(bucketId, String) + check(externalId, String) + }, + StudioJobs.ExecuteBucketAdLibOrAction, + { + playlistId: rundownPlaylistId, + bucketId, + externalId, + triggerMode: triggerMode ?? undefined, + } + ) + } async moveNextPart( connection: Meteor.Connection, event: string, @@ -541,6 +600,47 @@ export function registerRoutes(registerRoute: APIRegisterHook) } ) + registerRoute<{ playlistId: string }, { bucketId: string; externalId: string; actionType?: string }, object>( + 'post', + '/playlists/:playlistId/execute-bucket-adlib', + new Map([ + [404, [UserErrorMessage.RundownPlaylistNotFound]], + [ + 412, + [ + UserErrorMessage.InactiveRundown, + UserErrorMessage.NoCurrentPart, + UserErrorMessage.AdlibNotFound, + UserErrorMessage.BucketNotFound, + ], + ], + ]), + playlistsAPIFactory, + async (serverAPI, connection, event, params, body) => { + const rundownPlaylistId = protectString(params.playlistId) + const bucketId = protectString(body.bucketId) + const adLibExternalId = body.externalId + const actionTypeObj = body + const triggerMode = actionTypeObj ? (actionTypeObj as { actionType: string }).actionType : undefined + logger.info( + `API POST: execute-bucket-adlib ${rundownPlaylistId} ${bucketId} ${adLibExternalId} - triggerMode: ${triggerMode}` + ) + + check(rundownPlaylistId, String) + check(bucketId, String) + check(adLibExternalId, String) + + return await serverAPI.executeBucketAdLib( + connection, + event, + rundownPlaylistId, + bucketId, + adLibExternalId, + triggerMode + ) + } + ) + registerRoute<{ playlistId: string }, { delta: number }, PartId | null>( 'post', '/playlists/:playlistId/move-next-part', diff --git a/meteor/server/api/rest/v1/typeConversion.ts b/meteor/server/api/rest/v1/typeConversion.ts index e18434a0a4..36ffef9150 100644 --- a/meteor/server/api/rest/v1/typeConversion.ts +++ b/meteor/server/api/rest/v1/typeConversion.ts @@ -8,7 +8,7 @@ import { } from '@sofie-automation/blueprints-integration' import { PeripheralDevice, PeripheralDeviceType } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' -import { ShowStyleBaseId, ShowStyleVariantId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { BucketId, ShowStyleBaseId, ShowStyleVariantId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBStudio, IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' import { assertNever, getRandomId, literal } from '@sofie-automation/corelib/dist/lib' import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' @@ -20,6 +20,8 @@ import { } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { APIBlueprint, + APIBucket, + APIBucketComplete, APIOutputLayer, APIPeripheralDevice, APIShowStyleBase, @@ -32,6 +34,7 @@ import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowSt import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' import { Blueprints, ShowStyleBases, Studios } from '../../../collections' import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants' +import { Bucket } from '../../../../lib/collections/Buckets' /* This file contains functions that convert between the internal Sofie-Core types and types exposed to the external API. @@ -410,3 +413,23 @@ export function APIOutputLayerFrom(outputLayer: IOutputLayer): APIOutputLayer { isPgm: outputLayer.isPGM, } } + +export function bucketFrom(apiBucket: APIBucket, existingId?: BucketId): Bucket { + return { + _id: existingId ?? getRandomId(), + studioId: protectString(apiBucket.studioId), + name: apiBucket.name, + _rank: 0, + width: undefined, + buttonWidthScale: 1, + buttonHeightScale: 1, + } +} + +export function APIBucketFrom(bucket: Bucket): APIBucketComplete { + return { + id: unprotectString(bucket._id), + name: bucket.name, + studioId: unprotectString(bucket.studioId), + } +} diff --git a/packages/corelib/src/error.ts b/packages/corelib/src/error.ts index 17cd62b4de..2b176752db 100644 --- a/packages/corelib/src/error.ts +++ b/packages/corelib/src/error.ts @@ -56,6 +56,7 @@ export enum UserErrorMessage { ValidationFailed = 41, ScratchpadNotAllowed = 42, ScratchpadAlreadyActive = 43, + BucketNotFound = 44, } const UserErrorMessagesTranslations: { [key in UserErrorMessage]: string } = { @@ -111,6 +112,7 @@ const UserErrorMessagesTranslations: { [key in UserErrorMessage]: string } = { [UserErrorMessage.ValidationFailed]: t('Validation failed!'), [UserErrorMessage.ScratchpadNotAllowed]: t(`Scratchpad mode is not allowed`), [UserErrorMessage.ScratchpadAlreadyActive]: t(`Scratchpad mode is already active`), + [UserErrorMessage.BucketNotFound]: t(`Bucket not found!`), } export class UserError { diff --git a/packages/corelib/src/worker/studio.ts b/packages/corelib/src/worker/studio.ts index 4d298aa7f6..b0be32fe3e 100644 --- a/packages/corelib/src/worker/studio.ts +++ b/packages/corelib/src/worker/studio.ts @@ -1,6 +1,7 @@ import { PlayoutChangedResults } from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI' import { AdLibActionId, + BucketId, PartId, PartInstanceId, PieceId, @@ -90,6 +91,10 @@ export enum StudioJobs { * Execute an AdLib Action */ ExecuteAction = 'executeAction', + /** + * Execute a Bucket AdLib (Action) + */ + ExecuteBucketAdLibOrAction = 'executeBucketAdLibOrAction', /** * Take the currently Next:ed Part (start playing it) */ @@ -237,6 +242,11 @@ export interface ExecuteActionProps extends RundownPlayoutPropsBase { userData: any triggerMode?: string } +export interface ExecuteBucketAdLibOrActionProps extends RundownPlayoutPropsBase { + bucketId: BucketId + externalId: string + triggerMode?: string +} export interface ExecuteActionResult { queuedPartInstanceId?: PartInstanceId taken?: boolean @@ -333,6 +343,7 @@ export type StudioJobFunc = { [StudioJobs.SetNextSegment]: (data: SetNextSegmentProps) => PartId [StudioJobs.QueueNextSegment]: (data: QueueNextSegmentProps) => QueueNextSegmentResult [StudioJobs.ExecuteAction]: (data: ExecuteActionProps) => ExecuteActionResult + [StudioJobs.ExecuteBucketAdLibOrAction]: (data: ExecuteBucketAdLibOrActionProps) => ExecuteActionResult [StudioJobs.TakeNextPart]: (data: TakeNextPartProps) => void [StudioJobs.DisableNextPiece]: (data: DisableNextPieceProps) => void [StudioJobs.RemovePlaylist]: (data: RemovePlaylistProps) => void diff --git a/packages/job-worker/src/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index 8b191411f0..fb5396a14c 100644 --- a/packages/job-worker/src/playout/adlibAction.ts +++ b/packages/job-worker/src/playout/adlibAction.ts @@ -10,7 +10,7 @@ import { WatchedPackagesHelper } from '../blueprints/context/watchedPackages' import { JobContext, ProcessedShowStyleCompound } from '../jobs' import { getCurrentTime } from '../lib' import { ReadonlyDeep } from 'type-fest' -import { PlayoutModel } from './model/PlayoutModel' +import { PlayoutModel, PlayoutModelPreInit } from './model/PlayoutModel' import { syncPlayheadInfinitesForNextPartInstance } from './infinites' import { runJobWithPlaylistLock } from './lock' import { updateTimeline } from './timeline/generate' @@ -38,81 +38,93 @@ export async function handleExecuteAdlibAction( const initPlayoutModel = await loadPlayoutModelPreInit(context, lock, playlist, false) - const rundown = initPlayoutModel.getRundown(playlist.currentPartInfo.rundownId) - if (!rundown) throw new Error(`Current Rundown "${playlist.currentPartInfo.rundownId}" could not be found`) + return executeAdlibActionAndSaveModel(context, playlist, initPlayoutModel, data) + }) +} - const showStyle = await context.getShowStyleCompound(rundown.showStyleVariantId, rundown.showStyleBaseId) +export async function executeAdlibActionAndSaveModel( + context: JobContext, + playlist: DBRundownPlaylist, + initPlayoutModel: PlayoutModelPreInit, + data: ExecuteActionProps +): Promise { + if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown, undefined, 412) + if (!playlist.currentPartInfo) throw UserError.create(UserErrorMessage.NoCurrentPart, undefined, 412) - const blueprint = await context.getShowStyleBlueprint(showStyle._id) + const rundown = initPlayoutModel.getRundown(playlist.currentPartInfo.rundownId) + if (!rundown) throw new Error(`Current Rundown "${playlist.currentPartInfo.rundownId}" could not be found`) - if (!blueprint.blueprint.executeAction && !blueprint.blueprint.executeDataStoreAction) { - throw UserError.create(UserErrorMessage.ActionsNotSupported) - } + const showStyle = await context.getShowStyleCompound(rundown.showStyleVariantId, rundown.showStyleBaseId) - const watchedPackages = await WatchedPackagesHelper.create(context, context.studio._id, { - pieceId: data.actionDocId, - fromPieceType: { - $in: [ - ExpectedPackageDBType.ADLIB_ACTION, - ExpectedPackageDBType.BASELINE_ADLIB_ACTION, - ExpectedPackageDBType.BUCKET_ADLIB_ACTION, - ], - }, - }) + const blueprint = await context.getShowStyleBlueprint(showStyle._id) - const actionParameters: ExecuteActionParameters = { - actionId: data.actionId, - userData: data.userData, - triggerMode: data.triggerMode, - } + if (!blueprint.blueprint.executeAction && !blueprint.blueprint.executeDataStoreAction) { + throw UserError.create(UserErrorMessage.ActionsNotSupported) + } + + const watchedPackages = await WatchedPackagesHelper.create(context, context.studio._id, { + pieceId: data.actionDocId, + fromPieceType: { + $in: [ + ExpectedPackageDBType.ADLIB_ACTION, + ExpectedPackageDBType.BASELINE_ADLIB_ACTION, + ExpectedPackageDBType.BUCKET_ADLIB_ACTION, + ], + }, + }) + + const actionParameters: ExecuteActionParameters = { + actionId: data.actionId, + userData: data.userData, + triggerMode: data.triggerMode, + } + + try { + await executeDataStoreAction( + context, + playlist, + rundown, + showStyle, + blueprint, + watchedPackages, + actionParameters + ) + } catch (err) { + logger.error(`Error in showStyleBlueprint.executeDatastoreAction: ${stringifyError(err)}`) + } + + if (blueprint.blueprint.executeAction) { + // load a full model for the regular actions & executet the handler + const playoutModel = await createPlayoutModelfromInitModel(context, initPlayoutModel) + + const fullRundown = playoutModel.getRundown(rundown._id) + if (!fullRundown) throw new Error(`Rundown "${rundown._id}" missing between models`) try { - await executeDataStoreAction( + const res: ExecuteActionResult = await executeActionInner( context, - playlist, - rundown, + playoutModel, + fullRundown, showStyle, blueprint, watchedPackages, actionParameters ) - } catch (err) { - logger.error(`Error in showStyleBlueprint.executeDatastoreAction: ${stringifyError(err)}`) - } - if (blueprint.blueprint.executeAction) { - // load a full model for the regular actions & execute the handler - const playoutModel = await createPlayoutModelfromInitModel(context, initPlayoutModel) - - const fullRundown = playoutModel.getRundown(rundown._id) - if (!fullRundown) throw new Error(`Rundown "${rundown._id}" missing between models`) - - try { - const res: ExecuteActionResult = await executeActionInner( - context, - playoutModel, - fullRundown, - showStyle, - blueprint, - watchedPackages, - actionParameters - ) - - await playoutModel.saveAllToDatabase() - - return res - } catch (err) { - playoutModel.dispose() - throw err - } - } + await playoutModel.saveAllToDatabase() - // if we haven't returned yet, these defaults should be correct - return { - queuedPartInstanceId: undefined, - taken: false, + return res + } catch (err) { + playoutModel.dispose() + throw err } - }) + } + + // if we haven't returned yet, these defaults should be correct + return { + queuedPartInstanceId: undefined, + taken: false, + } } export interface ExecuteActionParameters { diff --git a/packages/job-worker/src/playout/bucketAdlibJobs.ts b/packages/job-worker/src/playout/bucketAdlibJobs.ts new file mode 100644 index 0000000000..a6cbff8248 --- /dev/null +++ b/packages/job-worker/src/playout/bucketAdlibJobs.ts @@ -0,0 +1,95 @@ +import { ExecuteActionResult, ExecuteBucketAdLibOrActionProps } from '@sofie-automation/corelib/dist/worker/studio' +import { JobContext } from '../jobs' +import { runJobWithPlaylistLock } from './lock' +import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/error' +import { BucketId, ShowStyleBaseId, ShowStyleVariantId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { innerStartOrQueueAdLibPiece } from './adlibUtils' +import { executeAdlibActionAndSaveModel } from './adlibAction' +import { RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { createPlayoutModelfromInitModel, loadPlayoutModelPreInit } from './model/implementation/LoadPlayoutModel' + +/** + * Execute an AdLib Action + */ +export async function handleExecuteBucketAdLibOrAction( + context: JobContext, + data: ExecuteBucketAdLibOrActionProps +): Promise { + return runJobWithPlaylistLock(context, data, async (playlist, lock) => { + // First load the minimum amount of data required to run the executeDataStoreAction handler + if (!playlist) throw new Error(`Job playlist "${data.playlistId}" not found `) + + if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown, undefined, 412) + if (!playlist.currentPartInfo) throw UserError.create(UserErrorMessage.NoCurrentPart, undefined, 412) + + const initCache = await loadPlayoutModelPreInit(context, lock, playlist, false) + + const rundown = initCache.getRundown(playlist.currentPartInfo.rundownId) + if (!rundown) throw new Error(`Current Rundown "${playlist.currentPartInfo.rundownId}" could not be found`) + + const { bucketAdLib, bucketAdLibAction } = await findBucketAdLibOrAction( + context, + data.bucketId, + data.externalId, + rundown.showStyleBaseId, + rundown.showStyleVariantId + ) + + if (bucketAdLib) { + if (playlist.holdState === RundownHoldState.ACTIVE || playlist.holdState === RundownHoldState.PENDING) { + throw UserError.create(UserErrorMessage.DuringHold) + } + + const playoutModel = await createPlayoutModelfromInitModel(context, initCache) + + const fullRundown = playoutModel.getRundown(rundown._id) + if (!fullRundown) throw new Error(`Rundown "${rundown._id}" missing between caches`) + + const partInstance = playoutModel.currentPartInstance + if (!partInstance) throw new Error(`PartInstance "${playlist.currentPartInfo.partInstanceId}" not found!`) + await innerStartOrQueueAdLibPiece( + context, + playoutModel, + fullRundown, + !!bucketAdLib.toBeQueued, + partInstance, + bucketAdLib + ) + await playoutModel.saveAllToDatabase() + return {} + } else if (bucketAdLibAction) { + return await executeAdlibActionAndSaveModel(context, playlist, initCache, { + actionDocId: bucketAdLibAction._id, + actionId: bucketAdLibAction.actionId, + playlistId: playlist._id, + userData: bucketAdLibAction.userData, + triggerMode: data.triggerMode, + }) + } + throw UserError.create(UserErrorMessage.AdlibNotFound) + }) +} + +async function findBucketAdLibOrAction( + context: JobContext, + bucketId: BucketId, + externalId: string, + showStyleBaseId: ShowStyleBaseId, + showStyleVariantId: ShowStyleVariantId +) { + const [bucketAdLib, bucketAdLibAction] = await Promise.all([ + context.directCollections.BucketAdLibPieces.findOne({ + bucketId, + externalId, + showStyleBaseId, + $or: [{ showStyleVariantId }, { showStyleVariantId: null }], + }), + context.directCollections.BucketAdLibActions.findOne({ + bucketId, + externalId, + showStyleBaseId, + $or: [{ showStyleVariantId }, { showStyleVariantId: null }], + }), + ]) + return { bucketAdLib, bucketAdLibAction } +} diff --git a/packages/job-worker/src/workers/studio/jobs.ts b/packages/job-worker/src/workers/studio/jobs.ts index f97b9a9dea..e99b971d21 100644 --- a/packages/job-worker/src/workers/studio/jobs.ts +++ b/packages/job-worker/src/workers/studio/jobs.ts @@ -45,6 +45,7 @@ import { handleTimelineTriggerTime, handleOnPlayoutPlaybackChanged } from '../.. import { handleExecuteAdlibAction } from '../../playout/adlibAction' import { handleTakeNextPart } from '../../playout/take' import { handleActivateScratchpad } from '../../playout/scratchpad' +import { handleExecuteBucketAdLibOrAction } from '../../playout/bucketAdlibJobs' type ExecutableFunction = ( context: JobContext, @@ -74,6 +75,7 @@ export const studioJobHandlers: StudioJobHandlers = { [StudioJobs.SetNextSegment]: handleSetNextSegment, [StudioJobs.QueueNextSegment]: handleQueueNextSegment, [StudioJobs.ExecuteAction]: handleExecuteAdlibAction, + [StudioJobs.ExecuteBucketAdLibOrAction]: handleExecuteBucketAdLibOrAction, [StudioJobs.TakeNextPart]: handleTakeNextPart, [StudioJobs.DisableNextPiece]: handleDisableNextPiece, [StudioJobs.RemovePlaylist]: handleRemoveRundownPlaylist, diff --git a/packages/openapi/api/actions.yaml b/packages/openapi/api/actions.yaml index a3a814fee1..7d39c4b22d 100644 --- a/packages/openapi/api/actions.yaml +++ b/packages/openapi/api/actions.yaml @@ -40,6 +40,8 @@ paths: $ref: 'definitions/playlists.yaml#/resources/deactivate' /playlists/{playlistId}/execute-adlib: $ref: 'definitions/playlists.yaml#/resources/executeAdLib' + /playlists/{playlistId}/execute-bucket-adlib: + $ref: 'definitions/playlists.yaml#/resources/executeBucketAdLib' /playlists/{playlistId}/move-next-part: $ref: 'definitions/playlists.yaml#/resources/moveNextPart' /playlists/{playlistId}/move-next-segment: @@ -84,3 +86,12 @@ paths: $ref: 'definitions/showstyles.yaml#/resources/showStyleVariant' /showstyles/{showStyleBaseId}/action: $ref: 'definitions/showstyles.yaml#/resources/showStyleBaseAction' + # bucket operations + /buckets: + $ref: 'definitions/buckets.yaml#/resources/buckets' + /buckets/{bucketId}: + $ref: 'definitions/buckets.yaml#/resources/bucket' + /buckets/{bucketId}/adlibs: + $ref: 'definitions/buckets.yaml#/resources/bucketAdlibs' + /buckets/{bucketId}/adlibs/{externalId}: + $ref: 'definitions/buckets.yaml#/resources/bucketAdlib' diff --git a/packages/openapi/api/definitions/blueprints.yaml b/packages/openapi/api/definitions/blueprints.yaml index 99237c3c9b..799d455065 100644 --- a/packages/openapi/api/definitions/blueprints.yaml +++ b/packages/openapi/api/definitions/blueprints.yaml @@ -1,5 +1,5 @@ -title: studios -description: Definitions for studios API +title: blueprints +description: Definitions for blueprints API resources: blueprints: get: diff --git a/packages/openapi/api/definitions/buckets.yaml b/packages/openapi/api/definitions/buckets.yaml new file mode 100644 index 0000000000..67fa9c2016 --- /dev/null +++ b/packages/openapi/api/definitions/buckets.yaml @@ -0,0 +1,291 @@ +title: buckets +description: Definitions for buckets API +resources: + buckets: + get: + operationId: buckets + tags: + - buckets + summary: Returns all buckets available in Sofie. + responses: + 200: + description: Command successfully handled - returns an array of buckets. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: array + items: + $ref: '#/components/schemas/bucket' + 500: + $ref: '#/components/responses/internalServerError' + post: + operationId: addBucket + tags: + - buckets + summary: Adds a Bucket. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/bucketBase' + responses: + 200: + description: Command successfully handled - returns a bucket id. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: string + description: Bucket Id. + 500: + $ref: '#/components/responses/internalServerError' + bucket: + get: + operationId: bucket + tags: + - buckets + summary: Returns some information about the specified bucket + parameters: + - name: bucketId + in: path + description: Requested bucket. + required: true + schema: + type: string + responses: + 200: + description: Command successfully handled - returns a bucket. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + $ref: '#/components/schemas/bucket' + 404: + $ref: '#/components/responses/bucketNotFound' + 500: + $ref: '#/components/responses/internalServerError' + delete: + operationId: deleteBucket + tags: + - buckets + summary: Deletes a bucket + parameters: + - name: bucketId + in: path + description: Bucket to remove. + required: true + schema: + type: string + responses: + 200: + description: Bucket successfuly removed. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + 404: + $ref: '#/components/responses/bucketNotFound' + 500: + $ref: '#/components/responses/internalServerError' + bucketAdlibs: + put: + operationId: importBucketAdlib + tags: + - buckets + summary: Imports a Bucket Adlib. + parameters: + - name: bucketId + in: path + description: Bucket to import the adlib to. + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/importAdlib' + responses: + 200: + description: Bucket adlib successfully imported. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + 500: + $ref: '#/components/responses/internalServerError' + delete: + operationId: deleteBucketAdlibs + tags: + - buckets + summary: Deletes all adlibs in a bucket + parameters: + - name: bucketId + in: path + description: Bucket to remove adlibs from. + required: true + schema: + type: string + responses: + 200: + description: Bucket Adlibs successfuly removed. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + 404: + $ref: '#/components/responses/bucketNotFound' + 500: + $ref: '#/components/responses/internalServerError' + bucketAdlib: + delete: + operationId: deleteBucketAdlib + tags: + - buckets + summary: Deletes a bucket adlib + parameters: + - name: bucketId + in: path + description: Bucket to remove the adlib from. + required: true + schema: + type: string + - name: externalId + in: path + description: External id of the bucket adlib to remove. + required: true + schema: + type: string + responses: + 200: + description: Bucket Adlib successfuly removed. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + 404: + $ref: '#/components/responses/bucketNotFound' + 500: + $ref: '#/components/responses/internalServerError' +components: + schemas: + bucketBase: + type: object + properties: + name: + type: string + description: Bucket Name. + studioId: + type: string + description: Id of the studio this bucket belongs to. + additionalProperties: false + example: + name: My Bucket + studioId: studio0 + bucket: + type: object + properties: + id: + type: string + description: Bucket Id. + name: + type: string + description: Bucket Name. + studioId: + type: string + description: Id of the studio this bucket belongs to. + additionalProperties: false + example: + id: 6jZ6NvpoikxuXqcm4 + name: My Bucket + studioId: studio0 + importAdlib: + type: object + properties: + externalId: + type: string + description: Id of the adlib recognizable by the external source. Unique within a bucket. If an adlib with the same `externalId` already exists in the bucket, it will be replaced. + showStyleBaseId: + type: string + description: Id of the ShowStyle to use when importing the adlib. + name: + type: string + description: Adlib Name. + payloadType: + type: string + description: Hint for the blueprints on how to process the payload. + payload: + description: Data that the blueprints can use to create the Adlib. + required: + - externalId + - showStyleBaseId + - name + - payloadType + additionalProperties: false + example: + externalId: my_lower_third + showStyleBaseId: showstyle0 + name: My Lower Third + payloadType: JSON + payload: + name: Joe + occupation: developer + responses: + bucketNotFound: + description: The specified Bucket does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + internalServerError: + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string diff --git a/packages/openapi/api/definitions/playlists.yaml b/packages/openapi/api/definitions/playlists.yaml index 9983a1a62f..bb459f6d52 100644 --- a/packages/openapi/api/definitions/playlists.yaml +++ b/packages/openapi/api/definitions/playlists.yaml @@ -139,6 +139,62 @@ resources: example: AdLib could not be found! 500: $ref: '#/components/responses/internalServerError' + executeBucketAdLib: + post: + operationId: executeBucketAdLib + tags: + - playlists + summary: Executes the requested Bucket AdLib/AdLib Action. This is a Bucket AdLib (Action) that has been previously inserted into a Bucket. + parameters: + - name: playlistId + in: path + description: Playlist to execute the Bucket AdLib in. + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + actionType: + type: string + description: An actionType string to specify a particular variation for the AdLibAction, valid strings are to be read from the status API + bucketId: + type: string + description: Bucket to execute the adlib from + externalId: + type: string + description: External Id of the Bucket AdLib to execute + required: + - bucketId + - externalId + example: + bucketId: 6jZ6NvpoikxuXqcm4 + externalId: my_lower_third + actionType: pvw + responses: + 200: + $ref: '#/components/responses/executeActionSuccess' + 404: + $ref: '#/components/responses/playlistNotFound' + 412: + description: Specified Playlist is not active, there is not an on-air Part instance or an adLib for the provided `externalId` cannot be found. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + example: AdLib could not be found! + 500: + $ref: '#/components/responses/internalServerError' moveNextPart: post: operationId: moveNextPart diff --git a/packages/openapi/src/__tests__/buckets.spec.ts b/packages/openapi/src/__tests__/buckets.spec.ts new file mode 100644 index 0000000000..e87fc9b0b0 --- /dev/null +++ b/packages/openapi/src/__tests__/buckets.spec.ts @@ -0,0 +1,101 @@ +// eslint-disable-next-line node/no-missing-import +import { Configuration, BucketsApi } from '../../client/ts' +import { checkServer } from '../checkServer' +import Logging from '../httpLogging' + +const httpLogging = false +let testServer = false +if (process.env.SERVER_TYPE === 'TEST') { + testServer = true +} + +describe('Network client', () => { + const config = new Configuration({ + basePath: process.env.SERVER_URL, + middleware: [new Logging(httpLogging)], + }) + + beforeAll(async () => await checkServer(config)) + + const bucketsApi = new BucketsApi(config) + test('can request all buckets available in Sofie', async () => { + const buckets = await bucketsApi.buckets() + expect(buckets.status).toBe(200) + expect(buckets).toHaveProperty('result') + expect(buckets.result.length).toBeGreaterThanOrEqual(1) + buckets.result.forEach((bucket) => { + expect(typeof bucket).toBe('object') + expect(typeof bucket.id).toBe('string') + expect(typeof bucket.name).toBe('string') + expect(typeof bucket.studioId).toBe('string') + }) + }) + + let bucketId = '' + test('can create a bucket', async () => { + const bucket = await bucketsApi.addBucket({ + bucketBase: { + name: 'My Bucket', + studioId: 'studio0', + }, + }) + expect(bucket.status).toBe(200) + expect(typeof bucket.result).toBe('string') + bucketId = bucket.result + }) + + test('can get a bucket', async () => { + const bucket = await bucketsApi.bucket({ + bucketId, + }) + expect(bucket.status).toBe(200) + expect(typeof bucket.result).toBe('object') + expect(typeof bucket.result.id).toBe('string') + expect(typeof bucket.result.name).toBe('string') + expect(typeof bucket.result.studioId).toBe('string') + }) + + if (testServer) { + test('can import bucket adLib', async () => { + const execute = await bucketsApi.importBucketAdlib({ + bucketId, + importAdlib: { + externalId: 'my_adlib', + name: 'My AdLib', + payloadType: '', + showStyleBaseId: '', + payload: {}, + }, + }) + expect(execute.status).toBe(200) + }) + } else { + test.todo('import a bucket adLib - need blueprint to support it') + } + + if (testServer) { + test('can delete a bucket adlib', async () => { + const deleted = await bucketsApi.deleteBucketAdlib({ + bucketId, + externalId: 'my_adlib', + }) + expect(deleted.status).toBe(200) + }) + } else { + test.todo('delete a bucket adLib - need to be able to create it') + } + + test('can empty a bucket', async () => { + const deleted = await bucketsApi.deleteBucketAdlibs({ + bucketId, + }) + expect(deleted.status).toBe(200) + }) + + test('can delete a bucket', async () => { + const deleted = await bucketsApi.deleteBucket({ + bucketId, + }) + expect(deleted.status).toBe(200) + }) +}) diff --git a/packages/openapi/src/__tests__/playlists.spec.ts b/packages/openapi/src/__tests__/playlists.spec.ts index 3b4c6be1ff..7b63d3194c 100644 --- a/packages/openapi/src/__tests__/playlists.spec.ts +++ b/packages/openapi/src/__tests__/playlists.spec.ts @@ -127,6 +127,18 @@ describe('Network client', () => { test.todo('execute adlib - need to have an adlib to run') } + if (testServer) { + test('can execute a bucket adLib', async () => { + const execute = await playlistsApi.executeBucketAdLib({ + playlistId: playlistIds[0], + executeBucketAdLibRequest: { bucketId: 'cIt0kEWuHOvQVMD', externalId: 'MDEKzCrBpgGWSs_' }, + }) + expect(execute.status).toBe(200) + }) + } else { + test.todo('execute a bucket adlib - need to have an adlib to run') + } + test('can deactivate a playlist', async () => { const deactive = await playlistsApi.deactivate({ playlistId: playlistIds[0] }) expect(deactive.status).toBe(200) From 677bf7b962b12985f5659c45b1beeaea986c151d Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Thu, 21 Dec 2023 15:23:16 +0000 Subject: [PATCH 201/479] chore: missed merge --- DEVELOPER.md | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/DEVELOPER.md b/DEVELOPER.md index e61322c832..dee605b3bb 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -106,8 +106,7 @@ The "Attach" configuration in `launch.json` supports debugging blueprints. Local blueprints repo needs to be added to the Visual Studio Code workspace under the name "Blueprints". -It is required to set `devtool` to `'inline-source-map'` and `output.devtoolModuleFilenameTemplate` to `'blueprint:///[resource-path]'` in webpack config of the blueprints. - +It is required to set `devtool` to `'inline-source-map'` and `output.devtoolModuleFilenameTemplate` to `'blueprint:///[resource-path]'` in webpack config of the blueprints. ## Translating Sofie @@ -130,6 +129,30 @@ The resulting JSON file will be placed in `meteor/public/locales/xx`, where it w Then submit this as a PR. +## Deprecations + +### ConfigManifests + +The ConfigManifests for Blueprints and Gateways was replaced with JSONSchema in R50. +However, one usage by AdlibActions for their userDataManifest remains as this is not something we are actively using. + +## Blueprint Migrations + +In R49, a replacement flow was added consisting of `validateConfig` and `applyConfig`. +It is no longer recommended to use the old migrations flow for showstyle and studio blueprints. + +### ExpectedMediaItems + +These are used for Media-manager which is no longer being developed. + +### Blueprints: getPieceABSessionId & getTimelineObjectAbSessionId + +With AB being a native concept supported by Sofie since R50, these are likely no longer useful to Blueprints. + +### MongoQuery `fields` specifier + +It is recommended to use `projection` instead, as it is functionally identical but follows recommended naming from mongodb. + ## Other info ### Version-Numbering Scheme @@ -144,7 +167,7 @@ The api of `blueprints-integration` is rather volatile, and often has breaking c ### Glossary -*Note: this list is not very complete but will be supplemented over time.* +_Note: this list is not very complete but will be supplemented over time._ @@ -175,4 +198,4 @@ The api of `blueprints-integration` is rather volatile, and often has breaking c ### Additional license information -Background image used for previewing graphical elements is based on "Sunset over dark forest" by Aliis Sinisalu: https://unsplash.com/photos/8NiAH5YRZPs used under the [Unsplash License](https://unsplash.com/license). \ No newline at end of file +Background image used for previewing graphical elements is based on "Sunset over dark forest" by Aliis Sinisalu: https://unsplash.com/photos/8NiAH5YRZPs used under the [Unsplash License](https://unsplash.com/license). From 82a5a72c5587a6a5e6c3683e50281aa6f264fbb0 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 4 Jan 2024 11:00:15 +0000 Subject: [PATCH 202/479] chore: fix lint --- meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx index 5805fb4f20..5ec5afe8e2 100644 --- a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx +++ b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx @@ -324,7 +324,7 @@ export class SegmentTimelineClass extends React.Component { - this.timeline = el ?? undefined + this.timeline = el } private convertTimeToPixels = (time: number) => { From 1b79d8211bf176946eb27b58db2fb8ffe29d8a67 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Mon, 8 Jan 2024 14:35:01 +0100 Subject: [PATCH 203/479] fix(RundownView): preserve scroll position is not preserved when Segments are moved around before the live Segment --- meteor/client/ui/RundownView.tsx | 232 ++++++++++++++++++++----------- 1 file changed, 148 insertions(+), 84 deletions(-) diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx index 9d64d91117..82098fe40b 100644 --- a/meteor/client/ui/RundownView.tsx +++ b/meteor/client/ui/RundownView.tsx @@ -1342,6 +1342,11 @@ interface IPropsWithReady extends IProps { subsReady: boolean } +interface IRundownViewContentSnapshot { + elementId: string + top: number +} + const RundownViewContent = translateWithTracker((props: Translated) => { const playlistId = props.playlistId @@ -1739,91 +1744,14 @@ const RundownViewContent = translateWithTracker { - if (!error.toString().match(/another scroll/)) console.warn(error) - }) - } - } else if ( - this.props.playlist && - prevProps.playlist && - prevProps.playlist.activationId && - !this.props.playlist.activationId - ) { - // reset followLiveSegments after deactivating a rundown - this.setState({ - followLiveSegments: true, - }) - } else if ( - this.props.playlist && - prevProps.playlist && - !prevProps.playlist.activationId && - this.props.playlist.activationId && - this.props.playlist.nextPartInfo - ) { - // scroll to next after activation - scrollToPartInstance(this.props.playlist.nextPartInfo.partInstanceId).catch((error) => { - if (!error.toString().match(/another scroll/)) console.warn(error) - }) - } else if ( - // after take - this.props.playlist && - prevProps.playlist && - this.props.playlist.currentPartInfo?.partInstanceId !== prevProps.playlist.currentPartInfo?.partInstanceId && - this.props.playlist.currentPartInfo && - this.state.followLiveSegments - ) { - scrollToPartInstance(this.props.playlist.currentPartInfo.partInstanceId, true).catch((error) => { - if (!error.toString().match(/another scroll/)) console.warn(error) - }) - } else if ( - this.props.playlist && - prevProps.playlist && - this.props.playlist.nextPartInfo?.partInstanceId !== prevProps.playlist.nextPartInfo?.partInstanceId && - this.props.playlist.currentPartInfo?.partInstanceId === prevProps.playlist.currentPartInfo?.partInstanceId && - this.props.playlist.nextPartInfo && - this.props.playlist.nextPartInfo.manuallySelected - ) { - scrollToPartInstance(this.props.playlist.nextPartInfo.partInstanceId, false).catch((error) => { - if (!error.toString().match(/another scroll/)) console.warn(error) - }) - } else if ( - // initial Rundown open - this.props.playlist && - this.props.playlist.currentPartInfo && - this.props.subsReady && - !prevProps.subsReady - ) { - // allow for some time for the Rundown to render - maintainFocusOnPartInstance(this.props.playlist.currentPartInfo.partInstanceId, 7000, true, true) - } - } + componentDidUpdate( + prevProps: IPropsWithReady & ITrackedProps, + prevState: IState, + snapshot: IRundownViewContentSnapshot | null + ) { + this.handleFollowLiveSegment(prevProps, snapshot) - if ( - typeof this.props.playlist !== typeof prevProps.playlist || - this.props.playlist?._id !== prevProps.playlist?._id || - !!this.props.playlist?.activationId !== !!prevProps.playlist?.activationId || - this.state.studioMode !== prevState.studioMode - ) { - if (this.props.playlist && this.props.playlist.activationId && this.state.studioMode && !getAllowDeveloper()) { - window.addEventListener('beforeunload', this.onBeforeUnload) - } else { - window.removeEventListener('beforeunload', this.onBeforeUnload) - } - } + this.handleBeforeUnloadEventAttach(prevProps, prevState) if ( this.props.playlist && @@ -1858,6 +1786,142 @@ const RundownViewContent = translateWithTracker('.segment-timeline.live') + if (liveSegmentEl) focalElement = liveSegmentEl + + if (!focalElement) { + const nextSegmentEl = document.querySelector('.segment-timeline.next') + if (nextSegmentEl) focalElement = nextSegmentEl + } + + if (!focalElement) return null + + const { top } = focalElement.getBoundingClientRect() + + return { + elementId: focalElement.id, + top: top, + } + } + + private handleFollowLiveSegment( + prevProps: IPropsWithReady & ITrackedProps, + snapshot: IRundownViewContentSnapshot | null + ) { + if (this.props.onlyShelf) return + + if ( + this.props.playlist && + prevProps.playlist && + prevProps.playlist.currentPartInfo?.partInstanceId !== this.props.playlist.currentPartInfo?.partInstanceId && + prevProps.playlist.nextPartInfo?.manuallySelected + ) { + // reset followLiveSegments after a manual set as next + this.setState({ + manualSetAsNext: false, + followLiveSegments: true, + }) + if (this.props.playlist.currentPartInfo) { + scrollToPartInstance(this.props.playlist.currentPartInfo?.partInstanceId, true).catch((error) => { + if (!error.toString().match(/another scroll/)) console.warn(error) + }) + } + } else if ( + this.props.playlist && + prevProps.playlist && + prevProps.playlist.activationId && + !this.props.playlist.activationId + ) { + // reset followLiveSegments after deactivating a rundown + this.setState({ + followLiveSegments: true, + }) + } else if ( + this.props.playlist && + prevProps.playlist && + !prevProps.playlist.activationId && + this.props.playlist.activationId && + this.props.playlist.nextPartInfo + ) { + // scroll to next after activation + scrollToPartInstance(this.props.playlist.nextPartInfo.partInstanceId).catch((error) => { + if (!error.toString().match(/another scroll/)) console.warn(error) + }) + } else if ( + // after take + this.props.playlist && + prevProps.playlist && + this.props.playlist.currentPartInfo?.partInstanceId !== prevProps.playlist.currentPartInfo?.partInstanceId && + this.props.playlist.currentPartInfo && + this.state.followLiveSegments + ) { + scrollToPartInstance(this.props.playlist.currentPartInfo.partInstanceId, true).catch((error) => { + if (!error.toString().match(/another scroll/)) console.warn(error) + }) + } else if ( + this.props.playlist && + prevProps.playlist && + this.props.playlist.nextPartInfo?.partInstanceId !== prevProps.playlist.nextPartInfo?.partInstanceId && + this.props.playlist.currentPartInfo?.partInstanceId === prevProps.playlist.currentPartInfo?.partInstanceId && + this.props.playlist.nextPartInfo && + this.props.playlist.nextPartInfo.manuallySelected + ) { + scrollToPartInstance(this.props.playlist.nextPartInfo.partInstanceId, false).catch((error) => { + if (!error.toString().match(/another scroll/)) console.warn(error) + }) + } else if ( + // initial Rundown open + this.props.playlist && + this.props.playlist.currentPartInfo && + this.props.subsReady && + !prevProps.subsReady + ) { + // allow for some time for the Rundown to render + maintainFocusOnPartInstance(this.props.playlist.currentPartInfo.partInstanceId, 7000, true, true) + } else if ( + this.props.playlist && + this.props.playlist.currentPartInfo?.partInstanceId === prevProps.playlist?.currentPartInfo?.partInstanceId && + this.props.playlist.nextPartInfo?.partInstanceId === prevProps.playlist?.nextPartInfo?.partInstanceId && + this.props.matchedSegments !== prevProps.matchedSegments && + this.state.followLiveSegments && + snapshot + ) { + // segments changed before the live segment + const focalElement = document.getElementById(snapshot.elementId) + if (!focalElement) return + const { top } = focalElement.getBoundingClientRect() + + const diff = top - snapshot.top + window.scrollBy({ + top: diff, + // @ts-expect-error 'instant' value has been introduced in browsers, but is not supported by TS 4.9 + behavior: 'instant', + }) + } + } + + private handleBeforeUnloadEventAttach(prevProps: ITrackedProps, prevState: IState) { + if (this.props.onlyShelf) return + + if ( + typeof this.props.playlist !== typeof prevProps.playlist || + this.props.playlist?._id !== prevProps.playlist?._id || + !!this.props.playlist?.activationId !== !!prevProps.playlist?.activationId || + this.state.studioMode !== prevState.studioMode + ) { + if (this.props.playlist && this.props.playlist.activationId && this.state.studioMode && !getAllowDeveloper()) { + window.addEventListener('beforeunload', this.onBeforeUnload) + } else { + window.removeEventListener('beforeunload', this.onBeforeUnload) + } + } + } + private handleMiniShelfRequeue(prevProps: IProps & ITrackedProps) { if (this.props.currentPartInstance?.segmentId !== prevProps.currentPartInstance?.segmentId) { this.keyboardQueuedPiece = undefined From d5f92213bee771d6cf78e4d51007f27e05b9fa98 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 9 Jan 2024 12:11:22 +0000 Subject: [PATCH 204/479] fix: validating studio blueprint config fails with DataCloneError --- packages/job-worker/src/playout/upgrade.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/job-worker/src/playout/upgrade.ts b/packages/job-worker/src/playout/upgrade.ts index df57d5a6b3..b03dc1d286 100644 --- a/packages/job-worker/src/playout/upgrade.ts +++ b/packages/job-worker/src/playout/upgrade.ts @@ -116,7 +116,8 @@ export async function handleBlueprintValidateConfigForStudio( }) const rawBlueprintConfig = applyAndValidateOverrides(context.studio.blueprintConfigWithOverrides).obj - const messages = blueprint.blueprint.validateConfig(blueprintContext, rawBlueprintConfig) + // This clone seems excessive, but without it a DataCloneError is generated when posting the result to the parent + const messages = clone(blueprint.blueprint.validateConfig(blueprintContext, rawBlueprintConfig)) return { messages: messages.map((msg) => ({ From 33394a7e494e1e2a32d8f8cc55b34df6e52747fb Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 9 Jan 2024 13:49:00 +0000 Subject: [PATCH 205/479] fix: IngestDataCache not being populated correctly SOFIE-2672 #1106 --- packages/job-worker/src/ingest/ingestCache.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/job-worker/src/ingest/ingestCache.ts b/packages/job-worker/src/ingest/ingestCache.ts index 8ef7ea32cb..1b86b27e38 100644 --- a/packages/job-worker/src/ingest/ingestCache.ts +++ b/packages/job-worker/src/ingest/ingestCache.ts @@ -137,6 +137,7 @@ export class RundownIngestDataCache { _id: changedId, }, replacement: newDoc, + upsert: true, }, }) } From 5a25fa69cc8ddb99160658362219a0298985c2eb Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Thu, 11 Jan 2024 10:37:30 +0100 Subject: [PATCH 206/479] chore(docs): OBS now uses WS protocol v5 --- packages/documentation/docs/user-guide/supported-devices.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/documentation/docs/user-guide/supported-devices.md b/packages/documentation/docs/user-guide/supported-devices.md index 55f27cd5ab..b5a2763851 100644 --- a/packages/documentation/docs/user-guide/supported-devices.md +++ b/packages/documentation/docs/user-guide/supported-devices.md @@ -107,7 +107,7 @@ _Note: this is not currently used in production by anyone we know of_ ## OBS -*Through OBS WebSocket v4 RPC API* +*Through OBS 28+ WebSocket API (a.k.a v5 Protocol)* * Current / Preview Scene * Current Transition From 45d233f3b71cde6b8c0d09ab53bb1aee3ceca133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20St=C3=A6rk=C3=A6r?= Date: Fri, 12 Jan 2024 10:08:07 +0100 Subject: [PATCH 207/479] fix: looks for the correct property itemSlug on ncsItems Backwards compatible, having a fallback to the old behaviour of (wrongly) looking for and using objSlug --- meteor/client/ui/Shelf/ExternalFramePanel.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/meteor/client/ui/Shelf/ExternalFramePanel.tsx b/meteor/client/ui/Shelf/ExternalFramePanel.tsx index 802ba5cd40..5ea3127755 100644 --- a/meteor/client/ui/Shelf/ExternalFramePanel.tsx +++ b/meteor/client/ui/Shelf/ExternalFramePanel.tsx @@ -221,6 +221,12 @@ export const ExternalFramePanel = withTranslation()( const mosTypes = getMosTypes(MOS_DATA_IS_STRICT) + const name = mosItem.Slug + ? mosTypes.mosString128.stringify(mosItem.Slug) + : mosItem.ObjectSlug + ? mosTypes.mosString128.stringify(mosItem.ObjectSlug) + : '' + doUserAction(t, e, UserAction.INGEST_BUCKET_ADLIB, (e, ts) => MeteorCall.userAction.bucketAdlibImport( e, @@ -229,7 +235,7 @@ export const ExternalFramePanel = withTranslation()( showStyleBaseId, literal({ externalId: mosItem.ObjectID ? mosTypes.mosString128.stringify(mosItem.ObjectID) : '', - name: mosItem.ObjectSlug ? mosTypes.mosString128.stringify(mosItem.ObjectSlug) : '', + name, payloadType: 'MOS', payload: stringifyMosObject(mosItem, MOS_DATA_IS_STRICT), }) From 3cd426ff7e6d7b28aef65bfb31f4874c34bae509 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 12 Jan 2024 10:44:39 +0000 Subject: [PATCH 208/479] fix: activate scratchpad segment failing --- .../src/playout/model/implementation/PlayoutModelImpl.ts | 2 +- .../src/playout/model/implementation/SavePlayoutModel.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts index 19375b50cb..dde06e76d8 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts @@ -379,7 +379,7 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou part: Omit ): PlayoutPartInstanceModel { const currentPartInstance = this.currentPartInstance - if (!currentPartInstance) throw new Error('No currentPartInstance') + if (currentPartInstance) throw new Error('Scratchpad can only be used before the first take') const scratchpadSegment = rundown.getScratchpadSegment() if (!scratchpadSegment) throw new Error('No scratchpad segment') diff --git a/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts b/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts index f430ba6b14..98c458d358 100644 --- a/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts +++ b/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts @@ -1,7 +1,7 @@ import { PartInstanceId, PieceInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import { DBSegment, SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { AnyBulkWriteOperation } from 'mongodb' import { JobContext } from '../../../jobs' @@ -30,6 +30,7 @@ export async function writeScratchpadSegments( filter: { rundownId: rundown.rundown._id, _id: { $ne: scratchpadSegment?._id ?? protectString('') }, + orphaned: SegmentOrphanedReason.SCRATCHPAD, }, }, }) From 825b5231d7afe6fd13393b44e4f39a41e6093adb Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 12 Jan 2024 10:45:47 +0000 Subject: [PATCH 209/479] feat: Rework `CacheForIngest` to `IngestModel` SOFIE-2672 (#1108) --- meteor/client/styles/rundownView.scss | 3 - meteor/client/ui/SegmentList/SegmentList.tsx | 6 +- .../ui/SegmentList/SegmentListContainer.tsx | 1 - .../ui/SegmentList/SegmentListHeader.tsx | 6 - .../SegmentStoryboard/SegmentStoryboard.tsx | 3 - .../ui/SegmentTimeline/SegmentTimeline.tsx | 3 - meteor/client/ui/Settings/Studio/Generic.tsx | 10 - meteor/lib/api/rest/v1/studios.ts | 1 - meteor/server/api/ingest/packageInfo.ts | 6 +- meteor/server/api/playout/DOCS.md | 1 - meteor/server/api/rest/v1/typeConversion.ts | 2 - meteor/server/migration/1_40_0.ts | 34 - meteor/server/migration/X_X_X.ts | 101 +++ .../corelib/src/dataModel/ExpectedPackages.ts | 19 +- .../corelib/src/dataModel/PeripheralDevice.ts | 3 +- packages/corelib/src/dataModel/Segment.ts | 4 - packages/corelib/src/dataModel/Studio.ts | 2 - packages/corelib/src/playout/infinites.ts | 31 +- .../worker-threads-and-locks.md | 4 +- packages/job-worker/ARCHITECTURE | 7 +- packages/job-worker/src/__mocks__/context.ts | 2 +- .../rundown-updatePartInstanceRanks.test.ts | 585 ++++++++------- .../src/blueprints/context/watchedPackages.ts | 62 +- packages/job-worker/src/cache/CacheBase.ts | 199 ------ .../job-worker/src/cache/CacheCollection.ts | 600 ---------------- packages/job-worker/src/cache/CacheObject.ts | 432 ------------ .../cache/__tests__/DatabaseCaches.test.ts | 324 --------- packages/job-worker/src/cache/lib.ts | 94 --- .../__tests__/expectedMediaItems.test.ts | 225 ------ .../ingest/__tests__/expectedPackages.test.ts | 156 ++++ .../src/ingest/__tests__/ingest.test.ts | 345 +-------- .../src/ingest/__tests__/updateNext.test.ts | 4 +- packages/job-worker/src/ingest/cache.ts | 265 ------- packages/job-worker/src/ingest/cleanup.ts | 32 - packages/job-worker/src/ingest/commit.ts | 344 ++++----- .../src/ingest/expectedMediaItems.ts | 59 +- .../job-worker/src/ingest/expectedPackages.ts | 272 +++---- .../src/ingest/expectedPlayoutItems.ts | 110 +-- .../src/ingest/generationRundown.ts | 219 ++---- .../src/ingest/generationSegment.ts | 245 ++----- packages/job-worker/src/ingest/ingestCache.ts | 2 +- .../job-worker/src/ingest/ingestPartJobs.ts | 8 +- .../src/ingest/ingestRundownJobs.ts | 22 +- .../src/ingest/ingestSegmentJobs.ts | 87 ++- packages/job-worker/src/ingest/lib.ts | 25 +- packages/job-worker/src/ingest/lock.ts | 62 +- .../src/ingest/model/IngestModel.ts | 273 +++++++ .../src/ingest/model/IngestPartModel.ts | 69 ++ .../src/ingest/model/IngestSegmentModel.ts | 82 +++ .../implementation/DocumentChangeTracker.ts | 124 ++++ .../implementation/ExpectedPackagesStore.ts | 187 +++++ .../model/implementation/IngestModelImpl.ts | 667 ++++++++++++++++++ .../implementation/IngestPartModelImpl.ts | 230 ++++++ .../implementation/IngestSegmentModelImpl.ts | 229 ++++++ .../model/implementation/LoadIngestModel.ts | 140 ++++ .../model/implementation/SaveIngestModel.ts | 70 ++ .../__tests__/DocumentChangeTracker.test.ts | 215 ++++++ .../__tests__/getDocumentChanges.test.ts | 50 ++ .../src/ingest/model/implementation/utils.ts | 103 +++ .../job-worker/src/ingest/mosDevice/diff.ts | 139 ++-- .../src/ingest/mosDevice/mosRundownJobs.ts | 27 +- .../src/ingest/mosDevice/mosStoryJobs.ts | 4 +- packages/job-worker/src/ingest/packageInfo.ts | 23 +- .../src/ingest/syncChangesToPartInstance.ts | 72 +- packages/job-worker/src/jobs/index.ts | 4 +- packages/job-worker/src/lib/lazy.ts | 25 +- packages/job-worker/src/playout/infinites.ts | 33 +- .../playout/model/PlayoutPartInstanceModel.ts | 2 +- .../model/implementation/LoadPlayoutModel.ts | 65 +- packages/job-worker/src/rundown.ts | 34 +- packages/live-status-gateway/package.json | 1 - .../src/topics/__tests__/utils.ts | 1 + packages/openapi/api/definitions/studios.yaml | 3 +- packages/package.json | 1 + packages/yarn.lock | 2 +- 75 files changed, 3925 insertions(+), 3977 deletions(-) delete mode 100644 packages/job-worker/src/cache/CacheBase.ts delete mode 100644 packages/job-worker/src/cache/CacheCollection.ts delete mode 100644 packages/job-worker/src/cache/CacheObject.ts delete mode 100644 packages/job-worker/src/cache/__tests__/DatabaseCaches.test.ts delete mode 100644 packages/job-worker/src/cache/lib.ts delete mode 100644 packages/job-worker/src/ingest/__tests__/expectedMediaItems.test.ts create mode 100644 packages/job-worker/src/ingest/__tests__/expectedPackages.test.ts delete mode 100644 packages/job-worker/src/ingest/cache.ts delete mode 100644 packages/job-worker/src/ingest/cleanup.ts create mode 100644 packages/job-worker/src/ingest/model/IngestModel.ts create mode 100644 packages/job-worker/src/ingest/model/IngestPartModel.ts create mode 100644 packages/job-worker/src/ingest/model/IngestSegmentModel.ts create mode 100644 packages/job-worker/src/ingest/model/implementation/DocumentChangeTracker.ts create mode 100644 packages/job-worker/src/ingest/model/implementation/ExpectedPackagesStore.ts create mode 100644 packages/job-worker/src/ingest/model/implementation/IngestModelImpl.ts create mode 100644 packages/job-worker/src/ingest/model/implementation/IngestPartModelImpl.ts create mode 100644 packages/job-worker/src/ingest/model/implementation/IngestSegmentModelImpl.ts create mode 100644 packages/job-worker/src/ingest/model/implementation/LoadIngestModel.ts create mode 100644 packages/job-worker/src/ingest/model/implementation/SaveIngestModel.ts create mode 100644 packages/job-worker/src/ingest/model/implementation/__tests__/DocumentChangeTracker.test.ts create mode 100644 packages/job-worker/src/ingest/model/implementation/__tests__/getDocumentChanges.test.ts create mode 100644 packages/job-worker/src/ingest/model/implementation/utils.ts diff --git a/meteor/client/styles/rundownView.scss b/meteor/client/styles/rundownView.scss index d590da90aa..a2601ee06b 100644 --- a/meteor/client/styles/rundownView.scss +++ b/meteor/client/styles/rundownView.scss @@ -739,9 +739,6 @@ svg.icon { grid-column: duration-onAirIn-split / segment-group-controls; } - .segment-timeline__unsynced { - text-transform: uppercase; - } } .segment-timeline__switch-view-mode-button { diff --git a/meteor/client/ui/SegmentList/SegmentList.tsx b/meteor/client/ui/SegmentList/SegmentList.tsx index 2d35dc4344..c3f8354a7e 100644 --- a/meteor/client/ui/SegmentList/SegmentList.tsx +++ b/meteor/client/ui/SegmentList/SegmentList.tsx @@ -1,6 +1,6 @@ import React, { ReactNode, useLayoutEffect, useMemo, useRef, useState } from 'react' import classNames from 'classnames' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist, RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { UIStateStorage } from '../../lib/UIStateStorage' import { PartUi, PieceUi, SegmentNoteCounts, SegmentUi } from '../SegmentContainer/withResolvedSegment' import { IContextMenuContext } from '../RundownView' @@ -16,8 +16,6 @@ import { useInView } from 'react-intersection-observer' import { getHeaderHeight } from '../../lib/viewPort' import { PartId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { NoteSeverity } from '@sofie-automation/blueprints-integration' -import { UIStudio } from '../../../lib/api/studios' -import { RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { CalculateTimingsPiece } from '@sofie-automation/corelib/dist/playout/timings' interface IProps { @@ -32,7 +30,6 @@ interface IProps { key: string segment: SegmentUi playlist: DBRundownPlaylist - studio: UIStudio parts: Array pieces: Map segmentNoteCounts: SegmentNoteCounts @@ -227,7 +224,6 @@ const SegmentListInner = React.forwardRef(function Segme pieces={props.pieces} segment={props.segment} playlist={props.playlist} - studio={props.studio} segmentNoteCounts={props.segmentNoteCounts} highlight={highlight} isLiveSegment={props.isLiveSegment} diff --git a/meteor/client/ui/SegmentList/SegmentListContainer.tsx b/meteor/client/ui/SegmentList/SegmentListContainer.tsx index 62aeb70687..9f316792da 100644 --- a/meteor/client/ui/SegmentList/SegmentListContainer.tsx +++ b/meteor/client/ui/SegmentList/SegmentListContainer.tsx @@ -196,7 +196,6 @@ export const SegmentListContainer = withResolvedSegment(function Segment parts={props.parts} pieces={props.pieces} playlist={props.playlist} - studio={props.studio} currentPartWillAutoNext={currentPartWillAutoNext} segmentNoteCounts={props.segmentNoteCounts} isLiveSegment={isLiveSegment} diff --git a/meteor/client/ui/SegmentList/SegmentListHeader.tsx b/meteor/client/ui/SegmentList/SegmentListHeader.tsx index 9755979d0c..d462854eb7 100644 --- a/meteor/client/ui/SegmentList/SegmentListHeader.tsx +++ b/meteor/client/ui/SegmentList/SegmentListHeader.tsx @@ -15,7 +15,6 @@ import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/Rund import { IContextMenuContext } from '../RundownView' import { NoteSeverity } from '@sofie-automation/blueprints-integration' import { CriticalIconSmall, WarningIconSmall } from '../../lib/ui/icons/notifications' -import { UIStudio } from '../../../lib/api/studios' import { CalculateTimingsPiece } from '@sofie-automation/corelib/dist/playout/timings' import { SegmentTimeAnchorTime } from '../RundownView/RundownTiming/SegmentTimeAnchorTime' @@ -26,7 +25,6 @@ export function SegmentListHeader({ parts, pieces, playlist, - studio, highlight, segmentNoteCounts, isLiveSegment, @@ -45,7 +43,6 @@ export function SegmentListHeader({ isDetachedStick: boolean segment: SegmentUi playlist: DBRundownPlaylist - studio: UIStudio parts: Array pieces: Map segmentNoteCounts: SegmentNoteCounts @@ -152,9 +149,6 @@ export function SegmentListHeader({ } /> )} - {studio.settings.preserveUnsyncedPlayingSegmentContents && segment.orphaned && ( - {t('Unsynced')} - )} )} diff --git a/meteor/client/ui/SegmentStoryboard/SegmentStoryboard.tsx b/meteor/client/ui/SegmentStoryboard/SegmentStoryboard.tsx index c5dfabe025..34274c549c 100644 --- a/meteor/client/ui/SegmentStoryboard/SegmentStoryboard.tsx +++ b/meteor/client/ui/SegmentStoryboard/SegmentStoryboard.tsx @@ -647,9 +647,6 @@ export const SegmentStoryboard = React.memo( } /> )} - {props.studio.settings.preserveUnsyncedPlayingSegmentContents && props.segment.orphaned && ( - {t('Unsynced')} - )} )} diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx index 5ec5afe8e2..c374166a89 100644 --- a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx +++ b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx @@ -1143,9 +1143,6 @@ export class SegmentTimelineClass extends React.Component )} - {this.props.studio.settings.preserveUnsyncedPlayingSegmentContents && this.props.segment.orphaned && ( - {t('Unsynced')} - )} )} diff --git a/meteor/client/ui/Settings/Studio/Generic.tsx b/meteor/client/ui/Settings/Studio/Generic.tsx index dbf2283dbe..ec5c041050 100644 --- a/meteor/client/ui/Settings/Studio/Generic.tsx +++ b/meteor/client/ui/Settings/Studio/Generic.tsx @@ -212,16 +212,6 @@ export const StudioGenericProperties = withTranslation()( className="mdinput" /> - diff --git a/meteor/client/ui/Shelf/AdLibPanelToolbar.tsx b/meteor/client/ui/Shelf/AdLibPanelToolbar.tsx index cb70e4f3ab..066df8b392 100644 --- a/meteor/client/ui/Shelf/AdLibPanelToolbar.tsx +++ b/meteor/client/ui/Shelf/AdLibPanelToolbar.tsx @@ -47,9 +47,13 @@ export function AdLibPanelToolbar(props: Readonly): JSX.Ele value={props.searchFilter || ''} /> {props.searchFilter && ( -
+
+ )}
From e6f5dd85987f1c54c85397d53320f32424b17862 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 1 Feb 2024 13:33:56 +0000 Subject: [PATCH 230/479] chore: update dependencies --- meteor/package.json | 78 +- meteor/server/api/rest/koa.ts | 9 +- meteor/yarn.lock | 925 ++++++++++-------- package.json | 6 +- packages/corelib/package.json | 2 +- packages/documentation/package.json | 4 +- packages/job-worker/package.json | 4 +- packages/live-status-gateway/package.json | 6 +- packages/mos-gateway/package.json | 4 +- packages/openapi/package.json | 4 +- packages/package.json | 24 +- packages/playout-gateway/package.json | 2 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 1080 +++++++++++++-------- yarn.lock | 24 +- 15 files changed, 1302 insertions(+), 872 deletions(-) diff --git a/meteor/package.json b/meteor/package.json index 3d239bd004..11e4777651 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -39,16 +39,16 @@ "watch-types": "run check-types --watch" }, "dependencies": { - "@babel/runtime": "^7.22.15", + "@babel/runtime": "^7.23.9", "@crello/react-lottie": "0.0.9", "@fortawesome/fontawesome-free": "^6.4.2", "@fortawesome/fontawesome-svg-core": "~6.4.2", "@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/react-fontawesome": "^0.2.0", "@jstarpl/react-contextmenu": "^2.15.0", - "@koa/cors": "^4.0.0", - "@koa/router": "^12.0.0", - "@mos-connection/helper": "^3.0.4", + "@koa/cors": "^5.0.0", + "@koa/router": "^12.0.1", + "@mos-connection/helper": "3.0.4", "@nrk/core-icons": "^9.6.0", "@popperjs/core": "^2.11.8", "@slack/webhook": "^6.1.0", @@ -60,8 +60,8 @@ "app-root-path": "^3.1.0", "bcrypt": "^5.1.1", "body-parser": "^1.20.2", - "classnames": "^2.3.2", - "core-js": "^3.32.2", + "classnames": "^2.5.1", + "core-js": "^3.35.1", "cubic-spline": "^3.0.3", "deep-extend": "0.6.0", "deepmerge": "^4.3.1", @@ -70,13 +70,13 @@ "i18next-http-backend": "^1.4.5", "immutability-helper": "^3.1.1", "indexof": "0.0.1", - "koa": "^2.14.2", + "koa": "^2.15.0", "koa-bodyparser": "^4.4.1", "lottie-web": "^5.12.2", - "meteor-node-stubs": "^1.2.5", - "moment": "^2.29.4", - "nanoid": "^3.3.6", - "node-gyp": "^9.4.0", + "meteor-node-stubs": "^1.2.7", + "moment": "^2.30.1", + "nanoid": "^3.3.7", + "node-gyp": "^9.4.1", "ntp-client": "^0.5.3", "object-path": "^0.11.8", "p-lazy": "^3.1.0", @@ -84,7 +84,7 @@ "promise.allsettled": "^1.0.7", "prop-types": "^15.8.1", "query-string": "^6.14.1", - "rc-tooltip": "^6.0.1", + "rc-tooltip": "^6.1.3", "react": "^18.2.0", "react-circular-progressbar": "^2.1.0", "react-datepicker": "^3.8.0", @@ -93,7 +93,7 @@ "react-dom": "^18.2.0", "react-hotkeys": "^2.0.0", "react-i18next": "^11.18.6", - "react-intersection-observer": "^9.5.2", + "react-intersection-observer": "^9.6.0", "react-moment": "^0.9.7", "react-popper": "^2.3.0", "react-router-dom": "^5.3.4", @@ -108,38 +108,38 @@ "velocity-react": "^1.4.3", "vm2": "^3.9.19", "webmidi": "^2.5.3", - "winston": "^3.10.0", + "winston": "^3.11.0", "xmlbuilder": "^15.1.1" }, "devDependencies": { - "@babel/core": "^7.22.20", - "@babel/plugin-transform-modules-commonjs": "^7.22.15", + "@babel/core": "^7.23.9", + "@babel/plugin-transform-modules-commonjs": "^7.23.3", "@shopify/jest-koa-mocks": "^5.1.1", "@sofie-automation/code-standard-preset": "~2.4.7", "@sofie-automation/eslint-plugin": "^0.1.1", - "@types/app-root-path": "^1.2.5", - "@types/body-parser": "^1.19.3", + "@types/app-root-path": "^1.2.8", + "@types/body-parser": "^1.19.5", "@types/classnames": "^2.3.1", - "@types/deep-extend": "^0.6.0", - "@types/fibers": "^3.1.2", - "@types/jest": "^29.5.5", - "@types/koa": "^2.13.9", - "@types/koa-bodyparser": "^4.3.10", - "@types/koa__cors": "^3.3.1", - "@types/koa__router": "^12.0.1", - "@types/node": "^14.18.62", - "@types/prop-types": "^15.7.6", - "@types/react": "^18.2.22", + "@types/deep-extend": "^0.6.2", + "@types/fibers": "^3.1.4", + "@types/jest": "^29.5.11", + "@types/koa": "^2.14.0", + "@types/koa-bodyparser": "^4.3.12", + "@types/koa__cors": "^5.0.0", + "@types/koa__router": "^12.0.4", + "@types/node": "^14.18.63", + "@types/prop-types": "^15.7.11", + "@types/react": "^18.2.51", "@types/react-circular-progressbar": "^1.1.0", "@types/react-datepicker": "^3.1.8", - "@types/react-dom": "^18.2.7", + "@types/react-dom": "^18.2.18", "@types/react-router": "^5.1.20", "@types/react-router-dom": "^5.3.3", - "@types/request": "^2.48.8", - "@types/semver": "^7.5.2", - "@types/sinon": "^10.0.16", - "@types/underscore": "1.11.9", - "@types/xml2js": "^0.4.12", + "@types/request": "^2.48.12", + "@types/semver": "^7.5.6", + "@types/sinon": "^10.0.20", + "@types/underscore": "1.11.15", + "@types/xml2js": "^0.4.14", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "@typescript-eslint/utils": "^5.62.0", @@ -147,10 +147,10 @@ "@xmldom/xmldom": "^0.8.10", "babel-jest": "^29.7.0", "ejson": "^2.2.3", - "eslint": "^8.49.0", + "eslint": "^8.56.0", "eslint-config-prettier": "^8.10.0", "eslint-plugin-custom-rules": "link:eslint-rules", - "eslint-plugin-jest": "^27.4.0", + "eslint-plugin-jest": "^27.6.3", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.33.2", @@ -167,9 +167,9 @@ "prettier": "^2.8.8", "sinon": "^14.0.2", "standard-version": "^9.5.0", - "ts-jest": "^29.1.1", - "typescript": "^4.9.4", - "xml2js": "^0.4.23", + "ts-jest": "^29.1.2", + "typescript": "^4.9.5", + "xml2js": "^0.6.2", "yargs": "^17.7.2" }, "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", diff --git a/meteor/server/api/rest/koa.ts b/meteor/server/api/rest/koa.ts index fef2466a7f..e1eddd0b52 100644 --- a/meteor/server/api/rest/koa.ts +++ b/meteor/server/api/rest/koa.ts @@ -32,7 +32,14 @@ export function bindKoaRouter(koaRouter: KoaRouter, bindPath: string): void { } await next() }) - app.use(cors()) + app.use( + cors({ + // Allow anything + origin(ctx) { + return ctx.get('Origin') || '*' + }, + }) + ) app.use(koaRouter.routes()).use(koaRouter.allowedMethods()) } diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 828c326c3f..b678eda2e9 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -33,68 +33,68 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.22.13": - version: 7.22.13 - resolution: "@babel/code-frame@npm:7.22.13" +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/code-frame@npm:7.23.5" dependencies: - "@babel/highlight": ^7.22.13 + "@babel/highlight": ^7.23.4 chalk: ^2.4.2 - checksum: 22e342c8077c8b77eeb11f554ecca2ba14153f707b85294fcf6070b6f6150aae88a7b7436dd88d8c9289970585f3fe5b9b941c5aa3aa26a6d5a8ef3f292da058 + checksum: d90981fdf56a2824a9b14d19a4c0e8db93633fd488c772624b4e83e0ceac6039a27cd298a247c3214faa952bf803ba23696172ae7e7235f3b97f43ba278c569a languageName: node linkType: hard -"@babel/compat-data@npm:^7.22.9": - version: 7.22.20 - resolution: "@babel/compat-data@npm:7.22.20" - checksum: efedd1d18878c10fde95e4d82b1236a9aba41395ef798cbb651f58dbf5632dbff475736c507b8d13d4c8f44809d41c0eb2ef0d694283af9ba5dd8339b6dab451 +"@babel/compat-data@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/compat-data@npm:7.23.5" + checksum: 06ce244cda5763295a0ea924728c09bae57d35713b675175227278896946f922a63edf803c322f855a3878323d48d0255a2a3023409d2a123483c8a69ebb4744 languageName: node linkType: hard -"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.22.20": - version: 7.22.20 - resolution: "@babel/core@npm:7.22.20" +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.23.9": + version: 7.23.9 + resolution: "@babel/core@npm:7.23.9" dependencies: "@ampproject/remapping": ^2.2.0 - "@babel/code-frame": ^7.22.13 - "@babel/generator": ^7.22.15 - "@babel/helper-compilation-targets": ^7.22.15 - "@babel/helper-module-transforms": ^7.22.20 - "@babel/helpers": ^7.22.15 - "@babel/parser": ^7.22.16 - "@babel/template": ^7.22.15 - "@babel/traverse": ^7.22.20 - "@babel/types": ^7.22.19 - convert-source-map: ^1.7.0 + "@babel/code-frame": ^7.23.5 + "@babel/generator": ^7.23.6 + "@babel/helper-compilation-targets": ^7.23.6 + "@babel/helper-module-transforms": ^7.23.3 + "@babel/helpers": ^7.23.9 + "@babel/parser": ^7.23.9 + "@babel/template": ^7.23.9 + "@babel/traverse": ^7.23.9 + "@babel/types": ^7.23.9 + convert-source-map: ^2.0.0 debug: ^4.1.0 gensync: ^1.0.0-beta.2 json5: ^2.2.3 semver: ^6.3.1 - checksum: 73663a079194b5dc406b2e2e5e50db81977d443e4faf7ef2c27e5836cd9a359e81e551115193dc9b1a93471275351a972e54904f4d3aa6cb156f51e26abf6765 + checksum: 634a511f74db52a5f5a283c1121f25e2227b006c095b84a02a40a9213842489cd82dc7d61cdc74e10b5bcd9bb0a4e28bab47635b54c7e2256d47ab57356e2a76 languageName: node linkType: hard -"@babel/generator@npm:^7.22.15, @babel/generator@npm:^7.7.2": - version: 7.22.15 - resolution: "@babel/generator@npm:7.22.15" +"@babel/generator@npm:^7.23.6, @babel/generator@npm:^7.7.2": + version: 7.23.6 + resolution: "@babel/generator@npm:7.23.6" dependencies: - "@babel/types": ^7.22.15 + "@babel/types": ^7.23.6 "@jridgewell/gen-mapping": ^0.3.2 "@jridgewell/trace-mapping": ^0.3.17 jsesc: ^2.5.1 - checksum: 5b2a3ccdc3634f6ea86e0a442722bcd430238369432d31f15b428a4ee8013c2f4f917b5b135bf4fc1d0a3e2f87f10fd4ce5d07955ecc2d3b9400a05c2a481374 + checksum: 1a1a1c4eac210f174cd108d479464d053930a812798e09fee069377de39a893422df5b5b146199ead7239ae6d3a04697b45fc9ac6e38e0f6b76374390f91fc6c languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.22.15": - version: 7.22.15 - resolution: "@babel/helper-compilation-targets@npm:7.22.15" +"@babel/helper-compilation-targets@npm:^7.23.6": + version: 7.23.6 + resolution: "@babel/helper-compilation-targets@npm:7.23.6" dependencies: - "@babel/compat-data": ^7.22.9 - "@babel/helper-validator-option": ^7.22.15 - browserslist: ^4.21.9 + "@babel/compat-data": ^7.23.5 + "@babel/helper-validator-option": ^7.23.5 + browserslist: ^4.22.2 lru-cache: ^5.1.1 semver: ^6.3.1 - checksum: ce85196769e091ae54dd39e4a80c2a9df1793da8588e335c383d536d54f06baf648d0a08fc873044f226398c4ded15c4ae9120ee18e7dfd7c639a68e3cdc9980 + checksum: c630b98d4527ac8fe2c58d9a06e785dfb2b73ec71b7c4f2ddf90f814b5f75b547f3c015f110a010fd31f76e3864daaf09f3adcd2f6acdbfb18a8de3a48717590 languageName: node linkType: hard @@ -105,13 +105,13 @@ __metadata: languageName: node linkType: hard -"@babel/helper-function-name@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-function-name@npm:7.22.5" +"@babel/helper-function-name@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/helper-function-name@npm:7.23.0" dependencies: - "@babel/template": ^7.22.5 - "@babel/types": ^7.22.5 - checksum: 6b1f6ce1b1f4e513bf2c8385a557ea0dd7fa37971b9002ad19268ca4384bbe90c09681fe4c076013f33deabc63a53b341ed91e792de741b4b35e01c00238177a + "@babel/template": ^7.22.15 + "@babel/types": ^7.23.0 + checksum: e44542257b2d4634a1f979244eb2a4ad8e6d75eb6761b4cfceb56b562f7db150d134bc538c8e6adca3783e3bc31be949071527aa8e3aab7867d1ad2d84a26e10 languageName: node linkType: hard @@ -133,9 +133,9 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.22.15, @babel/helper-module-transforms@npm:^7.22.20": - version: 7.22.20 - resolution: "@babel/helper-module-transforms@npm:7.22.20" +"@babel/helper-module-transforms@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/helper-module-transforms@npm:7.23.3" dependencies: "@babel/helper-environment-visitor": ^7.22.20 "@babel/helper-module-imports": ^7.22.15 @@ -144,7 +144,7 @@ __metadata: "@babel/helper-validator-identifier": ^7.22.20 peerDependencies: "@babel/core": ^7.0.0 - checksum: 8fce25362df8711bd4620f41c5c18769edfeafe7f8f1dae9691966ef368e57f9da68dfa1707cd63c834c89dc4eaa82c26f12ea33e88fd262ac62844b11dcc389 + checksum: 5d0895cfba0e16ae16f3aa92fee108517023ad89a855289c4eb1d46f7aef4519adf8e6f971e1d55ac20c5461610e17213f1144097a8f932e768a9132e2278d71 languageName: node linkType: hard @@ -173,55 +173,55 @@ __metadata: languageName: node linkType: hard -"@babel/helper-string-parser@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-string-parser@npm:7.22.5" - checksum: 836851ca5ec813077bbb303acc992d75a360267aa3b5de7134d220411c852a6f17de7c0d0b8c8dcc0f567f67874c00f4528672b2a4f1bc978a3ada64c8c78467 +"@babel/helper-string-parser@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/helper-string-parser@npm:7.23.4" + checksum: c0641144cf1a7e7dc93f3d5f16d5327465b6cf5d036b48be61ecba41e1eece161b48f46b7f960951b67f8c3533ce506b16dece576baef4d8b3b49f8c65410f90 languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.22.19, @babel/helper-validator-identifier@npm:^7.22.20": +"@babel/helper-validator-identifier@npm:^7.22.20": version: 7.22.20 resolution: "@babel/helper-validator-identifier@npm:7.22.20" checksum: 136412784d9428266bcdd4d91c32bcf9ff0e8d25534a9d94b044f77fe76bc50f941a90319b05aafd1ec04f7d127cd57a179a3716009ff7f3412ef835ada95bdc languageName: node linkType: hard -"@babel/helper-validator-option@npm:^7.22.15": - version: 7.22.15 - resolution: "@babel/helper-validator-option@npm:7.22.15" - checksum: 68da52b1e10002a543161494c4bc0f4d0398c8fdf361d5f7f4272e95c45d5b32d974896d44f6a0ea7378c9204988879d73613ca683e13bd1304e46d25ff67a8d +"@babel/helper-validator-option@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/helper-validator-option@npm:7.23.5" + checksum: 537cde2330a8aede223552510e8a13e9c1c8798afee3757995a7d4acae564124fe2bf7e7c3d90d62d3657434a74340a274b3b3b1c6f17e9a2be1f48af29cb09e languageName: node linkType: hard -"@babel/helpers@npm:^7.22.15": - version: 7.22.15 - resolution: "@babel/helpers@npm:7.22.15" +"@babel/helpers@npm:^7.23.9": + version: 7.23.9 + resolution: "@babel/helpers@npm:7.23.9" dependencies: - "@babel/template": ^7.22.15 - "@babel/traverse": ^7.22.15 - "@babel/types": ^7.22.15 - checksum: 49f61a93cbae4df3328bda67af5db743fead659ae4242571226c3596b7df78546189cdf991fed1eca33b559de8abf396a90a001f474a1bab351418f07b7ae6ef + "@babel/template": ^7.23.9 + "@babel/traverse": ^7.23.9 + "@babel/types": ^7.23.9 + checksum: 2678231192c0471dbc2fc403fb19456cc46b1afefcfebf6bc0f48b2e938fdb0fef2e0fe90c8c8ae1f021dae5012b700372e4b5d15867f1d7764616532e4a6324 languageName: node linkType: hard -"@babel/highlight@npm:^7.22.13": - version: 7.22.20 - resolution: "@babel/highlight@npm:7.22.20" +"@babel/highlight@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/highlight@npm:7.23.4" dependencies: "@babel/helper-validator-identifier": ^7.22.20 chalk: ^2.4.2 js-tokens: ^4.0.0 - checksum: 84bd034dca309a5e680083cd827a766780ca63cef37308404f17653d32366ea76262bd2364b2d38776232f2d01b649f26721417d507e8b4b6da3e4e739f6d134 + checksum: 643acecdc235f87d925979a979b539a5d7d1f31ae7db8d89047269082694122d11aa85351304c9c978ceeb6d250591ccadb06c366f358ccee08bb9c122476b89 languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.22.15, @babel/parser@npm:^7.22.16": - version: 7.22.16 - resolution: "@babel/parser@npm:7.22.16" +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9": + version: 7.23.9 + resolution: "@babel/parser@npm:7.23.9" bin: parser: ./bin/babel-parser.js - checksum: 944c756b5bdeb07b9fec16ecef6b3c61aff9d4c4b924abadcf01afa1840a740b8e2357ae00482b5b37daad6d2bfd848c947f27ad65138d687b6fdc924bc59edd + checksum: e7cd4960ac8671774e13803349da88d512f9292d7baa952173260d3e8f15620a28a3701f14f709d769209022f9e7b79965256b8be204fc550cfe783cdcabe7c7 languageName: node linkType: hard @@ -379,65 +379,65 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-commonjs@npm:^7.22.15": - version: 7.22.15 - resolution: "@babel/plugin-transform-modules-commonjs@npm:7.22.15" +"@babel/plugin-transform-modules-commonjs@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.23.3" dependencies: - "@babel/helper-module-transforms": ^7.22.15 + "@babel/helper-module-transforms": ^7.23.3 "@babel/helper-plugin-utils": ^7.22.5 "@babel/helper-simple-access": ^7.22.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: f8fc85fefa6be8626a378ca38fb84c7359043e7c692c854e9ee250a05121553b7f4a58e127099efe12662ec6bebbfd304ce638a0b4563d7cbd5982f3d877321c + checksum: 720a231ceade4ae4d2632478db4e7fecf21987d444942b72d523487ac8d715ca97de6c8f415c71e939595e1a4776403e7dc24ed68fe9125ad4acf57753c9bff7 languageName: node linkType: hard -"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.19.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.15, @babel/runtime@npm:^7.22.5, @babel/runtime@npm:^7.9.2": - version: 7.22.15 - resolution: "@babel/runtime@npm:7.22.15" +"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.19.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.5, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.9.2": + version: 7.23.9 + resolution: "@babel/runtime@npm:7.23.9" dependencies: regenerator-runtime: ^0.14.0 - checksum: 793296df1e41599a935a3d77ec01eb6088410d3fd4dbe4e92f06c6b7bb2f8355024e6d78621a3a35f44e0e23b0b59107f23d585384df4f3123256a1e1492040e + checksum: 6bbebe8d27c0c2dd275d1ac197fc1a6c00e18dab68cc7aaff0adc3195b45862bae9c4cc58975629004b0213955b2ed91e99eccb3d9b39cabea246c657323d667 languageName: node linkType: hard -"@babel/template@npm:^7.22.15, @babel/template@npm:^7.22.5, @babel/template@npm:^7.3.3": - version: 7.22.15 - resolution: "@babel/template@npm:7.22.15" +"@babel/template@npm:^7.22.15, @babel/template@npm:^7.23.9, @babel/template@npm:^7.3.3": + version: 7.23.9 + resolution: "@babel/template@npm:7.23.9" dependencies: - "@babel/code-frame": ^7.22.13 - "@babel/parser": ^7.22.15 - "@babel/types": ^7.22.15 - checksum: 1f3e7dcd6c44f5904c184b3f7fe280394b191f2fed819919ffa1e529c259d5b197da8981b6ca491c235aee8dbad4a50b7e31304aa531271cb823a4a24a0dd8fd + "@babel/code-frame": ^7.23.5 + "@babel/parser": ^7.23.9 + "@babel/types": ^7.23.9 + checksum: 6e67414c0f7125d7ecaf20c11fab88085fa98a96c3ef10da0a61e962e04fdf3a18a496a66047005ddd1bb682a7cc7842d556d1db2f3f3f6ccfca97d5e445d342 languageName: node linkType: hard -"@babel/traverse@npm:^7.22.15, @babel/traverse@npm:^7.22.20": - version: 7.22.20 - resolution: "@babel/traverse@npm:7.22.20" +"@babel/traverse@npm:^7.23.9": + version: 7.23.9 + resolution: "@babel/traverse@npm:7.23.9" dependencies: - "@babel/code-frame": ^7.22.13 - "@babel/generator": ^7.22.15 + "@babel/code-frame": ^7.23.5 + "@babel/generator": ^7.23.6 "@babel/helper-environment-visitor": ^7.22.20 - "@babel/helper-function-name": ^7.22.5 + "@babel/helper-function-name": ^7.23.0 "@babel/helper-hoist-variables": ^7.22.5 "@babel/helper-split-export-declaration": ^7.22.6 - "@babel/parser": ^7.22.16 - "@babel/types": ^7.22.19 - debug: ^4.1.0 + "@babel/parser": ^7.23.9 + "@babel/types": ^7.23.9 + debug: ^4.3.1 globals: ^11.1.0 - checksum: 97da9afa7f8f505ce52c36ac2531129bc4a0e250880aaf9b467dc044f30a5bce2b756c1af4d961958bc225659546e811a7d536ab3d920fd60921087989b841b9 + checksum: a932f7aa850e158c00c97aad22f639d48c72805c687290f6a73e30c5c4957c07f5d28310c9bf59648e2980fe6c9d16adeb2ff92a9ca0f97fa75739c1328fc6c3 languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.3.3, @babel/types@npm:^7.8.3": - version: 7.22.19 - resolution: "@babel/types@npm:7.22.19" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.6, @babel/types@npm:^7.23.9, @babel/types@npm:^7.3.3, @babel/types@npm:^7.8.3": + version: 7.23.9 + resolution: "@babel/types@npm:7.23.9" dependencies: - "@babel/helper-string-parser": ^7.22.5 - "@babel/helper-validator-identifier": ^7.22.19 + "@babel/helper-string-parser": ^7.23.4 + "@babel/helper-validator-identifier": ^7.22.20 to-fast-properties: ^2.0.0 - checksum: 2d69740e69b55ba36ece0c17d5afb7b7213b34297157df39ef9ba24965aff677c56f014413052ecc5b2fbbf26910c63e5bb24a969df84d7a17153750cf75915e + checksum: 0a9b008e9bfc89beb8c185e620fa0f8ed6c771f1e1b2e01e1596870969096fec7793898a1d64a035176abf1dd13e2668ee30bf699f2d92c210a8128f4b151e65 languageName: node linkType: hard @@ -455,6 +455,13 @@ __metadata: languageName: node linkType: hard +"@colors/colors@npm:^1.6.0": + version: 1.6.0 + resolution: "@colors/colors@npm:1.6.0" + checksum: aa209963e0c3218e80a4a20553ba8c0fbb6fa13140540b4e5f97923790be06801fc90172c1114fc8b7e888b3d012b67298cde6b9e81521361becfaee400c662f + languageName: node + linkType: hard + "@crello/react-lottie@npm:0.0.9": version: 0.0.9 resolution: "@crello/react-lottie@npm:0.0.9" @@ -514,9 +521,9 @@ __metadata: languageName: node linkType: hard -"@eslint/eslintrc@npm:^2.1.2": - version: 2.1.2 - resolution: "@eslint/eslintrc@npm:2.1.2" +"@eslint/eslintrc@npm:^2.1.4": + version: 2.1.4 + resolution: "@eslint/eslintrc@npm:2.1.4" dependencies: ajv: ^6.12.4 debug: ^4.3.2 @@ -527,14 +534,14 @@ __metadata: js-yaml: ^4.1.0 minimatch: ^3.1.2 strip-json-comments: ^3.1.1 - checksum: bc742a1e3b361f06fedb4afb6bf32cbd27171292ef7924f61c62f2aed73048367bcc7ac68f98c06d4245cd3fabc43270f844e3c1699936d4734b3ac5398814a7 + checksum: 10957c7592b20ca0089262d8c2a8accbad14b4f6507e35416c32ee6b4dbf9cad67dfb77096bbd405405e9ada2b107f3797fe94362e1c55e0b09d6e90dd149127 languageName: node linkType: hard -"@eslint/js@npm:8.49.0": - version: 8.49.0 - resolution: "@eslint/js@npm:8.49.0" - checksum: a6601807c8aeeefe866926ad92ed98007c034a735af20ff709009e39ad1337474243d47908500a3bde04e37bfba16bcf1d3452417f962e1345bc8756edd6b830 +"@eslint/js@npm:8.56.0": + version: 8.56.0 + resolution: "@eslint/js@npm:8.56.0" + checksum: 5804130574ef810207bdf321c265437814e7a26f4e6fac9b496de3206afd52f533e09ec002a3be06cd9adcc9da63e727f1883938e663c4e4751c007d5b58e539 languageName: node linkType: hard @@ -582,6 +589,13 @@ __metadata: languageName: node linkType: hard +"@gar/promisify@npm:^1.1.3": + version: 1.1.3 + resolution: "@gar/promisify@npm:1.1.3" + checksum: 4059f790e2d07bf3c3ff3e0fec0daa8144fe35c1f6e0111c9921bd32106adaa97a4ab096ad7dab1e28ee6a9060083c4d1a4ada42a7f5f3f7a96b8812e2b757c1 + languageName: node + linkType: hard + "@gulpjs/to-absolute-glob@npm:^4.0.0": version: 4.0.0 resolution: "@gulpjs/to-absolute-glob@npm:4.0.0" @@ -591,14 +605,14 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/config-array@npm:^0.11.11": - version: 0.11.11 - resolution: "@humanwhocodes/config-array@npm:0.11.11" +"@humanwhocodes/config-array@npm:^0.11.13": + version: 0.11.14 + resolution: "@humanwhocodes/config-array@npm:0.11.14" dependencies: - "@humanwhocodes/object-schema": ^1.2.1 - debug: ^4.1.1 + "@humanwhocodes/object-schema": ^2.0.2 + debug: ^4.3.1 minimatch: ^3.0.5 - checksum: db84507375ab77b8ffdd24f498a5b49ad6b64391d30dd2ac56885501d03964d29637e05b1ed5aefa09d57ac667e28028bc22d2da872bfcd619652fbdb5f4ca19 + checksum: 861ccce9eaea5de19546653bccf75bf09fe878bc39c3aab00aeee2d2a0e654516adad38dd1098aab5e3af0145bbcbf3f309bdf4d964f8dab9dcd5834ae4c02f2 languageName: node linkType: hard @@ -609,10 +623,10 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/object-schema@npm:^1.2.1": - version: 1.2.1 - resolution: "@humanwhocodes/object-schema@npm:1.2.1" - checksum: a824a1ec31591231e4bad5787641f59e9633827d0a2eaae131a288d33c9ef0290bd16fda8da6f7c0fcb014147865d12118df10db57f27f41e20da92369fcb3f1 +"@humanwhocodes/object-schema@npm:^2.0.2": + version: 2.0.2 + resolution: "@humanwhocodes/object-schema@npm:2.0.2" + checksum: 2fc11503361b5fb4f14714c700c02a3f4c7c93e9acd6b87a29f62c522d90470f364d6161b03d1cc618b979f2ae02aed1106fd29d302695d8927e2fc8165ba8ee languageName: node linkType: hard @@ -956,24 +970,25 @@ __metadata: languageName: node linkType: hard -"@koa/cors@npm:^4.0.0": - version: 4.0.0 - resolution: "@koa/cors@npm:4.0.0" +"@koa/cors@npm:^5.0.0": + version: 5.0.0 + resolution: "@koa/cors@npm:5.0.0" dependencies: vary: ^1.1.2 - checksum: e0760544823532f2d71d792e3076858e38bab9b1c090abea175f1319fd91ea58a1da384a2fe7f5108f1c681e3830b01f62a1cafe271d6406751976af443187aa + checksum: 050701fb57dede2fefe0217459782bab7c9488fd07ff1f87fff680005cab43e03b7509e6015ea68082aadb1b31fe3eea7858ebdc93a2cf6f26d36d071190d50c languageName: node linkType: hard -"@koa/router@npm:^12.0.0": - version: 12.0.0 - resolution: "@koa/router@npm:12.0.0" +"@koa/router@npm:^12.0.1": + version: 12.0.1 + resolution: "@koa/router@npm:12.0.1" dependencies: + debug: ^4.3.4 http-errors: ^2.0.0 koa-compose: ^4.1.0 methods: ^1.1.2 path-to-regexp: ^6.2.1 - checksum: 5529629f7517dba20319cf70b66c5a6111673019f6d196cd500a7b90e6663c99848c188f1bce17415d46c7b20b06566d432299a53affe0a15d05b9213917b9f6 + checksum: 4b8d3940cbf898bca86bfe62d5d4633c4e436a537410c801f6ac3f5e804bdd257559439ab94408a22f3bd0f6b25d88859bfb80fe3a6f3954b73f79a6fb04a339 languageName: node linkType: hard @@ -1005,7 +1020,7 @@ __metadata: languageName: node linkType: hard -"@mos-connection/helper@npm:^3.0.4": +"@mos-connection/helper@npm:3.0.4": version: 3.0.4 resolution: "@mos-connection/helper@npm:3.0.4" dependencies: @@ -1018,13 +1033,20 @@ __metadata: languageName: node linkType: hard -"@mos-connection/model@npm:3.0.4, @mos-connection/model@npm:^3.0.4": +"@mos-connection/model@npm:3.0.4": version: 3.0.4 resolution: "@mos-connection/model@npm:3.0.4" checksum: 7de15fba9a818260fb57847dbc51695036720a7f499d2fe262c691630673e83c01bdc3ea62261529e661f957074a6d92e8d38305485293c5392e073dab33989b languageName: node linkType: hard +"@mos-connection/model@npm:^3.0.7": + version: 3.0.7 + resolution: "@mos-connection/model@npm:3.0.7" + checksum: 4f7390a40fc5152bfcdb4e99eee243e3a1df248bf1f77e0a63d7d29d8db88cb8c9df5f231ab5d18e7cb1147ba8d39af5c621877221f343db630906e000c5b089 + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -1052,6 +1074,16 @@ __metadata: languageName: node linkType: hard +"@npmcli/fs@npm:^2.1.0": + version: 2.1.2 + resolution: "@npmcli/fs@npm:2.1.2" + dependencies: + "@gar/promisify": ^1.1.3 + semver: ^7.3.5 + checksum: 405074965e72d4c9d728931b64d2d38e6ea12066d4fad651ac253d175e413c06fe4350970c783db0d749181da8fe49c42d3880bd1cbc12cd68e3a7964d820225 + languageName: node + linkType: hard + "@npmcli/fs@npm:^3.1.0": version: 3.1.0 resolution: "@npmcli/fs@npm:3.1.0" @@ -1061,6 +1093,16 @@ __metadata: languageName: node linkType: hard +"@npmcli/move-file@npm:^2.0.0": + version: 2.0.1 + resolution: "@npmcli/move-file@npm:2.0.1" + dependencies: + mkdirp: ^1.0.4 + rimraf: ^3.0.2 + checksum: 52dc02259d98da517fae4cb3a0a3850227bdae4939dda1980b788a7670636ca2b4a01b58df03dd5f65c1e3cb70c50fa8ce5762b582b3f499ec30ee5ce1fd9380 + languageName: node + linkType: hard + "@nrk/core-icons@npm:^9.6.0": version: 9.6.0 resolution: "@nrk/core-icons@npm:9.6.0" @@ -1146,21 +1188,20 @@ __metadata: languageName: node linkType: hard -"@rc-component/trigger@npm:^1.0.4": - version: 1.16.1 - resolution: "@rc-component/trigger@npm:1.16.1" +"@rc-component/trigger@npm:^1.18.0": + version: 1.18.3 + resolution: "@rc-component/trigger@npm:1.18.3" dependencies: - "@babel/runtime": ^7.18.3 + "@babel/runtime": ^7.23.2 "@rc-component/portal": ^1.1.0 classnames: ^2.3.2 - rc-align: ^4.0.0 rc-motion: ^2.0.0 rc-resize-observer: ^1.3.1 - rc-util: ^5.33.0 + rc-util: ^5.38.0 peerDependencies: react: ">=16.9.0" react-dom: ">=16.9.0" - checksum: 96d81193dd425cc376ef07bf89f801ecba844070818de9d127bdc7a3d220006ff344be2561502ff4126229052bf32873ec64cc77fac1407f69c38f4ea7585b54 + checksum: 272098e67b4c09e8ee8f4fa2e55054c05f2ea5196e26859a2fd1dfb63ee4f02e4ca7b60d15704defb59b34151f042ff906099aee20d67a01066d2f0b76dd0c58 languageName: node linkType: hard @@ -1333,7 +1374,7 @@ __metadata: fast-clone: ^1.5.13 i18next: ^21.10.0 influx: ^5.9.3 - nanoid: ^3.3.6 + nanoid: ^3.3.7 object-path: ^0.11.8 prom-client: ^14.2.0 timecode: 0.0.4 @@ -1365,9 +1406,9 @@ __metadata: "@sofie-automation/shared-lib": 1.51.0-in-development amqplib: ^0.10.3 deepmerge: ^4.3.1 - elastic-apm-node: ^3.50.0 + elastic-apm-node: ^3.51.0 eventemitter3: ^4.0.7 - mongodb: ^5.9.0 + mongodb: ^5.9.2 p-lazy: ^3.1.0 p-timeout: ^4.1.0 superfly-timeline: 9.0.0 @@ -1383,7 +1424,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/shared-lib@portal:../packages/shared-lib::locator=automation-core%40workspace%3A." dependencies: - "@mos-connection/model": ^3.0.4 + "@mos-connection/model": ^3.0.7 timeline-state-resolver-types: 9.1.0-nightly-release51-20231211-110613-75e2cbebd.0 tslib: ^2.6.2 type-fest: ^3.13.1 @@ -1420,10 +1461,10 @@ __metadata: languageName: node linkType: hard -"@types/app-root-path@npm:^1.2.5": - version: 1.2.5 - resolution: "@types/app-root-path@npm:1.2.5" - checksum: d6a08494dadae4a751ddbf28c821665b01e51e20fe35cb04a122ffd56305d4d0a04107f4d9d2003ee20336a2949050b93bddf8645905e3ecc8b5b95f1d084936 +"@types/app-root-path@npm:^1.2.8": + version: 1.2.8 + resolution: "@types/app-root-path@npm:1.2.8" + checksum: 540640e6408b81632271b878d3aeb911e437b9777903a4b671e5c085f6a244fa6069c7c835a3c2ac278c45e6092edbb450aaf5311c2810552a7426bf186bf56f languageName: node linkType: hard @@ -1468,13 +1509,13 @@ __metadata: languageName: node linkType: hard -"@types/body-parser@npm:*, @types/body-parser@npm:^1.19.3": - version: 1.19.3 - resolution: "@types/body-parser@npm:1.19.3" +"@types/body-parser@npm:*, @types/body-parser@npm:^1.19.5": + version: 1.19.5 + resolution: "@types/body-parser@npm:1.19.5" dependencies: "@types/connect": "*" "@types/node": "*" - checksum: 932fa71437c275023799123680ef26ffd90efd37f51a1abe405e6ae6e5b4ad9511b7a3a8f5a12877ed1444a02b6286c0a137a98e914b3c61932390c83643cc2c + checksum: 1e251118c4b2f61029cc43b0dc028495f2d1957fe8ee49a707fb940f86a9bd2f9754230805598278fe99958b49e9b7e66eec8ef6a50ab5c1f6b93e1ba2aaba82 languageName: node linkType: hard @@ -1522,10 +1563,10 @@ __metadata: languageName: node linkType: hard -"@types/deep-extend@npm:^0.6.0": - version: 0.6.0 - resolution: "@types/deep-extend@npm:0.6.0" - checksum: af697880adcd3567ec101129037ead85b3b5243e78dccd27471cbd55256ddac669310bf86bfde486f4773d21411ddc4aa0a31e71db9d74dfc11ff4997eea5e49 +"@types/deep-extend@npm:^0.6.2": + version: 0.6.2 + resolution: "@types/deep-extend@npm:0.6.2" + checksum: 16da22923b4b1375f23888c4c28d1fa962a489ea40457a2135833831dbe4870a75894d29b632680dd2bcdb72a0a5c214f5286483f3f70ba10d52f3c816b5abd7 languageName: node linkType: hard @@ -1553,10 +1594,10 @@ __metadata: languageName: node linkType: hard -"@types/fibers@npm:^3.1.2": - version: 3.1.2 - resolution: "@types/fibers@npm:3.1.2" - checksum: 047f07c77f24e643b62de7a747582606a2a703a3896fe989a8570fb178f5dffcb5c8c49e79d40c83b3da3ad10e28c3e30b3c19938b61c40437ca1d907f747ac2 +"@types/fibers@npm:^3.1.4": + version: 3.1.4 + resolution: "@types/fibers@npm:3.1.4" + checksum: 36bb70198fb5b7f99b010c006ad0e77d473061cf03d4f63f1842040b7ecb62e8800041c9509b927ec59a1d5ac374d5e7c86b2fe0de2a9813f1538ab0248e5dea languageName: node linkType: hard @@ -1615,13 +1656,13 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:^29.5.5": - version: 29.5.5 - resolution: "@types/jest@npm:29.5.5" +"@types/jest@npm:^29.5.11": + version: 29.5.11 + resolution: "@types/jest@npm:29.5.11" dependencies: expect: ^29.0.0 pretty-format: ^29.0.0 - checksum: 56e55cde9949bcc0ee2fa34ce5b7c32c2bfb20e53424aa4ff3a210859eeaaa3fdf6f42f81a3f655238039cdaaaf108b054b7a8602f394e6c52b903659338d8c6 + checksum: f892a06ec9f0afa9a61cd7fa316ec614e21d4df1ad301b5a837787e046fcb40dfdf7f264a55e813ac6b9b633cb9d366bd5b8d1cea725e84102477b366df23fdd languageName: node linkType: hard @@ -1650,12 +1691,12 @@ __metadata: languageName: node linkType: hard -"@types/koa-bodyparser@npm:^4.3.10": - version: 4.3.10 - resolution: "@types/koa-bodyparser@npm:4.3.10" +"@types/koa-bodyparser@npm:^4.3.12": + version: 4.3.12 + resolution: "@types/koa-bodyparser@npm:4.3.12" dependencies: "@types/koa": "*" - checksum: 4b4cd176815a6c1fb0d593bfea03de1285e606d3a96e56ad3691144e35061750ed95e4ecf2ff8e25599d360a93646e29dbb167fdfaaa73ccf87ca5b6141ff0db + checksum: 645cc253c6b9b2e98252b1cdc75a4812cd6d3c228e426f9893a755324b7a6936559ec659a0ff288cb2642340b3cc4e2110167f24b84efc8e3b89c04fe67ed883 languageName: node linkType: hard @@ -1668,9 +1709,9 @@ __metadata: languageName: node linkType: hard -"@types/koa@npm:*, @types/koa@npm:^2.13.9": - version: 2.13.9 - resolution: "@types/koa@npm:2.13.9" +"@types/koa@npm:*, @types/koa@npm:^2.14.0": + version: 2.14.0 + resolution: "@types/koa@npm:2.14.0" dependencies: "@types/accepts": "*" "@types/content-disposition": "*" @@ -1680,25 +1721,25 @@ __metadata: "@types/keygrip": "*" "@types/koa-compose": "*" "@types/node": "*" - checksum: af9cd599c8e17e2ae0f4168a61d964e343f713d002b65fd995658d7addc6551ccadecfd32b3405cf44e4d360178ee4f972d6881533548261ae1f636a655d24b1 + checksum: 57d809e42350c9ddefa2150306355e40757877468bb027e0bd99f5aeb43cfaf8ba8b14761ea65e419d6fb4c2403a1f3ed0762872a9cf040dbd14357caca56548 languageName: node linkType: hard -"@types/koa__cors@npm:^3.3.1": - version: 3.3.1 - resolution: "@types/koa__cors@npm:3.3.1" +"@types/koa__cors@npm:^5.0.0": + version: 5.0.0 + resolution: "@types/koa__cors@npm:5.0.0" dependencies: "@types/koa": "*" - checksum: 816303d34c1b92627cfaea5327d25cff86d8eb969e5866af4c3769ec158c4575ab5b6fa01145eec0eeb61019648bc165981becf3a13001ec67a83ef9994e0e29 + checksum: ad8e6a482f1bb0e357e0051faec328a75e2978a24065a953032d5dba58ac08edf5ca66b03059551f0faf9e085b15ee7892e6ab03c9500af4be8bd258965479c9 languageName: node linkType: hard -"@types/koa__router@npm:^12.0.1": - version: 12.0.1 - resolution: "@types/koa__router@npm:12.0.1" +"@types/koa__router@npm:^12.0.4": + version: 12.0.4 + resolution: "@types/koa__router@npm:12.0.4" dependencies: "@types/koa": "*" - checksum: 91da726e1f3bb848fc8b0bb3ce257aabe4ad175574b3a11bc38b16aacec93f86ec451a70f60f3e53f13df795ace17f7cb4d47ccb63720270a931c776b0b2db33 + checksum: 0740e3c8dc3af2d163231ba585d24c3bcc697469a3bd4f042988ca3f61ad9ebc5caa20961a4abf87e92996c97ac3f9fdf9f24925cf834a9a9d21cec10aa11e73 languageName: node linkType: hard @@ -1730,10 +1771,10 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^14.18.62": - version: 14.18.62 - resolution: "@types/node@npm:14.18.62" - checksum: 1335b4d58d2a21c7f60b8b78d0902ec0653a44f1ead4e921c37dd5f836910e0b00370abfde02c57bc3a73a35ebcbf4ef86b598be9a9ec13981a9a6d40e98fab0 +"@types/node@npm:^14.18.63": + version: 14.18.63 + resolution: "@types/node@npm:14.18.63" + checksum: be909061a54931778c71c49dc562586c32f909c4b6197e3d71e6dac726d8bd9fccb9f599c0df99f52742b68153712b5097c0f00cac4e279fa894b0ea6719a8fd languageName: node linkType: hard @@ -1744,10 +1785,10 @@ __metadata: languageName: node linkType: hard -"@types/prop-types@npm:*, @types/prop-types@npm:^15.7.6": - version: 15.7.6 - resolution: "@types/prop-types@npm:15.7.6" - checksum: 5f2796c7330461a556c4d18035fb914b372f96b1619a4f8302d07e1ea708e06a2dbe666dfcd8ff03f64c625aa4c12b31f677d0298a32910f5ab7ee51521d8086 +"@types/prop-types@npm:*, @types/prop-types@npm:^15.7.11": + version: 15.7.11 + resolution: "@types/prop-types@npm:15.7.11" + checksum: 7519ff11d06fbf6b275029fe03fff9ec377b4cb6e864cac34d87d7146c7f5a7560fd164bdc1d2dbe00b60c43713631251af1fd3d34d46c69cd354602bc0c7c54 languageName: node linkType: hard @@ -1785,12 +1826,12 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:^18.2.7": - version: 18.2.7 - resolution: "@types/react-dom@npm:18.2.7" +"@types/react-dom@npm:^18.2.18": + version: 18.2.18 + resolution: "@types/react-dom@npm:18.2.18" dependencies: "@types/react": "*" - checksum: e02ea908289a7ad26053308248d2b87f6aeafd73d0e2de2a3d435947bcea0422599016ffd1c3e38ff36c42f5e1c87c7417f05b0a157e48649e4a02f21727d54f + checksum: 8e3da404c980e2b2a76da3852f812ea6d8b9d0e7f5923fbaf3bfbbbfa1d59116ff91c129de8f68e9b7668a67ae34484fe9df74d5a7518cf8591ec07a0c4dad57 languageName: node linkType: hard @@ -1815,26 +1856,26 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:*, @types/react@npm:^18.2.22": - version: 18.2.22 - resolution: "@types/react@npm:18.2.22" +"@types/react@npm:*, @types/react@npm:^18.2.51": + version: 18.2.51 + resolution: "@types/react@npm:18.2.51" dependencies: "@types/prop-types": "*" "@types/scheduler": "*" csstype: ^3.0.2 - checksum: 44289523dabaadcd3fd85689abb98f9ebcc8492d7e978348d1c986138acef4801030b279e89a19e38a6319e294bcea77559e37e0c803e4bacf2b8ae3a56ba587 + checksum: 1ab49e51e6314cefb9a162e9ac3a15a0474c82a0737f12f3a9628ccba555ab6645d2449152e7a5fc3ff650a7139a4718715af2c5ae9bc46b0533e98036706f84 languageName: node linkType: hard -"@types/request@npm:^2.48.8": - version: 2.48.8 - resolution: "@types/request@npm:2.48.8" +"@types/request@npm:^2.48.12": + version: 2.48.12 + resolution: "@types/request@npm:2.48.12" dependencies: "@types/caseless": "*" "@types/node": "*" "@types/tough-cookie": "*" form-data: ^2.5.0 - checksum: 0b7754941e08205dce51635d894ec524df276d2b83ca13b9aab723f9281acecf1108841e9554494cb1cb60f6d6ddbb47ebea97392bcf2bf607f035b3a9b4af45 + checksum: 20dfad0a46b4249bf42f09c51fbd4d02ec6738c5152194b5c7c69bab80b00eae9cc71df4489ffa929d0968d453ef7d0823d1f98871efed563a4fdb57bf0a4c58 languageName: node linkType: hard @@ -1845,10 +1886,10 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:^7.3.12, @types/semver@npm:^7.5.2": - version: 7.5.2 - resolution: "@types/semver@npm:7.5.2" - checksum: 743aa8a2b58e20b329c19bd2459152cb049d12fafab7279b90ac11e0f268c97efbcb606ea0c681cca03f79015381b40d9b1244349b354270bec3f939ed49f6e9 +"@types/semver@npm:^7.3.12, @types/semver@npm:^7.5.6": + version: 7.5.6 + resolution: "@types/semver@npm:7.5.6" + checksum: 563a0120ec0efcc326567db2ed920d5d98346f3638b6324ea6b50222b96f02a8add3c51a916b6897b51523aad8ac227d21d3dcf8913559f1bfc6c15b14d23037 languageName: node linkType: hard @@ -1873,12 +1914,12 @@ __metadata: languageName: node linkType: hard -"@types/sinon@npm:^10.0.16": - version: 10.0.16 - resolution: "@types/sinon@npm:10.0.16" +"@types/sinon@npm:^10.0.20": + version: 10.0.20 + resolution: "@types/sinon@npm:10.0.20" dependencies: "@types/sinonjs__fake-timers": "*" - checksum: 1216aac584500d6bf845ca76f57e82f8459cf9de4ed80a55e50aa4438360fc418789a42181e211c5d279e97f86a3a994e3c81e43971d540737caca0193242bbf + checksum: 7322771345c202b90057f8112e0d34b7339e5ae1827fb1bfe385fc9e38ed6a2f18b4c66e88d27d98c775f7f74fb1167c0c14f61ca64155786534541e6c6eb05f languageName: node linkType: hard @@ -1910,10 +1951,10 @@ __metadata: languageName: node linkType: hard -"@types/underscore@npm:1.11.9": - version: 1.11.9 - resolution: "@types/underscore@npm:1.11.9" - checksum: 432a65271cb5567784aeccd99aeea9af6a8bef00c709c3c6ef9f161a9ad03a9c8fc6ebfbff4ff9c26c474b84a4381d201cfc24a072225868755eef95f5152e72 +"@types/underscore@npm:1.11.15": + version: 1.11.15 + resolution: "@types/underscore@npm:1.11.15" + checksum: 25fdf6da96e0d11ca39a4740aab6fa3bd717e57301be4cb9e7893dc0ad6ce330992d0c8e0b02cac5c5ea86df6f8949c5a8f1fb95f3666b85418d399d3b1112e9 languageName: node linkType: hard @@ -1934,12 +1975,12 @@ __metadata: languageName: node linkType: hard -"@types/xml2js@npm:^0.4.12": - version: 0.4.12 - resolution: "@types/xml2js@npm:0.4.12" +"@types/xml2js@npm:^0.4.14": + version: 0.4.14 + resolution: "@types/xml2js@npm:0.4.14" dependencies: "@types/node": "*" - checksum: 6197f6d51d70ba7e6d3169ef6d58adacfeddeb2d8cfe4d2bb24eda86223c7dd77c82896c3aa3c636b3b1442cebc6d05959d1e65fa206dac2caeb99aa2c943716 + checksum: df9f106b9953dcdec7ba3304ebc56d6c2f61d49bf556d600bed439f94a1733f73ca0bf2d0f64330b402191622862d9d6058bab9d7e3dcb5b0fe51ebdc4372aac languageName: node linkType: hard @@ -2080,6 +2121,13 @@ __metadata: languageName: node linkType: hard +"@ungap/structured-clone@npm:^1.2.0": + version: 1.2.0 + resolution: "@ungap/structured-clone@npm:1.2.0" + checksum: 4f656b7b4672f2ce6e272f2427d8b0824ed11546a601d8d5412b9d7704e83db38a8d9f402ecdf2b9063fc164af842ad0ec4a55819f621ed7e7ea4d1efcc74524 + languageName: node + linkType: hard + "@welldone-software/why-did-you-render@npm:^4.3.2": version: 4.3.2 resolution: "@welldone-software/why-did-you-render@npm:4.3.2" @@ -2637,18 +2685,18 @@ __metadata: version: 0.0.0-use.local resolution: "automation-core@workspace:." dependencies: - "@babel/core": ^7.22.20 - "@babel/plugin-transform-modules-commonjs": ^7.22.15 - "@babel/runtime": ^7.22.15 + "@babel/core": ^7.23.9 + "@babel/plugin-transform-modules-commonjs": ^7.23.3 + "@babel/runtime": ^7.23.9 "@crello/react-lottie": 0.0.9 "@fortawesome/fontawesome-free": ^6.4.2 "@fortawesome/fontawesome-svg-core": ~6.4.2 "@fortawesome/free-solid-svg-icons": ^6.4.2 "@fortawesome/react-fontawesome": ^0.2.0 "@jstarpl/react-contextmenu": ^2.15.0 - "@koa/cors": ^4.0.0 - "@koa/router": ^12.0.0 - "@mos-connection/helper": ^3.0.4 + "@koa/cors": ^5.0.0 + "@koa/router": ^12.0.1 + "@mos-connection/helper": 3.0.4 "@nrk/core-icons": ^9.6.0 "@popperjs/core": ^2.11.8 "@shopify/jest-koa-mocks": ^5.1.1 @@ -2660,29 +2708,29 @@ __metadata: "@sofie-automation/job-worker": "portal:../packages/job-worker" "@sofie-automation/shared-lib": "portal:../packages/shared-lib" "@sofie-automation/sorensen": ^1.4.2 - "@types/app-root-path": ^1.2.5 - "@types/body-parser": ^1.19.3 + "@types/app-root-path": ^1.2.8 + "@types/body-parser": ^1.19.5 "@types/classnames": ^2.3.1 - "@types/deep-extend": ^0.6.0 - "@types/fibers": ^3.1.2 - "@types/jest": ^29.5.5 - "@types/koa": ^2.13.9 - "@types/koa-bodyparser": ^4.3.10 - "@types/koa__cors": ^3.3.1 - "@types/koa__router": ^12.0.1 - "@types/node": ^14.18.62 - "@types/prop-types": ^15.7.6 - "@types/react": ^18.2.22 + "@types/deep-extend": ^0.6.2 + "@types/fibers": ^3.1.4 + "@types/jest": ^29.5.11 + "@types/koa": ^2.14.0 + "@types/koa-bodyparser": ^4.3.12 + "@types/koa__cors": ^5.0.0 + "@types/koa__router": ^12.0.4 + "@types/node": ^14.18.63 + "@types/prop-types": ^15.7.11 + "@types/react": ^18.2.51 "@types/react-circular-progressbar": ^1.1.0 "@types/react-datepicker": ^3.1.8 - "@types/react-dom": ^18.2.7 + "@types/react-dom": ^18.2.18 "@types/react-router": ^5.1.20 "@types/react-router-dom": ^5.3.3 - "@types/request": ^2.48.8 - "@types/semver": ^7.5.2 - "@types/sinon": ^10.0.16 - "@types/underscore": 1.11.9 - "@types/xml2js": ^0.4.12 + "@types/request": ^2.48.12 + "@types/semver": ^7.5.6 + "@types/sinon": ^10.0.20 + "@types/underscore": 1.11.15 + "@types/xml2js": ^0.4.14 "@typescript-eslint/eslint-plugin": ^5.62.0 "@typescript-eslint/parser": ^5.62.0 "@typescript-eslint/utils": ^5.62.0 @@ -2692,16 +2740,16 @@ __metadata: babel-jest: ^29.7.0 bcrypt: ^5.1.1 body-parser: ^1.20.2 - classnames: ^2.3.2 - core-js: ^3.32.2 + classnames: ^2.5.1 + core-js: ^3.35.1 cubic-spline: ^3.0.3 deep-extend: 0.6.0 deepmerge: ^4.3.1 ejson: ^2.2.3 - eslint: ^8.49.0 + eslint: ^8.56.0 eslint-config-prettier: ^8.10.0 eslint-plugin-custom-rules: "link:eslint-rules" - eslint-plugin-jest: ^27.4.0 + eslint-plugin-jest: ^27.6.3 eslint-plugin-node: ^11.1.0 eslint-plugin-prettier: ^4.2.1 eslint-plugin-react: ^7.33.2 @@ -2717,15 +2765,15 @@ __metadata: indexof: 0.0.1 jest: ^29.7.0 jest-environment-jsdom: ^29.7.0 - koa: ^2.14.2 + koa: ^2.15.0 koa-bodyparser: ^4.4.1 legally: ^3.5.10 lottie-web: ^5.12.2 - meteor-node-stubs: ^1.2.5 + meteor-node-stubs: ^1.2.7 meteor-promise: 0.9.0 - moment: ^2.29.4 - nanoid: ^3.3.6 - node-gyp: ^9.4.0 + moment: ^2.30.1 + nanoid: ^3.3.7 + node-gyp: ^9.4.1 ntp-client: ^0.5.3 object-path: ^0.11.8 open-cli: ^7.2.0 @@ -2735,7 +2783,7 @@ __metadata: promise.allsettled: ^1.0.7 prop-types: ^15.8.1 query-string: ^6.14.1 - rc-tooltip: ^6.0.1 + rc-tooltip: ^6.1.3 react: ^18.2.0 react-circular-progressbar: ^2.1.0 react-datepicker: ^3.8.0 @@ -2744,7 +2792,7 @@ __metadata: react-dom: ^18.2.0 react-hotkeys: ^2.0.0 react-i18next: ^11.18.6 - react-intersection-observer: ^9.5.2 + react-intersection-observer: ^9.6.0 react-moment: ^0.9.7 react-popper: ^2.3.0 react-router-dom: ^5.3.4 @@ -2755,16 +2803,16 @@ __metadata: superfly-timeline: 9.0.0 threadedclass: ^1.2.1 timecode: 0.0.4 - ts-jest: ^29.1.1 + ts-jest: ^29.1.2 type-fest: ^3.13.1 - typescript: ^4.9.4 + typescript: ^4.9.5 underscore: ^1.13.6 velocity-animate: ^1.5.2 velocity-react: ^1.4.3 vm2: ^3.9.19 webmidi: ^2.5.3 - winston: ^3.10.0 - xml2js: ^0.4.23 + winston: ^3.11.0 + xml2js: ^0.6.2 xmlbuilder: ^15.1.1 yargs: ^17.7.2 languageName: unknown @@ -3087,17 +3135,17 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.21.9": - version: 4.21.10 - resolution: "browserslist@npm:4.21.10" +"browserslist@npm:^4.22.2": + version: 4.22.3 + resolution: "browserslist@npm:4.22.3" dependencies: - caniuse-lite: ^1.0.30001517 - electron-to-chromium: ^1.4.477 - node-releases: ^2.0.13 - update-browserslist-db: ^1.0.11 + caniuse-lite: ^1.0.30001580 + electron-to-chromium: ^1.4.648 + node-releases: ^2.0.14 + update-browserslist-db: ^1.0.13 bin: browserslist: cli.js - checksum: 1e27c0f111a35d1dd0e8fc2c61781b0daefabc2c9471b0b10537ce54843014bceb2a1ce4571af1a82b2bf1e6e6e05d38865916689a158f03bc2c7a4ec2577db8 + checksum: e62b17348e92143fe58181b02a6a97c4a98bd812d1dc9274673a54f73eec53dbed1c855ebf73e318ee00ee039f23c9a6d0e7629d24f3baef08c7a5b469742d57 languageName: node linkType: hard @@ -3190,6 +3238,32 @@ __metadata: languageName: node linkType: hard +"cacache@npm:^16.1.0": + version: 16.1.3 + resolution: "cacache@npm:16.1.3" + dependencies: + "@npmcli/fs": ^2.1.0 + "@npmcli/move-file": ^2.0.0 + chownr: ^2.0.0 + fs-minipass: ^2.1.0 + glob: ^8.0.1 + infer-owner: ^1.0.4 + lru-cache: ^7.7.1 + minipass: ^3.1.6 + minipass-collect: ^1.0.2 + minipass-flush: ^1.0.5 + minipass-pipeline: ^1.2.4 + mkdirp: ^1.0.4 + p-map: ^4.0.0 + promise-inflight: ^1.0.1 + rimraf: ^3.0.2 + ssri: ^9.0.0 + tar: ^6.1.11 + unique-filename: ^2.0.0 + checksum: d91409e6e57d7d9a3a25e5dcc589c84e75b178ae8ea7de05cbf6b783f77a5fae938f6e8fda6f5257ed70000be27a681e1e44829251bfffe4c10216002f8f14e6 + languageName: node + linkType: hard + "cacache@npm:^17.0.0": version: 17.1.4 resolution: "cacache@npm:17.1.4" @@ -3281,10 +3355,10 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.30001517": - version: 1.0.30001538 - resolution: "caniuse-lite@npm:1.0.30001538" - checksum: 94c5d55757a339c7cc175f08a024671e2b4e7c04f130b1015793303d637061347efb6ad84447c3b8137333e742d150b8ad9672716bbf2482646c2e63a56f6c55 +"caniuse-lite@npm:^1.0.30001580": + version: 1.0.30001582 + resolution: "caniuse-lite@npm:1.0.30001582" + checksum: 2fc420cb6e6080a9808781ff81a2f0d37d63897c8c981d477001be18e55c8ec33422cf966e49efdca61d4a5335d16a4d6a09d5bd6da22a8396d0dcd350b9b9da languageName: node linkType: hard @@ -3354,10 +3428,10 @@ __metadata: languageName: node linkType: hard -"classnames@npm:*, classnames@npm:2.x, classnames@npm:^2.2.1, classnames@npm:^2.2.5, classnames@npm:^2.2.6, classnames@npm:^2.3.1, classnames@npm:^2.3.2": - version: 2.3.2 - resolution: "classnames@npm:2.3.2" - checksum: 2c62199789618d95545c872787137262e741f9db13328e216b093eea91c85ef2bfb152c1f9e63027204e2559a006a92eb74147d46c800a9f96297ae1d9f96f4e +"classnames@npm:*, classnames@npm:^2.2.1, classnames@npm:^2.2.5, classnames@npm:^2.2.6, classnames@npm:^2.3.1, classnames@npm:^2.3.2, classnames@npm:^2.5.1": + version: 2.5.1 + resolution: "classnames@npm:2.5.1" + checksum: da424a8a6f3a96a2e87d01a432ba19315503294ac7e025f9fece656db6b6a0f7b5003bb1fbb51cbb0d9624d964f1b9bb35a51c73af9b2434c7b292c42231c1e5 languageName: node linkType: hard @@ -3844,7 +3918,7 @@ __metadata: languageName: node linkType: hard -"convert-source-map@npm:^1.6.0, convert-source-map@npm:^1.7.0": +"convert-source-map@npm:^1.6.0": version: 1.9.0 resolution: "convert-source-map@npm:1.9.0" checksum: dc55a1f28ddd0e9485ef13565f8f756b342f9a46c4ae18b843fe3c30c675d058d6a4823eff86d472f187b176f0adf51ea7b69ea38be34be4a63cbbf91b0593c8 @@ -3865,13 +3939,13 @@ __metadata: languageName: node linkType: hard -"cookies@npm:~0.8.0": - version: 0.8.0 - resolution: "cookies@npm:0.8.0" +"cookies@npm:~0.9.0": + version: 0.9.1 + resolution: "cookies@npm:0.9.1" dependencies: depd: ~2.0.0 keygrip: ~1.1.0 - checksum: 806055a44f128705265b1bc6a853058da18bf80dea3654ad99be20985b1fa1b14f86c1eef73644aab8071241f8a78acd57202b54c4c5c70769fc694fbb9c4edc + checksum: 213e4d14847b582fbd8a003203d3621a4b9fa792a315c37954e89332d38fac5bcc34ba92ef316ad6d5fe28f0187aaa115927fbbe2080744ad1707a93b4313247 languageName: node linkType: hard @@ -3882,10 +3956,10 @@ __metadata: languageName: node linkType: hard -"core-js@npm:^3.32.2": - version: 3.32.2 - resolution: "core-js@npm:3.32.2" - checksum: d6fac7e8eb054eefc211c76cd0a0ff07447a917122757d085f469f046ec888d122409c7db1a9601c3eb5fa767608ed380bcd219eace02bdf973da155680edeec +"core-js@npm:^3.35.1": + version: 3.35.1 + resolution: "core-js@npm:3.35.1" + checksum: e246af6b634be3763ffe3ce6ac4601b4dc5b928006fb6c95e5d08ecd82a2413bf36f00ffe178b89c9a8e94000288933a78a9881b2c9498e6cf312b031013b952 languageName: node linkType: hard @@ -4078,7 +4152,7 @@ __metadata: 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.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -4397,13 +4471,6 @@ __metadata: languageName: node linkType: hard -"dom-align@npm:^1.7.0": - version: 1.12.4 - resolution: "dom-align@npm:1.12.4" - checksum: ff5cfdb6e9c9e03e6d67a61b4633f25845f2385f67b1bd84a28aa2cb2c6b58eea53fde347b0d2439f0ba49cd6b80a7463f98569731cb14ec2542ecdeef19d165 - languageName: node - linkType: hard - "dom-helpers@npm:^3.4.0": version: 3.4.0 resolution: "dom-helpers@npm:3.4.0" @@ -4469,9 +4536,9 @@ __metadata: languageName: node linkType: hard -"elastic-apm-node@npm:^3.50.0": - version: 3.50.0 - resolution: "elastic-apm-node@npm:3.50.0" +"elastic-apm-node@npm:^3.51.0": + version: 3.51.0 + resolution: "elastic-apm-node@npm:3.51.0" dependencies: "@elastic/ecs-pino-format": ^1.2.0 "@opentelemetry/api": ^1.4.1 @@ -4511,14 +4578,14 @@ __metadata: sql-summary: ^1.0.1 stream-chopper: ^3.0.1 unicode-byte-truncate: ^1.0.0 - checksum: 485b742d30202e4436286d50c019bad0115b92c40f49130494e7bc35affb1e1dd764c6a14b6d21c54acf333f1925396fe5ab2ef86ce3bd5edc954e38fdc4e64e + checksum: e6a801e731d6a5178e7450c76e88b9a519823129986365b2f59c4ee8e02c2a0e624deacebce6e85ae0964f6ea876a9f562901ca0b3538f4b0452a24d7f1b0303 languageName: node linkType: hard -"electron-to-chromium@npm:^1.4.477": - version: 1.4.525 - resolution: "electron-to-chromium@npm:1.4.525" - checksum: b713e2a3f1f2a2a0303362e40012d548a2ba8b5edb5e2b71350afdc257d7f4e54e9c4a7f1eb331443fdd77093a4761617bfa418528baaafb7f05669ba4814f4a +"electron-to-chromium@npm:^1.4.648": + version: 1.4.653 + resolution: "electron-to-chromium@npm:1.4.653" + checksum: 5e1fb48e749811f4384cd7a9940585a124f46d2a9f5bdfd2d7e79685d55db448433da303bd0fb6c3665ed052299843b5661319e10119821defbc3be8ff8eb060 languageName: node linkType: hard @@ -4856,9 +4923,9 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-jest@npm:^27.2.1, eslint-plugin-jest@npm:^27.4.0": - version: 27.4.0 - resolution: "eslint-plugin-jest@npm:27.4.0" +"eslint-plugin-jest@npm:^27.2.1, eslint-plugin-jest@npm:^27.6.3": + version: 27.6.3 + resolution: "eslint-plugin-jest@npm:27.6.3" dependencies: "@typescript-eslint/utils": ^5.10.0 peerDependencies: @@ -4870,7 +4937,7 @@ __metadata: optional: true jest: optional: true - checksum: c33593dba87e750123555c2de32fb174d6f2c92342571492f8dbde01bf61a8ac229dff31bd08fea16c3ca2c4843fc2fec985459c351319c019016767ed1cd78e + checksum: e22e8dbd941b34bb95958f035ffabb94114506b294e74d6e411bc85bc9dc57888ffd3ebb5c28316a8b7cc9d391cca35557acc64bf815f48d1dcc5ea3d28fa43a languageName: node linkType: hard @@ -4974,17 +5041,18 @@ __metadata: languageName: node linkType: hard -"eslint@npm:^8.39.0, eslint@npm:^8.49.0": - version: 8.49.0 - resolution: "eslint@npm:8.49.0" +"eslint@npm:^8.39.0, eslint@npm:^8.56.0": + version: 8.56.0 + resolution: "eslint@npm:8.56.0" dependencies: "@eslint-community/eslint-utils": ^4.2.0 "@eslint-community/regexpp": ^4.6.1 - "@eslint/eslintrc": ^2.1.2 - "@eslint/js": 8.49.0 - "@humanwhocodes/config-array": ^0.11.11 + "@eslint/eslintrc": ^2.1.4 + "@eslint/js": 8.56.0 + "@humanwhocodes/config-array": ^0.11.13 "@humanwhocodes/module-importer": ^1.0.1 "@nodelib/fs.walk": ^1.2.8 + "@ungap/structured-clone": ^1.2.0 ajv: ^6.12.4 chalk: ^4.0.0 cross-spawn: ^7.0.2 @@ -5017,7 +5085,7 @@ __metadata: text-table: ^0.2.0 bin: eslint: bin/eslint.js - checksum: 4dfe257e1e42da2f9da872b05aaaf99b0f5aa022c1a91eee8f2af1ab72651b596366320c575ccd4e0469f7b4c97aff5bb85ae3323ebd6a293c3faef4028b0d81 + checksum: 883436d1e809b4a25d9eb03d42f584b84c408dbac28b0019f6ea07b5177940bf3cca86208f749a6a1e0039b63e085ee47aca1236c30721e91f0deef5cc5a5136 languageName: node linkType: hard @@ -5498,7 +5566,7 @@ __metadata: languageName: node linkType: hard -"fs-minipass@npm:^2.0.0": +"fs-minipass@npm:^2.0.0, fs-minipass@npm:^2.1.0": version: 2.1.0 resolution: "fs-minipass@npm:2.1.0" dependencies: @@ -5803,7 +5871,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^8.1.0": +"glob@npm:^8.0.1, glob@npm:^8.1.0": version: 8.1.0 resolution: "glob@npm:8.1.0" dependencies: @@ -6103,7 +6171,7 @@ __metadata: languageName: node linkType: hard -"http-cache-semantics@npm:^4.1.1": +"http-cache-semantics@npm:^4.1.0, http-cache-semantics@npm:^4.1.1": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" checksum: 83ac0bc60b17a3a36f9953e7be55e5c8f41acc61b22583060e8dedc9dd5e3607c823a88d0926f9150e571f90946835c7fe150732801010845c72cd8bbff1a236 @@ -6387,6 +6455,13 @@ __metadata: languageName: node linkType: hard +"infer-owner@npm:^1.0.4": + version: 1.0.4 + resolution: "infer-owner@npm:1.0.4" + checksum: 181e732764e4a0611576466b4b87dac338972b839920b2a8cde43642e4ed6bd54dc1fb0b40874728f2a2df9a1b097b8ff83b56d5f8f8e3927f837fdcb47d8a89 + languageName: node + linkType: hard + "inflation@npm:^2.0.0": version: 2.0.0 resolution: "inflation@npm:2.0.0" @@ -7698,15 +7773,15 @@ __metadata: languageName: node linkType: hard -"koa@npm:^2.13.4, koa@npm:^2.14.2": - version: 2.14.2 - resolution: "koa@npm:2.14.2" +"koa@npm:^2.13.4, koa@npm:^2.15.0": + version: 2.15.0 + resolution: "koa@npm:2.15.0" dependencies: accepts: ^1.3.5 cache-content-type: ^1.0.0 content-disposition: ~0.5.2 content-type: ^1.0.4 - cookies: ~0.8.0 + cookies: ~0.9.0 debug: ^4.3.2 delegates: ^1.0.0 depd: ^2.0.0 @@ -7725,7 +7800,7 @@ __metadata: statuses: ^1.5.0 type-is: ^1.6.16 vary: ^1.1.2 - checksum: 17fe3b8f5e0b4759004a942cc6ba2a9507299943a697dff9766b85f41f45caed4077ca2645ac9ad254d3359fffedfc4c9ebdd7a70493e5df8cdfac159a8ee835 + checksum: a97741f89f328f25ae94d82d0ee608377d89e086c73f2d868023e6050dea682ef93e0a5c80097f3aaad28121853aea50a7fb3c0c12ecc45798da2fd1255f580b languageName: node linkType: hard @@ -8062,6 +8137,30 @@ __metadata: languageName: node linkType: hard +"make-fetch-happen@npm:^10.0.3": + version: 10.2.1 + resolution: "make-fetch-happen@npm:10.2.1" + dependencies: + agentkeepalive: ^4.2.1 + cacache: ^16.1.0 + http-cache-semantics: ^4.1.0 + http-proxy-agent: ^5.0.0 + https-proxy-agent: ^5.0.0 + is-lambda: ^1.0.1 + lru-cache: ^7.7.1 + minipass: ^3.1.6 + minipass-collect: ^1.0.2 + minipass-fetch: ^2.0.3 + minipass-flush: ^1.0.5 + minipass-pipeline: ^1.2.4 + negotiator: ^0.6.3 + promise-retry: ^2.0.1 + socks-proxy-agent: ^7.0.0 + ssri: ^9.0.0 + checksum: 2332eb9a8ec96f1ffeeea56ccefabcb4193693597b132cd110734d50f2928842e22b84cfa1508e921b8385cdfd06dda9ad68645fed62b50fff629a580f5fb72c + languageName: node + linkType: hard + "make-fetch-happen@npm:^11.0.3": version: 11.1.1 resolution: "make-fetch-happen@npm:11.1.1" @@ -8222,9 +8321,9 @@ __metadata: languageName: node linkType: hard -"meteor-node-stubs@npm:^1.2.5": - version: 1.2.5 - resolution: "meteor-node-stubs@npm:1.2.5" +"meteor-node-stubs@npm:^1.2.7": + version: 1.2.7 + resolution: "meteor-node-stubs@npm:1.2.7" dependencies: assert: ^2.0.0 browserify-zlib: ^0.2.0 @@ -8250,7 +8349,7 @@ __metadata: url: ^0.11.0 util: ^0.12.4 vm-browserify: ^1.1.2 - checksum: 2529bce377342b2c01f97c397fe89490fce0149ecb37dba1b18d2f865753a25addea2c16dd212afcaa6b9aa01abec52c90721b65653d15ff59708d5bd9adef15 + checksum: 4e2c17b19cc3b1aa5e098a94008d74ebbed72b6ce1e855cd93d0e94f93b9c1db958dfb7ad9fd5126a9c815ff552cbde6c136b4c768804cf875b91da29c9469c8 languageName: node linkType: hard @@ -8404,6 +8503,21 @@ __metadata: languageName: node linkType: hard +"minipass-fetch@npm:^2.0.3": + version: 2.1.2 + resolution: "minipass-fetch@npm:2.1.2" + dependencies: + encoding: ^0.1.13 + minipass: ^3.1.6 + minipass-sized: ^1.0.3 + minizlib: ^2.1.2 + dependenciesMeta: + encoding: + optional: true + checksum: 3f216be79164e915fc91210cea1850e488793c740534985da017a4cbc7a5ff50506956d0f73bb0cb60e4fe91be08b6b61ef35101706d3ef5da2c8709b5f08f91 + languageName: node + linkType: hard + "minipass-fetch@npm:^3.0.0": version: 3.0.4 resolution: "minipass-fetch@npm:3.0.4" @@ -8446,7 +8560,7 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^3.0.0": +"minipass@npm:^3.0.0, minipass@npm:^3.1.1, minipass@npm:^3.1.6": version: 3.3.6 resolution: "minipass@npm:3.3.6" dependencies: @@ -8513,10 +8627,10 @@ __metadata: languageName: node linkType: hard -"moment@npm:^2.29.4": - version: 2.29.4 - resolution: "moment@npm:2.29.4" - checksum: 0ec3f9c2bcba38dc2451b1daed5daded747f17610b92427bebe1d08d48d8b7bdd8d9197500b072d14e326dd0ccf3e326b9e3d07c5895d3d49e39b6803b76e80e +"moment@npm:^2.30.1": + version: 2.30.1 + resolution: "moment@npm:2.30.1" + checksum: 859236bab1e88c3e5802afcf797fc801acdbd0ee509d34ea3df6eea21eb6bcc2abd4ae4e4e64aa7c986aa6cba563c6e62806218e6412a765010712e5fa121ba6 languageName: node linkType: hard @@ -8530,9 +8644,9 @@ __metadata: languageName: node linkType: hard -"mongodb@npm:^5.9.0": - version: 5.9.0 - resolution: "mongodb@npm:5.9.0" +"mongodb@npm:^5.9.2": + version: 5.9.2 + resolution: "mongodb@npm:5.9.2" dependencies: "@mongodb-js/saslprep": ^1.1.0 bson: ^5.5.0 @@ -8558,7 +8672,7 @@ __metadata: optional: true snappy: optional: true - checksum: 5fb828d6503b7291c0bcc984ed35bdd0e2f166cd9d637a5ff9e8c2e497ef6260eea5a3f700f2850200cd8108057c858f96e37394ed2953aff3b85fe760bbd777 + checksum: fad5621f4a9764d71c48f2955be57925e506b14a4a63c5b9209ff733a58b2524cf9c87f216f2f02de092fab20eaa033ff5ade45c5cfa7f1c8c6bce114cb22e3f languageName: node linkType: hard @@ -8590,12 +8704,12 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^3.3.6": - version: 3.3.6 - resolution: "nanoid@npm:3.3.6" +"nanoid@npm:^3.3.7": + version: 3.3.7 + resolution: "nanoid@npm:3.3.7" bin: nanoid: bin/nanoid.cjs - checksum: 7d0eda657002738aa5206107bd0580aead6c95c460ef1bdd0b1a87a9c7ae6277ac2e9b945306aaa5b32c6dcb7feaf462d0f552e7f8b5718abfc6ead5c94a71b3 + checksum: d36c427e530713e4ac6567d488b489a36582ef89da1d6d4e3b87eded11eb10d7042a877958c6f104929809b2ab0bafa17652b076cdf84324aa75b30b722204f2 languageName: node linkType: hard @@ -8693,7 +8807,28 @@ __metadata: languageName: node linkType: hard -"node-gyp@npm:^9.4.0, node-gyp@npm:latest": +"node-gyp@npm:^9.4.1": + version: 9.4.1 + resolution: "node-gyp@npm:9.4.1" + dependencies: + env-paths: ^2.2.0 + exponential-backoff: ^3.1.1 + glob: ^7.1.4 + graceful-fs: ^4.2.6 + make-fetch-happen: ^10.0.3 + nopt: ^6.0.0 + npmlog: ^6.0.0 + rimraf: ^3.0.2 + semver: ^7.3.5 + tar: ^6.1.2 + which: ^2.0.2 + bin: + node-gyp: bin/node-gyp.js + checksum: 8576c439e9e925ab50679f87b7dfa7aa6739e42822e2ad4e26c36341c0ba7163fdf5a946f0a67a476d2f24662bc40d6c97bd9e79ced4321506738e6b760a1577 + languageName: node + linkType: hard + +"node-gyp@npm:latest": version: 9.4.0 resolution: "node-gyp@npm:9.4.0" dependencies: @@ -8739,10 +8874,10 @@ __metadata: languageName: node linkType: hard -"node-releases@npm:^2.0.13": - version: 2.0.13 - resolution: "node-releases@npm:2.0.13" - checksum: 17ec8f315dba62710cae71a8dad3cd0288ba943d2ece43504b3b1aa8625bf138637798ab470b1d9035b0545996f63000a8a926e0f6d35d0996424f8b6d36dda3 +"node-releases@npm:^2.0.14": + version: 2.0.14 + resolution: "node-releases@npm:2.0.14" + checksum: 59443a2f77acac854c42d321bf1b43dea0aef55cd544c6a686e9816a697300458d4e82239e2d794ea05f7bbbc8a94500332e2d3ac3f11f52e4b16cbe638b3c41 languageName: node linkType: hard @@ -9623,6 +9758,13 @@ __metadata: languageName: node linkType: hard +"promise-inflight@npm:^1.0.1": + version: 1.0.1 + resolution: "promise-inflight@npm:1.0.1" + checksum: 22749483091d2c594261517f4f80e05226d4d5ecc1fc917e1886929da56e22b5718b7f2a75f3807e7a7d471bc3be2907fe92e6e8f373ddf5c64bae35b5af3981 + languageName: node + linkType: hard + "promise-retry@npm:^2.0.1": version: 2.0.1 resolution: "promise-retry@npm:2.0.1" @@ -9841,22 +9983,6 @@ __metadata: languageName: node linkType: hard -"rc-align@npm:^4.0.0": - version: 4.0.15 - resolution: "rc-align@npm:4.0.15" - dependencies: - "@babel/runtime": ^7.10.1 - classnames: 2.x - dom-align: ^1.7.0 - rc-util: ^5.26.0 - resize-observer-polyfill: ^1.5.1 - peerDependencies: - react: ">=16.9.0" - react-dom: ">=16.9.0" - checksum: dfb7d3bfaa8d4b9ead4dbd8d84d4033fbd7a3f2232e7797ab1f86545c043cbe3952575fcfa63361045e2d1fa3a07c54545e442d60b08e753f4d581dcd5da186e - languageName: node - linkType: hard - "rc-motion@npm:^2.0.0": version: 2.9.0 resolution: "rc-motion@npm:2.9.0" @@ -9886,30 +10012,30 @@ __metadata: languageName: node linkType: hard -"rc-tooltip@npm:^6.0.1": - version: 6.0.1 - resolution: "rc-tooltip@npm:6.0.1" +"rc-tooltip@npm:^6.1.3": + version: 6.1.3 + resolution: "rc-tooltip@npm:6.1.3" dependencies: "@babel/runtime": ^7.11.2 - "@rc-component/trigger": ^1.0.4 + "@rc-component/trigger": ^1.18.0 classnames: ^2.3.1 peerDependencies: react: ">=16.9.0" react-dom: ">=16.9.0" - checksum: fe7f617a4f4e0085d8f5eb5e8da5598f0164841c841f62f77966706ae604491246441a469aeb44f1dec7001bb4716ee81d11ec646e8889f4164fcba3a024eea5 + checksum: f67cf4e409d110f9f4098807303bd4590297f45ffb21d20f3c59a93de45b38c477eeee346e78b9c2e07bffd2ed53e22f435023113a08b553ed10e269754507e2 languageName: node linkType: hard -"rc-util@npm:^5.21.0, rc-util@npm:^5.24.4, rc-util@npm:^5.26.0, rc-util@npm:^5.27.0, rc-util@npm:^5.33.0": - version: 5.37.0 - resolution: "rc-util@npm:5.37.0" +"rc-util@npm:^5.21.0, rc-util@npm:^5.24.4, rc-util@npm:^5.27.0, rc-util@npm:^5.38.0": + version: 5.38.1 + resolution: "rc-util@npm:5.38.1" dependencies: "@babel/runtime": ^7.18.3 - react-is: ^16.12.0 + react-is: ^18.2.0 peerDependencies: react: ">=16.9.0" react-dom: ">=16.9.0" - checksum: 0a72644f0924235044eb20bbc73081356c1fb98459f6cce0f4b75807c82cb71c050e5f36cf6d82f8a8a07b5955632e1198b8884cd96fe2d8d92f2289f9058a7e + checksum: 40d0411fb5d6b0a187e718ff16c18f3d68eae3d7e4def43a9a9b2690b89cfce639077a69d683aa01302f8132394dd633baf76b07e5a3b8438fb706b1abb31937 languageName: node linkType: hard @@ -10020,23 +10146,27 @@ __metadata: languageName: node linkType: hard -"react-intersection-observer@npm:^9.5.2": - version: 9.5.2 - resolution: "react-intersection-observer@npm:9.5.2" +"react-intersection-observer@npm:^9.6.0": + version: 9.6.0 + resolution: "react-intersection-observer@npm:9.6.0" peerDependencies: react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 - checksum: cdbe40544930d59fc3820bce017fb688910cbecd7baaf382387a3d60d98a36e1d1e4bc27a876775e99b2a9f86762286a29d7bca21f3345f653cff29850c2641d + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + react-dom: + optional: true + checksum: fba57f1601b6c08ea0345de23c4e41b1e0042834dbef62fa419fc18952f9ceb577d180ef3d8c8e6b01c3d5cfc3265ae5a007a532892c376461bf655b0c764ef7 languageName: node linkType: hard -"react-is@npm:^16.12.0, react-is@npm:^16.13.1, react-is@npm:^16.6.0, react-is@npm:^16.7.0": +"react-is@npm:^16.13.1, react-is@npm:^16.6.0, react-is@npm:^16.7.0": version: 16.13.1 resolution: "react-is@npm:16.13.1" checksum: f7a19ac3496de32ca9ae12aa030f00f14a3d45374f1ceca0af707c831b2a6098ef0d6bdae51bd437b0a306d7f01d4677fcc8de7c0d331eb47ad0f46130e53c5f languageName: node linkType: hard -"react-is@npm:^18.0.0": +"react-is@npm:^18.0.0, react-is@npm:^18.2.0": version: 18.2.0 resolution: "react-is@npm:18.2.0" checksum: e72d0ba81b5922759e4aff17e0252bd29988f9642ed817f56b25a3e217e13eea8a7f2322af99a06edb779da12d5d636e9fda473d620df9a3da0df2a74141d53e @@ -11092,6 +11222,15 @@ __metadata: languageName: node linkType: hard +"ssri@npm:^9.0.0": + version: 9.0.1 + resolution: "ssri@npm:9.0.1" + dependencies: + minipass: ^3.1.1 + checksum: fb58f5e46b6923ae67b87ad5ef1c5ab6d427a17db0bead84570c2df3cd50b4ceb880ebdba2d60726588272890bae842a744e1ecce5bd2a2a582fccd5068309eb + languageName: node + linkType: hard + "stack-trace@npm:0.0.x": version: 0.0.10 resolution: "stack-trace@npm:0.0.10" @@ -11768,9 +11907,9 @@ __metadata: languageName: node linkType: hard -"ts-jest@npm:^29.1.1": - version: 29.1.1 - resolution: "ts-jest@npm:29.1.1" +"ts-jest@npm:^29.1.2": + version: 29.1.2 + resolution: "ts-jest@npm:29.1.2" dependencies: bs-logger: 0.x fast-json-stable-stringify: 2.x @@ -11797,7 +11936,7 @@ __metadata: optional: true bin: ts-jest: cli.js - checksum: a8c9e284ed4f819526749f6e4dc6421ec666f20ab44d31b0f02b4ed979975f7580b18aea4813172d43e39b29464a71899f8893dd29b06b4a351a3af8ba47b402 + checksum: a0ce0affc1b716c78c9ab55837829c42cb04b753d174a5c796bb1ddf9f0379fc20647b76fbe30edb30d9b23181908138d6b4c51ef2ae5e187b66635c295cefd5 languageName: node linkType: hard @@ -11983,7 +12122,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^4.9.4": +"typescript@npm:^4.9.5": version: 4.9.5 resolution: "typescript@npm:4.9.5" bin: @@ -11993,7 +12132,7 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@^4.9.4#~builtin": +"typescript@patch:typescript@^4.9.5#~builtin": version: 4.9.5 resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=23ec76" bin: @@ -12048,6 +12187,15 @@ __metadata: languageName: node linkType: hard +"unique-filename@npm:^2.0.0": + version: 2.0.1 + resolution: "unique-filename@npm:2.0.1" + dependencies: + unique-slug: ^3.0.0 + checksum: 807acf3381aff319086b64dc7125a9a37c09c44af7620bd4f7f3247fcd5565660ac12d8b80534dcbfd067e6fe88a67e621386dd796a8af828d1337a8420a255f + languageName: node + linkType: hard + "unique-filename@npm:^3.0.0": version: 3.0.0 resolution: "unique-filename@npm:3.0.0" @@ -12057,6 +12205,15 @@ __metadata: languageName: node linkType: hard +"unique-slug@npm:^3.0.0": + version: 3.0.0 + resolution: "unique-slug@npm:3.0.0" + dependencies: + imurmurhash: ^0.1.4 + checksum: 49f8d915ba7f0101801b922062ee46b7953256c93ceca74303bd8e6413ae10aa7e8216556b54dc5382895e8221d04f1efaf75f945c2e4a515b4139f77aa6640c + languageName: node + linkType: hard + "unique-slug@npm:^4.0.0": version: 4.0.0 resolution: "unique-slug@npm:4.0.0" @@ -12096,9 +12253,9 @@ __metadata: languageName: node linkType: hard -"update-browserslist-db@npm:^1.0.11": - version: 1.0.11 - resolution: "update-browserslist-db@npm:1.0.11" +"update-browserslist-db@npm:^1.0.13": + version: 1.0.13 + resolution: "update-browserslist-db@npm:1.0.13" dependencies: escalade: ^3.1.1 picocolors: ^1.0.0 @@ -12106,7 +12263,7 @@ __metadata: browserslist: ">= 4.21.0" bin: update-browserslist-db: cli.js - checksum: b98327518f9a345c7cad5437afae4d2ae7d865f9779554baf2a200fdf4bac4969076b679b1115434bd6557376bdd37ca7583d0f9b8f8e302d7d4cc1e91b5f231 + checksum: 1e47d80182ab6e4ad35396ad8b61008ae2a1330221175d0abd37689658bdb61af9b705bfc41057fd16682474d79944fb2d86767c5ed5ae34b6276b9bed353322 languageName: node linkType: hard @@ -12506,11 +12663,11 @@ __metadata: languageName: node linkType: hard -"winston@npm:^3.10.0": - version: 3.10.0 - resolution: "winston@npm:3.10.0" +"winston@npm:^3.11.0": + version: 3.11.0 + resolution: "winston@npm:3.11.0" dependencies: - "@colors/colors": 1.5.0 + "@colors/colors": ^1.6.0 "@dabh/diagnostics": ^2.0.2 async: ^3.2.3 is-stream: ^2.0.0 @@ -12521,7 +12678,7 @@ __metadata: stack-trace: 0.0.x triple-beam: ^1.3.0 winston-transport: ^4.5.0 - checksum: 47df0361220d12b46d1b3c98a1c380a3718321739d527a182ce7984fc20715e5b0b55db0bcd3fd076d1b1d3261903b890b053851cfd4bc028bda7951fa8ca2e0 + checksum: ca4454070f7a71b19f53c8c1765c59a013dab220edb49161b2e81917751d3e9edc3382430e4fb050feda04fb8463290ecab7cbc9240ec8d3d3b32a121849bbb0 languageName: node linkType: hard @@ -12604,13 +12761,13 @@ __metadata: languageName: node linkType: hard -"xml2js@npm:^0.4.23": - version: 0.4.23 - resolution: "xml2js@npm:0.4.23" +"xml2js@npm:^0.6.2": + version: 0.6.2 + resolution: "xml2js@npm:0.6.2" dependencies: sax: ">=0.6.0" xmlbuilder: ~11.0.0 - checksum: ca0cf2dfbf6deeaae878a891c8fbc0db6fd04398087084edf143cdc83d0509ad0fe199b890f62f39c4415cf60268a27a6aed0d343f0658f8779bd7add690fa98 + checksum: 458a83806193008edff44562c0bdb982801d61ee7867ae58fd35fab781e69e17f40dfeb8fc05391a4648c9c54012066d3955fe5d993ffbe4dc63399023f32ac2 languageName: node linkType: hard diff --git a/package.json b/package.json index 8386a036de..e232a83b9f 100644 --- a/package.json +++ b/package.json @@ -34,11 +34,11 @@ "test-all": "yarn install && run install-and-build && run check-types:meteor && run lint:packages && run lint:meteor && run test:packages && run test:meteor" }, "devDependencies": { - "concurrently": "^8.2.1", + "concurrently": "^8.2.2", "lint-staged": "^13.3.0", - "rimraf": "^5.0.1", + "rimraf": "^5.0.5", "semver": "^7.5.4", - "snyk-nodejs-lockfile-parser": "^1.52.3" + "snyk-nodejs-lockfile-parser": "^1.52.11" }, "packageManager": "yarn@3.5.0" } diff --git a/packages/corelib/package.json b/packages/corelib/package.json index ba6d7af096..f9548061bd 100644 --- a/packages/corelib/package.json +++ b/packages/corelib/package.json @@ -44,7 +44,7 @@ "fast-clone": "^1.5.13", "i18next": "^21.10.0", "influx": "^5.9.3", - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "object-path": "^0.11.8", "prom-client": "^14.2.0", "timecode": "0.0.4", diff --git a/packages/documentation/package.json b/packages/documentation/package.json index 99baeb7f43..3bbb794e45 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -15,8 +15,8 @@ "write-heading-ids": "docusaurus write-heading-ids" }, "devDependencies": { - "@docusaurus/core": "2.4.1", - "@docusaurus/preset-classic": "2.4.1", + "@docusaurus/core": "2.4.3", + "@docusaurus/preset-classic": "2.4.3", "@mdx-js/react": "^1.6.22", "@svgr/webpack": "^5.5.0", "clsx": "^1.2.1", diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index 92b1175c18..b461ac49f5 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -46,9 +46,9 @@ "@sofie-automation/shared-lib": "1.51.0-in-development", "amqplib": "^0.10.3", "deepmerge": "^4.3.1", - "elastic-apm-node": "^3.50.0", + "elastic-apm-node": "^3.51.0", "eventemitter3": "^4.0.7", - "mongodb": "^5.9.0", + "mongodb": "^5.9.2", "p-lazy": "^3.1.0", "p-timeout": "^4.1.0", "superfly-timeline": "9.0.0", diff --git a/packages/live-status-gateway/package.json b/packages/live-status-gateway/package.json index 2d72452119..ab07c893d2 100644 --- a/packages/live-status-gateway/package.json +++ b/packages/live-status-gateway/package.json @@ -62,14 +62,14 @@ "influx": "^5.9.3", "tslib": "^2.6.2", "underscore": "^1.13.6", - "winston": "^3.10.0", - "ws": "^8.14.2" + "winston": "^3.11.0", + "ws": "^8.16.0" }, "devDependencies": { "@asyncapi/generator": "1.10.9", "@asyncapi/html-template": "0.26.0", "@asyncapi/nodejs-ws-template": "0.9.33", - "type-fest": "^4.5.0" + "type-fest": "^4.10.2" }, "lint-staged": { "*.{css,json,md,scss}": [ diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index a733730475..0adb46089a 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -65,13 +65,13 @@ "production" ], "dependencies": { - "@mos-connection/connector": "^3.0.4", + "@mos-connection/connector": "^3.0.7", "@sofie-automation/server-core-integration": "1.51.0-in-development", "@sofie-automation/shared-lib": "1.51.0-in-development", "tslib": "^2.6.2", "type-fest": "^3.13.1", "underscore": "^1.13.6", - "winston": "^3.10.0" + "winston": "^3.11.0" }, "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", "lint-staged": { diff --git a/packages/openapi/package.json b/packages/openapi/package.json index 871e189599..f1aa6e169a 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -39,8 +39,8 @@ "tslib": "^2.6.2" }, "devDependencies": { - "@openapitools/openapi-generator-cli": "^2.7.0", - "eslint-plugin-yml": "^1.9.0", + "@openapitools/openapi-generator-cli": "^2.9.0", + "eslint-plugin-yml": "^1.12.2", "js-yaml": "^4.1.0", "wget-improved": "^3.4.0" }, diff --git a/packages/package.json b/packages/package.json index 7f2218d1d0..b2d9bd19ea 100644 --- a/packages/package.json +++ b/packages/package.json @@ -35,17 +35,17 @@ "eslint": "cd $INIT_CWD && \"$PROJECT_CWD/node_modules/.bin/eslint\"" }, "devDependencies": { - "@babel/core": "^7.22.20", - "@babel/plugin-transform-modules-commonjs": "^7.22.15", + "@babel/core": "^7.23.9", + "@babel/plugin-transform-modules-commonjs": "^7.23.3", "@sofie-automation/code-standard-preset": "~2.4.7", - "@types/amqplib": "^0.10.1", - "@types/debug": "^4.1.8", - "@types/ejson": "^2.2.0", + "@types/amqplib": "^0.10.4", + "@types/debug": "^4.1.12", + "@types/ejson": "^2.2.2", "@types/got": "^9.6.12", - "@types/jest": "^29.5.5", - "@types/node": "^14.18.62", - "@types/object-path": "^0.11.2", - "@types/underscore": "^1.11.9", + "@types/jest": "^29.5.11", + "@types/node": "^14.18.63", + "@types/object-path": "^0.11.4", + "@types/underscore": "^1.11.15", "babel-jest": "^29.7.0", "copyfiles": "^2.4.1", "jest": "^29.7.0", @@ -57,10 +57,10 @@ "pinst": "^3.0.0", "rimraf": "^4.4.1", "semver": "^7.5.4", - "ts-jest": "^29.1.1", - "ts-node": "^10.9.1", + "ts-jest": "^29.1.2", + "ts-node": "^10.9.2", "typedoc": "^0.23.28", - "typescript": "~4.9" + "typescript": "~4.9.5" }, "name": "packages", "packageManager": "yarn@3.5.0" diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index 0a48b93573..a129ca17da 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -63,7 +63,7 @@ "timeline-state-resolver": "9.1.0-nightly-release51-20231211-110613-75e2cbebd.0", "tslib": "^2.6.2", "underscore": "^1.13.6", - "winston": "^3.10.0" + "winston": "^3.11.0" }, "lint-staged": { "*.{css,json,md,scss}": [ diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 89a45a8bd5..027e3b0fa8 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -38,7 +38,7 @@ "/LICENSE" ], "dependencies": { - "@mos-connection/model": "^3.0.4", + "@mos-connection/model": "^3.0.7", "timeline-state-resolver-types": "9.1.0-nightly-release51-20231211-110613-75e2cbebd.0", "tslib": "^2.6.2", "type-fest": "^3.13.1" diff --git a/packages/yarn.lock b/packages/yarn.lock index d0489ce5c7..5470a10268 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -511,6 +511,16 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/code-frame@npm:7.23.5" + dependencies: + "@babel/highlight": ^7.23.4 + chalk: ^2.4.2 + checksum: d90981fdf56a2824a9b14d19a4c0e8db93633fd488c772624b4e83e0ceac6039a27cd298a247c3214faa952bf803ba23696172ae7e7235f3b97f43ba278c569a + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.22.20, @babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.22.9": version: 7.22.20 resolution: "@babel/compat-data@npm:7.22.20" @@ -518,6 +528,13 @@ __metadata: languageName: node linkType: hard +"@babel/compat-data@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/compat-data@npm:7.23.5" + checksum: 06ce244cda5763295a0ea924728c09bae57d35713b675175227278896946f922a63edf803c322f855a3878323d48d0255a2a3023409d2a123483c8a69ebb4744 + languageName: node + linkType: hard + "@babel/core@npm:7.12.9": version: 7.12.9 resolution: "@babel/core@npm:7.12.9" @@ -542,7 +559,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.18.6, @babel/core@npm:^7.19.6, @babel/core@npm:^7.22.20": +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.18.6, @babel/core@npm:^7.19.6": version: 7.22.20 resolution: "@babel/core@npm:7.22.20" dependencies: @@ -565,6 +582,29 @@ __metadata: languageName: node linkType: hard +"@babel/core@npm:^7.23.9": + version: 7.23.9 + resolution: "@babel/core@npm:7.23.9" + dependencies: + "@ampproject/remapping": ^2.2.0 + "@babel/code-frame": ^7.23.5 + "@babel/generator": ^7.23.6 + "@babel/helper-compilation-targets": ^7.23.6 + "@babel/helper-module-transforms": ^7.23.3 + "@babel/helpers": ^7.23.9 + "@babel/parser": ^7.23.9 + "@babel/template": ^7.23.9 + "@babel/traverse": ^7.23.9 + "@babel/types": ^7.23.9 + convert-source-map: ^2.0.0 + debug: ^4.1.0 + gensync: ^1.0.0-beta.2 + json5: ^2.2.3 + semver: ^6.3.1 + checksum: 634a511f74db52a5f5a283c1121f25e2227b006c095b84a02a40a9213842489cd82dc7d61cdc74e10b5bcd9bb0a4e28bab47635b54c7e2256d47ab57356e2a76 + languageName: node + linkType: hard + "@babel/generator@npm:^7.12.5, @babel/generator@npm:^7.18.7, @babel/generator@npm:^7.22.15, @babel/generator@npm:^7.7.2": version: 7.22.15 resolution: "@babel/generator@npm:7.22.15" @@ -577,6 +617,18 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.23.6": + version: 7.23.6 + resolution: "@babel/generator@npm:7.23.6" + dependencies: + "@babel/types": ^7.23.6 + "@jridgewell/gen-mapping": ^0.3.2 + "@jridgewell/trace-mapping": ^0.3.17 + jsesc: ^2.5.1 + checksum: 1a1a1c4eac210f174cd108d479464d053930a812798e09fee069377de39a893422df5b5b146199ead7239ae6d3a04697b45fc9ac6e38e0f6b76374390f91fc6c + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.22.5": version: 7.22.5 resolution: "@babel/helper-annotate-as-pure@npm:7.22.5" @@ -608,6 +660,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-compilation-targets@npm:^7.23.6": + version: 7.23.6 + resolution: "@babel/helper-compilation-targets@npm:7.23.6" + dependencies: + "@babel/compat-data": ^7.23.5 + "@babel/helper-validator-option": ^7.23.5 + browserslist: ^4.22.2 + lru-cache: ^5.1.1 + semver: ^6.3.1 + checksum: c630b98d4527ac8fe2c58d9a06e785dfb2b73ec71b7c4f2ddf90f814b5f75b547f3c015f110a010fd31f76e3864daaf09f3adcd2f6acdbfb18a8de3a48717590 + languageName: node + linkType: hard + "@babel/helper-create-class-features-plugin@npm:^7.22.11, @babel/helper-create-class-features-plugin@npm:^7.22.15, @babel/helper-create-class-features-plugin@npm:^7.22.5": version: 7.22.15 resolution: "@babel/helper-create-class-features-plugin@npm:7.22.15" @@ -672,6 +737,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-function-name@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/helper-function-name@npm:7.23.0" + dependencies: + "@babel/template": ^7.22.15 + "@babel/types": ^7.23.0 + checksum: e44542257b2d4634a1f979244eb2a4ad8e6d75eb6761b4cfceb56b562f7db150d134bc538c8e6adca3783e3bc31be949071527aa8e3aab7867d1ad2d84a26e10 + languageName: node + linkType: hard + "@babel/helper-hoist-variables@npm:^7.22.5": version: 7.22.5 resolution: "@babel/helper-hoist-variables@npm:7.22.5" @@ -714,6 +789,21 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-transforms@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/helper-module-transforms@npm:7.23.3" + dependencies: + "@babel/helper-environment-visitor": ^7.22.20 + "@babel/helper-module-imports": ^7.22.15 + "@babel/helper-simple-access": ^7.22.5 + "@babel/helper-split-export-declaration": ^7.22.6 + "@babel/helper-validator-identifier": ^7.22.20 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 5d0895cfba0e16ae16f3aa92fee108517023ad89a855289c4eb1d46f7aef4519adf8e6f971e1d55ac20c5461610e17213f1144097a8f932e768a9132e2278d71 + languageName: node + linkType: hard + "@babel/helper-optimise-call-expression@npm:^7.22.5": version: 7.22.5 resolution: "@babel/helper-optimise-call-expression@npm:7.22.5" @@ -797,6 +887,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/helper-string-parser@npm:7.23.4" + checksum: c0641144cf1a7e7dc93f3d5f16d5327465b6cf5d036b48be61ecba41e1eece161b48f46b7f960951b67f8c3533ce506b16dece576baef4d8b3b49f8c65410f90 + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.22.19, @babel/helper-validator-identifier@npm:^7.22.20, @babel/helper-validator-identifier@npm:^7.22.5": version: 7.22.20 resolution: "@babel/helper-validator-identifier@npm:7.22.20" @@ -811,6 +908,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-option@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/helper-validator-option@npm:7.23.5" + checksum: 537cde2330a8aede223552510e8a13e9c1c8798afee3757995a7d4acae564124fe2bf7e7c3d90d62d3657434a74340a274b3b3b1c6f17e9a2be1f48af29cb09e + languageName: node + linkType: hard + "@babel/helper-wrap-function@npm:^7.22.20": version: 7.22.20 resolution: "@babel/helper-wrap-function@npm:7.22.20" @@ -833,6 +937,17 @@ __metadata: languageName: node linkType: hard +"@babel/helpers@npm:^7.23.9": + version: 7.23.9 + resolution: "@babel/helpers@npm:7.23.9" + dependencies: + "@babel/template": ^7.23.9 + "@babel/traverse": ^7.23.9 + "@babel/types": ^7.23.9 + checksum: 2678231192c0471dbc2fc403fb19456cc46b1afefcfebf6bc0f48b2e938fdb0fef2e0fe90c8c8ae1f021dae5012b700372e4b5d15867f1d7764616532e4a6324 + languageName: node + linkType: hard + "@babel/highlight@npm:^7.22.13": version: 7.22.20 resolution: "@babel/highlight@npm:7.22.20" @@ -844,6 +959,17 @@ __metadata: languageName: node linkType: hard +"@babel/highlight@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/highlight@npm:7.23.4" + dependencies: + "@babel/helper-validator-identifier": ^7.22.20 + chalk: ^2.4.2 + js-tokens: ^4.0.0 + checksum: 643acecdc235f87d925979a979b539a5d7d1f31ae7db8d89047269082694122d11aa85351304c9c978ceeb6d250591ccadb06c366f358ccee08bb9c122476b89 + languageName: node + linkType: hard + "@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.12.7, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.18.8, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.22.15, @babel/parser@npm:^7.22.16": version: 7.22.16 resolution: "@babel/parser@npm:7.22.16" @@ -853,6 +979,15 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.23.9": + version: 7.23.9 + resolution: "@babel/parser@npm:7.23.9" + bin: + parser: ./bin/babel-parser.js + checksum: e7cd4960ac8671774e13803349da88d512f9292d7baa952173260d3e8f15620a28a3701f14f709d769209022f9e7b79965256b8be204fc550cfe783cdcabe7c7 + languageName: node + linkType: hard + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.22.15": version: 7.22.15 resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.22.15" @@ -1423,6 +1558,19 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-modules-commonjs@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.23.3" + dependencies: + "@babel/helper-module-transforms": ^7.23.3 + "@babel/helper-plugin-utils": ^7.22.5 + "@babel/helper-simple-access": ^7.22.5 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 720a231ceade4ae4d2632478db4e7fecf21987d444942b72d523487ac8d715ca97de6c8f415c71e939595e1a4776403e7dc24ed68fe9125ad4acf57753c9bff7 + languageName: node + linkType: hard + "@babel/plugin-transform-modules-systemjs@npm:^7.22.11": version: 7.22.11 resolution: "@babel/plugin-transform-modules-systemjs@npm:7.22.11" @@ -1983,6 +2131,17 @@ __metadata: languageName: node linkType: hard +"@babel/template@npm:^7.23.9": + version: 7.23.9 + resolution: "@babel/template@npm:7.23.9" + dependencies: + "@babel/code-frame": ^7.23.5 + "@babel/parser": ^7.23.9 + "@babel/types": ^7.23.9 + checksum: 6e67414c0f7125d7ecaf20c11fab88085fa98a96c3ef10da0a61e962e04fdf3a18a496a66047005ddd1bb682a7cc7842d556d1db2f3f3f6ccfca97d5e445d342 + languageName: node + linkType: hard + "@babel/traverse@npm:^7.12.9, @babel/traverse@npm:^7.18.8, @babel/traverse@npm:^7.22.15, @babel/traverse@npm:^7.22.20": version: 7.22.20 resolution: "@babel/traverse@npm:7.22.20" @@ -2001,6 +2160,24 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.23.9": + version: 7.23.9 + resolution: "@babel/traverse@npm:7.23.9" + dependencies: + "@babel/code-frame": ^7.23.5 + "@babel/generator": ^7.23.6 + "@babel/helper-environment-visitor": ^7.22.20 + "@babel/helper-function-name": ^7.23.0 + "@babel/helper-hoist-variables": ^7.22.5 + "@babel/helper-split-export-declaration": ^7.22.6 + "@babel/parser": ^7.23.9 + "@babel/types": ^7.23.9 + debug: ^4.3.1 + globals: ^11.1.0 + checksum: a932f7aa850e158c00c97aad22f639d48c72805c687290f6a73e30c5c4957c07f5d28310c9bf59648e2980fe6c9d16adeb2ff92a9ca0f97fa75739c1328fc6c3 + languageName: node + linkType: hard + "@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.6, @babel/types@npm:^7.12.7, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": version: 7.22.19 resolution: "@babel/types@npm:7.22.19" @@ -2012,6 +2189,17 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.23.0, @babel/types@npm:^7.23.6, @babel/types@npm:^7.23.9": + version: 7.23.9 + resolution: "@babel/types@npm:7.23.9" + dependencies: + "@babel/helper-string-parser": ^7.23.4 + "@babel/helper-validator-identifier": ^7.22.20 + to-fast-properties: ^2.0.0 + checksum: 0a9b008e9bfc89beb8c185e620fa0f8ed6c771f1e1b2e01e1596870969096fec7793898a1d64a035176abf1dd13e2668ee30bf699f2d92c210a8128f4b151e65 + languageName: node + linkType: hard + "@bcoe/v8-coverage@npm:^0.2.3": version: 0.2.3 resolution: "@bcoe/v8-coverage@npm:0.2.3" @@ -2026,6 +2214,13 @@ __metadata: languageName: node linkType: hard +"@colors/colors@npm:^1.6.0": + version: 1.6.0 + resolution: "@colors/colors@npm:1.6.0" + checksum: aa209963e0c3218e80a4a20553ba8c0fbb6fa13140540b4e5f97923790be06801fc90172c1114fc8b7e888b3d012b67298cde6b9e81521361becfaee400c662f + languageName: node + linkType: hard + "@cspotcode/source-map-support@npm:^0.8.0": version: 0.8.1 resolution: "@cspotcode/source-map-support@npm:0.8.1" @@ -2086,9 +2281,9 @@ __metadata: languageName: node linkType: hard -"@docusaurus/core@npm:2.4.1": - version: 2.4.1 - resolution: "@docusaurus/core@npm:2.4.1" +"@docusaurus/core@npm:2.4.3": + version: 2.4.3 + resolution: "@docusaurus/core@npm:2.4.3" dependencies: "@babel/core": ^7.18.6 "@babel/generator": ^7.18.7 @@ -2100,13 +2295,13 @@ __metadata: "@babel/runtime": ^7.18.6 "@babel/runtime-corejs3": ^7.18.6 "@babel/traverse": ^7.18.8 - "@docusaurus/cssnano-preset": 2.4.1 - "@docusaurus/logger": 2.4.1 - "@docusaurus/mdx-loader": 2.4.1 + "@docusaurus/cssnano-preset": 2.4.3 + "@docusaurus/logger": 2.4.3 + "@docusaurus/mdx-loader": 2.4.3 "@docusaurus/react-loadable": 5.5.2 - "@docusaurus/utils": 2.4.1 - "@docusaurus/utils-common": 2.4.1 - "@docusaurus/utils-validation": 2.4.1 + "@docusaurus/utils": 2.4.3 + "@docusaurus/utils-common": 2.4.3 + "@docusaurus/utils-validation": 2.4.3 "@slorber/static-site-generator-webpack-plugin": ^4.0.7 "@svgr/webpack": ^6.2.1 autoprefixer: ^10.4.7 @@ -2166,40 +2361,40 @@ __metadata: react-dom: ^16.8.4 || ^17.0.0 bin: docusaurus: bin/docusaurus.mjs - checksum: 40c887ef662f7679d803695d4193268c2c177c6d4e13b43b56cc519322522a1608b4bfc4999f6355be778ca7a0256f0d27ab18a19b352a9da1aed66e2644dc82 + checksum: cce7173ee131364857c16f70f94155ba0e1b044cde54045fb0cf62ad138f8d8ef093f5aba7c7617a9aa0545b3ee3930aec2e09f645daec015696968338963013 languageName: node linkType: hard -"@docusaurus/cssnano-preset@npm:2.4.1": - version: 2.4.1 - resolution: "@docusaurus/cssnano-preset@npm:2.4.1" +"@docusaurus/cssnano-preset@npm:2.4.3": + version: 2.4.3 + resolution: "@docusaurus/cssnano-preset@npm:2.4.3" dependencies: cssnano-preset-advanced: ^5.3.8 postcss: ^8.4.14 postcss-sort-media-queries: ^4.2.1 tslib: ^2.4.0 - checksum: d498345981288af2dcb8650bed3c3361cfe336541a8bda65743fbe8ee5746e81e723ba086e2e6249c3b283f4bc50b5c81cff15b0406969cd610bed345b3804ac + checksum: f4a4c60b075c23541da90e00ae26af2e7eaadf20d783b37b9110a5e34599e4e91947425e33bad58ba71abee81c85cca99f5d7d76575f53fbaf73617b55e39c62 languageName: node linkType: hard -"@docusaurus/logger@npm:2.4.1": - version: 2.4.1 - resolution: "@docusaurus/logger@npm:2.4.1" +"@docusaurus/logger@npm:2.4.3": + version: 2.4.3 + resolution: "@docusaurus/logger@npm:2.4.3" dependencies: chalk: ^4.1.2 tslib: ^2.4.0 - checksum: be81840f2df477ab633d8ced6fd3a512582e764a48d66b1c12bb20b5d4c717f349e254e33b00b9b53381dbdb24a3e3d0ca9b19511366244b3620fa19cc4c69dc + checksum: f026a8233aa317f16ce5b25c6785a431f319c52fc07a1b9e26f4b3df2197974e75830a16b6140314f8f4ef02dc19242106ec2ae1599740b26d516cc34c56102f languageName: node linkType: hard -"@docusaurus/mdx-loader@npm:2.4.1": - version: 2.4.1 - resolution: "@docusaurus/mdx-loader@npm:2.4.1" +"@docusaurus/mdx-loader@npm:2.4.3": + version: 2.4.3 + resolution: "@docusaurus/mdx-loader@npm:2.4.3" dependencies: "@babel/parser": ^7.18.8 "@babel/traverse": ^7.18.8 - "@docusaurus/logger": 2.4.1 - "@docusaurus/utils": 2.4.1 + "@docusaurus/logger": 2.4.3 + "@docusaurus/utils": 2.4.3 "@mdx-js/mdx": ^1.6.22 escape-html: ^1.0.3 file-loader: ^6.2.0 @@ -2216,16 +2411,16 @@ __metadata: peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: cf36bbde228a058869dfd770a85f130035a54e563b957a3cfc3191d06efdcfc272bb51b51e6225a0246b233e5d7d0ca1cb4df4b700b837aa72bbb0c9f6f6f5bd + checksum: 5a774f7ea5f484e888b2bd1bf8b182279e3788afec779eb8920cf468b92ab8d83a1ae8be51925074241a4d1a38d989cfb366d2baf0f67ed6f063342395a7ca8e languageName: node linkType: hard -"@docusaurus/module-type-aliases@npm:2.4.1": - version: 2.4.1 - resolution: "@docusaurus/module-type-aliases@npm:2.4.1" +"@docusaurus/module-type-aliases@npm:2.4.3": + version: 2.4.3 + resolution: "@docusaurus/module-type-aliases@npm:2.4.3" dependencies: "@docusaurus/react-loadable": 5.5.2 - "@docusaurus/types": 2.4.1 + "@docusaurus/types": 2.4.3 "@types/history": ^4.7.11 "@types/react": "*" "@types/react-router-config": "*" @@ -2235,21 +2430,21 @@ __metadata: peerDependencies: react: "*" react-dom: "*" - checksum: 9e328c7bc5cd40b399550995edbeeea5ce88be7eb75f4c49499e8fd05a8bbabf180dce4d1cae0185721629fc6e0f2e8fc513e3ce846080f9771f7a9bc1c45ba8 + checksum: 22ce1a6a20acc35cdd2ec57e55f29e65dbe0fb3a46aaa8c033ec78bf04cd3087f0523c816c744ed311095512dd686c83e0a8619cc1a2a937c27cd54527739c38 languageName: node linkType: hard -"@docusaurus/plugin-content-blog@npm:2.4.1": - version: 2.4.1 - resolution: "@docusaurus/plugin-content-blog@npm:2.4.1" - dependencies: - "@docusaurus/core": 2.4.1 - "@docusaurus/logger": 2.4.1 - "@docusaurus/mdx-loader": 2.4.1 - "@docusaurus/types": 2.4.1 - "@docusaurus/utils": 2.4.1 - "@docusaurus/utils-common": 2.4.1 - "@docusaurus/utils-validation": 2.4.1 +"@docusaurus/plugin-content-blog@npm:2.4.3": + version: 2.4.3 + resolution: "@docusaurus/plugin-content-blog@npm:2.4.3" + dependencies: + "@docusaurus/core": 2.4.3 + "@docusaurus/logger": 2.4.3 + "@docusaurus/mdx-loader": 2.4.3 + "@docusaurus/types": 2.4.3 + "@docusaurus/utils": 2.4.3 + "@docusaurus/utils-common": 2.4.3 + "@docusaurus/utils-validation": 2.4.3 cheerio: ^1.0.0-rc.12 feed: ^4.2.2 fs-extra: ^10.1.0 @@ -2262,21 +2457,21 @@ __metadata: peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 9d4e543b70d032d7edf0c986c45f36a088db76dc737a24374dcb877177b889fb0a5ed7b0a9c9ebb912523ef23ba26787d0fff59d9032b3e8075bdde9c072956a + checksum: 9fd41331c609b9488eea363e617e3763a814c75f83eb1b858cef402a0f5b96f67a342e25ff8c333489e550eb4d379eae09a88b986a97c25170fe203662e2f1ae languageName: node linkType: hard -"@docusaurus/plugin-content-docs@npm:2.4.1": - version: 2.4.1 - resolution: "@docusaurus/plugin-content-docs@npm:2.4.1" - dependencies: - "@docusaurus/core": 2.4.1 - "@docusaurus/logger": 2.4.1 - "@docusaurus/mdx-loader": 2.4.1 - "@docusaurus/module-type-aliases": 2.4.1 - "@docusaurus/types": 2.4.1 - "@docusaurus/utils": 2.4.1 - "@docusaurus/utils-validation": 2.4.1 +"@docusaurus/plugin-content-docs@npm:2.4.3": + version: 2.4.3 + resolution: "@docusaurus/plugin-content-docs@npm:2.4.3" + dependencies: + "@docusaurus/core": 2.4.3 + "@docusaurus/logger": 2.4.3 + "@docusaurus/mdx-loader": 2.4.3 + "@docusaurus/module-type-aliases": 2.4.3 + "@docusaurus/types": 2.4.3 + "@docusaurus/utils": 2.4.3 + "@docusaurus/utils-validation": 2.4.3 "@types/react-router-config": ^5.0.6 combine-promises: ^1.1.0 fs-extra: ^10.1.0 @@ -2289,132 +2484,132 @@ __metadata: peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 028eda178dc81a74c25fd2efddb47e44451af2b268b13d99ef2b60cf13da1443f3bce884fd4a8a7ae92fed8ef747308309074f9524753fd80a40b5252a237e37 + checksum: bc01201f64721131eb84f264e51c7497b8034d2a3d99d762169f5dc456c3d8882acfa01fdbaa8fdc6e2e220479b36e0c9e8e17397bf887884589535bdeaeb4bb languageName: node linkType: hard -"@docusaurus/plugin-content-pages@npm:2.4.1": - version: 2.4.1 - resolution: "@docusaurus/plugin-content-pages@npm:2.4.1" +"@docusaurus/plugin-content-pages@npm:2.4.3": + version: 2.4.3 + resolution: "@docusaurus/plugin-content-pages@npm:2.4.3" dependencies: - "@docusaurus/core": 2.4.1 - "@docusaurus/mdx-loader": 2.4.1 - "@docusaurus/types": 2.4.1 - "@docusaurus/utils": 2.4.1 - "@docusaurus/utils-validation": 2.4.1 + "@docusaurus/core": 2.4.3 + "@docusaurus/mdx-loader": 2.4.3 + "@docusaurus/types": 2.4.3 + "@docusaurus/utils": 2.4.3 + "@docusaurus/utils-validation": 2.4.3 fs-extra: ^10.1.0 tslib: ^2.4.0 webpack: ^5.73.0 peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 6af4eb7c064ed90158ad584eb64593473940b1880034a65fbcfde36116d6702b882bb9b0340141a5a48e67b0f84c03b8202b94171f5924d9f0c279cb68959a47 + checksum: 00439c2e1a1f345cd549739db13a3610b6d9f7ffa6cf7507ad6ac1f3c8d24041947acc2a446be7edf1a613cf354a50d1133aa28ddf64a0eff6ed8a31bf1a542f languageName: node linkType: hard -"@docusaurus/plugin-debug@npm:2.4.1": - version: 2.4.1 - resolution: "@docusaurus/plugin-debug@npm:2.4.1" +"@docusaurus/plugin-debug@npm:2.4.3": + version: 2.4.3 + resolution: "@docusaurus/plugin-debug@npm:2.4.3" dependencies: - "@docusaurus/core": 2.4.1 - "@docusaurus/types": 2.4.1 - "@docusaurus/utils": 2.4.1 + "@docusaurus/core": 2.4.3 + "@docusaurus/types": 2.4.3 + "@docusaurus/utils": 2.4.3 fs-extra: ^10.1.0 react-json-view: ^1.21.3 tslib: ^2.4.0 peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 0be51e9a881383ed76b6e8f369ca6f7754a4f6bd59093c6d28d955b9422a25e868f24b534eb08ba84a5524ae742edd4a052813767b2ea1e8767914dceffc19b8 + checksum: 88955828b72e463e04501cc6bedf802208e377ae0f4d72735625bcbb47918afc4f2588355c6914064cfdbe4945d3da6473ce76319aa1f66dd975b3b43c4c39b0 languageName: node linkType: hard -"@docusaurus/plugin-google-analytics@npm:2.4.1": - version: 2.4.1 - resolution: "@docusaurus/plugin-google-analytics@npm:2.4.1" +"@docusaurus/plugin-google-analytics@npm:2.4.3": + version: 2.4.3 + resolution: "@docusaurus/plugin-google-analytics@npm:2.4.3" dependencies: - "@docusaurus/core": 2.4.1 - "@docusaurus/types": 2.4.1 - "@docusaurus/utils-validation": 2.4.1 + "@docusaurus/core": 2.4.3 + "@docusaurus/types": 2.4.3 + "@docusaurus/utils-validation": 2.4.3 tslib: ^2.4.0 peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 9e754c0bc7779867af07cd77de36f5b491671a96fba5e3517458803465c24773357eb2f1400c41c80e69524cb2c7e9e353262335051aa54192eeae9d9eb055cb + checksum: 6e30de6b5c479493614a5552a295f07ffb9c83f3740a68c7d4dbac378b8288da7430f26cdc246d763855c6084ad86a6f87286e6c8b40f4817794bb1a04e109ea languageName: node linkType: hard -"@docusaurus/plugin-google-gtag@npm:2.4.1": - version: 2.4.1 - resolution: "@docusaurus/plugin-google-gtag@npm:2.4.1" +"@docusaurus/plugin-google-gtag@npm:2.4.3": + version: 2.4.3 + resolution: "@docusaurus/plugin-google-gtag@npm:2.4.3" dependencies: - "@docusaurus/core": 2.4.1 - "@docusaurus/types": 2.4.1 - "@docusaurus/utils-validation": 2.4.1 + "@docusaurus/core": 2.4.3 + "@docusaurus/types": 2.4.3 + "@docusaurus/utils-validation": 2.4.3 tslib: ^2.4.0 peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: ed529f2100599401e1c2aa772dca7c60fdb1990e44af3a9e476e1922f1370ef0dd0b5e6442f846bd942b74af63f3163ac85f1eefe1e85660b61ee60f2044c463 + checksum: 4aaac4d262b3bb7fc3f16620c5329b90db92bf28361ced54f2945fc0e4669483e2f36b076332e0ee9d11b6233cd2c81ca35c953119bad42171e62571c1692d6a languageName: node linkType: hard -"@docusaurus/plugin-google-tag-manager@npm:2.4.1": - version: 2.4.1 - resolution: "@docusaurus/plugin-google-tag-manager@npm:2.4.1" +"@docusaurus/plugin-google-tag-manager@npm:2.4.3": + version: 2.4.3 + resolution: "@docusaurus/plugin-google-tag-manager@npm:2.4.3" dependencies: - "@docusaurus/core": 2.4.1 - "@docusaurus/types": 2.4.1 - "@docusaurus/utils-validation": 2.4.1 + "@docusaurus/core": 2.4.3 + "@docusaurus/types": 2.4.3 + "@docusaurus/utils-validation": 2.4.3 tslib: ^2.4.0 peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: c5c6fce9c9eeae7cbeb277b9765a67d5c4403a3e04f634aadac6d4ba9901739a547d4ea023c83a76cfece2fdb2d175851acaa69abb2f190401b612adeab5524d + checksum: c3af89b4d41fab463d853cbfbe8f43d384f702dd09fd914fffcca01fdf94c282d1b98d762c9142fe21f6471f5dd643679e8d11344c95fdf6657aff0618c3c7a5 languageName: node linkType: hard -"@docusaurus/plugin-sitemap@npm:2.4.1": - version: 2.4.1 - resolution: "@docusaurus/plugin-sitemap@npm:2.4.1" - dependencies: - "@docusaurus/core": 2.4.1 - "@docusaurus/logger": 2.4.1 - "@docusaurus/types": 2.4.1 - "@docusaurus/utils": 2.4.1 - "@docusaurus/utils-common": 2.4.1 - "@docusaurus/utils-validation": 2.4.1 +"@docusaurus/plugin-sitemap@npm:2.4.3": + version: 2.4.3 + resolution: "@docusaurus/plugin-sitemap@npm:2.4.3" + dependencies: + "@docusaurus/core": 2.4.3 + "@docusaurus/logger": 2.4.3 + "@docusaurus/types": 2.4.3 + "@docusaurus/utils": 2.4.3 + "@docusaurus/utils-common": 2.4.3 + "@docusaurus/utils-validation": 2.4.3 fs-extra: ^10.1.0 sitemap: ^7.1.1 tslib: ^2.4.0 peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: aa6728278017c047b4ed1456e349b1a267d85b4bb0c422bb63e59fc28ccb0e286dc66918f44f6ade660e550b047e2b14796c54c96fde6eb69395770cf39cf306 + checksum: cf96b9f0e32cefa58e37a4bc2f0a112ea657f06faf47b780ec2ba39d5e2daca6486a73f3b376c56ad3bb42f3f0c3f70a783f1ce1964b74e2ba273e6f439e439b languageName: node linkType: hard -"@docusaurus/preset-classic@npm:2.4.1": - version: 2.4.1 - resolution: "@docusaurus/preset-classic@npm:2.4.1" - dependencies: - "@docusaurus/core": 2.4.1 - "@docusaurus/plugin-content-blog": 2.4.1 - "@docusaurus/plugin-content-docs": 2.4.1 - "@docusaurus/plugin-content-pages": 2.4.1 - "@docusaurus/plugin-debug": 2.4.1 - "@docusaurus/plugin-google-analytics": 2.4.1 - "@docusaurus/plugin-google-gtag": 2.4.1 - "@docusaurus/plugin-google-tag-manager": 2.4.1 - "@docusaurus/plugin-sitemap": 2.4.1 - "@docusaurus/theme-classic": 2.4.1 - "@docusaurus/theme-common": 2.4.1 - "@docusaurus/theme-search-algolia": 2.4.1 - "@docusaurus/types": 2.4.1 +"@docusaurus/preset-classic@npm:2.4.3": + version: 2.4.3 + resolution: "@docusaurus/preset-classic@npm:2.4.3" + dependencies: + "@docusaurus/core": 2.4.3 + "@docusaurus/plugin-content-blog": 2.4.3 + "@docusaurus/plugin-content-docs": 2.4.3 + "@docusaurus/plugin-content-pages": 2.4.3 + "@docusaurus/plugin-debug": 2.4.3 + "@docusaurus/plugin-google-analytics": 2.4.3 + "@docusaurus/plugin-google-gtag": 2.4.3 + "@docusaurus/plugin-google-tag-manager": 2.4.3 + "@docusaurus/plugin-sitemap": 2.4.3 + "@docusaurus/theme-classic": 2.4.3 + "@docusaurus/theme-common": 2.4.3 + "@docusaurus/theme-search-algolia": 2.4.3 + "@docusaurus/types": 2.4.3 peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: bad7f237ac03a9bc6206cb7a5d077d85d5a6316d34ff089c487ce92b8f6103ef22b04f35d637bdc31dddabd97d5babb1817852b9b97db7c33f3d4c7f33cb149d + checksum: a321badc44696adf4ab2d4a5d6c93f595e8c17988aec9609d325928a1d60f5e0205b23fe849b28ddaed24f7935829e86c402f6b761d6e65db4224270b9dd443c languageName: node linkType: hard @@ -2430,22 +2625,22 @@ __metadata: languageName: node linkType: hard -"@docusaurus/theme-classic@npm:2.4.1": - version: 2.4.1 - resolution: "@docusaurus/theme-classic@npm:2.4.1" - dependencies: - "@docusaurus/core": 2.4.1 - "@docusaurus/mdx-loader": 2.4.1 - "@docusaurus/module-type-aliases": 2.4.1 - "@docusaurus/plugin-content-blog": 2.4.1 - "@docusaurus/plugin-content-docs": 2.4.1 - "@docusaurus/plugin-content-pages": 2.4.1 - "@docusaurus/theme-common": 2.4.1 - "@docusaurus/theme-translations": 2.4.1 - "@docusaurus/types": 2.4.1 - "@docusaurus/utils": 2.4.1 - "@docusaurus/utils-common": 2.4.1 - "@docusaurus/utils-validation": 2.4.1 +"@docusaurus/theme-classic@npm:2.4.3": + version: 2.4.3 + resolution: "@docusaurus/theme-classic@npm:2.4.3" + dependencies: + "@docusaurus/core": 2.4.3 + "@docusaurus/mdx-loader": 2.4.3 + "@docusaurus/module-type-aliases": 2.4.3 + "@docusaurus/plugin-content-blog": 2.4.3 + "@docusaurus/plugin-content-docs": 2.4.3 + "@docusaurus/plugin-content-pages": 2.4.3 + "@docusaurus/theme-common": 2.4.3 + "@docusaurus/theme-translations": 2.4.3 + "@docusaurus/types": 2.4.3 + "@docusaurus/utils": 2.4.3 + "@docusaurus/utils-common": 2.4.3 + "@docusaurus/utils-validation": 2.4.3 "@mdx-js/react": ^1.6.22 clsx: ^1.2.1 copy-text-to-clipboard: ^3.0.1 @@ -2462,21 +2657,21 @@ __metadata: peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 058875d4c60f77f86b5d679b1ef99ed06101411f003d2d65fa4fe5ae6fbe5e5e6a291616268a18a29fdd84f0853cc4219a2c1801663b75f27c664b3ace7d009e + checksum: 215b7fa416f40ce68773265a168af47fa770583ebe33ec7b34c7e082dfe7c79252b589a6b26532cb0ab7dd089611a9cd0e20c94df097be320a227b98e3b3fbb8 languageName: node linkType: hard -"@docusaurus/theme-common@npm:2.4.1": - version: 2.4.1 - resolution: "@docusaurus/theme-common@npm:2.4.1" - dependencies: - "@docusaurus/mdx-loader": 2.4.1 - "@docusaurus/module-type-aliases": 2.4.1 - "@docusaurus/plugin-content-blog": 2.4.1 - "@docusaurus/plugin-content-docs": 2.4.1 - "@docusaurus/plugin-content-pages": 2.4.1 - "@docusaurus/utils": 2.4.1 - "@docusaurus/utils-common": 2.4.1 +"@docusaurus/theme-common@npm:2.4.3": + version: 2.4.3 + resolution: "@docusaurus/theme-common@npm:2.4.3" + dependencies: + "@docusaurus/mdx-loader": 2.4.3 + "@docusaurus/module-type-aliases": 2.4.3 + "@docusaurus/plugin-content-blog": 2.4.3 + "@docusaurus/plugin-content-docs": 2.4.3 + "@docusaurus/plugin-content-pages": 2.4.3 + "@docusaurus/utils": 2.4.3 + "@docusaurus/utils-common": 2.4.3 "@types/history": ^4.7.11 "@types/react": "*" "@types/react-router-config": "*" @@ -2489,22 +2684,22 @@ __metadata: peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 206db83caab59eadc5b8e5394d46b64ec8695bd20d4a3defe111c28094faf6de92481c3bb4e54c159a519bc782759031b121e17d7e0175d873a843f36630c539 + checksum: 76817f548705542124d708c804e724674ec9bf996a5cb2a5c9a2919416367567cca4a3fa6055589990c339f6e1fb9d3944e25ed30b79fabe191db00d6ef986ca languageName: node linkType: hard -"@docusaurus/theme-search-algolia@npm:2.4.1": - version: 2.4.1 - resolution: "@docusaurus/theme-search-algolia@npm:2.4.1" +"@docusaurus/theme-search-algolia@npm:2.4.3": + version: 2.4.3 + resolution: "@docusaurus/theme-search-algolia@npm:2.4.3" dependencies: "@docsearch/react": ^3.1.1 - "@docusaurus/core": 2.4.1 - "@docusaurus/logger": 2.4.1 - "@docusaurus/plugin-content-docs": 2.4.1 - "@docusaurus/theme-common": 2.4.1 - "@docusaurus/theme-translations": 2.4.1 - "@docusaurus/utils": 2.4.1 - "@docusaurus/utils-validation": 2.4.1 + "@docusaurus/core": 2.4.3 + "@docusaurus/logger": 2.4.3 + "@docusaurus/plugin-content-docs": 2.4.3 + "@docusaurus/theme-common": 2.4.3 + "@docusaurus/theme-translations": 2.4.3 + "@docusaurus/utils": 2.4.3 + "@docusaurus/utils-validation": 2.4.3 algoliasearch: ^4.13.1 algoliasearch-helper: ^3.10.0 clsx: ^1.2.1 @@ -2516,23 +2711,23 @@ __metadata: peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 00016804462e3ca961de96f477c397bf68bbfa7c641cfb95e76492ec00f2e0f8f5b19623cd6ad0fda31ad08aa29fa1a74185d9bd34f61437e7f36f711064f3ba + checksum: 665d244c25bff21dd45c983c9b85f9827d2dd58945b802d645370b5e7092820532faf488c0bc0ce88e8fc0088c7f56eb9abb96589cf3857372c1b61bba6cbed7 languageName: node linkType: hard -"@docusaurus/theme-translations@npm:2.4.1": - version: 2.4.1 - resolution: "@docusaurus/theme-translations@npm:2.4.1" +"@docusaurus/theme-translations@npm:2.4.3": + version: 2.4.3 + resolution: "@docusaurus/theme-translations@npm:2.4.3" dependencies: fs-extra: ^10.1.0 tslib: ^2.4.0 - checksum: cf21cd01db6426ccc29360fe9caca39e61ee5efde3796539e8292e212c25727227970f935050f294f0ab475f201720e32a1d09a7e40f2b08f56f69282f660da8 + checksum: 8424583a130b0d32b6adf578dc5daeefaad199019c8a6a23fbd67577209be64923cde59d423ea9d41d6e7cfc2318e7fa6a17a665e8ae1c871ce0880525f9b8fd languageName: node linkType: hard -"@docusaurus/types@npm:2.4.1": - version: 2.4.1 - resolution: "@docusaurus/types@npm:2.4.1" +"@docusaurus/types@npm:2.4.3": + version: 2.4.3 + resolution: "@docusaurus/types@npm:2.4.3" dependencies: "@types/history": ^4.7.11 "@types/react": "*" @@ -2545,13 +2740,13 @@ __metadata: peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: d44e91c9153802a5c63a0bd91e56654f901df837ac7b380dff8f165991728e88d29efa7c64c6522d0afdf8ec845613aff5867badb717d29b691392712f655936 + checksum: c123c45630e885b588f808baa06a97f8408a3381906f65cb92ae75732aedfca6ab2cada94f969c08e043b885b95298616440326259b789010e0986cbcd7a960b languageName: node linkType: hard -"@docusaurus/utils-common@npm:2.4.1": - version: 2.4.1 - resolution: "@docusaurus/utils-common@npm:2.4.1" +"@docusaurus/utils-common@npm:2.4.3": + version: 2.4.3 + resolution: "@docusaurus/utils-common@npm:2.4.3" dependencies: tslib: ^2.4.0 peerDependencies: @@ -2559,28 +2754,28 @@ __metadata: peerDependenciesMeta: "@docusaurus/types": optional: true - checksum: 475f05b94a879d9078564dbb081d91a953356f54d0a4384a8711281a62851b58d99df9b45664a30e40ac37733772cedd53a253d34a07fbd5b36bcce46ab200c8 + checksum: 1ae315d8d8ce7a0163a698ffdca55b734d21f336512138c128bc0fa2a8d224edbaad0c8dbd7a3de2e8ef734dc2656c505d09066dee4fc84819d153593abb8984 languageName: node linkType: hard -"@docusaurus/utils-validation@npm:2.4.1": - version: 2.4.1 - resolution: "@docusaurus/utils-validation@npm:2.4.1" +"@docusaurus/utils-validation@npm:2.4.3": + version: 2.4.3 + resolution: "@docusaurus/utils-validation@npm:2.4.3" dependencies: - "@docusaurus/logger": 2.4.1 - "@docusaurus/utils": 2.4.1 + "@docusaurus/logger": 2.4.3 + "@docusaurus/utils": 2.4.3 joi: ^17.6.0 js-yaml: ^4.1.0 tslib: ^2.4.0 - checksum: 44dc482770ea3932e68e58c0bb9503e1b3c73ce28565c438b0d68a7427ee91760ca84a5e150dfc4e04497e072fe4394050dd2af4c4ff43a227b1464e89d705a0 + checksum: d3472b3f7a0a029c2cef1f00bc9db403d5f7e74e2091eccbc45d06f5776a84fd73bd1a18cf3a8a3cc0348ce49f753a1300deac670c2a82c56070cc40ca9df06e languageName: node linkType: hard -"@docusaurus/utils@npm:2.4.1": - version: 2.4.1 - resolution: "@docusaurus/utils@npm:2.4.1" +"@docusaurus/utils@npm:2.4.3": + version: 2.4.3 + resolution: "@docusaurus/utils@npm:2.4.3" dependencies: - "@docusaurus/logger": 2.4.1 + "@docusaurus/logger": 2.4.3 "@svgr/webpack": ^6.2.1 escape-string-regexp: ^4.0.0 file-loader: ^6.2.0 @@ -2601,7 +2796,7 @@ __metadata: peerDependenciesMeta: "@docusaurus/types": optional: true - checksum: 4c7e49cabe6650b027c0f698344532fb81c8aaf912f17a287fad986e008ce7c6d34d372b44974488ce160ae43ef6aad4ac7b2790bc3fe2ab05ef5fa7b9506ce9 + checksum: dd1aa7688d1a4b2775e13a91d528608ceab33c57a921404d9a989867c31c8ef17fe3892e4f5680dfb4a783da7b9973e2077e907ff4ac172927433e606e8fa9b9 languageName: node linkType: hard @@ -3289,92 +3484,88 @@ __metadata: languageName: node linkType: hard -"@mos-connection/connector@npm:^3.0.4": - version: 3.0.4 - resolution: "@mos-connection/connector@npm:3.0.4" +"@mos-connection/connector@npm:^3.0.7": + version: 3.0.7 + resolution: "@mos-connection/connector@npm:3.0.7" dependencies: - "@mos-connection/helper": 3.0.4 - "@mos-connection/model": 3.0.4 - iconv-lite: ^0.6.2 - tslib: ^2.2.0 + "@mos-connection/helper": 3.0.7 + "@mos-connection/model": 3.0.7 + iconv-lite: ^0.6.3 + tslib: ^2.5.3 xml-js: ^1.6.11 xmlbuilder: ^15.1.1 - checksum: 7ec53e26aefac824787fc5cd51a0cd36ab1429d9fb4ca510a89a0d371bfcf5e5b82cc12b483c2faa924993c04a19eead87839304ea5d4c5dcc2c7ac8ceae2cd0 + checksum: 588a6c929eee17d272fe6cd15b8547b96bd36ad6e507cc6f0e6b879a479a63860e75c2e59287efe41fc78326bfa160b7a1e15a5dfbddb321f582a927cd64062f languageName: node linkType: hard -"@mos-connection/helper@npm:3.0.4": - version: 3.0.4 - resolution: "@mos-connection/helper@npm:3.0.4" +"@mos-connection/helper@npm:3.0.7": + version: 3.0.7 + resolution: "@mos-connection/helper@npm:3.0.7" dependencies: - "@mos-connection/model": 3.0.4 - iconv-lite: ^0.6.2 - tslib: ^2.2.0 + "@mos-connection/model": 3.0.7 + iconv-lite: ^0.6.3 + tslib: ^2.5.3 xml-js: ^1.6.11 xmlbuilder: ^15.1.1 - checksum: 43ade87fd9a7ca7e183377d36f4d3b0ae063180c8760657201e50169c0868647242c83adaf860c475b9959079512d42ceb43d8ed657356d02140d7aa93992d79 + checksum: 802c95955328d89e62aa0addbaf79ce339c93bce499d96b6d314991cd77acbcbd573e4d6eb48408de234708f908d608b20941f846fceaa22813ddb2e6bdf6b4e languageName: node linkType: hard -"@mos-connection/model@npm:3.0.4, @mos-connection/model@npm:^3.0.4": - version: 3.0.4 - resolution: "@mos-connection/model@npm:3.0.4" - checksum: 7de15fba9a818260fb57847dbc51695036720a7f499d2fe262c691630673e83c01bdc3ea62261529e661f957074a6d92e8d38305485293c5392e073dab33989b +"@mos-connection/model@npm:3.0.7, @mos-connection/model@npm:^3.0.7": + version: 3.0.7 + resolution: "@mos-connection/model@npm:3.0.7" + checksum: 4f7390a40fc5152bfcdb4e99eee243e3a1df248bf1f77e0a63d7d29d8db88cb8c9df5f231ab5d18e7cb1147ba8d39af5c621877221f343db630906e000c5b089 languageName: node linkType: hard -"@nestjs/axios@npm:0.1.0": - version: 0.1.0 - resolution: "@nestjs/axios@npm:0.1.0" - dependencies: - axios: 0.27.2 +"@nestjs/axios@npm:3.0.1": + version: 3.0.1 + resolution: "@nestjs/axios@npm:3.0.1" peerDependencies: - "@nestjs/common": ^7.0.0 || ^8.0.0 || ^9.0.0 + "@nestjs/common": ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + axios: ^1.3.1 reflect-metadata: ^0.1.12 rxjs: ^6.0.0 || ^7.0.0 - checksum: 72929b25caacb85517bae962b13d865a31aa3984aa9e55305e0a2306e54338fe51a7eb38ca38cab0fe8b4116fb35219bd02c8b0c4cac70e7b5aeb84d03a1db3f + checksum: 01cebc64efa7a1d9df7a9053fbc5124986a95078789a0f280a2a61c31601a70a35745e07fc385497b4f85c7bdb6216a3575728646f1e6e684c038ac31900129c languageName: node linkType: hard -"@nestjs/common@npm:9.3.11": - version: 9.3.11 - resolution: "@nestjs/common@npm:9.3.11" +"@nestjs/common@npm:10.3.0": + version: 10.3.0 + resolution: "@nestjs/common@npm:10.3.0" dependencies: iterare: 1.2.1 - tslib: 2.5.0 - uid: 2.0.1 + tslib: 2.6.2 + uid: 2.0.2 peerDependencies: - cache-manager: <=5 class-transformer: "*" class-validator: "*" reflect-metadata: ^0.1.12 rxjs: ^7.1.0 peerDependenciesMeta: - cache-manager: - optional: true class-transformer: optional: true class-validator: optional: true - checksum: c39dfa9f02268e3f7fa22c4e9eecff0cfbee39c4d4dd0efc757a52117696dc9aec1f87588252b8b267a18469a6c12987707708b44f97ccbb5c6297396c0d9c00 + checksum: c5444cb46bd4f4a4d28b5031f7c28a0cf9863bc2d5518910bfed6a49734f59e1ea08dd4651e2117ae82df81c933ef84f0963c5cdeee5ef1608cf1bd36ee291c5 languageName: node linkType: hard -"@nestjs/core@npm:9.3.11": - version: 9.3.11 - resolution: "@nestjs/core@npm:9.3.11" +"@nestjs/core@npm:10.3.0": + version: 10.3.0 + resolution: "@nestjs/core@npm:10.3.0" dependencies: "@nuxtjs/opencollective": 0.3.2 fast-safe-stringify: 2.1.1 iterare: 1.2.1 path-to-regexp: 3.2.0 - tslib: 2.5.0 - uid: 2.0.1 + tslib: 2.6.2 + uid: 2.0.2 peerDependencies: - "@nestjs/common": ^9.0.0 - "@nestjs/microservices": ^9.0.0 - "@nestjs/platform-express": ^9.0.0 - "@nestjs/websockets": ^9.0.0 + "@nestjs/common": ^10.0.0 + "@nestjs/microservices": ^10.0.0 + "@nestjs/platform-express": ^10.0.0 + "@nestjs/websockets": ^10.0.0 reflect-metadata: ^0.1.12 rxjs: ^7.1.0 peerDependenciesMeta: @@ -3384,7 +3575,7 @@ __metadata: optional: true "@nestjs/websockets": optional: true - checksum: 39d49e5b16cc260887233dd6701dbc53c073dc7522975d592f6c355db1f42fcf31eeaf300b9d03c63943f7330ecee910281cb78bb66d1c2d938d9b13491e70b4 + checksum: 7677b9fb97c8dec512c2a736c273ef08698b377af8c046bc5aad442ba3d35acbc17d177e76bf44a66678cae2ced2d265183e85be4190c501a195f16496df6396 languageName: node linkType: hard @@ -4072,29 +4263,30 @@ __metadata: languageName: node linkType: hard -"@openapitools/openapi-generator-cli@npm:^2.7.0": - version: 2.7.0 - resolution: "@openapitools/openapi-generator-cli@npm:2.7.0" +"@openapitools/openapi-generator-cli@npm:^2.9.0": + version: 2.9.0 + resolution: "@openapitools/openapi-generator-cli@npm:2.9.0" dependencies: - "@nestjs/axios": 0.1.0 - "@nestjs/common": 9.3.11 - "@nestjs/core": 9.3.11 + "@nestjs/axios": 3.0.1 + "@nestjs/common": 10.3.0 + "@nestjs/core": 10.3.0 "@nuxtjs/opencollective": 0.3.2 + axios: 1.6.5 chalk: 4.1.2 commander: 8.3.0 compare-versions: 4.1.4 concurrently: 6.5.1 console.table: 0.10.0 fs-extra: 10.1.0 - glob: 7.1.6 - inquirer: 8.2.5 + glob: 7.2.3 + inquirer: 8.2.6 lodash: 4.17.21 reflect-metadata: 0.1.13 - rxjs: 7.8.0 - tslib: 2.0.3 + rxjs: 7.8.1 + tslib: 2.6.2 bin: openapi-generator-cli: main.js - checksum: 92ca36779b43fe1e4868cd89bde4cb96918868aa62c8a69a9e199711d8e7093bab67f484d266fcbf37ca4ad87e4e91ea5759fb322c7999a299f5bfdc179065b8 + checksum: 102040381b42edbab4fa34aa49c3cf8188fdb892c3e251bcb5bdf7c599597f3572d52d44fed7bead5fcaec24d624491c20dc96d2f164fa44e7fc232c7d787f69 languageName: node linkType: hard @@ -4494,7 +4686,7 @@ __metadata: fast-clone: ^1.5.13 i18next: ^21.10.0 influx: ^5.9.3 - nanoid: ^3.3.6 + nanoid: ^3.3.7 object-path: ^0.11.8 prom-client: ^14.2.0 timecode: 0.0.4 @@ -4526,10 +4718,10 @@ __metadata: "@sofie-automation/shared-lib": 1.51.0-in-development amqplib: ^0.10.3 deepmerge: ^4.3.1 - elastic-apm-node: ^3.50.0 + elastic-apm-node: ^3.51.0 eventemitter3: ^4.0.7 jest-mock-extended: ^3.0.5 - mongodb: ^5.9.0 + mongodb: ^5.9.2 p-lazy: ^3.1.0 p-timeout: ^4.1.0 superfly-timeline: 9.0.0 @@ -4545,8 +4737,8 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/openapi@workspace:openapi" dependencies: - "@openapitools/openapi-generator-cli": ^2.7.0 - eslint-plugin-yml: ^1.9.0 + "@openapitools/openapi-generator-cli": ^2.9.0 + eslint-plugin-yml: ^1.12.2 js-yaml: ^4.1.0 tslib: ^2.6.2 wget-improved: ^3.4.0 @@ -4571,7 +4763,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: - "@mos-connection/model": ^3.0.4 + "@mos-connection/model": ^3.0.7 timeline-state-resolver-types: 9.1.0-nightly-release51-20231211-110613-75e2cbebd.0 tslib: ^2.6.2 type-fest: ^3.13.1 @@ -5185,12 +5377,12 @@ __metadata: languageName: node linkType: hard -"@types/amqplib@npm:^0.10.1": - version: 0.10.1 - resolution: "@types/amqplib@npm:0.10.1" +"@types/amqplib@npm:^0.10.4": + version: 0.10.4 + resolution: "@types/amqplib@npm:0.10.4" dependencies: "@types/node": "*" - checksum: 9f7dd964d15df82f73c26e84f028a5fe91ee96878e34625cf09d0153079007d3d506fcf9d327463227e6e30a357e0220bfb0848fb40c121ccd795154004d3733 + checksum: 920362c9967d9ddc0fbc90ae519aec7f635857d9dba640619af504306070a8da9baf688cc75d416f2fd4f1c5bd3de766adacefb6e0546a1d18e8c40bdc1d25f0 languageName: node linkType: hard @@ -5285,12 +5477,12 @@ __metadata: languageName: node linkType: hard -"@types/debug@npm:^4.1.8": - version: 4.1.8 - resolution: "@types/debug@npm:4.1.8" +"@types/debug@npm:^4.1.12": + version: 4.1.12 + resolution: "@types/debug@npm:4.1.12" dependencies: "@types/ms": "*" - checksum: a9a9bb40a199e9724aa944e139a7659173a9b274798ea7efbc277cb084bc37d32fc4c00877c3496fac4fed70a23243d284adb75c00b5fdabb38a22154d18e5df + checksum: 47876a852de8240bfdaf7481357af2b88cb660d30c72e73789abf00c499d6bc7cd5e52f41c915d1b9cd8ec9fef5b05688d7b7aef17f7f272c2d04679508d1053 languageName: node linkType: hard @@ -5303,10 +5495,10 @@ __metadata: languageName: node linkType: hard -"@types/ejson@npm:^2.2.0": - version: 2.2.0 - resolution: "@types/ejson@npm:2.2.0" - checksum: 0cbc610c58a60847822353782d133f7a3cf362223d58f6a2b6ce0ae6e1f22885dc8f2a6ca7676df2556ed631e74c5a4880575704b964fdef89d216dc69ac0ad6 +"@types/ejson@npm:^2.2.2": + version: 2.2.2 + resolution: "@types/ejson@npm:2.2.2" + checksum: 3f2eb25d98f93821156b071e647bca626563900ecb34bb81cdba33721ee7d046bb66db7d6de664312b9480821821a1d177cf5e146dc1f10adcf43cbd2e387bbe languageName: node linkType: hard @@ -5478,13 +5670,13 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:^29.5.5": - version: 29.5.5 - resolution: "@types/jest@npm:29.5.5" +"@types/jest@npm:^29.5.11": + version: 29.5.11 + resolution: "@types/jest@npm:29.5.11" dependencies: expect: ^29.0.0 pretty-format: ^29.0.0 - checksum: 56e55cde9949bcc0ee2fa34ce5b7c32c2bfb20e53424aa4ff3a210859eeaaa3fdf6f42f81a3f655238039cdaaaf108b054b7a8602f394e6c52b903659338d8c6 + checksum: f892a06ec9f0afa9a61cd7fa316ec614e21d4df1ad301b5a837787e046fcb40dfdf7f264a55e813ac6b9b633cb9d366bd5b8d1cea725e84102477b366df23fdd languageName: node linkType: hard @@ -5569,10 +5761,10 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^14.18.62": - version: 14.18.62 - resolution: "@types/node@npm:14.18.62" - checksum: 1335b4d58d2a21c7f60b8b78d0902ec0653a44f1ead4e921c37dd5f836910e0b00370abfde02c57bc3a73a35ebcbf4ef86b598be9a9ec13981a9a6d40e98fab0 +"@types/node@npm:^14.18.63": + version: 14.18.63 + resolution: "@types/node@npm:14.18.63" + checksum: be909061a54931778c71c49dc562586c32f909c4b6197e3d71e6dac726d8bd9fccb9f599c0df99f52742b68153712b5097c0f00cac4e279fa894b0ea6719a8fd languageName: node linkType: hard @@ -5597,10 +5789,10 @@ __metadata: languageName: node linkType: hard -"@types/object-path@npm:^0.11.2": - version: 0.11.2 - resolution: "@types/object-path@npm:0.11.2" - checksum: f97e1233e5d4840a33921c6aa2794f17da78dba68a93c560626383188eab0b0c4b10b53c3e8add61683623685c7534168713c19c9374cffdfe7c071848607c28 +"@types/object-path@npm:^0.11.4": + version: 0.11.4 + resolution: "@types/object-path@npm:0.11.4" + checksum: 7f1f5cb18b651d21e7861da176d8f87526c936ed949a8126a2692195cbe65734ed1a1a22c06a24a54afe1890483a3d6b074b402ebfca7a7567c1c287b588f563 languageName: node linkType: hard @@ -5820,10 +6012,10 @@ __metadata: languageName: node linkType: hard -"@types/underscore@npm:^1.11.9": - version: 1.11.9 - resolution: "@types/underscore@npm:1.11.9" - checksum: 432a65271cb5567784aeccd99aeea9af6a8bef00c709c3c6ef9f161a9ad03a9c8fc6ebfbff4ff9c26c474b84a4381d201cfc24a072225868755eef95f5152e72 +"@types/underscore@npm:^1.11.15": + version: 1.11.15 + resolution: "@types/underscore@npm:1.11.15" + checksum: 25fdf6da96e0d11ca39a4740aab6fa3bd717e57301be4cb9e7893dc0ad6ce330992d0c8e0b02cac5c5ea86df6f8949c5a8f1fb95f3666b85418d399d3b1112e9 languageName: node linkType: hard @@ -6989,13 +7181,14 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"axios@npm:0.27.2": - version: 0.27.2 - resolution: "axios@npm:0.27.2" +"axios@npm:1.6.5": + version: 1.6.5 + resolution: "axios@npm:1.6.5" dependencies: - follow-redirects: ^1.14.9 + follow-redirects: ^1.15.4 form-data: ^4.0.0 - checksum: 38cb7540465fe8c4102850c4368053c21683af85c5fdf0ea619f9628abbcb59415d1e22ebc8a6390d2bbc9b58a9806c874f139767389c862ec9b772235f06854 + proxy-from-env: ^1.1.0 + checksum: e28d67b2d9134cb4608c44d8068b0678cfdccc652742e619006f27264a30c7aba13b2cd19c6f1f52ae195b5232734925928fb192d5c85feea7edd2f273df206d languageName: node linkType: hard @@ -7478,6 +7671,20 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"browserslist@npm:^4.22.2": + version: 4.22.3 + resolution: "browserslist@npm:4.22.3" + dependencies: + caniuse-lite: ^1.0.30001580 + electron-to-chromium: ^1.4.648 + node-releases: ^2.0.14 + update-browserslist-db: ^1.0.13 + bin: + browserslist: cli.js + checksum: e62b17348e92143fe58181b02a6a97c4a98bd812d1dc9274673a54f73eec53dbed1c855ebf73e318ee00ee039f23c9a6d0e7629d24f3baef08c7a5b469742d57 + languageName: node + linkType: hard + "bs-logger@npm:0.x": version: 0.2.6 resolution: "bs-logger@npm:0.2.6" @@ -7816,6 +8023,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"caniuse-lite@npm:^1.0.30001580": + version: 1.0.30001582 + resolution: "caniuse-lite@npm:1.0.30001582" + checksum: 2fc420cb6e6080a9808781ff81a2f0d37d63897c8c981d477001be18e55c8ec33422cf966e49efdca61d4a5335d16a4d6a09d5bd6da22a8396d0dcd350b9b9da + languageName: node + linkType: hard + "caseless@npm:~0.12.0": version: 0.12.0 resolution: "caseless@npm:0.12.0" @@ -9238,7 +9452,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.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -9855,9 +10069,9 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"elastic-apm-node@npm:^3.50.0": - version: 3.50.0 - resolution: "elastic-apm-node@npm:3.50.0" +"elastic-apm-node@npm:^3.51.0": + version: 3.51.0 + resolution: "elastic-apm-node@npm:3.51.0" dependencies: "@elastic/ecs-pino-format": ^1.2.0 "@opentelemetry/api": ^1.4.1 @@ -9897,7 +10111,7 @@ asn1@evs-broadcast/node-asn1: sql-summary: ^1.0.1 stream-chopper: ^3.0.1 unicode-byte-truncate: ^1.0.0 - checksum: 485b742d30202e4436286d50c019bad0115b92c40f49130494e7bc35affb1e1dd764c6a14b6d21c54acf333f1925396fe5ab2ef86ce3bd5edc954e38fdc4e64e + checksum: e6a801e731d6a5178e7450c76e88b9a519823129986365b2f59c4ee8e02c2a0e624deacebce6e85ae0964f6ea876a9f562901ca0b3538f4b0452a24d7f1b0303 languageName: node linkType: hard @@ -9908,6 +10122,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"electron-to-chromium@npm:^1.4.648": + version: 1.4.653 + resolution: "electron-to-chromium@npm:1.4.653" + checksum: 5e1fb48e749811f4384cd7a9940585a124f46d2a9f5bdfd2d7e79685d55db448433da303bd0fb6c3665ed052299843b5661319e10119821defbc3be8ff8eb060 + languageName: node + linkType: hard + "emberplus-connection@npm:^0.2.1": version: 0.2.1 resolution: "emberplus-connection@npm:0.2.1" @@ -10294,6 +10515,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"eslint-compat-utils@npm:^0.4.0": + version: 0.4.1 + resolution: "eslint-compat-utils@npm:0.4.1" + dependencies: + semver: ^7.5.4 + peerDependencies: + eslint: ">=6.0.0" + checksum: 727ec5920178a4a235e7ecd455154e3fda5bdf213dacabe11db22921563fe42e11c0b5f52145e28106d6c8f51106cd86e410bbf0eb23180775a9dda82c35e3c2 + languageName: node + linkType: hard + "eslint-config-prettier@npm:^8.8.0": version: 8.10.0 resolution: "eslint-config-prettier@npm:8.10.0" @@ -10366,17 +10598,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"eslint-plugin-yml@npm:^1.9.0": - version: 1.9.0 - resolution: "eslint-plugin-yml@npm:1.9.0" +"eslint-plugin-yml@npm:^1.12.2": + version: 1.12.2 + resolution: "eslint-plugin-yml@npm:1.12.2" dependencies: debug: ^4.3.2 + eslint-compat-utils: ^0.4.0 lodash: ^4.17.21 natural-compare: ^1.4.0 yaml-eslint-parser: ^1.2.1 peerDependencies: eslint: ">=6.0.0" - checksum: d6ee8a32e1230e12aefc951040d06270f8c0fe9a7f93fa091a35eef05ebcf550556620ddabc65fcdb33700767445d6d07f71efd31021660ad51f1b8f0d0ed200 + checksum: 569ff28144a91125d43b4eba3e88c2fa1a04d63a6377953f0ed6ceeaff5877efe1002214129e527d87df0643b69e41ee2af954c17ca1c51228928540908ad0f3 languageName: node linkType: hard @@ -11217,7 +11450,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.14.0, follow-redirects@npm:^1.14.7, follow-redirects@npm:^1.14.9, follow-redirects@npm:^1.15.0": +"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.14.0, follow-redirects@npm:^1.14.7, follow-redirects@npm:^1.15.0": version: 1.15.3 resolution: "follow-redirects@npm:1.15.3" peerDependenciesMeta: @@ -11227,6 +11460,16 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"follow-redirects@npm:^1.15.4": + version: 1.15.5 + resolution: "follow-redirects@npm:1.15.5" + peerDependenciesMeta: + debug: + optional: true + checksum: 5ca49b5ce6f44338cbfc3546823357e7a70813cecc9b7b768158a1d32c1e62e7407c944402a918ea8c38ae2e78266312d617dc68783fac502cbb55e1047b34ec + languageName: node + linkType: hard + "for-each@npm:^0.3.3": version: 0.3.3 resolution: "for-each@npm:0.3.3" @@ -11807,17 +12050,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"glob@npm:7.1.6": - version: 7.1.6 - resolution: "glob@npm:7.1.6" +"glob@npm:7.2.3, glob@npm:^7.0.0, glob@npm:^7.0.5, glob@npm:^7.1.1, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6": + version: 7.2.3 + resolution: "glob@npm:7.2.3" dependencies: fs.realpath: ^1.0.0 inflight: ^1.0.4 inherits: 2 - minimatch: ^3.0.4 + minimatch: ^3.1.1 once: ^1.3.0 path-is-absolute: ^1.0.0 - checksum: 351d549dd90553b87c2d3f90ce11aed9e1093c74130440e7ae0592e11bbcd2ce7f0ebb8ba6bfe63aaf9b62166a7f4c80cb84490ae5d78408bb2572bf7d4ee0a6 + checksum: 29452e97b38fa704dabb1d1045350fb2467cf0277e155aa9ff7077e90ad81d1ea9d53d3ee63bd37c05b09a065e90f16aec4a65f5b8de401d1dac40bc5605d133 languageName: node linkType: hard @@ -11836,20 +12079,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"glob@npm:^7.0.0, glob@npm:^7.0.5, glob@npm:^7.1.1, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6": - version: 7.2.3 - resolution: "glob@npm:7.2.3" - dependencies: - fs.realpath: ^1.0.0 - inflight: ^1.0.4 - inherits: 2 - minimatch: ^3.1.1 - once: ^1.3.0 - path-is-absolute: ^1.0.0 - checksum: 29452e97b38fa704dabb1d1045350fb2467cf0277e155aa9ff7077e90ad81d1ea9d53d3ee63bd37c05b09a065e90f16aec4a65f5b8de401d1dac40bc5605d133 - languageName: node - linkType: hard - "glob@npm:^8.0.1": version: 8.1.0 resolution: "glob@npm:8.1.0" @@ -12647,7 +12876,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"iconv-lite@npm:^0.6.2": +"iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" dependencies: @@ -12911,30 +13140,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"inquirer@npm:8.2.5": - version: 8.2.5 - resolution: "inquirer@npm:8.2.5" - dependencies: - ansi-escapes: ^4.2.1 - chalk: ^4.1.1 - cli-cursor: ^3.1.0 - cli-width: ^3.0.0 - external-editor: ^3.0.3 - figures: ^3.0.0 - lodash: ^4.17.21 - mute-stream: 0.0.8 - ora: ^5.4.1 - run-async: ^2.4.0 - rxjs: ^7.5.5 - string-width: ^4.1.0 - strip-ansi: ^6.0.0 - through: ^2.3.6 - wrap-ansi: ^7.0.0 - checksum: f13ee4c444187786fb393609dedf6b30870115a57b603f2e6424f29a99abc13446fd45ee22461c33c9c40a92a60a8df62d0d6b25d74fc6676fa4cb211de55b55 - languageName: node - linkType: hard - -"inquirer@npm:^8.2.4": +"inquirer@npm:8.2.6, inquirer@npm:^8.2.4": version: 8.2.6 resolution: "inquirer@npm:8.2.6" dependencies: @@ -14875,10 +15081,10 @@ asn1@evs-broadcast/node-asn1: fast-clone: ^1.5.13 influx: ^5.9.3 tslib: ^2.6.2 - type-fest: ^4.5.0 + type-fest: ^4.10.2 underscore: ^1.13.6 - winston: ^3.10.0 - ws: ^8.14.2 + winston: ^3.11.0 + ws: ^8.16.0 languageName: unknown linkType: soft @@ -16004,9 +16210,9 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"mongodb@npm:^5.9.0": - version: 5.9.0 - resolution: "mongodb@npm:5.9.0" +"mongodb@npm:^5.9.2": + version: 5.9.2 + resolution: "mongodb@npm:5.9.2" dependencies: "@mongodb-js/saslprep": ^1.1.0 bson: ^5.5.0 @@ -16032,7 +16238,7 @@ asn1@evs-broadcast/node-asn1: optional: true snappy: optional: true - checksum: 5fb828d6503b7291c0bcc984ed35bdd0e2f166cd9d637a5ff9e8c2e497ef6260eea5a3f700f2850200cd8108057c858f96e37394ed2953aff3b85fe760bbd777 + checksum: fad5621f4a9764d71c48f2955be57925e506b14a4a63c5b9209ff733a58b2524cf9c87f216f2f02de092fab20eaa033ff5ade45c5cfa7f1c8c6bce114cb22e3f languageName: node linkType: hard @@ -16047,13 +16253,13 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "mos-gateway@workspace:mos-gateway" dependencies: - "@mos-connection/connector": ^3.0.4 + "@mos-connection/connector": ^3.0.7 "@sofie-automation/server-core-integration": 1.51.0-in-development "@sofie-automation/shared-lib": 1.51.0-in-development tslib: ^2.6.2 type-fest: ^3.13.1 underscore: ^1.13.6 - winston: ^3.10.0 + winston: ^3.11.0 languageName: unknown linkType: soft @@ -16137,6 +16343,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"nanoid@npm:^3.3.7": + version: 3.3.7 + resolution: "nanoid@npm:3.3.7" + bin: + nanoid: bin/nanoid.cjs + checksum: d36c427e530713e4ac6567d488b489a36582ef89da1d6d4e3b87eded11eb10d7042a877958c6f104929809b2ab0bafa17652b076cdf84324aa75b30b722204f2 + languageName: node + linkType: hard + "nanotimer@npm:^0.3.15": version: 0.3.15 resolution: "nanotimer@npm:0.3.15" @@ -16352,6 +16567,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"node-releases@npm:^2.0.14": + version: 2.0.14 + resolution: "node-releases@npm:2.0.14" + checksum: 59443a2f77acac854c42d321bf1b43dea0aef55cd544c6a686e9816a697300458d4e82239e2d794ea05f7bbbc8a94500332e2d3ac3f11f52e4b16cbe638b3c41 + languageName: node + linkType: hard + "nodemon@npm:^2.0.22": version: 2.0.22 resolution: "nodemon@npm:2.0.22" @@ -17474,17 +17696,17 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "packages@workspace:." dependencies: - "@babel/core": ^7.22.20 - "@babel/plugin-transform-modules-commonjs": ^7.22.15 + "@babel/core": ^7.23.9 + "@babel/plugin-transform-modules-commonjs": ^7.23.3 "@sofie-automation/code-standard-preset": ~2.4.7 - "@types/amqplib": ^0.10.1 - "@types/debug": ^4.1.8 - "@types/ejson": ^2.2.0 + "@types/amqplib": ^0.10.4 + "@types/debug": ^4.1.12 + "@types/ejson": ^2.2.2 "@types/got": ^9.6.12 - "@types/jest": ^29.5.5 - "@types/node": ^14.18.62 - "@types/object-path": ^0.11.2 - "@types/underscore": ^1.11.9 + "@types/jest": ^29.5.11 + "@types/node": ^14.18.63 + "@types/object-path": ^0.11.4 + "@types/underscore": ^1.11.15 babel-jest: ^29.7.0 copyfiles: ^2.4.1 jest: ^29.7.0 @@ -17496,10 +17718,10 @@ asn1@evs-broadcast/node-asn1: pinst: ^3.0.0 rimraf: ^4.4.1 semver: ^7.5.4 - ts-jest: ^29.1.1 - ts-node: ^10.9.1 + ts-jest: ^29.1.2 + ts-node: ^10.9.2 typedoc: ^0.23.28 - typescript: ~4.9 + typescript: ~4.9.5 languageName: unknown linkType: soft @@ -17998,7 +18220,7 @@ asn1@evs-broadcast/node-asn1: timeline-state-resolver: 9.1.0-nightly-release51-20231211-110613-75e2cbebd.0 tslib: ^2.6.2 underscore: ^1.13.6 - winston: ^3.10.0 + winston: ^3.11.0 languageName: unknown linkType: soft @@ -19973,12 +20195,12 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"rxjs@npm:7.8.0": - version: 7.8.0 - resolution: "rxjs@npm:7.8.0" +"rxjs@npm:7.8.1, rxjs@npm:^7.5.4, rxjs@npm:^7.5.5": + version: 7.8.1 + resolution: "rxjs@npm:7.8.1" dependencies: tslib: ^2.1.0 - checksum: 61b4d4fd323c1043d8d6ceb91f24183b28bcf5def4f01ca111511d5c6b66755bc5578587fe714ef5d67cf4c9f2e26f4490d4e1d8cabf9bd5967687835e9866a2 + checksum: de4b53db1063e618ec2eca0f7965d9137cabe98cf6be9272efe6c86b47c17b987383df8574861bcced18ebd590764125a901d5506082be84a8b8e364bf05f119 languageName: node linkType: hard @@ -19991,15 +20213,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"rxjs@npm:^7.5.4, rxjs@npm:^7.5.5": - version: 7.8.1 - resolution: "rxjs@npm:7.8.1" - dependencies: - tslib: ^2.1.0 - checksum: de4b53db1063e618ec2eca0f7965d9137cabe98cf6be9272efe6c86b47c17b987383df8574861bcced18ebd590764125a901d5506082be84a8b8e364bf05f119 - languageName: node - linkType: hard - "safe-array-concat@npm:^1.0.0, safe-array-concat@npm:^1.0.1": version: 1.0.1 resolution: "safe-array-concat@npm:1.0.1" @@ -20641,8 +20854,8 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "sofie-documentation@workspace:documentation" dependencies: - "@docusaurus/core": 2.4.1 - "@docusaurus/preset-classic": 2.4.1 + "@docusaurus/core": 2.4.3 + "@docusaurus/preset-classic": 2.4.3 "@mdx-js/react": ^1.6.22 "@svgr/webpack": ^5.5.0 clsx: ^1.2.1 @@ -21950,9 +22163,9 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"ts-jest@npm:^29.1.1": - version: 29.1.1 - resolution: "ts-jest@npm:29.1.1" +"ts-jest@npm:^29.1.2": + version: 29.1.2 + resolution: "ts-jest@npm:29.1.2" dependencies: bs-logger: 0.x fast-json-stable-stringify: 2.x @@ -21979,7 +22192,7 @@ asn1@evs-broadcast/node-asn1: optional: true bin: ts-jest: cli.js - checksum: a8c9e284ed4f819526749f6e4dc6421ec666f20ab44d31b0f02b4ed979975f7580b18aea4813172d43e39b29464a71899f8893dd29b06b4a351a3af8ba47b402 + checksum: a0ce0affc1b716c78c9ab55837829c42cb04b753d174a5c796bb1ddf9f0379fc20647b76fbe30edb30d9b23181908138d6b4c51ef2ae5e187b66635c295cefd5 languageName: node linkType: hard @@ -22021,6 +22234,44 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"ts-node@npm:^10.9.2": + version: 10.9.2 + resolution: "ts-node@npm:10.9.2" + dependencies: + "@cspotcode/source-map-support": ^0.8.0 + "@tsconfig/node10": ^1.0.7 + "@tsconfig/node12": ^1.0.7 + "@tsconfig/node14": ^1.0.0 + "@tsconfig/node16": ^1.0.2 + acorn: ^8.4.1 + acorn-walk: ^8.1.1 + arg: ^4.1.0 + create-require: ^1.1.0 + diff: ^4.0.1 + make-error: ^1.1.1 + v8-compile-cache-lib: ^3.0.1 + yn: 3.1.1 + peerDependencies: + "@swc/core": ">=1.2.50" + "@swc/wasm": ">=1.2.50" + "@types/node": "*" + typescript: ">=2.7" + peerDependenciesMeta: + "@swc/core": + optional: true + "@swc/wasm": + optional: true + bin: + ts-node: dist/bin.js + ts-node-cwd: dist/bin-cwd.js + ts-node-esm: dist/bin-esm.js + ts-node-script: dist/bin-script.js + ts-node-transpile-only: dist/bin-transpile.js + ts-script: dist/bin-script-deprecated.js + checksum: fde256c9073969e234526e2cfead42591b9a2aec5222bac154b0de2fa9e4ceb30efcd717ee8bc785a56f3a119bdd5aa27b333d9dbec94ed254bd26f8944c67ac + languageName: node + linkType: hard + "tsconfig-paths@npm:^4.1.2": version: 4.2.0 resolution: "tsconfig-paths@npm:4.2.0" @@ -22032,17 +22283,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"tslib@npm:2.0.3": - version: 2.0.3 - resolution: "tslib@npm:2.0.3" - checksum: 00fcdd1f9995c9f8eb6a4a1ad03f55bc95946321b7f55434182dddac259d4e095fedf78a84f73b6e32dd3f881d9281f09cb583123d3159ed4bdac9ad7393ef8b - languageName: node - linkType: hard - -"tslib@npm:2.5.0": - version: 2.5.0 - resolution: "tslib@npm:2.5.0" - checksum: ae3ed5f9ce29932d049908ebfdf21b3a003a85653a9a140d614da6b767a93ef94f460e52c3d787f0e4f383546981713f165037dc2274df212ea9f8a4541004e1 +"tslib@npm:2.6.2, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.5.0, tslib@npm:^2.5.1, tslib@npm:^2.5.3, tslib@npm:^2.6.0, tslib@npm:^2.6.2": + version: 2.6.2 + resolution: "tslib@npm:2.6.2" + checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad languageName: node linkType: hard @@ -22053,13 +22297,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.5.0, tslib@npm:^2.5.1, tslib@npm:^2.6.0, tslib@npm:^2.6.2": - version: 2.6.2 - resolution: "tslib@npm:2.6.2" - checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad - languageName: node - linkType: hard - "tsutils@npm:^3.21.0": version: 3.21.0 resolution: "tsutils@npm:3.21.0" @@ -22209,10 +22446,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"type-fest@npm:^4.5.0": - version: 4.5.0 - resolution: "type-fest@npm:4.5.0" - checksum: cddc3900f19671e16ec49080e5c06c197d5554a75a1b758ff6d07af1089925ade4bcc9d1af080bd4a4a4fdf973a90ad4a83229203c854a444c511f8a3daeb56d +"type-fest@npm:^4.10.2": + version: 4.10.2 + resolution: "type-fest@npm:4.10.2" + checksum: ef75736d51c10a885f955c07aed8f46103a8c9ae93742a75fbbdf023dd0e7169c524ebef292f37de19806051fb1bdd96c4098a0101c5f869f80db73bcb484bb1 languageName: node linkType: hard @@ -22319,7 +22556,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"typescript@npm:^3 || ^4, typescript@npm:^4.9.3, typescript@npm:~4.9": +"typescript@npm:^3 || ^4, typescript@npm:^4.9.3, typescript@npm:~4.9.5": version: 4.9.5 resolution: "typescript@npm:4.9.5" bin: @@ -22329,7 +22566,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"typescript@patch:typescript@^3 || ^4#~builtin, typescript@patch:typescript@^4.9.3#~builtin, typescript@patch:typescript@~4.9#~builtin": +"typescript@patch:typescript@^3 || ^4#~builtin, typescript@patch:typescript@^4.9.3#~builtin, typescript@patch:typescript@~4.9.5#~builtin": version: 4.9.5 resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=23ec76" bin: @@ -22362,12 +22599,12 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"uid@npm:2.0.1": - version: 2.0.1 - resolution: "uid@npm:2.0.1" +"uid@npm:2.0.2": + version: 2.0.2 + resolution: "uid@npm:2.0.2" dependencies: "@lukeed/csprng": ^1.0.0 - checksum: 0a3c697d8dd1f3b647afa35c411b11fd8fa2fb6dbd8a49fe109a4aa5214068c2c58781aa6e4516dfd16f0fc524fb7bba0833e9c1dc1ed3f1965b520349be9ad5 + checksum: 98aabddcd6fe46f9b331b0378a93ee9cc51474348ada02006df9d10b4abc783ed596748ed9f20d7f6c5ff395dbcd1e764a65a68db6f39a31c95ae85ef13fe979 languageName: node linkType: hard @@ -22711,6 +22948,20 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"update-browserslist-db@npm:^1.0.13": + version: 1.0.13 + resolution: "update-browserslist-db@npm:1.0.13" + dependencies: + escalade: ^3.1.1 + picocolors: ^1.0.0 + peerDependencies: + browserslist: ">= 4.21.0" + bin: + update-browserslist-db: cli.js + checksum: 1e47d80182ab6e4ad35396ad8b61008ae2a1330221175d0abd37689658bdb61af9b705bfc41057fd16682474d79944fb2d86767c5ed5ae34b6276b9bed353322 + languageName: node + linkType: hard + "update-notifier@npm:^5.1.0": version: 5.1.0 resolution: "update-notifier@npm:5.1.0" @@ -23566,11 +23817,11 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"winston@npm:^3.10.0": - version: 3.10.0 - resolution: "winston@npm:3.10.0" +"winston@npm:^3.11.0": + version: 3.11.0 + resolution: "winston@npm:3.11.0" dependencies: - "@colors/colors": 1.5.0 + "@colors/colors": ^1.6.0 "@dabh/diagnostics": ^2.0.2 async: ^3.2.3 is-stream: ^2.0.0 @@ -23581,7 +23832,7 @@ asn1@evs-broadcast/node-asn1: stack-trace: 0.0.x triple-beam: ^1.3.0 winston-transport: ^4.5.0 - checksum: 47df0361220d12b46d1b3c98a1c380a3718321739d527a182ce7984fc20715e5b0b55db0bcd3fd076d1b1d3261903b890b053851cfd4bc028bda7951fa8ca2e0 + checksum: ca4454070f7a71b19f53c8c1765c59a013dab220edb49161b2e81917751d3e9edc3382430e4fb050feda04fb8463290ecab7cbc9240ec8d3d3b32a121849bbb0 languageName: node linkType: hard @@ -23762,7 +24013,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"ws@npm:^8.13.0, ws@npm:^8.14.2": +"ws@npm:^8.13.0": version: 8.14.2 resolution: "ws@npm:8.14.2" peerDependencies: @@ -23777,6 +24028,21 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"ws@npm:^8.16.0": + version: 8.16.0 + resolution: "ws@npm:8.16.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: feb3eecd2bae82fa8a8beef800290ce437d8b8063bdc69712725f21aef77c49cb2ff45c6e5e7fce622248f9c7abaee506bae0a9064067ffd6935460c7357321b + languageName: node + linkType: hard + "xdg-basedir@npm:^4.0.0": version: 4.0.0 resolution: "xdg-basedir@npm:4.0.0" diff --git a/yarn.lock b/yarn.lock index 21fe05b125..f645baad53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -421,11 +421,11 @@ __metadata: version: 0.0.0-use.local resolution: "automation-core@workspace:." dependencies: - concurrently: ^8.2.1 + concurrently: ^8.2.2 lint-staged: ^13.3.0 - rimraf: ^5.0.1 + rimraf: ^5.0.5 semver: ^7.5.4 - snyk-nodejs-lockfile-parser: ^1.52.3 + snyk-nodejs-lockfile-parser: ^1.52.11 languageName: unknown linkType: soft @@ -645,9 +645,9 @@ __metadata: languageName: node linkType: hard -"concurrently@npm:^8.2.1": - version: 8.2.1 - resolution: "concurrently@npm:8.2.1" +"concurrently@npm:^8.2.2": + version: 8.2.2 + resolution: "concurrently@npm:8.2.2" dependencies: chalk: ^4.1.2 date-fns: ^2.30.0 @@ -661,7 +661,7 @@ __metadata: bin: conc: dist/bin/concurrently.js concurrently: dist/bin/concurrently.js - checksum: 216cb16d5b301cbd9c657b19430836d1686fe8fa9b9ef35ef7ac601e1a5cf6535166a3e57de446696dbd5e7e3f45d78fc70f33c5fd4bb565342cd5e752c5b069 + checksum: 8ac774df06869773438f1bf91025180c52d5b53139bc86cf47659136c0d97461d0579c515d848d1e945d4e3e0cafe646b2ea18af8d74259b46abddcfe39b2c6c languageName: node linkType: hard @@ -1849,7 +1849,7 @@ __metadata: languageName: node linkType: hard -"rimraf@npm:^5.0.1": +"rimraf@npm:^5.0.5": version: 5.0.5 resolution: "rimraf@npm:5.0.5" dependencies: @@ -1969,9 +1969,9 @@ __metadata: languageName: node linkType: hard -"snyk-nodejs-lockfile-parser@npm:^1.52.3": - version: 1.52.3 - resolution: "snyk-nodejs-lockfile-parser@npm:1.52.3" +"snyk-nodejs-lockfile-parser@npm:^1.52.11": + version: 1.52.11 + resolution: "snyk-nodejs-lockfile-parser@npm:1.52.11" dependencies: "@snyk/dep-graph": ^2.3.0 "@snyk/graphlib": 2.1.9-patch.3 @@ -1991,7 +1991,7 @@ __metadata: uuid: ^8.3.0 bin: parse-nodejs-lockfile: bin/index.js - checksum: 5c94d7d3127fc964d72476b7afaf3d9ab0366724d77748d46287bb31ff1f1706d207651634700bd0f99f333a8283d763da1c364bc47740ea17d6db3c31bc4dc6 + checksum: b40e456f184f627d57cf1cac81995829eaaf4597fa69b288996db43f8f177bcda302b9081c29d5ce427fb03b7c3e27943a4e386a3c23e9dd5412887b62125aeb languageName: node linkType: hard From d92bf8d030dbfd160cfced407f671fe7e1a58a8e Mon Sep 17 00:00:00 2001 From: Krzysztof Zegzula Date: Fri, 2 Feb 2024 11:38:55 +0100 Subject: [PATCH 231/479] fix: remove adlib action's publicData from userLog SOFIE-2939 (#1137) ...by removing an "optimization" that fetched the `privateData` in the rest API's `executeAdLib`, which behaved differently from the Meteor API --- meteor/server/api/rest/v1/playlists.ts | 5 ++- meteor/server/api/userActions.ts | 1 - packages/corelib/src/worker/studio.ts | 1 - .../__tests__/playout-executeAction.test.ts | 7 ---- .../job-worker/src/playout/adlibAction.ts | 33 +++++++++---------- 5 files changed, 17 insertions(+), 30 deletions(-) diff --git a/meteor/server/api/rest/v1/playlists.ts b/meteor/server/api/rest/v1/playlists.ts index 644820749f..7fa1dcfb47 100644 --- a/meteor/server/api/rest/v1/playlists.ts +++ b/meteor/server/api/rest/v1/playlists.ts @@ -109,10 +109,10 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { segmentAdLibPiece, bucketAdLibPiece, AdLibActions.findOneAsync(adLibId as AdLibActionId, { - projection: { _id: 1, actionId: 1, userData: 1, privateData: 1 }, + projection: { _id: 1, actionId: 1, userData: 1 }, }), RundownBaselineAdLibActions.findOneAsync(adLibId as RundownBaselineAdLibActionId, { - projection: { _id: 1, actionId: 1, userData: 1, privateData: 1 }, + projection: { _id: 1, actionId: 1, userData: 1 }, }), ] ) @@ -204,7 +204,6 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { actionId: adLibActionDoc.actionId, userData: adLibActionDoc.userData, triggerMode: triggerMode ?? undefined, - privateData: adLibActionDoc.privateData, } ) } else { diff --git a/meteor/server/api/userActions.ts b/meteor/server/api/userActions.ts index 64d342fd32..9c5f563ce4 100644 --- a/meteor/server/api/userActions.ts +++ b/meteor/server/api/userActions.ts @@ -415,7 +415,6 @@ class ServerUserActionAPI actionId, userData, triggerMode: triggerMode ?? undefined, - privateData: null, } ) } diff --git a/packages/corelib/src/worker/studio.ts b/packages/corelib/src/worker/studio.ts index ca32dbe9ee..c4adacf10f 100644 --- a/packages/corelib/src/worker/studio.ts +++ b/packages/corelib/src/worker/studio.ts @@ -242,7 +242,6 @@ export interface ExecuteActionProps extends RundownPlayoutPropsBase { actionId: string userData: any triggerMode?: string - privateData?: unknown | undefined | null } export interface ExecuteBucketAdLibOrActionProps extends RundownPlayoutPropsBase { bucketId: BucketId diff --git a/packages/job-worker/src/playout/__tests__/playout-executeAction.test.ts b/packages/job-worker/src/playout/__tests__/playout-executeAction.test.ts index 814af9f3e8..0f294660bc 100644 --- a/packages/job-worker/src/playout/__tests__/playout-executeAction.test.ts +++ b/packages/job-worker/src/playout/__tests__/playout-executeAction.test.ts @@ -57,7 +57,6 @@ describe('Playout API', () => { actionDocId: actionDocId, actionId: actionId, userData: userData, - privateData: undefined, }) ).rejects.toMatchUserError(UserErrorMessage.ActionsNotSupported) @@ -73,7 +72,6 @@ describe('Playout API', () => { actionDocId, actionId, userData, - privateData: undefined, }) ).rejects.toMatchUserError(UserErrorMessage.InternalError) @@ -99,7 +97,6 @@ describe('Playout API', () => { actionDocId, actionId, userData, - privateData: undefined, }) expect(syncPlayheadInfinitesForNextPartInstanceMock).toHaveBeenCalledTimes(0) @@ -127,7 +124,6 @@ describe('Playout API', () => { actionDocId, actionId, userData, - privateData: undefined, }) expect(syncPlayheadInfinitesForNextPartInstanceMock).toHaveBeenCalledTimes(1) @@ -155,7 +151,6 @@ describe('Playout API', () => { actionDocId, actionId, userData, - privateData: undefined, }) expect(syncPlayheadInfinitesForNextPartInstanceMock).toHaveBeenCalledTimes(1) @@ -179,7 +174,6 @@ describe('Playout API', () => { actionDocId, actionId, userData, - privateData: undefined, }) expect(takeNextPartMock).toHaveBeenCalledTimes(1) @@ -202,7 +196,6 @@ describe('Playout API', () => { actionDocId, actionId, userData, - privateData: undefined, }) expect(takeNextPartMock).toHaveBeenCalledTimes(0) diff --git a/packages/job-worker/src/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index cfc0c17e6f..932bceb3dc 100644 --- a/packages/job-worker/src/playout/adlibAction.ts +++ b/packages/job-worker/src/playout/adlibAction.ts @@ -81,30 +81,27 @@ export async function executeAdlibActionAndSaveModel( }, }) - if (data.privateData === null) { - const [adLibAction, baselineAdLibAction, bucketAdLibAction] = await Promise.all([ - context.directCollections.AdLibActions.findOne(data.actionDocId as AdLibActionId, { - projection: { _id: 1, privateData: 1 }, - }), - context.directCollections.RundownBaselineAdLibActions.findOne( - data.actionDocId as RundownBaselineAdLibActionId, - { - projection: { _id: 1, privateData: 1 }, - } - ), - context.directCollections.BucketAdLibActions.findOne(data.actionDocId as BucketAdLibActionId, { + const [adLibAction, baselineAdLibAction, bucketAdLibAction] = await Promise.all([ + context.directCollections.AdLibActions.findOne(data.actionDocId as AdLibActionId, { + projection: { _id: 1, privateData: 1 }, + }), + context.directCollections.RundownBaselineAdLibActions.findOne( + data.actionDocId as RundownBaselineAdLibActionId, + { projection: { _id: 1, privateData: 1 }, - }), - ]) - const adLibActionDoc = adLibAction ?? baselineAdLibAction ?? bucketAdLibAction - data.privateData = adLibActionDoc?.privateData - } + } + ), + context.directCollections.BucketAdLibActions.findOne(data.actionDocId as BucketAdLibActionId, { + projection: { _id: 1, privateData: 1 }, + }), + ]) + const adLibActionDoc = adLibAction ?? baselineAdLibAction ?? bucketAdLibAction const actionParameters: ExecuteActionParameters = { actionId: data.actionId, userData: data.userData, triggerMode: data.triggerMode, - privateData: data.privateData, + privateData: adLibActionDoc?.privateData, } try { From c4af929d5becc1625e2e32fe14ff3d56c5c832d4 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Fri, 2 Feb 2024 11:42:02 +0100 Subject: [PATCH 232/479] chore: lint --- meteor/client/styles/shelf/adLibPanel.scss | 2 +- meteor/client/styles/shelf/dashboard.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/meteor/client/styles/shelf/adLibPanel.scss b/meteor/client/styles/shelf/adLibPanel.scss index 1e5f697adb..3db5fe9e02 100644 --- a/meteor/client/styles/shelf/adLibPanel.scss +++ b/meteor/client/styles/shelf/adLibPanel.scss @@ -119,7 +119,7 @@ $adlib-item-selected-color: var(--adlib-item-selected-color); > .adlib-panel__list-view__toolbar__filter__clear { position: absolute; right: 0.5em; - top: 0.35em; + top: 0.35em; opacity: 0.3; cursor: pointer; background: none; diff --git a/meteor/client/styles/shelf/dashboard.scss b/meteor/client/styles/shelf/dashboard.scss index b768a268a1..90d2bb1d0c 100644 --- a/meteor/client/styles/shelf/dashboard.scss +++ b/meteor/client/styles/shelf/dashboard.scss @@ -161,7 +161,7 @@ $dashboard-button-height: 5.625em; > .adlib-panel__list-view__toolbar__filter__clear { position: absolute; right: 0.5em; - top: 0.35em; + top: 0.35em; opacity: 0.3; cursor: pointer; background: none; From 2eb016e1c2bc00d19bd725c5990b55a0b81a43e6 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 2 Feb 2024 12:58:08 +0000 Subject: [PATCH 233/479] chore: update tsr --- meteor/yarn.lock | 10 +-- packages/playout-gateway/package.json | 2 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 123 ++++++++++++-------------- 4 files changed, 66 insertions(+), 71 deletions(-) diff --git a/meteor/yarn.lock b/meteor/yarn.lock index b678eda2e9..43780f4a53 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1425,7 +1425,7 @@ __metadata: resolution: "@sofie-automation/shared-lib@portal:../packages/shared-lib::locator=automation-core%40workspace%3A." dependencies: "@mos-connection/model": ^3.0.7 - timeline-state-resolver-types: 9.1.0-nightly-release51-20231211-110613-75e2cbebd.0 + timeline-state-resolver-types: 9.1.0-nightly-release51-20240202-124852-30953c1e0.0 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: node @@ -11745,12 +11745,12 @@ __metadata: languageName: node linkType: hard -"timeline-state-resolver-types@npm:9.1.0-nightly-release51-20231211-110613-75e2cbebd.0": - version: 9.1.0-nightly-release51-20231211-110613-75e2cbebd.0 - resolution: "timeline-state-resolver-types@npm:9.1.0-nightly-release51-20231211-110613-75e2cbebd.0" +"timeline-state-resolver-types@npm:9.1.0-nightly-release51-20240202-124852-30953c1e0.0": + version: 9.1.0-nightly-release51-20240202-124852-30953c1e0.0 + resolution: "timeline-state-resolver-types@npm:9.1.0-nightly-release51-20240202-124852-30953c1e0.0" dependencies: tslib: ^2.5.1 - checksum: 64038c242b007e59897fb62c117753960ff1406885bd3f2b566bc031b6ecbbb0ec5cefae3c7f875baa85a1284feabf25f5da4cb1de72ec279ce6f815d91a5c8d + checksum: e5ec6a10e26357c7964a0cf3f6c9c02392ee0b9f492a48a9a1d0d172f563fc44ce48562da28521e4ed957908c5043d582b36aa7a12d52cd57d7f8433c90af022 languageName: node linkType: hard diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index a129ca17da..6b9d902b0a 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -60,7 +60,7 @@ "@sofie-automation/shared-lib": "1.51.0-in-development", "debug": "^4.3.4", "influx": "^5.9.3", - "timeline-state-resolver": "9.1.0-nightly-release51-20231211-110613-75e2cbebd.0", + "timeline-state-resolver": "9.1.0-nightly-release51-20240202-124852-30953c1e0.0", "tslib": "^2.6.2", "underscore": "^1.13.6", "winston": "^3.11.0" diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 027e3b0fa8..128417931e 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -39,7 +39,7 @@ ], "dependencies": { "@mos-connection/model": "^3.0.7", - "timeline-state-resolver-types": "9.1.0-nightly-release51-20231211-110613-75e2cbebd.0", + "timeline-state-resolver-types": "9.1.0-nightly-release51-20240202-124852-30953c1e0.0", "tslib": "^2.6.2", "type-fest": "^3.13.1" }, diff --git a/packages/yarn.lock b/packages/yarn.lock index 7d2ae6e4f4..97ff333264 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -3330,6 +3330,13 @@ __metadata: languageName: node linkType: hard +"@msgpack/msgpack@npm:^2.7.1": + version: 2.8.0 + resolution: "@msgpack/msgpack@npm:2.8.0" + checksum: bead9393f57239007a2fe455df5277cbc03b125f14f310162a652b81471dcf3ab6780eaa24b36e20aa742998910a6840147d08b7267063b8e2de5d40c624360e + languageName: node + linkType: hard + "@nestjs/axios@npm:3.0.1": version: 3.0.1 resolution: "@nestjs/axios@npm:3.0.1" @@ -4576,7 +4583,7 @@ __metadata: resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: "@mos-connection/model": ^3.0.7 - timeline-state-resolver-types: 9.1.0-nightly-release51-20231211-110613-75e2cbebd.0 + timeline-state-resolver-types: 9.1.0-nightly-release51-20240202-124852-30953c1e0.0 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: unknown @@ -6927,16 +6934,16 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"atem-state@npm:1.2.0-nightly-master-20231129-144508-fb53d10.0": - version: 1.2.0-nightly-master-20231129-144508-fb53d10.0 - resolution: "atem-state@npm:1.2.0-nightly-master-20231129-144508-fb53d10.0" +"atem-state@npm:1.2.0-nightly-master-20240202-124617-5b52568.0": + version: 1.2.0-nightly-master-20240202-124617-5b52568.0 + resolution: "atem-state@npm:1.2.0-nightly-master-20240202-124617-5b52568.0" dependencies: deepmerge: ^4.3.1 tslib: ^2.6.2 type-fest: ^3.13.1 peerDependencies: - atem-connection: 3.3.2 - checksum: 25687ec294ae9af0ed803c06c845ec430a4f26ed61cb7b257dd6ae8155b6d70f50368ece4cde42a769a2098961ed112d62ff633ca258fa776035aa66be21a171 + atem-connection: 3.4 + checksum: 473c305838b6e5540beba5433dd8c1bd9212734b578e130b6eca6aee274f70ef3d469412bb34ea069e89921f15bf7fa320adfb26658698fc99d82040c34d7a84 languageName: node linkType: hard @@ -7828,18 +7835,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"casparcg-connection@npm:^6.1.1": - version: 6.2.1 - resolution: "casparcg-connection@npm:6.2.1" - dependencies: - eventemitter3: ^5.0.1 - tslib: ^2.5.0 - xml2js: ^0.6.2 - checksum: a086810faf0dbf91fc2987c1f4062696d5396fcbfeee16bc8b969a1a5edaac30657179c25164b1b62d9f79789ceee2e842822fe30dece420893e6fab50a467dd - languageName: node - linkType: hard - -"casparcg-state@npm:^3.0.2": +"casparcg-state@npm:3.0.3": version: 3.0.3 resolution: "casparcg-state@npm:3.0.3" dependencies: @@ -8905,6 +8901,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"crypto-js@npm:^4.1.1": + version: 4.2.0 + resolution: "crypto-js@npm:4.2.0" + checksum: f051666dbc077c8324777f44fbd3aaea2986f198fe85092535130d17026c7c2ccf2d23ee5b29b36f7a4a07312db2fae23c9094b644cc35f7858b1b4fcaf27774 + languageName: node + linkType: hard + "crypto-random-string@npm:^2.0.0": version: 2.0.0 resolution: "crypto-random-string@npm:2.0.0" @@ -12623,12 +12626,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"hyperdeck-connection@npm:^0.5.0": - version: 0.5.0 - resolution: "hyperdeck-connection@npm:0.5.0" +"hyperdeck-connection@npm:2.0.0-nightly-master-20231205-105655-157a483.0": + version: 2.0.0-nightly-master-20231205-105655-157a483.0 + resolution: "hyperdeck-connection@npm:2.0.0-nightly-master-20231205-105655-157a483.0" dependencies: - tslib: ^2.3.1 - checksum: 629ad01a767f06af8cfa6910dea4ba3e083f3750955aeda6ce7c264fee50aa938b5437c9903b475f239cef112151c93f6f129e2ac91d32ab7f3c74a57e41fb01 + eventemitter3: ^4.0.7 + tslib: ^2.6.2 + checksum: 2c4e7f437cfe040903545a52b09664343130d668e546843db752f342b2ce474d90377011d4c5632feb8aa4f6b5474544cd7bef605e7f471eb3f89b76c3753d20 languageName: node linkType: hard @@ -13565,12 +13569,12 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"isomorphic-ws@npm:^4.0.1": - version: 4.0.1 - resolution: "isomorphic-ws@npm:4.0.1" +"isomorphic-ws@npm:^5.0.0": + version: 5.0.0 + resolution: "isomorphic-ws@npm:5.0.0" peerDependencies: ws: "*" - checksum: d7190eadefdc28bdb93d67b5f0c603385aaf87724fa2974abb382ac1ec9756ed2cfb27065cbe76122879c2d452e2982bc4314317f3d6c737ddda6c047328771a + checksum: e20eb2aee09ba96247465fda40c6d22c1153394c0144fa34fe6609f341af4c8c564f60ea3ba762335a7a9c306809349f9b863c8beedf2beea09b299834ad5398 languageName: node linkType: hard @@ -17007,15 +17011,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"obs-websocket-js@npm:^4.0.3": - version: 4.0.3 - resolution: "obs-websocket-js@npm:4.0.3" +"obs-websocket-js@npm:^5.0.4": + version: 5.0.4 + resolution: "obs-websocket-js@npm:5.0.4" dependencies: - debug: ^4.1.0 - isomorphic-ws: ^4.0.1 - sha.js: ^2.4.9 - ws: ^7.2.0 - checksum: 54e267b3ba6616fbdaf152bbd4ce76657a71c6c30c639bcb90199b127f02bddb587d20b98bf33b81e5063b0708b23acf324bdf1ce9db41482281a5aa88dcbcc1 + "@msgpack/msgpack": ^2.7.1 + crypto-js: ^4.1.1 + debug: ^4.3.2 + eventemitter3: ^5.0.1 + isomorphic-ws: ^5.0.0 + type-fest: ^3.11.0 + ws: ^8.13.0 + checksum: 5fab50747624abebe18a186f1cacd3f2ba72fe2a642f43a05babcc4b8da6342f4bcce31e8aacd7a895debf17a88eaff2222e423c9c55d2ba6b116ebb05cc4c2e languageName: node linkType: hard @@ -17975,7 +17982,7 @@ asn1@evs-broadcast/node-asn1: "@sofie-automation/shared-lib": 1.51.0-in-development debug: ^4.3.4 influx: ^5.9.3 - timeline-state-resolver: 9.1.0-nightly-release51-20231211-110613-75e2cbebd.0 + timeline-state-resolver: 9.1.0-nightly-release51-20240202-124852-30953c1e0.0 tslib: ^2.6.2 underscore: ^1.13.6 winston: ^3.11.0 @@ -20318,18 +20325,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"sha.js@npm:^2.4.9": - version: 2.4.11 - resolution: "sha.js@npm:2.4.11" - dependencies: - inherits: ^2.0.1 - safe-buffer: ^5.0.1 - bin: - sha.js: ./bin.js - checksum: ebd3f59d4b799000699097dadb831c8e3da3eb579144fd7eb7a19484cbcbb7aca3c68ba2bb362242eb09e33217de3b4ea56e4678184c334323eca24a58e3ad07 - languageName: node - linkType: hard - "shallow-clone-shim@npm:^2.0.0": version: 2.0.0 resolution: "shallow-clone-shim@npm:2.0.0" @@ -21598,34 +21593,34 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"timeline-state-resolver-types@npm:9.1.0-nightly-release51-20231211-110613-75e2cbebd.0": - version: 9.1.0-nightly-release51-20231211-110613-75e2cbebd.0 - resolution: "timeline-state-resolver-types@npm:9.1.0-nightly-release51-20231211-110613-75e2cbebd.0" +"timeline-state-resolver-types@npm:9.1.0-nightly-release51-20240202-124852-30953c1e0.0": + version: 9.1.0-nightly-release51-20240202-124852-30953c1e0.0 + resolution: "timeline-state-resolver-types@npm:9.1.0-nightly-release51-20240202-124852-30953c1e0.0" dependencies: tslib: ^2.5.1 - checksum: 64038c242b007e59897fb62c117753960ff1406885bd3f2b566bc031b6ecbbb0ec5cefae3c7f875baa85a1284feabf25f5da4cb1de72ec279ce6f815d91a5c8d + checksum: e5ec6a10e26357c7964a0cf3f6c9c02392ee0b9f492a48a9a1d0d172f563fc44ce48562da28521e4ed957908c5043d582b36aa7a12d52cd57d7f8433c90af022 languageName: node linkType: hard -"timeline-state-resolver@npm:9.1.0-nightly-release51-20231211-110613-75e2cbebd.0": - version: 9.1.0-nightly-release51-20231211-110613-75e2cbebd.0 - resolution: "timeline-state-resolver@npm:9.1.0-nightly-release51-20231211-110613-75e2cbebd.0" +"timeline-state-resolver@npm:9.1.0-nightly-release51-20240202-124852-30953c1e0.0": + version: 9.1.0-nightly-release51-20240202-124852-30953c1e0.0 + resolution: "timeline-state-resolver@npm:9.1.0-nightly-release51-20240202-124852-30953c1e0.0" dependencies: "@tv2media/v-connection": ^7.3.2 atem-connection: 3.4.0 - atem-state: 1.2.0-nightly-master-20231129-144508-fb53d10.0 + atem-state: 1.2.0-nightly-master-20240202-124617-5b52568.0 cacheable-lookup: ^5.0.3 - casparcg-connection: ^6.1.1 - casparcg-state: ^3.0.2 + casparcg-connection: 6.2.0 + casparcg-state: 3.0.3 debug: ^4.3.4 deepmerge: ^4.3.1 emberplus-connection: ^0.2.1 eventemitter3: ^4.0.7 got: ^11.8.6 hpagent: ^1.2.0 - hyperdeck-connection: ^0.5.0 + hyperdeck-connection: 2.0.0-nightly-master-20231205-105655-157a483.0 klona: ^2.0.6 - obs-websocket-js: ^4.0.3 + obs-websocket-js: ^5.0.4 osc: ^2.4.4 p-all: ^3.0.0 p-queue: ^6.6.2 @@ -21633,7 +21628,7 @@ asn1@evs-broadcast/node-asn1: sprintf-js: ^1.1.2 superfly-timeline: ^9.0.0 threadedclass: ^1.2.1 - timeline-state-resolver-types: 9.1.0-nightly-release51-20231211-110613-75e2cbebd.0 + timeline-state-resolver-types: 9.1.0-nightly-release51-20240202-124852-30953c1e0.0 tslib: ^2.5.1 tv-automation-quantel-gateway-client: ^3.1.7 type-fest: ^3.10.0 @@ -21641,7 +21636,7 @@ asn1@evs-broadcast/node-asn1: utf-8-validate: ^5.0.10 ws: ^7.5.9 xml-js: ^1.6.11 - checksum: eea2807ee6cad17992459fb6fb617bb1300ae4c7fe5fafd5f1d60e392b33928bd6ac5a655a51c69ee851d69f7c2b3b4a2e153c1f4ca7c05098e1b104c84970a3 + checksum: c3bb968755cd547b498c6ec311ad321e7721f2d7c69eac68c1244060e3c1083971c4b43a2ec8a9c5e4f6c56e38fc45da355e0b1643634bbd1de13eaee7cd9d81 languageName: node linkType: hard @@ -22159,7 +22154,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"type-fest@npm:^3.1.0, type-fest@npm:^3.10.0, type-fest@npm:^3.13.1": +"type-fest@npm:^3.1.0, type-fest@npm:^3.10.0, type-fest@npm:^3.11.0, type-fest@npm:^3.13.1": version: 3.13.1 resolution: "type-fest@npm:3.13.1" checksum: c06b0901d54391dc46de3802375f5579868949d71f93b425ce564e19a428a0d411ae8d8cb0e300d330071d86152c3ea86e744c3f2860a42a79585b6ec2fdae8e @@ -23704,7 +23699,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"ws@npm:^7.2.0, ws@npm:^7.3.1, ws@npm:^7.4.6, ws@npm:^7.5.9": +"ws@npm:^7.3.1, ws@npm:^7.4.6, ws@npm:^7.5.9": version: 7.5.9 resolution: "ws@npm:7.5.9" peerDependencies: From 64bf4d55abd3d16a56c2a43392929ac927585e5a Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 6 Feb 2024 09:53:22 +0000 Subject: [PATCH 234/479] fix: replace `vm2` with builtin `vm` SOFIE-2535 (#1136) --- meteor/__mocks__/_vm2.ts | 21 --------------------- meteor/client/lib/__tests__/rundown.test.ts | 1 - meteor/package.json | 1 - meteor/server/api/blueprints/cache.ts | 15 +++++++-------- meteor/yarn.lock | 18 ++---------------- packages/job-worker/package.json | 3 +-- packages/job-worker/src/blueprints/cache.ts | 15 +++++++-------- packages/yarn.lock | 17 ++--------------- 8 files changed, 19 insertions(+), 72 deletions(-) delete mode 100644 meteor/__mocks__/_vm2.ts diff --git a/meteor/__mocks__/_vm2.ts b/meteor/__mocks__/_vm2.ts deleted file mode 100644 index 4d177eaf63..0000000000 --- a/meteor/__mocks__/_vm2.ts +++ /dev/null @@ -1,21 +0,0 @@ -class VMMock { - run(str: string): any { - return eval('let ' + str) - } -} - -class VMScriptMock { - constructor(private str: string) {} - toString() { - return this.str - } -} - -function mockSetup() { - return { - VM: VMMock, - VMScript: VMScriptMock, - } -} - -jest.mock('vm2', () => mockSetup(), { virtual: true }) diff --git a/meteor/client/lib/__tests__/rundown.test.ts b/meteor/client/lib/__tests__/rundown.test.ts index 5aaaa5d7bf..13cd06aca2 100644 --- a/meteor/client/lib/__tests__/rundown.test.ts +++ b/meteor/client/lib/__tests__/rundown.test.ts @@ -1,4 +1,3 @@ -import '../../../__mocks__/_vm2' import { testInFiber } from '../../../__mocks__/helpers/jest' import { setupDefaultStudioEnvironment, diff --git a/meteor/package.json b/meteor/package.json index 11e4777651..d643019e75 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -106,7 +106,6 @@ "underscore": "^1.13.6", "velocity-animate": "^1.5.2", "velocity-react": "^1.4.3", - "vm2": "^3.9.19", "webmidi": "^2.5.3", "winston": "^3.11.0", "xmlbuilder": "^15.1.1" diff --git a/meteor/server/api/blueprints/cache.ts b/meteor/server/api/blueprints/cache.ts index 85d1b27dd5..e2d542494c 100644 --- a/meteor/server/api/blueprints/cache.ts +++ b/meteor/server/api/blueprints/cache.ts @@ -1,21 +1,20 @@ -import { VM, VMScript } from 'vm2' +import * as vm from 'vm' import { logger } from '../../logging' import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { SomeBlueprintManifest } from '@sofie-automation/blueprints-integration' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' export function evalBlueprint(blueprint: Pick): SomeBlueprintManifest { - const vm = new VM({ - sandbox: {}, - }) - const blueprintPath = `db:///blueprint/${blueprint.name || blueprint._id}-bundle.js` - const script = new VMScript( + const context = vm.createContext({}, {}) + const script = new vm.Script( `__run_result = ${blueprint.code} __run_result || blueprint`, - blueprintPath + { + filename: blueprintPath, + } ) - const entry = vm.run(script) + const entry = script.runInContext(context) const manifest: SomeBlueprintManifest = entry.default diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 43780f4a53..74c9931cb3 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1416,7 +1416,6 @@ __metadata: tslib: ^2.6.2 type-fest: ^3.13.1 underscore: ^1.13.6 - vm2: ^3.9.19 languageName: node linkType: soft @@ -2265,14 +2264,14 @@ __metadata: languageName: node linkType: hard -"acorn-walk@npm:^8.0.0, acorn-walk@npm:^8.0.2, acorn-walk@npm:^8.2.0": +"acorn-walk@npm:^8.0.0, acorn-walk@npm:^8.0.2": version: 8.2.0 resolution: "acorn-walk@npm:8.2.0" checksum: 1715e76c01dd7b2d4ca472f9c58968516a4899378a63ad5b6c2d668bba8da21a71976c14ec5f5b75f887b6317c4ae0b897ab141c831d741dc76024d8745f1ad1 languageName: node linkType: hard -"acorn@npm:^8.0.4, acorn@npm:^8.1.0, acorn@npm:^8.7.0, acorn@npm:^8.8.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": +"acorn@npm:^8.0.4, acorn@npm:^8.1.0, acorn@npm:^8.8.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": version: 8.10.0 resolution: "acorn@npm:8.10.0" bin: @@ -2809,7 +2808,6 @@ __metadata: underscore: ^1.13.6 velocity-animate: ^1.5.2 velocity-react: ^1.4.3 - vm2: ^3.9.19 webmidi: ^2.5.3 winston: ^3.11.0 xml2js: ^0.6.2 @@ -12453,18 +12451,6 @@ __metadata: languageName: node linkType: hard -"vm2@npm:^3.9.19": - version: 3.9.19 - resolution: "vm2@npm:3.9.19" - dependencies: - acorn: ^8.7.0 - acorn-walk: ^8.2.0 - bin: - vm2: bin/vm2 - checksum: fc6cf553134145cd7bb5246985bf242b056e3fb5ea71e2eef6710b2a5d6c6119cc6bc960435ff62480ee82efb43369be8f4db07b6690916ae7d3b2e714f395d8 - languageName: node - linkType: hard - "void-elements@npm:3.1.0": version: 3.1.0 resolution: "void-elements@npm:3.1.0" diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index b461ac49f5..211d45bcf4 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -55,8 +55,7 @@ "threadedclass": "^1.2.1", "tslib": "^2.6.2", "type-fest": "^3.13.1", - "underscore": "^1.13.6", - "vm2": "^3.9.19" + "underscore": "^1.13.6" }, "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", "lint-staged": { diff --git a/packages/job-worker/src/blueprints/cache.ts b/packages/job-worker/src/blueprints/cache.ts index 5d935b4aeb..e9d6b62719 100644 --- a/packages/job-worker/src/blueprints/cache.ts +++ b/packages/job-worker/src/blueprints/cache.ts @@ -5,7 +5,7 @@ import { StudioBlueprintManifest, SystemBlueprintManifest, } from '@sofie-automation/blueprints-integration' -import { VM, VMScript } from 'vm2' +import * as vm from 'vm' import { ReadonlyDeep } from 'type-fest' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' @@ -53,18 +53,17 @@ export async function parseBlueprintDocument( if (blueprint.code) { let manifest: SomeBlueprintManifest try { - const vm = new VM({ - sandbox: {}, - }) - const blueprintPath = `db:///blueprint/${blueprint.name || blueprint._id}-bundle.js` - const script = new VMScript( + const context = vm.createContext({}, {}) + const script = new vm.Script( `__run_result = ${blueprint.code} __run_result || blueprint`, - blueprintPath + { + filename: blueprintPath, + } ) // Future: we should look at freezing the object inside the vm - const entry = vm.run(script) + const entry = script.runInContext(context) manifest = entry.default } catch (e) { throw new Error(`Syntax error in blueprint "${blueprint._id}": ${stringifyError(e)}`) diff --git a/packages/yarn.lock b/packages/yarn.lock index 97ff333264..176ba73835 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -4548,7 +4548,6 @@ __metadata: tslib: ^2.6.2 type-fest: ^3.13.1 underscore: ^1.13.6 - vm2: ^3.9.19 languageName: unknown linkType: soft @@ -6311,7 +6310,7 @@ __metadata: languageName: node linkType: hard -"acorn-walk@npm:^8.0.0, acorn-walk@npm:^8.1.1, acorn-walk@npm:^8.2.0": +"acorn-walk@npm:^8.0.0, acorn-walk@npm:^8.1.1": version: 8.2.0 resolution: "acorn-walk@npm:8.2.0" checksum: 1715e76c01dd7b2d4ca472f9c58968516a4899378a63ad5b6c2d668bba8da21a71976c14ec5f5b75f887b6317c4ae0b897ab141c831d741dc76024d8745f1ad1 @@ -6327,7 +6326,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.0.4, acorn@npm:^8.2.4, acorn@npm:^8.4.1, acorn@npm:^8.7.0, acorn@npm:^8.7.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": +"acorn@npm:^8.0.4, acorn@npm:^8.2.4, acorn@npm:^8.4.1, acorn@npm:^8.7.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": version: 8.10.0 resolution: "acorn@npm:8.10.0" bin: @@ -22991,18 +22990,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"vm2@npm:^3.9.19": - version: 3.9.19 - resolution: "vm2@npm:3.9.19" - dependencies: - acorn: ^8.7.0 - acorn-walk: ^8.2.0 - bin: - vm2: bin/vm2 - checksum: fc6cf553134145cd7bb5246985bf242b056e3fb5ea71e2eef6710b2a5d6c6119cc6bc960435ff62480ee82efb43369be8f4db07b6690916ae7d3b2e714f395d8 - languageName: node - linkType: hard - "vscode-oniguruma@npm:^1.7.0": version: 1.7.0 resolution: "vscode-oniguruma@npm:1.7.0" From 24647442de293cb72563635a6ee4f4f029218ee7 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Tue, 6 Feb 2024 15:23:34 +0100 Subject: [PATCH 235/479] fix: restore ALL data from snapshot, for debugging --- meteor/client/ui/Settings/SnapshotsView.tsx | 29 +++-- meteor/lib/api/shapshot.ts | 2 +- meteor/server/api/snapshot.ts | 118 ++++++++++++++++---- packages/corelib/src/worker/studio.ts | 8 ++ packages/job-worker/src/playout/snapshot.ts | 15 +++ 5 files changed, 143 insertions(+), 29 deletions(-) diff --git a/meteor/client/ui/Settings/SnapshotsView.tsx b/meteor/client/ui/Settings/SnapshotsView.tsx index e4ef4f4c2e..42d7bb4c62 100644 --- a/meteor/client/ui/Settings/SnapshotsView.tsx +++ b/meteor/client/ui/Settings/SnapshotsView.tsx @@ -29,7 +29,8 @@ interface IProps { } } interface IState { - uploadFileKey: number // Used to force clear the input after use + uploadFileKey: string // Used to force clear the input after use + uploadFileKey2: string // Used to force clear the input after use editSnapshotId: SnapshotId | null removeSnapshots: boolean } @@ -66,13 +67,14 @@ const SnapshotsViewContent = withTranslation()( constructor(props: Translated) { super(props) this.state = { - uploadFileKey: Date.now(), + uploadFileKey: `${Date.now()}_1`, + uploadFileKey2: `${Date.now()}_2`, editSnapshotId: null, removeSnapshots: false, } } - onUploadFile(e: React.ChangeEvent) { + onUploadFile(e: React.ChangeEvent, restoreDebugData: boolean) { const { t } = this.props const file = e.target.files?.[0] @@ -83,7 +85,8 @@ const SnapshotsViewContent = withTranslation()( const reader = new FileReader() reader.onload = (e2) => { this.setState({ - uploadFileKey: Date.now(), + uploadFileKey: `${Date.now()}_1`, + uploadFileKey2: `${Date.now()}_2`, }) const uploadFileContents = ((e2.target as any) || {}).result @@ -98,6 +101,7 @@ const SnapshotsViewContent = withTranslation()( body: uploadFileContents, headers: { 'content-type': 'application/json', + 'restore-debug-data': restoreDebugData ? '1' : '0', }, }) .then(() => { @@ -123,7 +127,9 @@ const SnapshotsViewContent = withTranslation()( }, onDiscard: () => { this.setState({ - uploadFileKey: Date.now(), // to clear input field + // to clear input field: + uploadFileKey: `${Date.now()}_1`, + uploadFileKey2: `${Date.now()}_2`, }) }, }) @@ -139,7 +145,7 @@ const SnapshotsViewContent = withTranslation()( message: `Do you really want to restore the snapshot ${snapshot.name}?`, onAccept: () => { MeteorCall.snapshot - .restoreSnapshot(snapshotId) + .restoreSnapshot(snapshotId, false) .then(() => { // todo: replace this with something else doModalDialog({ @@ -310,12 +316,21 @@ const SnapshotsViewContent = withTranslation()( this.onUploadFile(e)} + onChange={(e) => this.onUploadFile(e, false)} key={this.state.uploadFileKey} > {t('Upload Snapshot')} + this.onUploadFile(e, true)} + key={this.state.uploadFileKey2} + > + + {t('Upload Snapshot (for debugging)')} +

{t('Restore from Stored Snapshots')}

diff --git a/meteor/lib/api/shapshot.ts b/meteor/lib/api/shapshot.ts index c29077d588..cc307c9a8e 100644 --- a/meteor/lib/api/shapshot.ts +++ b/meteor/lib/api/shapshot.ts @@ -9,7 +9,7 @@ export interface NewSnapshotAPI { full?: boolean ): Promise storeDebugSnapshot(hashedToken: string, studioId: StudioId, reason: string): Promise - restoreSnapshot(snapshotId: SnapshotId): Promise + restoreSnapshot(snapshotId: SnapshotId, restoreDebugData: boolean): Promise removeSnapshot(snapshotId: SnapshotId): Promise } diff --git a/meteor/server/api/snapshot.ts b/meteor/server/api/snapshot.ts index 9b96c94614..0bbe8510d9 100644 --- a/meteor/server/api/snapshot.ts +++ b/meteor/server/api/snapshot.ts @@ -55,8 +55,11 @@ import { SystemWriteAccess } from '../security/system' import { saveIntoDb, sumChanges } from '../lib/database' import * as fs from 'fs' import { ExpectedPackageWorkStatus } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackageWorkStatuses' -import { PackageContainerPackageStatusDB } from '@sofie-automation/corelib/dist/dataModel/PackageContainerPackageStatus' -import { PackageInfoDB } from '@sofie-automation/corelib/dist/dataModel/PackageInfos' +import { + PackageContainerPackageStatusDB, + getPackageContainerPackageId, +} from '@sofie-automation/corelib/dist/dataModel/PackageContainerPackageStatus' +import { PackageInfoDB, getPackageInfoId } from '@sofie-automation/corelib/dist/dataModel/PackageInfos' import { checkStudioExists } from '../optimizations' import { CoreRundownPlaylistSnapshot } from '@sofie-automation/corelib/dist/snapshots' import { QueueStudioJob } from '../worker/worker' @@ -67,6 +70,7 @@ import { getSystemStorePath, PackageInfo } from '../coreSystem' import { JSONBlobParse, JSONBlobStringify } from '@sofie-automation/shared-lib/dist/lib/JSONBlob' import { BlueprintId, + ExpectedPackageId, OrganizationId, PeripheralDeviceId, RundownPlaylistId, @@ -485,7 +489,12 @@ async function retreiveSnapshot(snapshotId: SnapshotId, cred0: Credentials): Pro return readSnapshot } -async function restoreFromSnapshot(snapshot: AnySnapshot): Promise { +async function restoreFromSnapshot( + /** The snapshot data to restore */ + snapshot: AnySnapshot, + /** Whether to restore debug data (used in debugging) */ + restoreDebugData: boolean +): Promise { // Determine what kind of snapshot if (!_.isObject(snapshot)) throw new Meteor.Error(500, `Restore input data is not an object`) @@ -523,7 +532,7 @@ async function restoreFromSnapshot(snapshot: AnySnapshot): Promise { if (!studioId) throw new Meteor.Error(500, `No Studio found`) // A snapshot of a rundownPlaylist - return restoreFromRundownPlaylistSnapshot(snapshot as RundownPlaylistSnapshot, studioId) + return restoreFromRundownPlaylistSnapshot(snapshot as RundownPlaylistSnapshot, studioId, restoreDebugData) } else if (snapshot.snapshot.type === SnapshotType.SYSTEM) { // A snapshot of a system return restoreFromSystemSnapshot(snapshot as SystemSnapshot) @@ -534,7 +543,9 @@ async function restoreFromSnapshot(snapshot: AnySnapshot): Promise { async function restoreFromRundownPlaylistSnapshot( snapshot: RundownPlaylistSnapshot, - studioId: StudioId + studioId: StudioId, + /** Whether to restore debug data (PackageInfo, PackageOnPackageContainer etc, used in debugging) */ + restoreDebugData: boolean ): Promise { if (!isVersionSupported(parseVersion(snapshot.version || '0.18.0'))) { throw new Meteor.Error(400, `Cannot restore, the snapshot comes from an older, unsupported version of Sofie`) @@ -543,17 +554,76 @@ async function restoreFromRundownPlaylistSnapshot( const queuedJob = await QueueStudioJob(StudioJobs.RestorePlaylistSnapshot, studioId, { snapshotJson: JSONBlobStringify(omit(snapshot, 'mediaObjects', 'userActions')), }) - await queuedJob.complete - - // Restore the collections that the worker is unaware of - await Promise.all([ - // saveIntoDb(UserActionsLog, {}, snapshot.userActions), - saveIntoDb( - MediaObjects, - { _id: { $in: _.map(snapshot.mediaObjects, (mediaObject) => mediaObject._id) } }, - snapshot.mediaObjects - ), - ]) + const restoreResult = await queuedJob.complete + + if (restoreDebugData) { + const expectedPackageIdMap = new Map( + restoreResult.remappedIds.expectedPackageId + ) + + const mediaObjects = snapshot.mediaObjects.map((o) => { + return { + ...o, + studioId, + } + }) + const expectedPackageWorkStatuses = snapshot.expectedPackageWorkStatuses.map((o) => { + return { + ...o, + studioId, + fromPackages: o.fromPackages.map((p) => ({ + ...p, + id: expectedPackageIdMap.get(p.id) || p.id, + })), + } + }) + const packageContainerPackageStatuses = snapshot.packageContainerPackageStatuses.map((o) => { + const packageId = expectedPackageIdMap.get(o.packageId) || o.packageId + + const id = getPackageContainerPackageId(studioId, o.containerId, packageId) + return { + ...o, + _id: id, + studioId, + packageId: packageId, + } + }) + const packageInfos = snapshot.packageInfos.map((o) => { + const packageId = expectedPackageIdMap.get(o.packageId) || o.packageId + const id = getPackageInfoId(packageId, o.type) + return { + ...o, + _id: id, + studioId, + packageId, + } + }) + + // Restore the collections that the worker is unaware of + await Promise.all([ + saveIntoDb(MediaObjects, { _id: { $in: mediaObjects.map((o) => o._id) } }, mediaObjects), + // userActions + saveIntoDb( + ExpectedPackageWorkStatuses, + { + _id: { + $in: _.map(expectedPackageWorkStatuses, (o) => o._id), + }, + }, + expectedPackageWorkStatuses + ), + saveIntoDb( + PackageContainerPackageStatuses, + { + _id: { + $in: _.map(packageContainerPackageStatuses, (o) => o._id), + }, + }, + packageContainerPackageStatuses + ), + saveIntoDb(PackageInfos, { _id: { $in: _.map(snapshot.packageInfos, (o) => o._id) } }, packageInfos), + ]) + } } async function restoreFromSystemSnapshot(snapshot: SystemSnapshot): Promise { @@ -659,14 +729,18 @@ export async function storeDebugSnapshot( const s = await createDebugSnapshot(studioId, organizationId) return storeSnaphot(s, organizationId, reason) } -export async function restoreSnapshot(context: MethodContext, snapshotId: SnapshotId): Promise { +export async function restoreSnapshot( + context: MethodContext, + snapshotId: SnapshotId, + restoreDebugData: boolean +): Promise { check(snapshotId, String) const { cred } = await OrganizationContentWriteAccess.snapshot(context) if (Settings.enableUserAccounts && isResolvedCredentials(cred)) { if (cred.user && !cred.user.superAdmin) throw new Meteor.Error(401, 'Only Super Admins can store Snapshots') } const snapshot = await retreiveSnapshot(snapshotId, context) - return restoreFromSnapshot(snapshot) + return restoreFromSnapshot(snapshot, restoreDebugData) } export async function removeSnapshot(context: MethodContext, snapshotId: SnapshotId): Promise { check(snapshotId, String) @@ -741,7 +815,9 @@ if (!Settings.enableUserAccounts) { const snapshot = ctx.request.body as any if (!snapshot) throw new Meteor.Error(400, 'Restore Snapshot: Missing request body') - await restoreFromSnapshot(snapshot) + const restoreDebugData = ctx.headers['restore-debug-data'] === '1' + + await restoreFromSnapshot(snapshot, restoreDebugData) ctx.response.status = 200 ctx.response.body = content @@ -788,8 +864,8 @@ class ServerSnapshotAPI extends MethodContextAPI implements NewSnapshotAPI { async storeDebugSnapshot(hashedToken: string, studioId: StudioId, reason: string) { return storeDebugSnapshot(this, hashedToken, studioId, reason) } - async restoreSnapshot(snapshotId: SnapshotId) { - return restoreSnapshot(this, snapshotId) + async restoreSnapshot(snapshotId: SnapshotId, restoreDebugData: boolean) { + return restoreSnapshot(this, snapshotId, restoreDebugData) } async removeSnapshot(snapshotId: SnapshotId) { return removeSnapshot(this, snapshotId) diff --git a/packages/corelib/src/worker/studio.ts b/packages/corelib/src/worker/studio.ts index ca32dbe9ee..f82b8a063a 100644 --- a/packages/corelib/src/worker/studio.ts +++ b/packages/corelib/src/worker/studio.ts @@ -3,6 +3,7 @@ import { AdLibActionId, BucketAdLibActionId, BucketId, + ExpectedPackageId, PartId, PartInstanceId, PieceId, @@ -307,6 +308,13 @@ export interface RestorePlaylistSnapshotProps { } export interface RestorePlaylistSnapshotResult { playlistId: RundownPlaylistId + remappedIds: { + rundownId: [RundownId, RundownId][] + segmentId: [SegmentId, SegmentId][] + partId: [PartId, PartId][] + partInstanceId: [PartInstanceId, PartInstanceId][] + expectedPackageId: [ExpectedPackageId, ExpectedPackageId][] + } } export interface BlueprintValidateConfigForStudioResult { diff --git a/packages/job-worker/src/playout/snapshot.ts b/packages/job-worker/src/playout/snapshot.ts index 3ad9c1a21d..bb58c6263e 100644 --- a/packages/job-worker/src/playout/snapshot.ts +++ b/packages/job-worker/src/playout/snapshot.ts @@ -1,6 +1,7 @@ import { ExpectedPackageDBType, getExpectedPackageId } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' import { AdLibActionId, + ExpectedPackageId, PartId, PartInstanceId, PieceId, @@ -268,6 +269,7 @@ export async function handleRestorePlaylistSnapshot( piece._id = getRandomId() pieceIdMap.set(oldId, piece._id) } + for (const adlib of [ ...snapshot.adLibPieces, ...snapshot.adLibActions, @@ -306,7 +308,10 @@ export async function handleRestorePlaylistSnapshot( 'previous' ) + const expectedPackageIdMap = new Map() for (const expectedPackage of snapshot.expectedPackages) { + const oldId = expectedPackage._id + switch (expectedPackage.fromPieceType) { case ExpectedPackageDBType.PIECE: case ExpectedPackageDBType.ADLIB_PIECE: @@ -316,6 +321,7 @@ export async function handleRestorePlaylistSnapshot( expectedPackage.pieceId = pieceIdMap.get(expectedPackage.pieceId) || getRandomIdAndWarn(`expectedPackage.pieceId=${expectedPackage.pieceId}`) + expectedPackage._id = getExpectedPackageId(expectedPackage.pieceId, expectedPackage.blueprintPackageId) break @@ -339,6 +345,8 @@ export async function handleRestorePlaylistSnapshot( assertNever(expectedPackage) break } + + expectedPackageIdMap.set(oldId, expectedPackage._id) } snapshot.playlist.rundownIdsInOrder = snapshot.playlist.rundownIdsInOrder.map((id) => rundownIdMap.get(id) ?? id) @@ -480,6 +488,13 @@ export async function handleRestorePlaylistSnapshot( logger.info(`Restore done`) return { playlistId: playlistId, + remappedIds: { + rundownId: Array.from(rundownIdMap.entries()), + segmentId: Array.from(segmentIdMap.entries()), + partId: Array.from(partIdMap.entries()), + partInstanceId: Array.from(partInstanceIdMap.entries()), + expectedPackageId: Array.from(expectedPackageIdMap.entries()), + }, } } From 4ebb0f2be891da67c153496a0224fd2eb7413b28 Mon Sep 17 00:00:00 2001 From: ianshade Date: Tue, 6 Feb 2024 21:32:53 +0100 Subject: [PATCH 236/479] refactor(live-status-gw): extract activePieces to a separate topic --- .../live-status-gateway/api/asyncapi.yaml | 7 + .../api/schemas/activePieces.yaml | 26 ++++ .../api/schemas/activePlaylist.yaml | 9 +- .../src/collections/pieceInstancesHandler.ts | 41 +++++- .../src/collections/showStyleBaseHandler.ts | 72 ++++++++-- .../src/liveStatusServer.ts | 17 ++- .../src/topics/__tests__/activePieces.spec.ts | 68 ++++++++++ .../topics/__tests__/activePlaylist.spec.ts | 102 +------------- .../src/topics/__tests__/adLibs.spec.ts | 5 +- .../src/topics/__tests__/utils.ts | 18 +-- .../src/topics/activePiecesTopic.ts | 125 ++++++++++++++++++ .../src/topics/activePlaylistTopic.ts | 112 +++++----------- .../src/topics/adLibsTopic.ts | 53 ++------ .../src/topics/helpers/pieceStatus.ts | 26 ++++ .../live-status-gateway/src/topics/root.ts | 1 + 15 files changed, 421 insertions(+), 261 deletions(-) create mode 100644 packages/live-status-gateway/api/schemas/activePieces.yaml create mode 100644 packages/live-status-gateway/src/topics/__tests__/activePieces.spec.ts create mode 100644 packages/live-status-gateway/src/topics/activePiecesTopic.ts create mode 100644 packages/live-status-gateway/src/topics/helpers/pieceStatus.ts diff --git a/packages/live-status-gateway/api/asyncapi.yaml b/packages/live-status-gateway/api/asyncapi.yaml index fbd7a68445..e2aad3e33e 100644 --- a/packages/live-status-gateway/api/asyncapi.yaml +++ b/packages/live-status-gateway/api/asyncapi.yaml @@ -34,6 +34,7 @@ channels: - $ref: '#/components/messages/subscriptionStatus' - $ref: '#/components/messages/studio' - $ref: '#/components/messages/activePlaylist' + - $ref: '#/components/messages/activePieces' - $ref: '#/components/messages/segments' - $ref: '#/components/messages/adLibs' components: @@ -85,6 +86,12 @@ components: description: Active Playlist status payload: $ref: './schemas/activePlaylist.yaml#/$defs/activePlaylist' + activePieces: + name: activePieces + summary: Active Pieces status + description: Pieces from the active Playlist that are currently active (on air) + payload: + $ref: './schemas/activePieces.yaml#/$defs/activePieces' segments: name: segments description: Segments in active Playlist diff --git a/packages/live-status-gateway/api/schemas/activePieces.yaml b/packages/live-status-gateway/api/schemas/activePieces.yaml new file mode 100644 index 0000000000..01d777f443 --- /dev/null +++ b/packages/live-status-gateway/api/schemas/activePieces.yaml @@ -0,0 +1,26 @@ +title: Active Pieces +description: Active Pieces schema for websocket subscriptions +$defs: + activePieces: + type: object + properties: + event: + type: string + const: activePieces + rundownPlaylistId: + description: Unique id of the rundown playlist, or null if no playlist is active + oneOf: + - type: string + - type: 'null' + activePieces: + description: Pieces that are currently active (on air) + type: array + items: + $ref: './activePlaylist.yaml#/$defs/piece' + required: [event, rundownPlaylistId, activePieces] + additionalProperties: false + examples: + - event: activePieces + rundownPlaylistId: 'OKAgZmZ0Buc99lE_2uPPSKVbMrQ_' + activePieces: + - $ref: './activePlaylist.yaml#/$defs/piece/examples/0' diff --git a/packages/live-status-gateway/api/schemas/activePlaylist.yaml b/packages/live-status-gateway/api/schemas/activePlaylist.yaml index 987742c1bf..059fe43add 100644 --- a/packages/live-status-gateway/api/schemas/activePlaylist.yaml +++ b/packages/live-status-gateway/api/schemas/activePlaylist.yaml @@ -27,12 +27,7 @@ $defs: nextPart: description: The next Part - if empty, no part will follow live part $ref: '#/$defs/part' - activePieces: - description: Pieces that are currently active (live) - type: array - items: - $ref: '#/$defs/piece' - required: [event, id, name, rundownIds, currentPart, currentSegment, nextPart, activePieces] + required: [event, id, name, rundownIds, currentPart, currentSegment, nextPart] additionalProperties: false examples: - event: activePlaylist @@ -45,8 +40,6 @@ $defs: $ref: '#/$defs/currentSegment/examples/0' nextPart: $ref: '#/$defs/part/examples/0' - activePieces: - - $ref: '#/$defs/piece/examples/0' partBase: type: object properties: diff --git a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts index 52f165b7a7..0737261104 100644 --- a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts +++ b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts @@ -11,15 +11,17 @@ import _ = require('underscore') import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { PartInstanceId, PieceInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' +export type PieceInstanceMin = Omit + export interface SelectedPieceInstances { // Pieces reported by the Playout Gateway as active - active: PieceInstance[] + active: PieceInstanceMin[] // Pieces present in the current part instance - currentPartInstance: PieceInstance[] + currentPartInstance: PieceInstanceMin[] // Pieces present in the current part instance - nextPartInstance: PieceInstance[] + nextPartInstance: PieceInstanceMin[] } export class PieceInstancesHandler @@ -78,7 +80,15 @@ export class PieceInstancesHandler this._collectionData.active = active hasAnythingChanged = true } - if (!areElementsShallowEqual(this._collectionData.currentPartInstance, inCurrentPartInstance)) { + if ( + !areElementsShallowEqual(this._collectionData.currentPartInstance, inCurrentPartInstance) && + this._collectionData.currentPartInstance.some((pieceInstance, index) => { + return !arePropertiesShallowEqual(inCurrentPartInstance[index], pieceInstance, [ + 'reportedStartedPlayback', + 'reportedStoppedPlayback', + ]) + }) + ) { this._collectionData.currentPartInstance = inCurrentPartInstance hasAnythingChanged = true } @@ -171,3 +181,26 @@ export class PieceInstancesHandler ) } } + +export function arePropertiesShallowEqual>( + a: T, + b: T, + omitProperties: Array +): boolean { + if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) { + return false + } + + const keysA = Object.keys(a).filter((key) => !omitProperties.includes(key)) + const keysB = Object.keys(b).filter((key) => !omitProperties.includes(key)) + + if (keysA.length !== keysB.length) return false + + for (const key of keysA) { + if (!keysB.includes(key) || a[key] !== b[key]) { + return false + } + } + + return true +} diff --git a/packages/live-status-gateway/src/collections/showStyleBaseHandler.ts b/packages/live-status-gateway/src/collections/showStyleBaseHandler.ts index f790e33b61..ad21ae80d2 100644 --- a/packages/live-status-gateway/src/collections/showStyleBaseHandler.ts +++ b/packages/live-status-gateway/src/collections/showStyleBaseHandler.ts @@ -2,17 +2,26 @@ import { Logger } from 'winston' import { CoreHandler } from '../coreHandler' import { CollectionBase, Collection, CollectionObserver } from '../wsHandler' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' +import { DBShowStyleBase, OutputLayers, SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { IOutputLayer, ISourceLayer } from '@sofie-automation/blueprints-integration' + +export interface ShowStyleBaseExt extends DBShowStyleBase { + sourceLayerNamesById: ReadonlyMap + outputLayerNamesById: ReadonlyMap +} export class ShowStyleBaseHandler - extends CollectionBase - implements Collection, CollectionObserver + extends CollectionBase + implements Collection, CollectionObserver { public observerName: string private _showStyleBaseId: ShowStyleBaseId | undefined + private _sourceLayersMap: Map = new Map() + private _outputLayersMap: Map = new Map() constructor(logger: Logger, coreHandler: CoreHandler) { super( @@ -28,10 +37,8 @@ export class ShowStyleBaseHandler async changed(id: ShowStyleBaseId, changeType: string): Promise { this._logger.info(`${this._name} ${changeType} ${id}`) if (!this._collectionName) return - const collection = this._core.getCollection(this._collectionName) - if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) if (this._showStyleBaseId) { - this._collectionData = collection.findOne(this._showStyleBaseId) + this.updateCollectionData() await this.notify(this._collectionData) } } @@ -61,11 +68,58 @@ export class ShowStyleBaseHandler void this.changed(id, 'changed').catch(this._logger.error) } - const collection = this._core.getCollection(this._collectionName) - if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) - this._collectionData = collection.findOne(this._showStyleBaseId) + this.updateCollectionData() await this.notify(this._collectionData) } } } + + updateCollectionData(): void { + const collection = this._core.getCollection(this._collectionName) + if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) + if (!this._showStyleBaseId) return + const showStyleBase = collection.findOne(this._showStyleBaseId) + if (!showStyleBase) { + this._collectionData = undefined + return + } + const sourceLayers: SourceLayers = showStyleBase + ? applyAndValidateOverrides(showStyleBase.sourceLayersWithOverrides).obj + : {} + const outputLayers: OutputLayers = showStyleBase + ? applyAndValidateOverrides(showStyleBase.outputLayersWithOverrides).obj + : {} + this._logger.info( + `${this._name} received showStyleBase update with sourceLayers [${Object.values( + sourceLayers + ).map( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (s) => s!.name + )}]` + ) + this._logger.info( + `${this._name} received showStyleBase update with outputLayers [${Object.values( + outputLayers + ).map( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (s) => s!.name + )}]` + ) + this._sourceLayersMap.clear() + this._outputLayersMap.clear() + for (const [layerId, sourceLayer] of Object.entries(sourceLayers)) { + if (sourceLayer === undefined || sourceLayer === null) continue + this._sourceLayersMap.set(layerId, sourceLayer.name) + } + for (const [layerId, outputLayer] of Object.entries(outputLayers)) { + if (outputLayer === undefined || outputLayer === null) continue + this._outputLayersMap.set(layerId, outputLayer.name) + } + const showStyleBaseExt = { + ...showStyleBase, + sourceLayerNamesById: this._sourceLayersMap, + outputLayerNamesById: this._outputLayersMap, + } + this._collectionData = showStyleBaseExt + } } diff --git a/packages/live-status-gateway/src/liveStatusServer.ts b/packages/live-status-gateway/src/liveStatusServer.ts index 461fdf3ef3..addc8c3934 100644 --- a/packages/live-status-gateway/src/liveStatusServer.ts +++ b/packages/live-status-gateway/src/liveStatusServer.ts @@ -11,7 +11,7 @@ import { SegmentHandler } from './collections/segmentHandler' import { PartInstancesHandler } from './collections/partInstancesHandler' import { AdLibActionsHandler } from './collections/adLibActionsHandler' import { GlobalAdLibActionsHandler } from './collections/globalAdLibActionsHandler' -import { RootChannel } from './topics/root' +import { RootChannel, StatusChannels } from './topics/root' import { StudioTopic } from './topics/studioTopic' import { ActivePlaylistTopic } from './topics/activePlaylistTopic' import { AdLibsHandler } from './collections/adLibsHandler' @@ -22,6 +22,7 @@ import { PartHandler } from './collections/partHandler' import { PartsHandler } from './collections/partsHandler' import { PieceInstancesHandler } from './collections/pieceInstancesHandler' import { AdLibsTopic } from './topics/adLibsTopic' +import { ActivePiecesTopic } from './topics/activePiecesTopic' export class LiveStatusServer { _logger: Logger @@ -39,14 +40,16 @@ export class LiveStatusServer { const rootChannel = new RootChannel(this._logger) const studioTopic = new StudioTopic(this._logger) + const activePiecesTopic = new ActivePiecesTopic(this._logger) const activePlaylistTopic = new ActivePlaylistTopic(this._logger) const segmentsTopic = new SegmentsTopic(this._logger) const adLibsTopic = new AdLibsTopic(this._logger) - rootChannel.addTopic('studio', studioTopic) - rootChannel.addTopic('activePlaylist', activePlaylistTopic) - rootChannel.addTopic('segments', segmentsTopic) - rootChannel.addTopic('adLibs', adLibsTopic) + rootChannel.addTopic(StatusChannels.studio, studioTopic) + rootChannel.addTopic(StatusChannels.activePlaylist, activePlaylistTopic) + rootChannel.addTopic(StatusChannels.activePieces, activePiecesTopic) + rootChannel.addTopic(StatusChannels.segments, segmentsTopic) + rootChannel.addTopic(StatusChannels.adLibs, adLibsTopic) const studioHandler = new StudioHandler(this._logger, this._coreHandler) await studioHandler.init() @@ -104,6 +107,10 @@ export class LiveStatusServer { await partsHandler.subscribe(activePlaylistTopic) await pieceInstancesHandler.subscribe(activePlaylistTopic) + await playlistHandler.subscribe(activePiecesTopic) + await showStyleBaseHandler.subscribe(activePiecesTopic) + await pieceInstancesHandler.subscribe(activePiecesTopic) + await playlistHandler.subscribe(segmentsTopic) await segmentsHandler.subscribe(segmentsTopic) await partsHandler.subscribe(segmentsTopic) diff --git a/packages/live-status-gateway/src/topics/__tests__/activePieces.spec.ts b/packages/live-status-gateway/src/topics/__tests__/activePieces.spec.ts new file mode 100644 index 0000000000..574d2e5d39 --- /dev/null +++ b/packages/live-status-gateway/src/topics/__tests__/activePieces.spec.ts @@ -0,0 +1,68 @@ +import { makeMockLogger, makeMockSubscriber, makeTestPlaylist, makeTestShowStyleBase } from './utils' +import { PlaylistHandler } from '../../collections/playlistHandler' +import { ShowStyleBaseExt, ShowStyleBaseHandler } from '../../collections/showStyleBaseHandler' +import { protectString } from '@sofie-automation/server-core-integration/dist' +import { PartialDeep } from 'type-fest' +import { literal } from '@sofie-automation/corelib/dist/lib' +import { PieceInstancesHandler, SelectedPieceInstances } from '../../collections/pieceInstancesHandler' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { ActivePiecesStatus, ActivePiecesTopic } from '../activePiecesTopic' + +describe('ActivePiecesTopic', () => { + it('provides active pieces', async () => { + const topic = new ActivePiecesTopic(makeMockLogger()) + const mockSubscriber = makeMockSubscriber() + + const currentPartInstanceId = 'CURRENT_PART_INSTANCE_ID' + + const playlist = makeTestPlaylist() + playlist.activationId = protectString('somethingRandom') + playlist.currentPartInfo = { + consumesQueuedSegmentId: false, + manuallySelected: false, + partInstanceId: protectString(currentPartInstanceId), + rundownId: playlist.rundownIdsInOrder[0], + } + await topic.update(PlaylistHandler.name, playlist) + + const testShowStyleBase = makeTestShowStyleBase() + await topic.update(ShowStyleBaseHandler.name, testShowStyleBase as ShowStyleBaseExt) + + const testPieceInstances: PartialDeep = { + currentPartInstance: [], + nextPartInstance: [], + active: [ + literal>({ + _id: protectString('PIECE_1'), + piece: { + name: 'Piece 1', + outputLayerId: 'pgm', + sourceLayerId: 'layer0', + tags: ['my_tag'], + }, + }), + ] as PieceInstance[], + } + await topic.update(PieceInstancesHandler.name, testPieceInstances as SelectedPieceInstances) + + topic.addSubscriber(mockSubscriber) + + const expectedStatus: Partial = { + event: 'activePieces', + + activePieces: [ + { + id: 'PIECE_1', + name: 'Piece 1', + sourceLayer: 'Layer 0', + outputLayer: 'PGM', + tags: ['my_tag'], + }, + ], + } + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockSubscriber.send).toHaveBeenCalledTimes(1) + expect(JSON.parse(mockSubscriber.send.mock.calls[0][0] as string)).toMatchObject(expectedStatus) + }) +}) diff --git a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts index 72dc9d1893..151139a321 100644 --- a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts @@ -1,8 +1,7 @@ import { ActivePlaylistStatus, ActivePlaylistTopic } from '../activePlaylistTopic' import { makeMockLogger, makeMockSubscriber, makeTestPlaylist, makeTestShowStyleBase } from './utils' import { PlaylistHandler } from '../../collections/playlistHandler' -import { ShowStyleBaseHandler } from '../../collections/showStyleBaseHandler' -import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' +import { ShowStyleBaseExt, ShowStyleBaseHandler } from '../../collections/showStyleBaseHandler' import { PartInstancesHandler, SelectedPartInstances } from '../../collections/partInstancesHandler' import { protectString, unprotectString, unprotectStringArray } from '@sofie-automation/server-core-integration/dist' import { PartialDeep } from 'type-fest' @@ -10,8 +9,6 @@ import { literal } from '@sofie-automation/corelib/dist/lib' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { PartsHandler } from '../../collections/partsHandler' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' -import { PieceInstancesHandler, SelectedPieceInstances } from '../../collections/pieceInstancesHandler' -import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' function makeEmptyTestPartInstances(): SelectedPartInstances { return { @@ -22,32 +19,6 @@ function makeEmptyTestPartInstances(): SelectedPartInstances { } } -function makeTestPartInstances(currentPartInstanceId: string): PartialDeep { - const part1: Partial = { - _id: protectString('PART_1'), - title: 'Test Part', - segmentId: protectString('SEGMENT_1'), - expectedDurationWithPreroll: 10000, - expectedDuration: 10000, - } - const testPartInstances: PartialDeep = { - current: { - _id: currentPartInstanceId, - part: part1, - timings: { plannedStartedPlayback: 1600000060000 }, - }, - firstInSegmentPlayout: {}, - inCurrentSegment: [ - literal>({ - _id: protectString(currentPartInstanceId), - part: part1, - timings: { plannedStartedPlayback: 1600000060000 }, - }), - ] as DBPartInstance[], - } - return testPartInstances -} - describe('ActivePlaylistTopic', () => { it('notifies subscribers', async () => { const topic = new ActivePlaylistTopic(makeMockLogger()) @@ -58,7 +29,7 @@ describe('ActivePlaylistTopic', () => { await topic.update(PlaylistHandler.name, playlist) const testShowStyleBase = makeTestShowStyleBase() - await topic.update(ShowStyleBaseHandler.name, testShowStyleBase as DBShowStyleBase) + await topic.update(ShowStyleBaseHandler.name, testShowStyleBase as ShowStyleBaseExt) const testPartInstancesMap = makeEmptyTestPartInstances() await topic.update(PartInstancesHandler.name, testPartInstancesMap) @@ -73,7 +44,6 @@ describe('ActivePlaylistTopic', () => { nextPart: null, currentSegment: null, rundownIds: unprotectStringArray(playlist.rundownIdsInOrder), - activePieces: [], } // eslint-disable-next-line @typescript-eslint/unbound-method @@ -98,7 +68,7 @@ describe('ActivePlaylistTopic', () => { await topic.update(PlaylistHandler.name, playlist) const testShowStyleBase = makeTestShowStyleBase() - await topic.update(ShowStyleBaseHandler.name, testShowStyleBase as DBShowStyleBase) + await topic.update(ShowStyleBaseHandler.name, testShowStyleBase as ShowStyleBaseExt) const part1: Partial = { _id: protectString('PART_1'), title: 'Test Part', @@ -147,72 +117,6 @@ describe('ActivePlaylistTopic', () => { }, }, rundownIds: unprotectStringArray(playlist.rundownIdsInOrder), - activePieces: [], - } - - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(mockSubscriber.send).toHaveBeenCalledTimes(1) - expect(JSON.parse(mockSubscriber.send.mock.calls[0][0] as string)).toMatchObject(expectedStatus) - }) - - it('provides active pieces', async () => { - const topic = new ActivePlaylistTopic(makeMockLogger()) - const mockSubscriber = makeMockSubscriber() - - const currentPartInstanceId = 'CURRENT_PART_INSTANCE_ID' - - const playlist = makeTestPlaylist() - playlist.activationId = protectString('somethingRandom') - playlist.currentPartInfo = { - consumesQueuedSegmentId: false, - manuallySelected: false, - partInstanceId: protectString(currentPartInstanceId), - rundownId: playlist.rundownIdsInOrder[0], - } - await topic.update(PlaylistHandler.name, playlist) - - const testPartInstances = makeTestPartInstances(currentPartInstanceId) - await topic.update(PartInstancesHandler.name, testPartInstances as SelectedPartInstances) - - const testShowStyleBase = makeTestShowStyleBase() - await topic.update(ShowStyleBaseHandler.name, testShowStyleBase as DBShowStyleBase) - - const testPieceInstances: PartialDeep = { - currentPartInstance: [], - nextPartInstance: [], - active: [ - literal>({ - _id: protectString('PIECE_1'), - piece: { - name: 'Piece 1', - outputLayerId: 'pgm', - sourceLayerId: 'layer0', - tags: ['my_tag'], - }, - }), - ] as PieceInstance[], - } - await topic.update(PieceInstancesHandler.name, testPieceInstances as SelectedPieceInstances) - - await topic.update(PartsHandler.name, [testPartInstances.current!.part] as DBPart[]) - - topic.addSubscriber(mockSubscriber) - - const expectedStatus: Partial = { - event: 'activePlaylist', - name: playlist.name, - id: unprotectString(playlist._id), - nextPart: null, - rundownIds: unprotectStringArray(playlist.rundownIdsInOrder), - activePieces: [ - { - id: 'PIECE_1', - name: 'Piece 1', - sourceLayer: 'Layer 0', - outputLayer: 'PGM', - tags: ['my_tag'], - }, - ], } // eslint-disable-next-line @typescript-eslint/unbound-method diff --git a/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts b/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts index 2fb5a406d5..4f87e80048 100644 --- a/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts @@ -2,8 +2,7 @@ import { protectString, unprotectString } from '@sofie-automation/server-core-in import { makeMockLogger, makeMockSubscriber, makeTestPlaylist, makeTestShowStyleBase } from './utils' import { AdLibsStatus, AdLibsTopic } from '../adLibsTopic' import { PlaylistHandler } from '../../collections/playlistHandler' -import { ShowStyleBaseHandler } from '../../collections/showStyleBaseHandler' -import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' +import { ShowStyleBaseExt, ShowStyleBaseHandler } from '../../collections/showStyleBaseHandler' import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' import { AdLibActionsHandler } from '../../collections/adLibActionsHandler' @@ -60,7 +59,7 @@ describe('ActivePlaylistTopic', () => { await topic.update(PlaylistHandler.name, playlist) const testShowStyleBase = makeTestShowStyleBase() - await topic.update(ShowStyleBaseHandler.name, testShowStyleBase as DBShowStyleBase) + await topic.update(ShowStyleBaseHandler.name, testShowStyleBase as ShowStyleBaseExt) const testAdLibActions = makeTestAdLibActions() await topic.update(AdLibActionsHandler.name, testAdLibActions) diff --git a/packages/live-status-gateway/src/topics/__tests__/utils.ts b/packages/live-status-gateway/src/topics/__tests__/utils.ts index 82c73f332c..0bedea142a 100644 --- a/packages/live-status-gateway/src/topics/__tests__/utils.ts +++ b/packages/live-status-gateway/src/topics/__tests__/utils.ts @@ -1,10 +1,9 @@ -import { SourceLayerType } from '@sofie-automation/blueprints-integration' import { PlaylistTimingType } from '@sofie-automation/blueprints-integration/dist/documents/playlistTiming' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' // eslint-disable-next-line node/no-extraneous-import import { mock, MockProxy } from 'jest-mock-extended' +import { ShowStyleBaseExt } from '../../collections/showStyleBaseHandler' import { Logger } from 'winston' import { WebSocket } from 'ws' @@ -36,18 +35,9 @@ export function makeTestPlaylist(id?: string): DBRundownPlaylist { } } -export function makeTestShowStyleBase(): Pick< - DBShowStyleBase, - 'sourceLayersWithOverrides' | 'outputLayersWithOverrides' -> { +export function makeTestShowStyleBase(): Pick { return { - sourceLayersWithOverrides: { - defaults: { layer0: { _id: 'layer0', name: 'Layer 0', _rank: 0, type: SourceLayerType.VT } }, - overrides: [], - }, - outputLayersWithOverrides: { - defaults: { pgm: { _id: 'pgm', name: 'PGM', _rank: 0, isPGM: true } }, - overrides: [], - }, + sourceLayerNamesById: new Map([['layer0', 'Layer 0']]), + outputLayerNamesById: new Map([['pgm', 'PGM']]), } } diff --git a/packages/live-status-gateway/src/topics/activePiecesTopic.ts b/packages/live-status-gateway/src/topics/activePiecesTopic.ts new file mode 100644 index 0000000000..cb7abda6d8 --- /dev/null +++ b/packages/live-status-gateway/src/topics/activePiecesTopic.ts @@ -0,0 +1,125 @@ +import { Logger } from 'winston' +import { WebSocket } from 'ws' +import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { literal } from '@sofie-automation/shared-lib/dist/lib/lib' +import { WebSocketTopicBase, WebSocketTopic, CollectionObserver } from '../wsHandler' +import { PlaylistHandler } from '../collections/playlistHandler' +import { ShowStyleBaseExt, ShowStyleBaseHandler } from '../collections/showStyleBaseHandler' +import _ = require('underscore') +import { SelectedPieceInstances, PieceInstancesHandler, PieceInstanceMin } from '../collections/pieceInstancesHandler' +import { toPieceStatus } from './helpers/pieceStatus' +import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' + +const THROTTLE_PERIOD_MS = 100 + +interface PieceStatus { + id: string + name: string + sourceLayer: string + outputLayer: string + tags?: string[] +} + +export interface ActivePiecesStatus { + event: 'activePieces' + rundownPlaylistId: string | null + activePieces: PieceStatus[] +} + +export class ActivePiecesTopic + extends WebSocketTopicBase + implements + WebSocketTopic, + CollectionObserver, + CollectionObserver, + CollectionObserver +{ + public observerName = ActivePiecesTopic.name + private _activePlaylistId: RundownPlaylistId | undefined + private _activePieceInstances: PieceInstanceMin[] | undefined + private _showStyleBaseExt: ShowStyleBaseExt | undefined + private throttledSendStatusToAll: () => void + + constructor(logger: Logger) { + super(ActivePiecesTopic.name, logger) + this.throttledSendStatusToAll = _.throttle(this.sendStatusToAll.bind(this), THROTTLE_PERIOD_MS, { + leading: false, + trailing: true, + }) + } + + addSubscriber(ws: WebSocket): void { + super.addSubscriber(ws) + this.sendStatus([ws]) + } + + sendStatus(subscribers: Iterable): void { + const message = this._activePlaylistId + ? literal({ + event: 'activePieces', + rundownPlaylistId: unprotectString(this._activePlaylistId), + activePieces: + this._activePieceInstances?.map((piece) => toPieceStatus(piece, this._showStyleBaseExt)) ?? [], + }) + : literal({ + event: 'activePieces', + rundownPlaylistId: null, + activePieces: [], + }) + + for (const subscriber of subscribers) { + this.sendMessage(subscriber, message) + } + } + + async update( + source: string, + data: DBRundownPlaylist | ShowStyleBaseExt | SelectedPieceInstances | undefined + ): Promise { + let hasAnythingChanged = false + switch (source) { + case PlaylistHandler.name: { + const rundownPlaylist = data ? (data as DBRundownPlaylist) : undefined + this._logger.info( + `${this._name} received playlist update ${rundownPlaylist?._id}, activationId ${rundownPlaylist?.activationId}` + ) + const previousActivePlaylistId = this._activePlaylistId + this._activePlaylistId = unprotectString(rundownPlaylist?.activationId) + ? rundownPlaylist?._id + : undefined + + if (previousActivePlaylistId !== this._activePlaylistId) { + hasAnythingChanged = true + } + break + } + case ShowStyleBaseHandler.name: { + const showStyleBaseExt = data ? (data as ShowStyleBaseExt) : undefined + this._logger.info(`${this._name} received showStyleBase update from ${source}`) + this._showStyleBaseExt = showStyleBaseExt + hasAnythingChanged = true + break + } + case PieceInstancesHandler.name: { + const pieceInstances = data as SelectedPieceInstances + this._logger.info(`${this._name} received pieceInstances update from ${source}`) + if (pieceInstances.active !== this._activePieceInstances) { + hasAnythingChanged = true + } + this._activePieceInstances = pieceInstances.active + break + } + default: + throw new Error(`${this._name} received unsupported update from ${source}}`) + } + + if (hasAnythingChanged) { + this.throttledSendStatusToAll() + } + } + + private sendStatusToAll() { + this.sendStatus(this._subscribers) + } +} diff --git a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts index e912b92bfb..f0c5559064 100644 --- a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts +++ b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts @@ -2,34 +2,22 @@ import { Logger } from 'winston' import { WebSocket } from 'ws' import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { DBShowStyleBase, OutputLayers, SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' -import { IOutputLayer, ISourceLayer } from '@sofie-automation/blueprints-integration' import { literal } from '@sofie-automation/shared-lib/dist/lib/lib' import { WebSocketTopicBase, WebSocketTopic, CollectionObserver } from '../wsHandler' import { SelectedPartInstances, PartInstancesHandler } from '../collections/partInstancesHandler' -import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { PlaylistHandler } from '../collections/playlistHandler' -import { ShowStyleBaseHandler } from '../collections/showStyleBaseHandler' +import { ShowStyleBaseExt, ShowStyleBaseHandler } from '../collections/showStyleBaseHandler' import { CurrentSegmentTiming, calculateCurrentSegmentTiming } from './helpers/segmentTiming' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { PartsHandler } from '../collections/partsHandler' import _ = require('underscore') import { PartTiming, calculateCurrentPartTiming } from './helpers/partTiming' -import { SelectedPieceInstances } from '../collections/pieceInstancesHandler' -import { PieceInstancesHandler } from '../collections/pieceInstancesHandler' -import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { SelectedPieceInstances, PieceInstancesHandler, PieceInstanceMin } from '../collections/pieceInstancesHandler' +import { PieceStatus, toPieceStatus } from './helpers/pieceStatus' const THROTTLE_PERIOD_MS = 100 -interface PieceStatus { - id: string - name: string - sourceLayer: string - outputLayer: string - tags?: string[] -} - interface PartStatus { id: string segmentId: string @@ -55,7 +43,6 @@ export interface ActivePlaylistStatus { currentPart: CurrentPartStatus | null currentSegment: CurrentSegmentStatus | null nextPart: PartStatus | null - activePieces: PieceStatus[] } export class ActivePlaylistTopic @@ -63,20 +50,21 @@ export class ActivePlaylistTopic implements WebSocketTopic, CollectionObserver, + CollectionObserver, CollectionObserver, CollectionObserver, CollectionObserver { public observerName = ActivePlaylistTopic.name - private _sourceLayersMap: Map = new Map() - private _outputLayersMap: Map = new Map() private _activePlaylist: DBRundownPlaylist | undefined private _currentPartInstance: DBPartInstance | undefined private _nextPartInstance: DBPartInstance | undefined private _firstInstanceInSegmentPlayout: DBPartInstance | undefined private _partInstancesInCurrentSegment: DBPartInstance[] = [] private _partsBySegmentId: Record = {} - private _pieceInstances: SelectedPieceInstances | undefined + private _pieceInstancesInCurrentPartInstance: PieceInstanceMin[] | undefined + private _pieceInstancesInNextPartInstance: PieceInstanceMin[] | undefined + private _showStyleBaseExt: ShowStyleBaseExt | undefined private throttledSendStatusToAll: () => void constructor(logger: Logger) { @@ -96,10 +84,10 @@ export class ActivePlaylistTopic if ( this._currentPartInstance?._id !== this._activePlaylist?.currentPartInfo?.partInstanceId || this._nextPartInstance?._id !== this._activePlaylist?.nextPartInfo?.partInstanceId || - (this._pieceInstances?.currentPartInstance[0] && - this._pieceInstances.currentPartInstance[0].partInstanceId !== this._currentPartInstance?._id) || - (this._pieceInstances?.nextPartInstance[0] && - this._pieceInstances.nextPartInstance[0].partInstanceId !== this._nextPartInstance?._id) + (this._pieceInstancesInCurrentPartInstance?.[0] && + this._pieceInstancesInCurrentPartInstance?.[0].partInstanceId !== this._currentPartInstance?._id) || + (this._pieceInstancesInNextPartInstance?.[0] && + this._pieceInstancesInNextPartInstance?.[0].partInstanceId !== this._nextPartInstance?._id) ) { // data is inconsistent, let's wait return @@ -126,8 +114,8 @@ export class ActivePlaylistTopic this._partInstancesInCurrentSegment ), pieces: - this._pieceInstances?.currentPartInstance.map((piece) => - this.toPieceStatus(piece) + this._pieceInstancesInCurrentPartInstance?.map((piece) => + toPieceStatus(piece, this._showStyleBaseExt) ) ?? [], }) : null, @@ -150,11 +138,11 @@ export class ActivePlaylistTopic autoNext: nextPart.autoNext, segmentId: unprotectString(nextPart.segmentId), pieces: - this._pieceInstances?.nextPartInstance.map((piece) => this.toPieceStatus(piece)) ?? - [], + this._pieceInstancesInCurrentPartInstance?.map((piece) => + toPieceStatus(piece, this._showStyleBaseExt) + ) ?? [], }) : null, - activePieces: this._pieceInstances?.active.map((piece) => this.toPieceStatus(piece)) ?? [], }) : literal({ event: 'activePlaylist', @@ -164,7 +152,6 @@ export class ActivePlaylistTopic currentPart: null, currentSegment: null, nextPart: null, - activePieces: [], }) for (const subscriber of subscribers) { @@ -176,12 +163,13 @@ export class ActivePlaylistTopic source: string, data: | DBRundownPlaylist - | DBShowStyleBase + | ShowStyleBaseExt | SelectedPartInstances | DBPart[] | SelectedPieceInstances | undefined ): Promise { + let hasAnythingChanged = false switch (source) { case PlaylistHandler.name: { const rundownPlaylist = data ? (data as DBRundownPlaylist) : undefined @@ -189,41 +177,14 @@ export class ActivePlaylistTopic `${this._name} received playlist update ${rundownPlaylist?._id}, activationId ${rundownPlaylist?.activationId}` ) this._activePlaylist = unprotectString(rundownPlaylist?.activationId) ? rundownPlaylist : undefined + hasAnythingChanged = true break } case ShowStyleBaseHandler.name: { - const sourceLayers: SourceLayers = data - ? applyAndValidateOverrides((data as DBShowStyleBase).sourceLayersWithOverrides).obj - : {} - const outputLayers: OutputLayers = data - ? applyAndValidateOverrides((data as DBShowStyleBase).outputLayersWithOverrides).obj - : {} - this._logger.info( - `${this._name} received showStyleBase update with sourceLayers [${Object.values< - ISourceLayer | undefined - >(sourceLayers).map( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (s) => s!.name - )}]` - ) - this._logger.info( - `${this._name} received showStyleBase update with outputLayers [${Object.values< - IOutputLayer | undefined - >(outputLayers).map( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (s) => s!.name - )}]` - ) - this._sourceLayersMap.clear() - this._outputLayersMap.clear() - for (const [layerId, sourceLayer] of Object.entries(sourceLayers)) { - if (sourceLayer === undefined || sourceLayer === null) continue - this._sourceLayersMap.set(layerId, sourceLayer.name) - } - for (const [layerId, outputLayer] of Object.entries(outputLayers)) { - if (outputLayer === undefined || outputLayer === null) continue - this._outputLayersMap.set(layerId, outputLayer.name) - } + const showStyleBaseExt = data ? (data as ShowStyleBaseExt) : undefined + this._logger.info(`${this._name} received showStyleBase update from ${source}`) + this._showStyleBaseExt = showStyleBaseExt + hasAnythingChanged = true break } case PartInstancesHandler.name: { @@ -235,39 +196,38 @@ export class ActivePlaylistTopic this._nextPartInstance = partInstances.next this._firstInstanceInSegmentPlayout = partInstances.firstInSegmentPlayout this._partInstancesInCurrentSegment = partInstances.inCurrentSegment + hasAnythingChanged = true break } case PartsHandler.name: { this._partsBySegmentId = _.groupBy(data as DBPart[], 'segmentId') this._logger.info(`${this._name} received parts update from ${source}`) + hasAnythingChanged = true // TODO: can this be smarter? break } case PieceInstancesHandler.name: { const pieceInstances = data as SelectedPieceInstances this._logger.info(`${this._name} received pieceInstances update from ${source}`) - this._pieceInstances = pieceInstances + if ( + pieceInstances.currentPartInstance !== this._pieceInstancesInCurrentPartInstance || + pieceInstances.nextPartInstance !== this._pieceInstancesInNextPartInstance + ) { + hasAnythingChanged = true + } + this._pieceInstancesInCurrentPartInstance = pieceInstances.currentPartInstance + this._pieceInstancesInNextPartInstance = pieceInstances.nextPartInstance break } default: throw new Error(`${this._name} received unsupported update from ${source}}`) } - this.throttledSendStatusToAll() + if (hasAnythingChanged) { + this.throttledSendStatusToAll() + } } private sendStatusToAll() { this.sendStatus(this._subscribers) } - - private toPieceStatus(pieceInstance: PieceInstance): PieceStatus { - const sourceLayerName = this._sourceLayersMap.get(pieceInstance.piece.sourceLayerId) - const outputLayerName = this._outputLayersMap.get(pieceInstance.piece.outputLayerId) - return { - id: unprotectString(pieceInstance._id), - name: pieceInstance.piece.name, - sourceLayer: sourceLayerName ?? 'invalid', - outputLayer: outputLayerName ?? 'invalid', - tags: pieceInstance.piece.tags, - } - } } diff --git a/packages/live-status-gateway/src/topics/adLibsTopic.ts b/packages/live-status-gateway/src/topics/adLibsTopic.ts index 17a7ae99b3..359d06ca4d 100644 --- a/packages/live-status-gateway/src/topics/adLibsTopic.ts +++ b/packages/live-status-gateway/src/topics/adLibsTopic.ts @@ -12,14 +12,8 @@ import { AdLibActionsHandler } from '../collections/adLibActionsHandler' import { GlobalAdLibActionsHandler } from '../collections/globalAdLibActionsHandler' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' -import { - IBlueprintActionManifestDisplayContent, - IOutputLayer, - ISourceLayer, -} from '@sofie-automation/blueprints-integration' -import { ShowStyleBaseHandler } from '../collections/showStyleBaseHandler' -import { DBShowStyleBase, OutputLayers, SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' -import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { IBlueprintActionManifestDisplayContent } from '@sofie-automation/blueprints-integration' +import { ShowStyleBaseExt, ShowStyleBaseHandler } from '../collections/showStyleBaseHandler' import { interpollateTranslation } from '@sofie-automation/corelib/dist/TranslatableMessage' import { AdLibsHandler } from '../collections/adLibsHandler' import { GlobalAdLibsHandler } from '../collections/globalAdLibsHandler' @@ -52,13 +46,14 @@ export class AdLibsTopic implements WebSocketTopic, CollectionObserver, + CollectionObserver, CollectionObserver, CollectionObserver { public observerName = AdLibsTopic.name private _activePlaylist: DBRundownPlaylist | undefined - private _sourceLayersMap: Map = new Map() - private _outputLayersMap: Map = new Map() + private _sourceLayersMap: ReadonlyMap = new Map() + private _outputLayersMap: ReadonlyMap = new Map() private _adLibActions: AdLibAction[] | undefined private _abLibs: AdLibPiece[] | undefined private _globalAdLibActions: RundownBaselineAdLibAction[] | undefined @@ -192,7 +187,7 @@ export class AdLibsTopic source: string, data: | DBRundownPlaylist - | DBShowStyleBase + | ShowStyleBaseExt | AdLibAction[] | RundownBaselineAdLibAction[] | AdLibPiece[] @@ -232,38 +227,10 @@ export class AdLibsTopic break } case ShowStyleBaseHandler.name: { - const sourceLayers: SourceLayers = data - ? applyAndValidateOverrides((data as DBShowStyleBase).sourceLayersWithOverrides).obj - : {} - const outputLayers: OutputLayers = data - ? applyAndValidateOverrides((data as DBShowStyleBase).outputLayersWithOverrides).obj - : {} - this._logger.info( - `${this._name} received showStyleBase update with sourceLayers [${Object.values< - ISourceLayer | undefined - >(sourceLayers).map( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (s) => s!.name - )}]` - ) - this._logger.info( - `${this._name} received showStyleBase update with outputLayers [${Object.values< - IOutputLayer | undefined - >(outputLayers).map( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (s) => s!.name - )}]` - ) - this._sourceLayersMap.clear() - this._outputLayersMap.clear() - for (const [layerId, sourceLayer] of Object.entries(sourceLayers)) { - if (sourceLayer === undefined || sourceLayer === null) continue - this._sourceLayersMap.set(layerId, sourceLayer.name) - } - for (const [layerId, outputLayer] of Object.entries(outputLayers)) { - if (outputLayer === undefined || outputLayer === null) continue - this._outputLayersMap.set(layerId, outputLayer.name) - } + const showStyleBaseExt = data ? (data as ShowStyleBaseExt) : undefined + this._logger.info(`${this._name} received showStyleBase update from ${source}`) + this._sourceLayersMap = showStyleBaseExt?.sourceLayerNamesById ?? new Map() + this._outputLayersMap = showStyleBaseExt?.outputLayerNamesById ?? new Map() break } default: diff --git a/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts b/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts new file mode 100644 index 0000000000..c1e136fd21 --- /dev/null +++ b/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts @@ -0,0 +1,26 @@ +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { unprotectString } from '@sofie-automation/server-core-integration' +import { ShowStyleBaseExt } from '../../collections/showStyleBaseHandler' + +export interface PieceStatus { + id: string + name: string + sourceLayer: string + outputLayer: string + tags?: string[] +} + +export function toPieceStatus( + pieceInstance: PieceInstance, + showStyleBaseExt: ShowStyleBaseExt | undefined +): PieceStatus { + const sourceLayerName = showStyleBaseExt?.sourceLayerNamesById.get(pieceInstance.piece.sourceLayerId) + const outputLayerName = showStyleBaseExt?.outputLayerNamesById.get(pieceInstance.piece.outputLayerId) + return { + id: unprotectString(pieceInstance._id), + name: pieceInstance.piece.name, + sourceLayer: sourceLayerName ?? 'invalid', + outputLayer: outputLayerName ?? 'invalid', + tags: pieceInstance.piece.tags, + } +} diff --git a/packages/live-status-gateway/src/topics/root.ts b/packages/live-status-gateway/src/topics/root.ts index 698478e86c..1097daf141 100644 --- a/packages/live-status-gateway/src/topics/root.ts +++ b/packages/live-status-gateway/src/topics/root.ts @@ -34,6 +34,7 @@ interface SubscriptionResponse { export enum StatusChannels { studio = 'studio', activePlaylist = 'activePlaylist', + activePieces = 'activePieces', segments = 'segments', adLibs = 'adLibs', } From a7ab6e428b05e678d3def3df124b76d35040942b Mon Sep 17 00:00:00 2001 From: ianshade Date: Tue, 6 Feb 2024 22:13:22 +0100 Subject: [PATCH 237/479] feat(live-status-gw): expose publicData --- .../src/topics/__tests__/activePieces.spec.ts | 4 +- .../topics/__tests__/activePlaylist.spec.ts | 13 +- .../src/topics/__tests__/adLibs.spec.ts | 4 + .../topics/__tests__/segmentsTopic.spec.ts | 115 +++++++++++++++--- .../src/topics/__tests__/utils.ts | 1 + .../src/topics/activePiecesTopic.ts | 10 +- .../src/topics/activePlaylistTopic.ts | 28 +++-- .../src/topics/adLibsTopic.ts | 5 + .../src/topics/helpers/pieceStatus.ts | 4 +- .../src/topics/segmentsTopic.ts | 2 + 10 files changed, 149 insertions(+), 37 deletions(-) diff --git a/packages/live-status-gateway/src/topics/__tests__/activePieces.spec.ts b/packages/live-status-gateway/src/topics/__tests__/activePieces.spec.ts index 574d2e5d39..7e7f469e13 100644 --- a/packages/live-status-gateway/src/topics/__tests__/activePieces.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/activePieces.spec.ts @@ -39,6 +39,7 @@ describe('ActivePiecesTopic', () => { outputLayerId: 'pgm', sourceLayerId: 'layer0', tags: ['my_tag'], + publicData: { c: 'd' }, }, }), ] as PieceInstance[], @@ -47,7 +48,7 @@ describe('ActivePiecesTopic', () => { topic.addSubscriber(mockSubscriber) - const expectedStatus: Partial = { + const expectedStatus: PartialDeep = { event: 'activePieces', activePieces: [ @@ -57,6 +58,7 @@ describe('ActivePiecesTopic', () => { sourceLayer: 'Layer 0', outputLayer: 'PGM', tags: ['my_tag'], + publicData: { c: 'd' }, }, ], } diff --git a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts index 151139a321..91f28d6d08 100644 --- a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts @@ -44,11 +44,14 @@ describe('ActivePlaylistTopic', () => { nextPart: null, currentSegment: null, rundownIds: unprotectStringArray(playlist.rundownIdsInOrder), + publicData: undefined, } // eslint-disable-next-line @typescript-eslint/unbound-method expect(mockSubscriber.send).toHaveBeenCalledTimes(1) - expect(JSON.parse(mockSubscriber.send.mock.calls[0][0] as string)).toMatchObject(expectedStatus) + expect(JSON.parse(mockSubscriber.send.mock.calls[0][0] as string)).toMatchObject( + JSON.parse(JSON.stringify(expectedStatus)) + ) }) it('provides segment and part', async () => { @@ -75,6 +78,7 @@ describe('ActivePlaylistTopic', () => { segmentId: protectString('SEGMENT_1'), expectedDurationWithPreroll: 10000, expectedDuration: 10000, + publicData: { b: 'c' }, } const testPartInstances: PartialDeep = { current: { @@ -107,6 +111,8 @@ describe('ActivePlaylistTopic', () => { segmentId: 'SEGMENT_1', timing: { startTime: 1600000060000, expectedDurationMs: 10000, projectedEndTime: 1600000070000 }, pieces: [], + autoNext: undefined, + publicData: { b: 'c' }, }, nextPart: null, currentSegment: { @@ -117,10 +123,13 @@ describe('ActivePlaylistTopic', () => { }, }, rundownIds: unprotectStringArray(playlist.rundownIdsInOrder), + publicData: { a: 'b' }, } // eslint-disable-next-line @typescript-eslint/unbound-method expect(mockSubscriber.send).toHaveBeenCalledTimes(1) - expect(JSON.parse(mockSubscriber.send.mock.calls[0][0] as string)).toMatchObject(expectedStatus) + expect(JSON.parse(mockSubscriber.send.mock.calls[0][0] as string)).toMatchObject( + JSON.parse(JSON.stringify(expectedStatus)) + ) }) }) diff --git a/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts b/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts index 4f87e80048..0ba5af4013 100644 --- a/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts @@ -25,6 +25,7 @@ function makeTestAdLibActions(): AdLibAction[] { externalId: 'NCS_ACTION_0', userData: {}, userDataManifest: {}, + publicData: { a: 'b' }, }, ] } @@ -45,6 +46,7 @@ function makeTestGlobalAdLibActions(): RundownBaselineAdLibAction[] { externalId: 'NCS_GLOBAL_ACTION_0', userData: {}, userDataManifest: {}, + publicData: { c: 'd' }, }, ] } @@ -82,6 +84,7 @@ describe('ActivePlaylistTopic', () => { outputLayer: 'PGM', sourceLayer: 'Layer 0', tags: ['adlib_tag'], + publicData: { a: 'b' }, }, ], globalAdLibs: [ @@ -92,6 +95,7 @@ describe('ActivePlaylistTopic', () => { outputLayer: 'PGM', sourceLayer: 'Layer 0', tags: ['global_adlib_tag'], + publicData: { c: 'd' }, }, ], } diff --git a/packages/live-status-gateway/src/topics/__tests__/segmentsTopic.spec.ts b/packages/live-status-gateway/src/topics/__tests__/segmentsTopic.spec.ts index fc5851702f..05d7a1649f 100644 --- a/packages/live-status-gateway/src/topics/__tests__/segmentsTopic.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/segmentsTopic.spec.ts @@ -154,10 +154,34 @@ describe('SegmentsTopic', () => { event: 'segments', rundownPlaylistId: unprotectString(testPlaylist._id), segments: [ - { id: '1_1', rundownId: RUNDOWN_1_ID, name: 'Segment 1_1', timing: { expectedDurationMs: 0 } }, - { id: '1_2', rundownId: RUNDOWN_1_ID, name: 'Segment 1_2', timing: { expectedDurationMs: 0 } }, - { id: '2_1', rundownId: RUNDOWN_2_ID, name: 'Segment 2_1', timing: { expectedDurationMs: 0 } }, - { id: '2_2', rundownId: RUNDOWN_2_ID, name: 'Segment 2_2', timing: { expectedDurationMs: 0 } }, + { + id: '1_1', + rundownId: RUNDOWN_1_ID, + name: 'Segment 1_1', + timing: { expectedDurationMs: 0 }, + publicData: undefined, + }, + { + id: '1_2', + rundownId: RUNDOWN_1_ID, + name: 'Segment 1_2', + timing: { expectedDurationMs: 0 }, + publicData: undefined, + }, + { + id: '2_1', + rundownId: RUNDOWN_2_ID, + name: 'Segment 2_1', + timing: { expectedDurationMs: 0 }, + publicData: undefined, + }, + { + id: '2_2', + rundownId: RUNDOWN_2_ID, + name: 'Segment 2_2', + timing: { expectedDurationMs: 0 }, + publicData: undefined, + }, ], } expect(mockSubscriber.send.mock.calls).toEqual([[JSON.stringify(expectedStatus)]]) @@ -188,10 +212,34 @@ describe('SegmentsTopic', () => { event: 'segments', rundownPlaylistId: unprotectString(testPlaylist._id), segments: [ - { id: '2_1', rundownId: RUNDOWN_2_ID, name: 'Segment 2_1', timing: { expectedDurationMs: 0 } }, - { id: '2_2', rundownId: RUNDOWN_2_ID, name: 'Segment 2_2', timing: { expectedDurationMs: 0 } }, - { id: '1_1', rundownId: RUNDOWN_1_ID, name: 'Segment 1_1', timing: { expectedDurationMs: 0 } }, - { id: '1_2', rundownId: RUNDOWN_1_ID, name: 'Segment 1_2', timing: { expectedDurationMs: 0 } }, + { + id: '2_1', + rundownId: RUNDOWN_2_ID, + name: 'Segment 2_1', + timing: { expectedDurationMs: 0 }, + publicData: undefined, + }, + { + id: '2_2', + rundownId: RUNDOWN_2_ID, + name: 'Segment 2_2', + timing: { expectedDurationMs: 0 }, + publicData: undefined, + }, + { + id: '1_1', + rundownId: RUNDOWN_1_ID, + name: 'Segment 1_1', + timing: { expectedDurationMs: 0 }, + publicData: undefined, + }, + { + id: '1_2', + rundownId: RUNDOWN_1_ID, + name: 'Segment 1_2', + timing: { expectedDurationMs: 0 }, + publicData: undefined, + }, ], } expect(mockSubscriber.send.mock.calls).toEqual([[JSON.stringify(expectedStatus)]]) @@ -251,26 +299,37 @@ describe('SegmentsTopic', () => { rundownId: RUNDOWN_1_ID, name: 'Segment 1_1', timing: { expectedDurationMs: 0, budgetDurationMs: 5000 }, + publicData: undefined, }, { id: '1_2', rundownId: RUNDOWN_1_ID, name: 'Segment 1_2', timing: { expectedDurationMs: 0, budgetDurationMs: 15000 }, + publicData: undefined, + }, + { + id: '2_1', + rundownId: RUNDOWN_2_ID, + name: 'Segment 2_1', + timing: { expectedDurationMs: 0 }, + publicData: undefined, }, - { id: '2_1', rundownId: RUNDOWN_2_ID, name: 'Segment 2_1', timing: { expectedDurationMs: 0 } }, { id: '2_2', rundownId: RUNDOWN_2_ID, name: 'Segment 2_2', timing: { expectedDurationMs: 0, budgetDurationMs: 51000 }, + publicData: undefined, }, ], } // eslint-disable-next-line @typescript-eslint/unbound-method expect(mockSubscriber.send).toHaveBeenCalledTimes(1) - expect(JSON.parse(mockSubscriber.send.mock.calls[0][0] as string)).toEqual(expectedStatus) + expect(JSON.parse(mockSubscriber.send.mock.calls[0][0] as string)).toEqual( + JSON.parse(JSON.stringify(expectedStatus)) + ) }) it('exposes expectedDuration', async () => { @@ -322,10 +381,34 @@ describe('SegmentsTopic', () => { event: 'segments', rundownPlaylistId: unprotectString(testPlaylist._id), segments: [ - { id: '1_1', rundownId: RUNDOWN_1_ID, name: 'Segment 1_1', timing: { expectedDurationMs: 5000 } }, - { id: '1_2', rundownId: RUNDOWN_1_ID, name: 'Segment 1_2', timing: { expectedDurationMs: 15000 } }, - { id: '2_1', rundownId: RUNDOWN_2_ID, name: 'Segment 2_1', timing: { expectedDurationMs: 0 } }, - { id: '2_2', rundownId: RUNDOWN_2_ID, name: 'Segment 2_2', timing: { expectedDurationMs: 51000 } }, + { + id: '1_1', + rundownId: RUNDOWN_1_ID, + name: 'Segment 1_1', + timing: { expectedDurationMs: 5000 }, + publicData: undefined, + }, + { + id: '1_2', + rundownId: RUNDOWN_1_ID, + name: 'Segment 1_2', + timing: { expectedDurationMs: 15000 }, + publicData: undefined, + }, + { + id: '2_1', + rundownId: RUNDOWN_2_ID, + name: 'Segment 2_1', + timing: { expectedDurationMs: 0 }, + publicData: undefined, + }, + { + id: '2_2', + rundownId: RUNDOWN_2_ID, + name: 'Segment 2_2', + timing: { expectedDurationMs: 51000 }, + publicData: undefined, + }, ], } expect(mockSubscriber.send.mock.calls).toEqual([[JSON.stringify(expectedStatus)]]) @@ -358,6 +441,8 @@ describe('SegmentsTopic', () => { { id: '1_2', rundownId: RUNDOWN_1_ID, name: 'Segment 1_2', identifier: 'SomeIdentifier' }, ], } as SegmentsStatus - expect(JSON.parse(mockSubscriber.send.mock.calls[0][0] as string)).toMatchObject(expectedStatus) + expect(JSON.parse(mockSubscriber.send.mock.calls[0][0] as string)).toMatchObject( + JSON.parse(JSON.stringify(expectedStatus)) + ) }) }) diff --git a/packages/live-status-gateway/src/topics/__tests__/utils.ts b/packages/live-status-gateway/src/topics/__tests__/utils.ts index 0bedea142a..730b5fac52 100644 --- a/packages/live-status-gateway/src/topics/__tests__/utils.ts +++ b/packages/live-status-gateway/src/topics/__tests__/utils.ts @@ -32,6 +32,7 @@ export function makeTestPlaylist(id?: string): DBRundownPlaylist { rundownIdsInOrder: [protectString(RUNDOWN_1_ID), protectString(RUNDOWN_2_ID)], studioId: protectString('STUDIO_1'), timing: { type: PlaylistTimingType.None }, + publicData: { a: 'b' }, } } diff --git a/packages/live-status-gateway/src/topics/activePiecesTopic.ts b/packages/live-status-gateway/src/topics/activePiecesTopic.ts index cb7abda6d8..e90d8e71af 100644 --- a/packages/live-status-gateway/src/topics/activePiecesTopic.ts +++ b/packages/live-status-gateway/src/topics/activePiecesTopic.ts @@ -8,19 +8,11 @@ import { PlaylistHandler } from '../collections/playlistHandler' import { ShowStyleBaseExt, ShowStyleBaseHandler } from '../collections/showStyleBaseHandler' import _ = require('underscore') import { SelectedPieceInstances, PieceInstancesHandler, PieceInstanceMin } from '../collections/pieceInstancesHandler' -import { toPieceStatus } from './helpers/pieceStatus' +import { PieceStatus, toPieceStatus } from './helpers/pieceStatus' import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' const THROTTLE_PERIOD_MS = 100 -interface PieceStatus { - id: string - name: string - sourceLayer: string - outputLayer: string - tags?: string[] -} - export interface ActivePiecesStatus { event: 'activePieces' rundownPlaylistId: string | null diff --git a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts index f0c5559064..359dee9f66 100644 --- a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts +++ b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts @@ -22,8 +22,9 @@ interface PartStatus { id: string segmentId: string name: string - autoNext?: boolean + autoNext: boolean | undefined pieces: PieceStatus[] + publicData: unknown } interface CurrentPartStatus extends PartStatus { @@ -43,6 +44,7 @@ export interface ActivePlaylistStatus { currentPart: CurrentPartStatus | null currentSegment: CurrentSegmentStatus | null nextPart: PartStatus | null + publicData: unknown } export class ActivePlaylistTopic @@ -81,14 +83,7 @@ export class ActivePlaylistTopic } sendStatus(subscribers: Iterable): void { - if ( - this._currentPartInstance?._id !== this._activePlaylist?.currentPartInfo?.partInstanceId || - this._nextPartInstance?._id !== this._activePlaylist?.nextPartInfo?.partInstanceId || - (this._pieceInstancesInCurrentPartInstance?.[0] && - this._pieceInstancesInCurrentPartInstance?.[0].partInstanceId !== this._currentPartInstance?._id) || - (this._pieceInstancesInNextPartInstance?.[0] && - this._pieceInstancesInNextPartInstance?.[0].partInstanceId !== this._nextPartInstance?._id) - ) { + if (this.isDataInconsistent()) { // data is inconsistent, let's wait return } @@ -117,6 +112,7 @@ export class ActivePlaylistTopic this._pieceInstancesInCurrentPartInstance?.map((piece) => toPieceStatus(piece, this._showStyleBaseExt) ) ?? [], + publicData: currentPart.publicData, }) : null, currentSegment: @@ -141,8 +137,10 @@ export class ActivePlaylistTopic this._pieceInstancesInCurrentPartInstance?.map((piece) => toPieceStatus(piece, this._showStyleBaseExt) ) ?? [], + publicData: nextPart.publicData, }) : null, + publicData: this._activePlaylist.publicData, }) : literal({ event: 'activePlaylist', @@ -152,6 +150,7 @@ export class ActivePlaylistTopic currentPart: null, currentSegment: null, nextPart: null, + publicData: undefined, }) for (const subscriber of subscribers) { @@ -159,6 +158,17 @@ export class ActivePlaylistTopic } } + private isDataInconsistent() { + return ( + this._currentPartInstance?._id !== this._activePlaylist?.currentPartInfo?.partInstanceId || + this._nextPartInstance?._id !== this._activePlaylist?.nextPartInfo?.partInstanceId || + (this._pieceInstancesInCurrentPartInstance?.[0] && + this._pieceInstancesInCurrentPartInstance?.[0].partInstanceId !== this._currentPartInstance?._id) || + (this._pieceInstancesInNextPartInstance?.[0] && + this._pieceInstancesInNextPartInstance?.[0].partInstanceId !== this._nextPartInstance?._id) + ) + } + async update( source: string, data: diff --git a/packages/live-status-gateway/src/topics/adLibsTopic.ts b/packages/live-status-gateway/src/topics/adLibsTopic.ts index 359d06ca4d..4bf7c3cc3b 100644 --- a/packages/live-status-gateway/src/topics/adLibsTopic.ts +++ b/packages/live-status-gateway/src/topics/adLibsTopic.ts @@ -39,6 +39,7 @@ interface AdLibStatus { outputLayer: string actionType: AdLibActionType[] tags?: string[] + publicData: unknown } export class AdLibsTopic @@ -101,6 +102,7 @@ export class AdLibsTopic outputLayer: outputLayerName ?? 'invalid', actionType: triggerModes, tags: action.display.tags, + publicData: action.publicData, }) }) ) @@ -118,6 +120,7 @@ export class AdLibsTopic outputLayer: outputLayerName ?? 'invalid', actionType: [], tags: adLib.tags, + publicData: adLib.publicData, }) }) ) @@ -147,6 +150,7 @@ export class AdLibsTopic outputLayer: outputLayerName ?? 'invalid', actionType: triggerModes, tags: action.display.tags, + publicData: action.publicData, }) }) ) @@ -164,6 +168,7 @@ export class AdLibsTopic outputLayer: outputLayerName ?? 'invalid', actionType: [], tags: adLib.tags, + publicData: adLib.publicData, }) }) ) diff --git a/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts b/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts index c1e136fd21..02005aa377 100644 --- a/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts +++ b/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts @@ -7,7 +7,8 @@ export interface PieceStatus { name: string sourceLayer: string outputLayer: string - tags?: string[] + tags: string[] | undefined + publicData: unknown } export function toPieceStatus( @@ -22,5 +23,6 @@ export function toPieceStatus( sourceLayer: sourceLayerName ?? 'invalid', outputLayer: outputLayerName ?? 'invalid', tags: pieceInstance.piece.tags, + publicData: pieceInstance.piece.publicData, } } diff --git a/packages/live-status-gateway/src/topics/segmentsTopic.ts b/packages/live-status-gateway/src/topics/segmentsTopic.ts index a037f15ec0..e4d63da93c 100644 --- a/packages/live-status-gateway/src/topics/segmentsTopic.ts +++ b/packages/live-status-gateway/src/topics/segmentsTopic.ts @@ -21,6 +21,7 @@ interface SegmentStatus { rundownId: string name: string timing: SegmentTiming + publicData: unknown } export interface SegmentsStatus { @@ -69,6 +70,7 @@ export class SegmentsTopic name: segment.name, timing: calculateSegmentTiming(this._partsBySegment[segmentId] ?? []), identifier: segment.identifier, + publicData: segment.publicData, } }), } From 5e92a9e5a2e2faab429e2a30f9ca147c88bb07c2 Mon Sep 17 00:00:00 2001 From: ianshade Date: Tue, 6 Feb 2024 22:20:32 +0100 Subject: [PATCH 238/479] chore(live-status-gw): bump asyncapi version to 2.0.0 --- packages/live-status-gateway/api/asyncapi.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/live-status-gateway/api/asyncapi.yaml b/packages/live-status-gateway/api/asyncapi.yaml index e2aad3e33e..1f7d363471 100644 --- a/packages/live-status-gateway/api/asyncapi.yaml +++ b/packages/live-status-gateway/api/asyncapi.yaml @@ -2,7 +2,7 @@ asyncapi: 2.5.0 info: title: Sofie Live Status Service description: This service provides subscriptions for status updates from Sofie - version: 1.0.0 + version: 2.0.0 license: name: MIT License url: http://opensource.org/licenses/MIT From fdf189dd9a059c72298afcdd8865ddb636e26e20 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 8 Feb 2024 15:10:24 +0000 Subject: [PATCH 239/479] fix: only load packages during ingest which have `listenToPackageInfoUpdates: true` --- .../src/blueprints/context/watchedPackages.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/job-worker/src/blueprints/context/watchedPackages.ts b/packages/job-worker/src/blueprints/context/watchedPackages.ts index e7748312cc..d85821569e 100644 --- a/packages/job-worker/src/blueprints/context/watchedPackages.ts +++ b/packages/job-worker/src/blueprints/context/watchedPackages.ts @@ -70,13 +70,10 @@ export class WatchedPackagesHelper { } } - // Load all the packages and the infos that are watched - const watchedPackageInfos = await context.directCollections.PackageInfos.findFetch({ - studioId: context.studio._id, - packageId: { $in: packages.map((p) => p._id) }, - }) - - return new WatchedPackagesHelper(packages, watchedPackageInfos) + return this.#createFromPackages( + context, + packages.filter((pkg) => !!pkg.listenToPackageInfoUpdates) + ) } /** @@ -101,6 +98,13 @@ export class WatchedPackagesHelper { } } + return this.#createFromPackages( + context, + packages.filter((pkg) => !!pkg.listenToPackageInfoUpdates) + ) + } + + static async #createFromPackages(context: JobContext, packages: ReadonlyDeep[]) { // Load all the packages and the infos that are watched const watchedPackageInfos = packages.length > 0 From 9f6be1d51fa46d7853f2c95c48fcb8298306db98 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 8 Feb 2024 16:19:06 +0000 Subject: [PATCH 240/479] fix: add `count` method to job-worker `ICollection` --- packages/job-worker/src/__mocks__/collection.ts | 9 ++++++++- packages/job-worker/src/db/collection.ts | 15 ++++++++++++++- packages/job-worker/src/db/collections.ts | 2 ++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/job-worker/src/__mocks__/collection.ts b/packages/job-worker/src/__mocks__/collection.ts index cf8ab926ab..3da3c0cd4c 100644 --- a/packages/job-worker/src/__mocks__/collection.ts +++ b/packages/job-worker/src/__mocks__/collection.ts @@ -32,7 +32,7 @@ import { } from '@sofie-automation/corelib/dist/mongo' import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' import EventEmitter = require('eventemitter3') -import { AnyBulkWriteOperation, Collection, FindOptions } from 'mongodb' +import { AnyBulkWriteOperation, Collection, CountOptions, FindOptions } from 'mongodb' import { ReadonlyDeep } from 'type-fest' import { IChangeStream, IChangeStreamEvents, ICollection, IDirectCollections, MongoModifier, MongoQuery } from '../db' import _ = require('underscore') @@ -154,6 +154,13 @@ export class MockMongoCollection }> imp }) return docs[0] } + async count(selector?: MongoQuery | TDoc['_id'], options?: CountOptions): Promise { + this.#ops.push({ type: 'count', args: [selector, options] }) + + const docs = await this.findFetchInner(selector, options) + return docs.length + } + async insertOne(doc: TDoc | ReadonlyDeep): Promise { this.#ops.push({ type: 'insertOne', args: [doc._id] }) diff --git a/packages/job-worker/src/db/collection.ts b/packages/job-worker/src/db/collection.ts index 681981ebca..f37d446710 100644 --- a/packages/job-worker/src/db/collection.ts +++ b/packages/job-worker/src/db/collection.ts @@ -1,6 +1,6 @@ import { ProtectedString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { EventEmitter } from 'eventemitter3' -import { AnyBulkWriteOperation, ChangeStream, Collection as MongoCollection, FindOptions } from 'mongodb' +import { AnyBulkWriteOperation, ChangeStream, Collection as MongoCollection, FindOptions, CountOptions } from 'mongodb' import { IChangeStreamEvents } from '.' import { startSpanManual } from '../profiler' import { IChangeStream, ICollection, MongoModifier, MongoQuery } from './collections' @@ -58,6 +58,19 @@ class WrappedCollection }> implements I return res ?? undefined } + async count(selector: MongoQuery | TDoc['_id'], options?: CountOptions): Promise { + const span = startSpanManual('WrappedCollection.count') + if (span) { + span.addLabels({ + collection: this.name, + query: JSON.stringify(selector), + }) + } + const res = await this.#collection.countDocuments(selector as any, options) + if (span) span.end() + return res + } + async insertOne(doc: TDoc): Promise { const span = startSpanManual('WrappedCollection.insertOne') if (span) { diff --git a/packages/job-worker/src/db/collections.ts b/packages/job-worker/src/db/collections.ts index fed561aec6..faa2a64fc2 100644 --- a/packages/job-worker/src/db/collections.ts +++ b/packages/job-worker/src/db/collections.ts @@ -7,6 +7,7 @@ import { UpdateFilter, Collection as MongoCollection, ChangeStreamDocument, + CountOptions, } from 'mongodb' import { wrapMongoCollection } from './collection' import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' @@ -53,6 +54,7 @@ export interface IReadOnlyCollection }> findFetch(selector?: MongoQuery, options?: FindOptions): Promise> findOne(selector?: MongoQuery | TDoc['_id'], options?: FindOptions): Promise + count(selector?: MongoQuery | TDoc['_id'], options?: CountOptions): Promise /** * Watch the collection for changes From 7f6e2f8b78648d1e813bc368fd2bfd410900e35c Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 8 Feb 2024 15:33:49 +0000 Subject: [PATCH 241/479] chore: refactoring --- .../src/ingest/generationSegment.ts | 448 +++++++++++------- .../src/ingest/model/IngestModel.ts | 8 +- .../model/implementation/IngestModelImpl.ts | 15 +- 3 files changed, 307 insertions(+), 164 deletions(-) diff --git a/packages/job-worker/src/ingest/generationSegment.ts b/packages/job-worker/src/ingest/generationSegment.ts index 83409d0a63..2ca0175c05 100644 --- a/packages/job-worker/src/ingest/generationSegment.ts +++ b/packages/job-worker/src/ingest/generationSegment.ts @@ -1,21 +1,30 @@ -import { SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { BlueprintId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { SegmentNote, PartNote } from '@sofie-automation/corelib/dist/dataModel/Notes' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { DBSegment, SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' import { literal } from '@sofie-automation/corelib/dist/lib' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' -import { SegmentUserContext } from '../blueprints/context' +import { RawPartNote, SegmentUserContext } from '../blueprints/context' import { WatchedPackagesHelper } from '../blueprints/context/watchedPackages' import { postProcessAdLibActions, postProcessAdLibPieces, postProcessPieces } from '../blueprints/postProcess' import { logger } from '../logging' import { IngestModel, IngestModelReadonly } from './model/IngestModel' import { LocalIngestSegment, LocalIngestRundown } from './ingestCache' import { getSegmentId, getPartId, canSegmentBeUpdated } from './lib' -import { JobContext } from '../jobs' +import { JobContext, ProcessedShowStyleCompound } from '../jobs' import { CommitIngestData } from './lock' -import { BlueprintResultSegment, NoteSeverity } from '@sofie-automation/blueprints-integration' +import { + BlueprintResultPart, + BlueprintResultSegment, + IngestSegment, + NoteSeverity, +} from '@sofie-automation/blueprints-integration' import { wrapTranslatableMessageFromBlueprints } from '@sofie-automation/corelib/dist/TranslatableMessage' import { updateExpectedPackagesForPartModel } from './expectedPackages' +import { IngestSegmentModel } from './model/IngestSegmentModel' +import { ReadonlyDeep } from 'type-fest' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { WrappedShowStyleBlueprint } from '../blueprints/cache' async function getWatchedPackagesHelper( context: JobContext, @@ -27,7 +36,7 @@ async function getWatchedPackagesHelper( return allRundownWatchedPackages0 } else { const segmentExternalIds = ingestSegments.map((s) => s.externalId) - return WatchedPackagesHelper.createForIngestSegment(context, ingestModel, segmentExternalIds) + return WatchedPackagesHelper.createForIngestSegments(context, ingestModel, segmentExternalIds) } } @@ -49,7 +58,7 @@ export async function calculateSegmentsFromIngestData( const rundown = ingestModel.getRundown() - const changedSegmentIds: SegmentId[] = [] + let changedSegmentIds: SegmentId[] = [] if (ingestSegments.length > 0) { const pShowStyle = context.getShowStyleCompound(rundown.showStyleVariantId, rundown.showStyleBaseId) @@ -65,175 +74,290 @@ export async function calculateSegmentsFromIngestData( const allRundownWatchedPackages = await pAllRundownWatchedPackages - for (const ingestSegment of ingestSegments) { - const segmentId = getSegmentId(ingestModel.rundownId, ingestSegment.externalId) + changedSegmentIds = await Promise.all( + ingestSegments.map(async (ingestSegment) => + updateSegmentFromIngestData2( + context, + showStyle, + blueprint, + allRundownWatchedPackages, + ingestModel, + ingestSegment + ) + ) + ) + } - // Ensure the parts are sorted by rank - ingestSegment.parts.sort((a, b) => a.rank - b.rank) + span?.end() + return changedSegmentIds +} - // Filter down to the packages for this segment - const watchedPackages = allRundownWatchedPackages.filter( - context, - (p) => 'segmentId' in p && p.segmentId === segmentId - ) +async function updateSegmentFromIngestData2( + context: JobContext, + showStyle: ReadonlyDeep, + blueprint: ReadonlyDeep, + allRundownWatchedPackages: WatchedPackagesHelper, + ingestModel: IngestModel, + ingestSegment: LocalIngestSegment +): Promise { + // Ensure the parts are sorted by rank + ingestSegment.parts.sort((a, b) => a.rank - b.rank) + + // Filter down to the packages for this segment + const segmentId = ingestModel.getSegmentIdFromExternalId(ingestSegment.externalId) + const segmentWatchedPackages = allRundownWatchedPackages.filter( + context, + (p) => 'segmentId' in p && p.segmentId === segmentId + ) - const context2 = new SegmentUserContext( - { - name: `getSegment=${ingestSegment.name}`, - // Note: this intentionally does not include the segmentId, as parts may be moved between segemnts later on - // This isn't much entropy, blueprints may want to add more for each Part they generate - identifier: `rundownId=${rundown._id}`, - }, - context, - showStyle, - rundown, - watchedPackages - ) - let blueprintSegment0: BlueprintResultSegment | null = null - try { - blueprintSegment0 = await blueprint.blueprint.getSegment(context2, ingestSegment) - } catch (err) { - logger.error(`Error in showStyleBlueprint.getSegment: ${stringifyError(err)}`) - blueprintSegment0 = null - } + const updatedSegmentModel = await regenerateSegmentAndUpdateModel( + context, + showStyle, + blueprint, + ingestModel, + ingestSegment, + segmentWatchedPackages + ) - if (!blueprintSegment0) { - // Something went wrong when generating the segment - - const newSegment = literal({ - _id: segmentId, - rundownId: rundown._id, - externalId: ingestSegment.externalId, - externalModified: ingestSegment.modified, - _rank: ingestSegment.rank, - notes: [ - { - type: NoteSeverity.ERROR, - message: wrapTranslatableMessageFromBlueprints( - { - key: 'Internal Error generating segment', - }, - [blueprint.blueprintId] - ), - origin: { - name: '', // TODO - }, - }, - ], - name: ingestSegment.name, - }) - ingestModel.replaceSegment(newSegment) - changedSegmentIds.push(newSegment._id) + preserveOrphanedSegmentPositionInRundown(context, ingestModel, updatedSegmentModel.segment) - continue // Don't generate any content for the faulty segment - } - const blueprintSegment: BlueprintResultSegment = blueprintSegment0 - - // Ensure all parts have a valid externalId set on them - const knownPartExternalIds = blueprintSegment.parts.map((p) => p.part.externalId) - - const segmentNotes: SegmentNote[] = [] - for (const note of context2.notes) { - if (!note.partExternalId || knownPartExternalIds.indexOf(note.partExternalId) === -1) { - segmentNotes.push( - literal({ - type: note.type, - message: wrapTranslatableMessageFromBlueprints(note.message, [blueprint.blueprintId]), - origin: { - name: '', // TODO - }, - }) - ) - } - } + return updatedSegmentModel.segment._id +} - const newSegment = literal({ - ...blueprintSegment.segment, - _id: segmentId, - rundownId: rundown._id, - externalId: ingestSegment.externalId, - externalModified: ingestSegment.modified, - _rank: ingestSegment.rank, - notes: segmentNotes, - }) - const segmentModel = ingestModel.replaceSegment(newSegment) - changedSegmentIds.push(newSegment._id) - - blueprintSegment.parts.forEach((blueprintPart, i) => { - const partId = getPartId(rundown._id, blueprintPart.part.externalId) - - const notes: PartNote[] = [] - - for (const note of context2.notes) { - if (note.partExternalId === blueprintPart.part.externalId) { - notes.push( - literal({ - type: note.type, - message: wrapTranslatableMessageFromBlueprints(note.message, [blueprint.blueprintId]), - origin: { - name: '', // TODO - }, - }) - ) - } - } +async function regenerateSegmentAndUpdateModel( + context: JobContext, + showStyle: ReadonlyDeep, + blueprint: ReadonlyDeep, + ingestModel: IngestModel, + ingestSegment: LocalIngestSegment, + watchedPackages: WatchedPackagesHelper +): Promise { + const rundown = ingestModel.getRundown() - const part = literal({ - ...blueprintPart.part, - _id: partId, - rundownId: rundown._id, - segmentId: newSegment._id, - _rank: i, // This gets updated to a rank unique within its segment in a later step - notes: notes, - invalidReason: blueprintPart.part.invalidReason - ? { - ...blueprintPart.part.invalidReason, - message: wrapTranslatableMessageFromBlueprints( - blueprintPart.part.invalidReason.message, - [blueprint.blueprintId] - ), - } - : undefined, - - expectedDurationWithPreroll: undefined, // Below - }) + const blueprintResult = await generateSegmentWithBlueprints( + context, + showStyle, + blueprint, + rundown, + ingestSegment, + watchedPackages + ) - // Update pieces - const processedPieces = postProcessPieces( - context, - blueprintPart.pieces, - blueprint.blueprintId, - rundown._id, - newSegment._id, - part._id, - false, - part.invalid - ) - const adlibPieces = postProcessAdLibPieces( - context, - blueprint.blueprintId, - rundown._id, - part._id, - blueprintPart.adLibPieces - ) + if (!blueprintResult) { + // Something went wrong when generating the segment - const adlibActions = postProcessAdLibActions( - blueprint.blueprintId, - rundown._id, - part._id, - blueprintPart.actions || [] - ) + return ingestModel.replaceSegment(createInternalErrorSegment(blueprint.blueprintId, ingestSegment)) + } - const partModel = segmentModel.replacePart(part, processedPieces, adlibPieces, adlibActions) - updateExpectedPackagesForPartModel(context, partModel) - }) + return updateModelWithGeneratedSegment( + context, + blueprint.blueprintId, + ingestModel, + ingestSegment, + blueprintResult.blueprintSegment, + blueprintResult.blueprintNotes + ) +} - preserveOrphanedSegmentPositionInRundown(context, ingestModel, newSegment) +async function generateSegmentWithBlueprints( + context: JobContext, + showStyle: ReadonlyDeep, + blueprint: ReadonlyDeep, + rundown: ReadonlyDeep, + ingestSegment: IngestSegment, + watchedPackages: WatchedPackagesHelper +): Promise<{ + blueprintSegment: BlueprintResultSegment + blueprintNotes: RawPartNote[] +} | null> { + const blueprintContext = new SegmentUserContext( + { + name: `getSegment=${ingestSegment.name}`, + // Note: this intentionally does not include the segmentId, as parts may be moved between segemnts later on + // This isn't much entropy, blueprints may want to add more for each Part they generate + identifier: `rundownId=${rundown._id}`, + }, + context, + showStyle, + rundown, + watchedPackages + ) + + try { + const blueprintSegment = await blueprint.blueprint.getSegment(blueprintContext, ingestSegment) + return { + blueprintSegment, + blueprintNotes: blueprintContext.notes, } + } catch (err) { + logger.error(`Error in showStyleBlueprint.getSegment: ${stringifyError(err)}`) + return null } +} - span?.end() - return changedSegmentIds +function createInternalErrorSegment( + blueprintId: BlueprintId, + ingestSegment: LocalIngestSegment +): Omit { + return { + externalId: ingestSegment.externalId, + externalModified: ingestSegment.modified, + _rank: ingestSegment.rank, + notes: [ + { + type: NoteSeverity.ERROR, + message: wrapTranslatableMessageFromBlueprints( + { + key: 'Internal Error generating segment', + }, + [blueprintId] + ), + origin: { + name: '', // TODO + }, + }, + ], + name: ingestSegment.name, + } +} + +function updateModelWithGeneratedSegment( + context: JobContext, + blueprintId: BlueprintId, + ingestModel: IngestModel, + ingestSegment: LocalIngestSegment, + blueprintSegment: BlueprintResultSegment, + blueprintNotes: RawPartNote[] +): IngestSegmentModel { + // Ensure all parts have a valid externalId set on them + const knownPartExternalIds = new Set(blueprintSegment.parts.map((p) => p.part.externalId)) + + const segmentNotes = extractAndWrapSegmentNotes(blueprintId, blueprintNotes, knownPartExternalIds) + + const segmentModel = ingestModel.replaceSegment( + literal>({ + ...blueprintSegment.segment, + externalId: ingestSegment.externalId, + externalModified: ingestSegment.modified, + _rank: ingestSegment.rank, + notes: segmentNotes, + }) + ) + + blueprintSegment.parts.forEach((blueprintPart, i) => { + updateModelWithGeneratedPart(context, blueprintId, segmentModel, blueprintNotes, blueprintPart, i) + }) + + return segmentModel +} + +function extractAndWrapSegmentNotes( + blueprintId: BlueprintId, + blueprintNotes: RawPartNote[], + knownPartExternalIds: Set +): SegmentNote[] { + const segmentNotes: SegmentNote[] = [] + + for (const note of blueprintNotes) { + if (!note.partExternalId || !knownPartExternalIds.has(note.partExternalId)) { + segmentNotes.push( + literal({ + type: note.type, + message: wrapTranslatableMessageFromBlueprints(note.message, [blueprintId]), + origin: { + name: '', // TODO + }, + }) + ) + } + } + + return segmentNotes +} + +function extractAndWrapPartNotes( + blueprintId: BlueprintId, + blueprintNotes: RawPartNote[], + partExternalId: string +): PartNote[] { + const partNotes: PartNote[] = [] + + for (const note of blueprintNotes) { + if (note.partExternalId === partExternalId) { + partNotes.push( + literal({ + type: note.type, + message: wrapTranslatableMessageFromBlueprints(note.message, [blueprintId]), + origin: { + name: '', // TODO + }, + }) + ) + } + } + + return partNotes +} + +function updateModelWithGeneratedPart( + context: JobContext, + blueprintId: BlueprintId, + segmentModel: IngestSegmentModel, + blueprintNotes: RawPartNote[], + blueprintPart: BlueprintResultPart, + i: number +): void { + const partId = getPartId(segmentModel.segment.rundownId, blueprintPart.part.externalId) + + const partNotes = extractAndWrapPartNotes(blueprintId, blueprintNotes, blueprintPart.part.externalId) + + const part = literal({ + ...blueprintPart.part, + _id: partId, + rundownId: segmentModel.segment.rundownId, + segmentId: segmentModel.segment._id, + _rank: i, // This gets updated to a rank unique within its segment in a later step + notes: partNotes, + invalidReason: blueprintPart.part.invalidReason + ? { + ...blueprintPart.part.invalidReason, + message: wrapTranslatableMessageFromBlueprints(blueprintPart.part.invalidReason.message, [ + blueprintId, + ]), + } + : undefined, + + expectedDurationWithPreroll: undefined, // Below + }) + + // Update pieces + const processedPieces = postProcessPieces( + context, + blueprintPart.pieces, + blueprintId, + segmentModel.segment.rundownId, + segmentModel.segment._id, + part._id, + false, + part.invalid + ) + const adlibPieces = postProcessAdLibPieces( + context, + blueprintId, + segmentModel.segment.rundownId, + part._id, + blueprintPart.adLibPieces + ) + + const adlibActions = postProcessAdLibActions( + blueprintId, + segmentModel.segment.rundownId, + part._id, + blueprintPart.actions || [] + ) + + const partModel = segmentModel.replacePart(part, processedPieces, adlibPieces, adlibActions) + updateExpectedPackagesForPartModel(context, partModel) } /** @@ -247,7 +371,7 @@ export async function calculateSegmentsFromIngestData( function preserveOrphanedSegmentPositionInRundown( context: JobContext, ingestModel: IngestModel, - newSegment: DBSegment + newSegment: ReadonlyDeep ) { if (context.studio.settings.preserveOrphanedSegmentPositionInRundown) { // When we have orphaned segments, try to keep the order correct when adding and removing other segments diff --git a/packages/job-worker/src/ingest/model/IngestModel.ts b/packages/job-worker/src/ingest/model/IngestModel.ts index 7255867f8b..82937d71b5 100644 --- a/packages/job-worker/src/ingest/model/IngestModel.ts +++ b/packages/job-worker/src/ingest/model/IngestModel.ts @@ -155,6 +155,12 @@ export interface IngestModel extends IngestModelReadonly, BaseModel { */ getSegmentByExternalId(externalId: string): IngestSegmentModel | undefined + /** + * Get the internal `_id` of a segment from the `externalId` + * @param externalId External id of the Segment + */ + getSegmentIdFromExternalId(externalId: string): SegmentId + /** * Get a Segment from the Rundown * @param id Id of the Segment @@ -185,7 +191,7 @@ export interface IngestModel extends IngestModelReadonly, BaseModel { * Replace or insert a Segment in this Rundown * @param segment New Segment data */ - replaceSegment(segment: DBSegment): IngestSegmentModel + replaceSegment(segment: Omit): IngestSegmentModel /** * Change the id of a Segment in this Rundown. diff --git a/packages/job-worker/src/ingest/model/implementation/IngestModelImpl.ts b/packages/job-worker/src/ingest/model/implementation/IngestModelImpl.ts index a993617259..0f5b9606de 100644 --- a/packages/job-worker/src/ingest/model/implementation/IngestModelImpl.ts +++ b/packages/job-worker/src/ingest/model/implementation/IngestModelImpl.ts @@ -269,6 +269,14 @@ export class IngestModelImpl implements IngestModel, DatabasePersistedModel { return this.getSegment(segmentId) } + /** + * Get the internal `_id` of a segment from the `externalId` + * @param externalId External id of the Segment + */ + getSegmentIdFromExternalId(externalId: string): SegmentId { + return getSegmentId(this.rundownId, externalId) + } + /** * Get a Segment from the Rundown * @param id Id of the Segment @@ -355,7 +363,12 @@ export class IngestModelImpl implements IngestModel, DatabasePersistedModel { } } - replaceSegment(segment: DBSegment): IngestSegmentModel { + replaceSegment(rawSegment: Omit): IngestSegmentModel { + const segment: DBSegment = { + ...rawSegment, + _id: this.getSegmentIdFromExternalId(rawSegment.externalId), + rundownId: this.rundownId, + } const oldSegment = this.segmentsImpl.get(segment._id) const newSegment = new IngestSegmentModelImpl(true, segment, [], oldSegment?.segmentModel) From 64fb1da27e041e91a9faf70a30d602f017131764 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 9 Feb 2024 13:18:49 +0000 Subject: [PATCH 242/479] feat: check if segment should be immediately regenerated during ingest SOFIE-2887 This is to catch cases where the blueprints define a dependency on a 'new' expectedPackage A, which already exists in the database so can be immediately fulfilled. --- .../src/blueprints/context/watchedPackages.ts | 42 ++++++++----- .../src/ingest/generationSegment.ts | 59 +++++++++++++++++-- .../implementation/IngestPartModelImpl.ts | 13 +++- .../src/ingest/model/implementation/utils.ts | 6 ++ 4 files changed, 101 insertions(+), 19 deletions(-) diff --git a/packages/job-worker/src/blueprints/context/watchedPackages.ts b/packages/job-worker/src/blueprints/context/watchedPackages.ts index d85821569e..afbc1a3c99 100644 --- a/packages/job-worker/src/blueprints/context/watchedPackages.ts +++ b/packages/job-worker/src/blueprints/context/watchedPackages.ts @@ -5,7 +5,7 @@ import { } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' import { PackageInfoDB } from '@sofie-automation/corelib/dist/dataModel/PackageInfos' import { JobContext } from '../../jobs' -import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { ExpectedPackageId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { Filter as FilterQuery } from 'mongodb' import { PackageInfo } from '@sofie-automation/blueprints-integration' import { unprotectObjectArray } from '@sofie-automation/corelib/dist/protectedString' @@ -16,10 +16,16 @@ import { ReadonlyDeep } from 'type-fest' * This is a helper class to simplify exposing packageInfo to various places in the blueprints */ export class WatchedPackagesHelper { + private readonly packages = new Map>() + private constructor( - private readonly packages: ReadonlyDeep, + packages: ReadonlyDeep, private readonly packageInfos: ReadonlyDeep - ) {} + ) { + for (const pkg of packages) { + this.packages.set(pkg._id, pkg) + } + } /** * Create a helper with no packages. This should be used where the api is in place, but the update flow hasnt been implemented yet so we don't want to expose any data @@ -80,16 +86,16 @@ export class WatchedPackagesHelper { * Create a helper, and populate it with data from an IngestModel * @param studioId The studio this is for * @param ingestModel Model to fetch data for - * @param segmentExternelIds ExternalId of Segments to be loaded + * @param segmentExternalIds ExternalId of Segments to be loaded */ - static async createForIngestSegment( + static async createForIngestSegments( context: JobContext, ingestModel: IngestModelReadonly, - segmentExternelIds: string[] + segmentExternalIds: string[] ): Promise { const packages: ReadonlyDeep[] = [] - for (const externalId of segmentExternelIds) { + for (const externalId of segmentExternalIds) { const segment = ingestModel.getSegmentByExternalId(externalId) if (!segment) continue // First ingest of the Segment @@ -123,7 +129,10 @@ export class WatchedPackagesHelper { * @param func A filter to check if each package should be included */ filter(_context: JobContext, func: (pkg: ReadonlyDeep) => boolean): WatchedPackagesHelper { - const watchedPackages = this.packages.filter(func) + const watchedPackages: ReadonlyDeep[] = [] + for (const pkg of this.packages.values()) { + if (func(pkg)) watchedPackages.push(pkg) + } const newPackageIds = new Set(watchedPackages.map((p) => p._id)) const watchedPackageInfos = this.packageInfos.filter((info) => newPackageIds.has(info.packageId)) @@ -131,13 +140,18 @@ export class WatchedPackagesHelper { return new WatchedPackagesHelper(watchedPackages, watchedPackageInfos) } + getPackage(packageId: ExpectedPackageId): ReadonlyDeep | undefined { + return this.packages.get(packageId) + } + getPackageInfo(packageId: string): Readonly> { - const pkg = this.packages.find((pkg) => pkg.blueprintPackageId === packageId) - if (pkg) { - const info = this.packageInfos.filter((p) => p.packageId === pkg._id) - return unprotectObjectArray(info) - } else { - return [] + for (const pkg of this.packages.values()) { + if (pkg.blueprintPackageId === packageId) { + const info = this.packageInfos.filter((p) => p.packageId === pkg._id) + return unprotectObjectArray(info) + } } + + return [] } } diff --git a/packages/job-worker/src/ingest/generationSegment.ts b/packages/job-worker/src/ingest/generationSegment.ts index 2ca0175c05..21f14875bb 100644 --- a/packages/job-worker/src/ingest/generationSegment.ts +++ b/packages/job-worker/src/ingest/generationSegment.ts @@ -1,4 +1,4 @@ -import { BlueprintId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { BlueprintId, ExpectedPackageId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { SegmentNote, PartNote } from '@sofie-automation/corelib/dist/dataModel/Notes' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { DBSegment, SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' @@ -76,7 +76,7 @@ export async function calculateSegmentsFromIngestData( changedSegmentIds = await Promise.all( ingestSegments.map(async (ingestSegment) => - updateSegmentFromIngestData2( + regenerateSegmentAndUpdateModelFull( context, showStyle, blueprint, @@ -92,7 +92,7 @@ export async function calculateSegmentsFromIngestData( return changedSegmentIds } -async function updateSegmentFromIngestData2( +async function regenerateSegmentAndUpdateModelFull( context: JobContext, showStyle: ReadonlyDeep, blueprint: ReadonlyDeep, @@ -110,7 +110,7 @@ async function updateSegmentFromIngestData2( (p) => 'segmentId' in p && p.segmentId === segmentId ) - const updatedSegmentModel = await regenerateSegmentAndUpdateModel( + let updatedSegmentModel = await regenerateSegmentAndUpdateModel( context, showStyle, blueprint, @@ -119,6 +119,29 @@ async function updateSegmentFromIngestData2( segmentWatchedPackages ) + // Future: this would be better to go before `updateModelWithGeneratedSegment`, but we need the final ids of the ExpectedPackages + const shouldRegenerateSegment = await checkIfSegmentReferencesUnloadedPackageInfos( + context, + updatedSegmentModel, + segmentWatchedPackages + ) + if (shouldRegenerateSegment) { + logger.info(`Regenerating segment ${ingestSegment.name} due to existing PackageInfos being found`) + const reloadedWatchedPackages = await WatchedPackagesHelper.createForIngestSegments(context, ingestModel, [ + ingestSegment.externalId, + ]) + + // Regenerate the segment once, with the updated packages + updatedSegmentModel = await regenerateSegmentAndUpdateModel( + context, + showStyle, + blueprint, + ingestModel, + ingestSegment, + reloadedWatchedPackages + ) + } + preserveOrphanedSegmentPositionInRundown(context, ingestModel, updatedSegmentModel.segment) return updatedSegmentModel.segment._id @@ -159,6 +182,34 @@ async function regenerateSegmentAndUpdateModel( ) } +async function checkIfSegmentReferencesUnloadedPackageInfos( + context: JobContext, + segmentModel: IngestSegmentModel, + segmentWatchedPackages: WatchedPackagesHelper +) { + const expectedPackageIdsToCheck = new Set() + // check if there are any updates right away? + for (const part of segmentModel.parts) { + for (const expectedPackage of part.expectedPackages) { + if (expectedPackage.listenToPackageInfoUpdates) { + const loadedPackage = segmentWatchedPackages.getPackage(expectedPackage._id) + if (!loadedPackage) { + // The package didn't exist prior to the blueprint running + expectedPackageIdsToCheck.add(expectedPackage._id) + } + } + } + } + + if (expectedPackageIdsToCheck.size > 0) { + const areThereAnyData = await context.directCollections.PackageInfos.count({ + packageId: { $in: Array.from(expectedPackageIdsToCheck) }, + }) + return areThereAnyData > 0 + } + return false +} + async function generateSegmentWithBlueprints( context: JobContext, showStyle: ReadonlyDeep, diff --git a/packages/job-worker/src/ingest/model/implementation/IngestPartModelImpl.ts b/packages/job-worker/src/ingest/model/implementation/IngestPartModelImpl.ts index 52b89f459f..a830b34cb4 100644 --- a/packages/job-worker/src/ingest/model/implementation/IngestPartModelImpl.ts +++ b/packages/job-worker/src/ingest/model/implementation/IngestPartModelImpl.ts @@ -10,7 +10,13 @@ import { ExpectedPlayoutItemRundown } from '@sofie-automation/corelib/dist/dataM import { ExpectedPackageFromRundown } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { ExpectedPackagesStore } from './ExpectedPackagesStore' -import { diffAndReturnLatestObjects, DocumentChanges, getDocumentChanges, setValuesAndTrackChanges } from './utils' +import { + addManyToSet, + diffAndReturnLatestObjects, + DocumentChanges, + getDocumentChanges, + setValuesAndTrackChanges, +} from './utils' export class IngestPartModelImpl implements IngestPartModel { readonly partImpl: DBPart @@ -186,6 +192,11 @@ export class IngestPartModelImpl implements IngestPartModel { this.#partHasChanges = true } + // Anything already marked as changed should still be marked as changed + addManyToSet(this.#piecesWithChanges, previousModel.#piecesWithChanges) + addManyToSet(this.#adLibPiecesWithChanges, previousModel.#adLibPiecesWithChanges) + addManyToSet(this.#adLibActionsWithChanges, previousModel.#adLibActionsWithChanges) + // Diff the objects, but don't update the stored copies diffAndReturnLatestObjects(this.#piecesWithChanges, previousModel.#pieces, this.#pieces) diffAndReturnLatestObjects(this.#adLibPiecesWithChanges, previousModel.#adLibPieces, this.#adLibPieces) diff --git a/packages/job-worker/src/ingest/model/implementation/utils.ts b/packages/job-worker/src/ingest/model/implementation/utils.ts index 8bfbd30e6f..ce204b3dff 100644 --- a/packages/job-worker/src/ingest/model/implementation/utils.ts +++ b/packages/job-worker/src/ingest/model/implementation/utils.ts @@ -67,6 +67,12 @@ export function setValuesAndTrackChanges } } } +export function addManyToSet(set: Set, iter: Iterable): void { + for (const val of iter) { + set.add(val) + } +} + export interface DocumentChanges }> { currentIds: T['_id'][] deletedIds: T['_id'][] From 4e3a359c4e0216b4b360d2a9d3e9ad439a8bc1da Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 9 Feb 2024 14:58:52 +0000 Subject: [PATCH 243/479] fix: refactor part generation, to remove some 'owned' properties from the `replacePart` method signature --- packages/corelib/src/playout/timings.ts | 4 ++- .../src/ingest/generationSegment.ts | 26 +++++++------------ .../src/ingest/model/IngestModel.ts | 16 +++++++----- .../src/ingest/model/IngestSegmentModel.ts | 15 ++++++++++- .../model/implementation/IngestModelImpl.ts | 9 +++++-- .../implementation/IngestSegmentModelImpl.ts | 22 +++++++++++++--- 6 files changed, 62 insertions(+), 30 deletions(-) diff --git a/packages/corelib/src/playout/timings.ts b/packages/corelib/src/playout/timings.ts index 08c3b6865e..616203e421 100644 --- a/packages/corelib/src/playout/timings.ts +++ b/packages/corelib/src/playout/timings.ts @@ -165,8 +165,10 @@ function calculateExpectedDurationWithPreroll(rawDuration: number, timings: Part return Math.max(0, rawDuration + timings.toPartDelay - (timings.fromPartRemaining - timings.toPartDelay)) } +export type CalculateExpectedDurationPart = Pick + export function calculatePartExpectedDurationWithPreroll( - part: DBPart, + part: CalculateExpectedDurationPart, pieces: ReadonlyDeep ): number | undefined { if (part.expectedDuration === undefined) return undefined diff --git a/packages/job-worker/src/ingest/generationSegment.ts b/packages/job-worker/src/ingest/generationSegment.ts index 21f14875bb..0d9694b370 100644 --- a/packages/job-worker/src/ingest/generationSegment.ts +++ b/packages/job-worker/src/ingest/generationSegment.ts @@ -1,6 +1,5 @@ import { BlueprintId, ExpectedPackageId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { SegmentNote, PartNote } from '@sofie-automation/corelib/dist/dataModel/Notes' -import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { DBSegment, SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' import { literal } from '@sofie-automation/corelib/dist/lib' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' @@ -8,9 +7,9 @@ import { RawPartNote, SegmentUserContext } from '../blueprints/context' import { WatchedPackagesHelper } from '../blueprints/context/watchedPackages' import { postProcessAdLibActions, postProcessAdLibPieces, postProcessPieces } from '../blueprints/postProcess' import { logger } from '../logging' -import { IngestModel, IngestModelReadonly } from './model/IngestModel' +import { IngestModel, IngestModelReadonly, IngestReplaceSegmentType } from './model/IngestModel' import { LocalIngestSegment, LocalIngestRundown } from './ingestCache' -import { getSegmentId, getPartId, canSegmentBeUpdated } from './lib' +import { getSegmentId, canSegmentBeUpdated } from './lib' import { JobContext, ProcessedShowStyleCompound } from '../jobs' import { CommitIngestData } from './lock' import { @@ -21,7 +20,7 @@ import { } from '@sofie-automation/blueprints-integration' import { wrapTranslatableMessageFromBlueprints } from '@sofie-automation/corelib/dist/TranslatableMessage' import { updateExpectedPackagesForPartModel } from './expectedPackages' -import { IngestSegmentModel } from './model/IngestSegmentModel' +import { IngestReplacePartType, IngestSegmentModel } from './model/IngestSegmentModel' import { ReadonlyDeep } from 'type-fest' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { WrappedShowStyleBlueprint } from '../blueprints/cache' @@ -249,7 +248,7 @@ async function generateSegmentWithBlueprints( function createInternalErrorSegment( blueprintId: BlueprintId, ingestSegment: LocalIngestSegment -): Omit { +): IngestReplaceSegmentType { return { externalId: ingestSegment.externalId, externalModified: ingestSegment.modified, @@ -286,7 +285,7 @@ function updateModelWithGeneratedSegment( const segmentNotes = extractAndWrapSegmentNotes(blueprintId, blueprintNotes, knownPartExternalIds) const segmentModel = ingestModel.replaceSegment( - literal>({ + literal({ ...blueprintSegment.segment, externalId: ingestSegment.externalId, externalModified: ingestSegment.modified, @@ -358,15 +357,12 @@ function updateModelWithGeneratedPart( blueprintPart: BlueprintResultPart, i: number ): void { - const partId = getPartId(segmentModel.segment.rundownId, blueprintPart.part.externalId) + const partId = segmentModel.getPartIdFromExternalId(blueprintPart.part.externalId) const partNotes = extractAndWrapPartNotes(blueprintId, blueprintNotes, blueprintPart.part.externalId) - const part = literal({ + const part = literal({ ...blueprintPart.part, - _id: partId, - rundownId: segmentModel.segment.rundownId, - segmentId: segmentModel.segment._id, _rank: i, // This gets updated to a rank unique within its segment in a later step notes: partNotes, invalidReason: blueprintPart.part.invalidReason @@ -377,8 +373,6 @@ function updateModelWithGeneratedPart( ]), } : undefined, - - expectedDurationWithPreroll: undefined, // Below }) // Update pieces @@ -388,7 +382,7 @@ function updateModelWithGeneratedPart( blueprintId, segmentModel.segment.rundownId, segmentModel.segment._id, - part._id, + partId, false, part.invalid ) @@ -396,14 +390,14 @@ function updateModelWithGeneratedPart( context, blueprintId, segmentModel.segment.rundownId, - part._id, + partId, blueprintPart.adLibPieces ) const adlibActions = postProcessAdLibActions( blueprintId, segmentModel.segment.rundownId, - part._id, + partId, blueprintPart.actions || [] ) diff --git a/packages/job-worker/src/ingest/model/IngestModel.ts b/packages/job-worker/src/ingest/model/IngestModel.ts index 82937d71b5..5a6f16a1fc 100644 --- a/packages/job-worker/src/ingest/model/IngestModel.ts +++ b/packages/job-worker/src/ingest/model/IngestModel.ts @@ -98,6 +98,12 @@ export interface IngestModelReadonly { */ getSegmentByExternalId(externalId: string): IngestSegmentModelReadonly | undefined + /** + * Get the internal `_id` of a segment from the `externalId` + * @param externalId External id of the Segment + */ + getSegmentIdFromExternalId(externalId: string): SegmentId + /** * Get a Segment from the Rundown * @param id Id of the Segment @@ -155,12 +161,6 @@ export interface IngestModel extends IngestModelReadonly, BaseModel { */ getSegmentByExternalId(externalId: string): IngestSegmentModel | undefined - /** - * Get the internal `_id` of a segment from the `externalId` - * @param externalId External id of the Segment - */ - getSegmentIdFromExternalId(externalId: string): SegmentId - /** * Get a Segment from the Rundown * @param id Id of the Segment @@ -191,7 +191,7 @@ export interface IngestModel extends IngestModelReadonly, BaseModel { * Replace or insert a Segment in this Rundown * @param segment New Segment data */ - replaceSegment(segment: Omit): IngestSegmentModel + replaceSegment(segment: IngestReplaceSegmentType): IngestSegmentModel /** * Change the id of a Segment in this Rundown. @@ -277,3 +277,5 @@ export interface IngestModel extends IngestModelReadonly, BaseModel { */ appendRundownNotes(...notes: RundownNote[]): void } + +export type IngestReplaceSegmentType = Omit diff --git a/packages/job-worker/src/ingest/model/IngestSegmentModel.ts b/packages/job-worker/src/ingest/model/IngestSegmentModel.ts index a3ac1de178..74546c424f 100644 --- a/packages/job-worker/src/ingest/model/IngestSegmentModel.ts +++ b/packages/job-worker/src/ingest/model/IngestSegmentModel.ts @@ -19,6 +19,12 @@ export interface IngestSegmentModelReadonly { */ readonly parts: IngestPartModelReadonly[] + /** + * Get the internal `_id` of a Part from the `externalId` + * @param externalId External id of the Part + */ + getPartIdFromExternalId(externalId: string): PartId + /** * Get a Part which belongs to this Segment * @param id Id of the Part @@ -78,5 +84,12 @@ export interface IngestSegmentModel extends IngestSegmentModelReadonly { * @param adLibPiece AdLib Pieces to add to the Part * @param adLibActions AdLib Actions to add to the Part */ - replacePart(part: DBPart, pieces: Piece[], adLibPiece: AdLibPiece[], adLibActions: AdLibAction[]): IngestPartModel + replacePart( + part: IngestReplacePartType, + pieces: Piece[], + adLibPiece: AdLibPiece[], + adLibActions: AdLibAction[] + ): IngestPartModel } + +export type IngestReplacePartType = Omit diff --git a/packages/job-worker/src/ingest/model/implementation/IngestModelImpl.ts b/packages/job-worker/src/ingest/model/implementation/IngestModelImpl.ts index 0f5b9606de..d58e4d78fe 100644 --- a/packages/job-worker/src/ingest/model/implementation/IngestModelImpl.ts +++ b/packages/job-worker/src/ingest/model/implementation/IngestModelImpl.ts @@ -43,7 +43,12 @@ import { IngestPartModelImpl } from './IngestPartModelImpl' import { DatabasePersistedModel } from '../../../modelBase' import { ExpectedPackagesStore } from './ExpectedPackagesStore' import { ReadonlyDeep } from 'type-fest' -import { ExpectedPackageForIngestModel, ExpectedPackageForIngestModelBaseline, IngestModel } from '../IngestModel' +import { + ExpectedPackageForIngestModel, + ExpectedPackageForIngestModelBaseline, + IngestModel, + IngestReplaceSegmentType, +} from '../IngestModel' import { RundownNote } from '@sofie-automation/corelib/dist/dataModel/Notes' import { diffAndReturnLatestObjects } from './utils' import _ = require('underscore') @@ -363,7 +368,7 @@ export class IngestModelImpl implements IngestModel, DatabasePersistedModel { } } - replaceSegment(rawSegment: Omit): IngestSegmentModel { + replaceSegment(rawSegment: IngestReplaceSegmentType): IngestSegmentModel { const segment: DBSegment = { ...rawSegment, _id: this.getSegmentIdFromExternalId(rawSegment.externalId), diff --git a/packages/job-worker/src/ingest/model/implementation/IngestSegmentModelImpl.ts b/packages/job-worker/src/ingest/model/implementation/IngestSegmentModelImpl.ts index a81d0e92ad..a80978445e 100644 --- a/packages/job-worker/src/ingest/model/implementation/IngestSegmentModelImpl.ts +++ b/packages/job-worker/src/ingest/model/implementation/IngestSegmentModelImpl.ts @@ -1,7 +1,7 @@ import { PartId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ReadonlyDeep } from 'type-fest' import { DBSegment, SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' -import { IngestSegmentModel } from '../IngestSegmentModel' +import { IngestReplacePartType, IngestSegmentModel } from '../IngestSegmentModel' import _ = require('underscore') import { IngestPartModelImpl } from './IngestPartModelImpl' import { IngestPartModel } from '../IngestPartModel' @@ -11,6 +11,7 @@ import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { calculatePartExpectedDurationWithPreroll } from '@sofie-automation/corelib/dist/playout/timings' import { clone } from '@sofie-automation/corelib/dist/lib' +import { getPartId } from '../../lib' /** * A light wrapper around the IngestPartModel, so that we can track the deletions while still accessing the contents @@ -160,6 +161,10 @@ export class IngestSegmentModelImpl implements IngestSegmentModel { } } + getPartIdFromExternalId(externalId: string): PartId { + return getPartId(this.segment.rundownId, externalId) + } + getPart(id: PartId): IngestPartModel | undefined { const partEntry = this.partsImpl.get(id) if (partEntry && !partEntry.deleted) return partEntry.partModel @@ -199,8 +204,19 @@ export class IngestSegmentModelImpl implements IngestSegmentModel { return ids } - replacePart(part: DBPart, pieces: Piece[], adLibPiece: AdLibPiece[], adLibActions: AdLibAction[]): IngestPartModel { - part.expectedDurationWithPreroll = calculatePartExpectedDurationWithPreroll(part, pieces) + replacePart( + rawPart: IngestReplacePartType, + pieces: Piece[], + adLibPiece: AdLibPiece[], + adLibActions: AdLibAction[] + ): IngestPartModel { + const part: DBPart = { + ...rawPart, + _id: this.getPartIdFromExternalId(rawPart.externalId), + rundownId: this.segment.rundownId, + segmentId: this.segment._id, + expectedDurationWithPreroll: calculatePartExpectedDurationWithPreroll(rawPart, pieces), + } // We don't need to worry about this being present on other Segments. The caller must make sure it gets removed if needed, // and when persisting a duplicates check is performed. If there is a copy on another Segment, every document will have changes From 12779cb911c7db04998ff5d085ee787a69fb31fd Mon Sep 17 00:00:00 2001 From: ianshade Date: Tue, 13 Feb 2024 11:35:21 +0100 Subject: [PATCH 244/479] chore(live-status-gw): add `publicData` to asyncapi definitions --- .../api/schemas/activePlaylist.yaml | 14 ++++++++++++++ .../live-status-gateway/api/schemas/adLibs.yaml | 4 ++++ .../live-status-gateway/api/schemas/segments.yaml | 4 ++++ 3 files changed, 22 insertions(+) diff --git a/packages/live-status-gateway/api/schemas/activePlaylist.yaml b/packages/live-status-gateway/api/schemas/activePlaylist.yaml index 059fe43add..dfc9327e8a 100644 --- a/packages/live-status-gateway/api/schemas/activePlaylist.yaml +++ b/packages/live-status-gateway/api/schemas/activePlaylist.yaml @@ -27,6 +27,8 @@ $defs: nextPart: description: The next Part - if empty, no part will follow live part $ref: '#/$defs/part' + publicData: + description: Optional arbitrary data required: [event, id, name, rundownIds, currentPart, currentSegment, nextPart] additionalProperties: false examples: @@ -40,6 +42,8 @@ $defs: $ref: '#/$defs/currentSegment/examples/0' nextPart: $ref: '#/$defs/part/examples/0' + publicData: + category: 'Evening News' partBase: type: object properties: @@ -61,6 +65,8 @@ $defs: type: array items: $ref: '#/$defs/piece' + publicData: + description: Optional arbitrary data required: [id, name, segmentId, pieces] additionalProperties: false examples: @@ -70,6 +76,8 @@ $defs: autoNext: false pieces: - $ref: '#/$defs/piece/examples/0' + publicData: + partType: 'intro' part: oneOf: - $ref: '#/$defs/partBase' @@ -114,6 +122,8 @@ $defs: projectedEndTime: 1600000075000 pieces: - $ref: '#/$defs/piece/examples/0' + publicData: + partType: 'intro' currentSegment: type: object properties: @@ -162,6 +172,8 @@ $defs: type: array items: type: string + publicData: + description: Optional arbitrary data required: [id, name, sourceLayer, outputLayer] additionalProperties: false examples: @@ -170,3 +182,5 @@ $defs: sourceLayer: 'Camera' outputLayer: 'PGM' tags: ['camera'] + publicData: + switcherSource: 1 diff --git a/packages/live-status-gateway/api/schemas/adLibs.yaml b/packages/live-status-gateway/api/schemas/adLibs.yaml index a53c370e79..2d66ec8c0b 100644 --- a/packages/live-status-gateway/api/schemas/adLibs.yaml +++ b/packages/live-status-gateway/api/schemas/adLibs.yaml @@ -65,6 +65,8 @@ $defs: type: array items: type: string + publicData: + description: Optional arbitrary data required: [id, name, sourceLayer, actionType] additionalProperties: false examples: @@ -75,3 +77,5 @@ $defs: - name: pvw label: Preview tags: ['music_video'] + publicData: + fileName: MV000123.mxf diff --git a/packages/live-status-gateway/api/schemas/segments.yaml b/packages/live-status-gateway/api/schemas/segments.yaml index 0949e0b3c1..29180418ea 100644 --- a/packages/live-status-gateway/api/schemas/segments.yaml +++ b/packages/live-status-gateway/api/schemas/segments.yaml @@ -49,6 +49,8 @@ $defs: description: Budget duration of the segment (milliseconds) type: number required: [expectedDurationMs] + publicData: + description: Optional arbitrary data required: [id, rundownId, name, timing] additionalProperties: false examples: @@ -58,3 +60,5 @@ $defs: timing: expectedDurationMs: 15000 budgetDurationMs: 20000 + publicData: + containsLiveSource: true From 43dfa10874c42a1d0cb2925e06cae52f0d0eec04 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Wed, 14 Feb 2024 14:35:20 +0100 Subject: [PATCH 245/479] fix: rename 'onPresenterScreen' description in GUI --- meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx | 3 ++- meteor/i18n/nb.po | 2 -- meteor/i18n/nn.po | 3 --- meteor/i18n/template.pot | 5 +---- meteor/public/locales/nb/translations.json | 3 +-- meteor/public/locales/nn/translations.json | 3 +-- 6 files changed, 5 insertions(+), 14 deletions(-) diff --git a/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx b/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx index 3202f1db91..d1c9e79b6e 100644 --- a/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx +++ b/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx @@ -394,11 +394,12 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: )} {(value, handleUpdate) => } diff --git a/meteor/i18n/nb.po b/meteor/i18n/nb.po index 7bad0941af..3f32216e70 100644 --- a/meteor/i18n/nb.po +++ b/meteor/i18n/nb.po @@ -2118,8 +2118,6 @@ msgstr "Er en gjesteinngang" msgid "Is hidden" msgstr "Er skjult" -msgid "Display on Presenter's Screen" -msgstr "Vis på presentatørskjerm" msgid "Pieces on this layer can be cleared" msgstr "Elementer på dette laget kan tømmes" diff --git a/meteor/i18n/nn.po b/meteor/i18n/nn.po index 640afec757..0ea3e9ca7f 100644 --- a/meteor/i18n/nn.po +++ b/meteor/i18n/nn.po @@ -2122,9 +2122,6 @@ msgstr "Er ein gjesteinngang" msgid "Is hidden" msgstr "Er skjult" -msgid "Display on Presenter's Screen" -msgstr "Vis på presentatørskjerm" - msgid "Pieces on this layer can be cleared" msgstr "Element på dette laget kan tømmas" diff --git a/meteor/i18n/template.pot b/meteor/i18n/template.pot index 086c3aaf9d..bebf4b2bc7 100644 --- a/meteor/i18n/template.pot +++ b/meteor/i18n/template.pot @@ -2113,9 +2113,6 @@ msgstr "" msgid "Is hidden" msgstr "" -msgid "Display on Presenter's Screen" -msgstr "" - msgid "Display in a column in List View" msgstr "" @@ -3328,4 +3325,4 @@ msgctxt "°°°°°°plural" msgid "" "There are {{count}} documents that can be removed, do you want to " "continue?°°°°°°" -msgstr "" \ No newline at end of file +msgstr "" diff --git a/meteor/public/locales/nb/translations.json b/meteor/public/locales/nb/translations.json index 3f648c0ba8..3c2f69b37b 100644 --- a/meteor/public/locales/nb/translations.json +++ b/meteor/public/locales/nb/translations.json @@ -601,7 +601,6 @@ "Is a Live Remote Input": "Er en RM", "Is a Guest Input": "Er en gjesteinngang", "Is hidden": "Er skjult", - "Display on Presenter's Screen": "Vis på presentatørskjerm", "Pieces on this layer can be cleared": "Elementer på dette laget kan tømmes", "Pieces on this layer are sticky": "Elementer i dette laget er sticky", "Only Pieces present in rundown are sticky": "Kun elementer tilstede i kjøreplanen er sticky", @@ -972,4 +971,4 @@ "This clip ends with {{type}} frames after {{count}} second°°°°°°_°°°°°°plural": "Klippet slutter med {{frames}} {{type}} frame°°°°°°", "{{frames}} {{type}} frame detected within the clip°°°°°°_°°°°°°plural": "{{frames}} {{type}} frames oppdaget inne i klippet°°°°°°", "{{frames}} {{type}} frame detected in clip°°°°°°_°°°°°°plural": "{{frames}} {{type}} frames oppdaget i klippet°°°°°°" -} \ No newline at end of file +} diff --git a/meteor/public/locales/nn/translations.json b/meteor/public/locales/nn/translations.json index bc4029251a..e99a0d5e94 100644 --- a/meteor/public/locales/nn/translations.json +++ b/meteor/public/locales/nn/translations.json @@ -601,7 +601,6 @@ "Is a Live Remote Input": "Er ein RM", "Is a Guest Input": "Er ein gjesteinngang", "Is hidden": "Er skjult", - "Display on Presenter's Screen": "Vis på presentatørskjerm", "Pieces on this layer can be cleared": "Element på dette laget kan tømmas", "Pieces on this layer are sticky": "Element på dette laget er sticky", "Only Pieces present in rundown are sticky": "Kun element til stades i køyreplanen er sticky", @@ -971,4 +970,4 @@ "This clip ends with {{type}} frames after {{count}} second°°°°°°_°°°°°°plural": "Klipp sluttar {{frames}} {{type}} frame°°°°°°", "{{frames}} {{type}} frame detected within the clip°°°°°°_°°°°°°plural": "{{frames}} {{type}} frame oppdaga inne i klippet°°°°°°", "{{frames}} {{type}} frame detected in clip°°°°°°_°°°°°°plural": "{{frames}} {{type}} frame oppdaga i klippet°°°°°°" -} \ No newline at end of file +} From 4c5f4528e37963a7b306837348727da293a2015e Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Thu, 15 Feb 2024 13:05:48 +0100 Subject: [PATCH 246/479] chore: use the same version of mos-connection libraries across the packages --- meteor/package.json | 2 +- meteor/yarn.lock | 27 ++++++++++----------------- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/meteor/package.json b/meteor/package.json index d643019e75..b9df1cfcc2 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -48,7 +48,7 @@ "@jstarpl/react-contextmenu": "^2.15.0", "@koa/cors": "^5.0.0", "@koa/router": "^12.0.1", - "@mos-connection/helper": "3.0.4", + "@mos-connection/helper": "3.0.7", "@nrk/core-icons": "^9.6.0", "@popperjs/core": "^2.11.8", "@slack/webhook": "^6.1.0", diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 74c9931cb3..19ddd3901b 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1020,27 +1020,20 @@ __metadata: languageName: node linkType: hard -"@mos-connection/helper@npm:3.0.4": - version: 3.0.4 - resolution: "@mos-connection/helper@npm:3.0.4" +"@mos-connection/helper@npm:3.0.7": + version: 3.0.7 + resolution: "@mos-connection/helper@npm:3.0.7" dependencies: - "@mos-connection/model": 3.0.4 - iconv-lite: ^0.6.2 - tslib: ^2.2.0 + "@mos-connection/model": 3.0.7 + iconv-lite: ^0.6.3 + tslib: ^2.5.3 xml-js: ^1.6.11 xmlbuilder: ^15.1.1 - checksum: 43ade87fd9a7ca7e183377d36f4d3b0ae063180c8760657201e50169c0868647242c83adaf860c475b9959079512d42ceb43d8ed657356d02140d7aa93992d79 - languageName: node - linkType: hard - -"@mos-connection/model@npm:3.0.4": - version: 3.0.4 - resolution: "@mos-connection/model@npm:3.0.4" - checksum: 7de15fba9a818260fb57847dbc51695036720a7f499d2fe262c691630673e83c01bdc3ea62261529e661f957074a6d92e8d38305485293c5392e073dab33989b + checksum: 802c95955328d89e62aa0addbaf79ce339c93bce499d96b6d314991cd77acbcbd573e4d6eb48408de234708f908d608b20941f846fceaa22813ddb2e6bdf6b4e languageName: node linkType: hard -"@mos-connection/model@npm:^3.0.7": +"@mos-connection/model@npm:3.0.7, @mos-connection/model@npm:^3.0.7": version: 3.0.7 resolution: "@mos-connection/model@npm:3.0.7" checksum: 4f7390a40fc5152bfcdb4e99eee243e3a1df248bf1f77e0a63d7d29d8db88cb8c9df5f231ab5d18e7cb1147ba8d39af5c621877221f343db630906e000c5b089 @@ -2695,7 +2688,7 @@ __metadata: "@jstarpl/react-contextmenu": ^2.15.0 "@koa/cors": ^5.0.0 "@koa/router": ^12.0.1 - "@mos-connection/helper": 3.0.4 + "@mos-connection/helper": 3.0.7 "@nrk/core-icons": ^9.6.0 "@popperjs/core": ^2.11.8 "@shopify/jest-koa-mocks": ^5.1.1 @@ -11945,7 +11938,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.2.0, tslib@npm:^2.5.1, tslib@npm:^2.6.0, tslib@npm:^2.6.2": +"tslib@npm:^2.5.1, tslib@npm:^2.5.3, tslib@npm:^2.6.0, tslib@npm:^2.6.2": version: 2.6.2 resolution: "tslib@npm:2.6.2" checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad From 64985cf56a4bdc66758e6dad956650ef3caf051b Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Thu, 15 Feb 2024 13:15:58 +0100 Subject: [PATCH 247/479] chore: fix test snapshot --- meteor/client/lib/parsers/mos/__tests__/mosSample2.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/meteor/client/lib/parsers/mos/__tests__/mosSample2.json b/meteor/client/lib/parsers/mos/__tests__/mosSample2.json index 8ba96e6c63..476df39810 100644 --- a/meteor/client/lib/parsers/mos/__tests__/mosSample2.json +++ b/meteor/client/lib/parsers/mos/__tests__/mosSample2.json @@ -15,8 +15,7 @@ "MosScope": "OBJECT", "MosSchema": "undefined", "MosPayload": { - "objType": "VIDEO", - "text": "VIDEO" + "objType": "VIDEO" } } ], From b739afc00c24b9ee68ca359adeb42c3fb8e8231a Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 15 Feb 2024 14:29:20 +0000 Subject: [PATCH 248/479] chore: update fibers dev-dependency --- meteor/package.json | 2 +- meteor/yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/meteor/package.json b/meteor/package.json index b9df1cfcc2..104bedab27 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -154,7 +154,7 @@ "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.33.2", "fast-clone": "^1.5.13", - "fibers-npm": "npm:fibers@4.0.3", + "fibers-npm": "npm:fibers@5.0.3", "glob": "^8.1.0", "i18next-conv": "^10.2.0", "i18next-scanner": "^4.4.0", diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 19ddd3901b..2c6fc12536 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -2746,7 +2746,7 @@ __metadata: eslint-plugin-prettier: ^4.2.1 eslint-plugin-react: ^7.33.2 fast-clone: ^1.5.13 - fibers-npm: "npm:fibers@4.0.3" + fibers-npm: "npm:fibers@5.0.3" glob: ^8.1.0 i18next: ^21.10.0 i18next-browser-languagedetector: ^6.1.8 @@ -5358,12 +5358,12 @@ __metadata: languageName: node linkType: hard -"fibers-npm@npm:fibers@4.0.3": - version: 4.0.3 - resolution: "fibers@npm:4.0.3" +"fibers-npm@npm:fibers@5.0.3": + version: 5.0.3 + resolution: "fibers@npm:5.0.3" dependencies: detect-libc: ^1.0.3 - checksum: a51e9f792a17f32acdbcac02855e4d1bef42aa391cf9bb7044b21c6eb67deb8a84d1eb0e388977a6b79ca2c85fccdd108d5697ce573fe97eb86a3ec93bfb7d45 + checksum: d66c5e18a911aab3480b846e1c837e5c7cfacb27a2a5fe512919865eaecef33cdd4abc14d777191a6a93473dc52356d48549c91a2a7b8b3450544c44104b23f3 languageName: node linkType: hard From f28dfe7776c2612ed1661dad98d786a2fd722673 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 23 Feb 2024 12:11:21 +0000 Subject: [PATCH 249/479] chore: update yarn lockfile --- .../FloatingInspectors/NoraFloatingInspector.tsx | 2 +- meteor/yarn.lock | 16 +--------------- packages/package.json | 1 + packages/yarn.lock | 16 +--------------- 4 files changed, 4 insertions(+), 31 deletions(-) diff --git a/meteor/client/ui/FloatingInspectors/NoraFloatingInspector.tsx b/meteor/client/ui/FloatingInspectors/NoraFloatingInspector.tsx index ecd1a8ca9d..c5d234d244 100644 --- a/meteor/client/ui/FloatingInspectors/NoraFloatingInspector.tsx +++ b/meteor/client/ui/FloatingInspectors/NoraFloatingInspector.tsx @@ -182,7 +182,7 @@ export class NoraPreviewRenderer extends React.Component<{}, IStateHeader> { } private getElStyle(dimensions: { width: number; height: number } | undefined) { - const style = { ...this.state.style } + const style: Record = { ...this.state.style } style.visibility = this.state.show ? 'visible' : 'hidden' if (dimensions) { diff --git a/meteor/yarn.lock b/meteor/yarn.lock index ec85d195ba..1db78d6780 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -8777,21 +8777,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.6.7": - version: 2.7.0 - resolution: "node-fetch@npm:2.7.0" - dependencies: - whatwg-url: ^5.0.0 - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - checksum: d76d2f5edb451a3f05b15115ec89fc6be39de37c6089f1b6368df03b91e1633fd379a7e01b7ab05089a25034b2023d959b47e59759cb38d88341b2459e89d6e5 - languageName: node - linkType: hard - -"node-fetch@npm:^2.7.0": +"node-fetch@npm:^2.6.7, node-fetch@npm:^2.7.0": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: diff --git a/packages/package.json b/packages/package.json index b2d9bd19ea..c379c53211 100644 --- a/packages/package.json +++ b/packages/package.json @@ -44,6 +44,7 @@ "@types/got": "^9.6.12", "@types/jest": "^29.5.11", "@types/node": "^14.18.63", + "@types/node-fetch": "^2.6.11", "@types/object-path": "^0.11.4", "@types/underscore": "^1.11.15", "babel-jest": "^29.7.0", diff --git a/packages/yarn.lock b/packages/yarn.lock index 6a6f1044ca..135c7d0db3 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -16259,21 +16259,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.12, node-fetch@npm:^2.6.7": - version: 2.7.0 - resolution: "node-fetch@npm:2.7.0" - dependencies: - whatwg-url: ^5.0.0 - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - checksum: d76d2f5edb451a3f05b15115ec89fc6be39de37c6089f1b6368df03b91e1633fd379a7e01b7ab05089a25034b2023d959b47e59759cb38d88341b2459e89d6e5 - languageName: node - linkType: hard - -"node-fetch@npm:^2.7.0": +"node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.12, node-fetch@npm:^2.6.7, node-fetch@npm:^2.7.0": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: From 56059f401a2f3cdfaf62901cae8bd5734e408fb1 Mon Sep 17 00:00:00 2001 From: ianshade Date: Fri, 23 Feb 2024 14:43:44 +0100 Subject: [PATCH 250/479] fix(live-status-gw): wrong pieces in nextPart --- packages/live-status-gateway/src/topics/activePlaylistTopic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts index 359dee9f66..f07d3e337f 100644 --- a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts +++ b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts @@ -134,7 +134,7 @@ export class ActivePlaylistTopic autoNext: nextPart.autoNext, segmentId: unprotectString(nextPart.segmentId), pieces: - this._pieceInstancesInCurrentPartInstance?.map((piece) => + this._pieceInstancesInNextPartInstance?.map((piece) => toPieceStatus(piece, this._showStyleBaseExt) ) ?? [], publicData: nextPart.publicData, From 748b7a0aca8ef290d58ebb6f56bc5a323b9566a4 Mon Sep 17 00:00:00 2001 From: ianshade Date: Fri, 23 Feb 2024 14:40:25 +0100 Subject: [PATCH 251/479] fix(live-status-gw): not sending updates when relevant data has actually changed --- .../src/collections/partInstancesHandler.ts | 23 ++++++++---- .../src/collections/pieceInstancesHandler.ts | 37 ++++++++++++------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/packages/live-status-gateway/src/collections/partInstancesHandler.ts b/packages/live-status-gateway/src/collections/partInstancesHandler.ts index 661d7a9f74..d249f906d0 100644 --- a/packages/live-status-gateway/src/collections/partInstancesHandler.ts +++ b/packages/live-status-gateway/src/collections/partInstancesHandler.ts @@ -138,18 +138,25 @@ export class PartInstancesHandler void this.changed(id, 'removed').catch(this._logger.error) } - const hasAnythingChanged = this.updateCollectionData() - if (hasAnythingChanged) { - await this.notify(this._collectionData) - } + await this.updateAndNotify() } else if (this._subscriptionId) { - // nothing relevant has changed + await this.updateAndNotify() } else { - this.clearCollectionData() - await this.notify(this._collectionData) + await this.clearAndNotify() } } else { - this.clearCollectionData() + await this.clearAndNotify() + } + } + + private async clearAndNotify() { + this.clearCollectionData() + await this.notify(this._collectionData) + } + + private async updateAndNotify() { + const hasAnythingChanged = this.updateCollectionData() + if (hasAnythingChanged) { await this.notify(this._collectionData) } } diff --git a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts index 0737261104..75beae6c57 100644 --- a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts +++ b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts @@ -82,12 +82,13 @@ export class PieceInstancesHandler } if ( !areElementsShallowEqual(this._collectionData.currentPartInstance, inCurrentPartInstance) && - this._collectionData.currentPartInstance.some((pieceInstance, index) => { - return !arePropertiesShallowEqual(inCurrentPartInstance[index], pieceInstance, [ - 'reportedStartedPlayback', - 'reportedStoppedPlayback', - ]) - }) + (this._collectionData.currentPartInstance.length !== inCurrentPartInstance.length || + this._collectionData.currentPartInstance.some((pieceInstance, index) => { + return !arePropertiesShallowEqual(inCurrentPartInstance[index], pieceInstance, [ + 'reportedStartedPlayback', + 'reportedStoppedPlayback', + ]) + })) ) { this._collectionData.currentPartInstance = inCurrentPartInstance hasAnythingChanged = true @@ -153,15 +154,11 @@ export class PieceInstancesHandler void this.changed(id, 'removed').catch(this._logger.error) } - const hasAnythingChanged = this.updateCollectionData() - if (hasAnythingChanged) { - await this.notify(this._collectionData) - } + await this.updateAndNotify() } else if (this._subscriptionId) { - // nothing relevant has changed + await this.updateAndNotify() } else { - this.clearCollectionData() - await this.notify(this._collectionData) + await this.clearAndNotify() } } else { this.clearCollectionData() @@ -169,6 +166,18 @@ export class PieceInstancesHandler } } + private async clearAndNotify() { + this.clearCollectionData() + await this.notify(this._collectionData) + } + + private async updateAndNotify() { + const hasAnythingChanged = this.updateCollectionData() + if (hasAnythingChanged) { + await this.notify(this._collectionData) + } + } + private isPieceInstanceActive(pieceInstance: PieceInstance) { return ( pieceInstance.reportedStoppedPlayback == null && @@ -187,7 +196,7 @@ export function arePropertiesShallowEqual>( b: T, omitProperties: Array ): boolean { - if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) { + if (typeof a !== 'object' || a == null || typeof b !== 'object' || b == null) { return false } From c925f3115e6c3d84f2f0ed6c6260705a037c1bff Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 28 Feb 2024 14:36:33 +0100 Subject: [PATCH 252/479] chore: update tsr --- meteor/yarn.lock | 14 ++--- packages/playout-gateway/package.json | 2 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 73 +++++++++++++++------------ 4 files changed, 51 insertions(+), 40 deletions(-) diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 1db78d6780..9ea0aabe5a 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1418,7 +1418,7 @@ __metadata: resolution: "@sofie-automation/shared-lib@portal:../packages/shared-lib::locator=automation-core%40workspace%3A." dependencies: "@mos-connection/model": ^3.0.7 - timeline-state-resolver-types: 9.1.0-nightly-release51-20240202-124852-30953c1e0.0 + timeline-state-resolver-types: 9.1.0-nightly-release51-20240228-132329-b7ceb6950.0 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: node @@ -11748,12 +11748,12 @@ __metadata: languageName: node linkType: hard -"timeline-state-resolver-types@npm:9.1.0-nightly-release51-20240202-124852-30953c1e0.0": - version: 9.1.0-nightly-release51-20240202-124852-30953c1e0.0 - resolution: "timeline-state-resolver-types@npm:9.1.0-nightly-release51-20240202-124852-30953c1e0.0" +"timeline-state-resolver-types@npm:9.1.0-nightly-release51-20240228-132329-b7ceb6950.0": + version: 9.1.0-nightly-release51-20240228-132329-b7ceb6950.0 + resolution: "timeline-state-resolver-types@npm:9.1.0-nightly-release51-20240228-132329-b7ceb6950.0" dependencies: - tslib: ^2.5.1 - checksum: e5ec6a10e26357c7964a0cf3f6c9c02392ee0b9f492a48a9a1d0d172f563fc44ce48562da28521e4ed957908c5043d582b36aa7a12d52cd57d7f8433c90af022 + tslib: ^2.6.2 + checksum: da20544cc1ce9118c318ceccec240cc2f05c7eb415da9365625bdeffafb508b358d4f51842ca7ec9f10f44f25e6c9956cda2d74c22bf138bb869bcceacbbc2f2 languageName: node linkType: hard @@ -11950,7 +11950,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.5.1, tslib@npm:^2.5.3, tslib@npm:^2.6.0, tslib@npm:^2.6.2": +"tslib@npm:^2.5.3, tslib@npm:^2.6.0, tslib@npm:^2.6.2": version: 2.6.2 resolution: "tslib@npm:2.6.2" checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index 6b9d902b0a..70a1e85da2 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -60,7 +60,7 @@ "@sofie-automation/shared-lib": "1.51.0-in-development", "debug": "^4.3.4", "influx": "^5.9.3", - "timeline-state-resolver": "9.1.0-nightly-release51-20240202-124852-30953c1e0.0", + "timeline-state-resolver": "9.1.0-nightly-release51-20240228-132329-b7ceb6950.0", "tslib": "^2.6.2", "underscore": "^1.13.6", "winston": "^3.11.0" diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 128417931e..addfaffbe9 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -39,7 +39,7 @@ ], "dependencies": { "@mos-connection/model": "^3.0.7", - "timeline-state-resolver-types": "9.1.0-nightly-release51-20240202-124852-30953c1e0.0", + "timeline-state-resolver-types": "9.1.0-nightly-release51-20240228-132329-b7ceb6950.0", "tslib": "^2.6.2", "type-fest": "^3.13.1" }, diff --git a/packages/yarn.lock b/packages/yarn.lock index 135c7d0db3..cfc7476e32 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -4583,7 +4583,7 @@ __metadata: resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: "@mos-connection/model": ^3.0.7 - timeline-state-resolver-types: 9.1.0-nightly-release51-20240202-124852-30953c1e0.0 + timeline-state-resolver-types: 9.1.0-nightly-release51-20240228-132329-b7ceb6950.0 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: unknown @@ -6926,9 +6926,9 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"atem-connection@npm:3.4.0": - version: 3.4.0 - resolution: "atem-connection@npm:3.4.0" +"atem-connection@npm:3.5.0": + version: 3.5.0 + resolution: "atem-connection@npm:3.5.0" dependencies: "@julusian/freetype2": ^1.1.2 debug: ^4.3.4 @@ -6940,7 +6940,7 @@ asn1@evs-broadcast/node-asn1: threadedclass: ^1.2.1 tslib: ^2.6.2 wavefile: ^8.4.6 - checksum: 03a0c4ce624afe404f74735a37f469776d6eaab942731b950ff4e4bfe434021858fb54ffd8d7732e7290f28302646b4420265c86cfe3af421b68638216d31856 + checksum: 5eb4412eff357b95305b7aa30727862310ba3a6280216d7b2d50a1bbb1d9861306da087386dd722ede2905e8e2ea73bf586964fa51b542b3643995385ca62e30 languageName: node linkType: hard @@ -7686,7 +7686,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"cacheable-lookup@npm:^5.0.3": +"cacheable-lookup@npm:^5.0.3, cacheable-lookup@npm:^5.0.4": version: 5.0.4 resolution: "cacheable-lookup@npm:5.0.4" checksum: 763e02cf9196bc9afccacd8c418d942fc2677f22261969a4c2c2e760fa44a2351a81557bd908291c3921fe9beb10b976ba8fa50c5ca837c5a0dd945f16468f2d @@ -7845,6 +7845,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"casparcg-connection@npm:6.2.1": + version: 6.2.1 + resolution: "casparcg-connection@npm:6.2.1" + dependencies: + eventemitter3: ^5.0.1 + tslib: ^2.5.0 + xml2js: ^0.6.2 + checksum: a086810faf0dbf91fc2987c1f4062696d5396fcbfeee16bc8b969a1a5edaac30657179c25164b1b62d9f79789ceee2e842822fe30dece420893e6fab50a467dd + languageName: node + linkType: hard + "casparcg-state@npm:3.0.3": version: 3.0.3 resolution: "casparcg-state@npm:3.0.3" @@ -12636,13 +12647,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"hyperdeck-connection@npm:2.0.0-nightly-master-20231205-105655-157a483.0": - version: 2.0.0-nightly-master-20231205-105655-157a483.0 - resolution: "hyperdeck-connection@npm:2.0.0-nightly-master-20231205-105655-157a483.0" +"hyperdeck-connection@npm:2.0.0": + version: 2.0.0 + resolution: "hyperdeck-connection@npm:2.0.0" dependencies: eventemitter3: ^4.0.7 tslib: ^2.6.2 - checksum: 2c4e7f437cfe040903545a52b09664343130d668e546843db752f342b2ce474d90377011d4c5632feb8aa4f6b5474544cd7bef605e7f471eb3f89b76c3753d20 + checksum: 86fd42d64771b9ce58eb4c542cd7f12ee3ab932ff97bc114840c52c09c3052a431f896b8b5900be2c2f2419182e50ab080bcb60a141285d9de2ba52fce4c8d61 languageName: node linkType: hard @@ -17993,7 +18004,7 @@ asn1@evs-broadcast/node-asn1: "@sofie-automation/shared-lib": 1.51.0-in-development debug: ^4.3.4 influx: ^5.9.3 - timeline-state-resolver: 9.1.0-nightly-release51-20240202-124852-30953c1e0.0 + timeline-state-resolver: 9.1.0-nightly-release51-20240228-132329-b7ceb6950.0 tslib: ^2.6.2 underscore: ^1.13.6 winston: ^3.11.0 @@ -20838,7 +20849,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"sprintf-js@npm:^1.1.2": +"sprintf-js@npm:^1.1.3": version: 1.1.3 resolution: "sprintf-js@npm:1.1.3" checksum: a3fdac7b49643875b70864a9d9b469d87a40dfeaf5d34d9d0c5b1cda5fd7d065531fcb43c76357d62254c57184a7b151954156563a4d6a747015cfb41021cad0 @@ -21604,24 +21615,24 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"timeline-state-resolver-types@npm:9.1.0-nightly-release51-20240202-124852-30953c1e0.0": - version: 9.1.0-nightly-release51-20240202-124852-30953c1e0.0 - resolution: "timeline-state-resolver-types@npm:9.1.0-nightly-release51-20240202-124852-30953c1e0.0" +"timeline-state-resolver-types@npm:9.1.0-nightly-release51-20240228-132329-b7ceb6950.0": + version: 9.1.0-nightly-release51-20240228-132329-b7ceb6950.0 + resolution: "timeline-state-resolver-types@npm:9.1.0-nightly-release51-20240228-132329-b7ceb6950.0" dependencies: - tslib: ^2.5.1 - checksum: e5ec6a10e26357c7964a0cf3f6c9c02392ee0b9f492a48a9a1d0d172f563fc44ce48562da28521e4ed957908c5043d582b36aa7a12d52cd57d7f8433c90af022 + tslib: ^2.6.2 + checksum: da20544cc1ce9118c318ceccec240cc2f05c7eb415da9365625bdeffafb508b358d4f51842ca7ec9f10f44f25e6c9956cda2d74c22bf138bb869bcceacbbc2f2 languageName: node linkType: hard -"timeline-state-resolver@npm:9.1.0-nightly-release51-20240202-124852-30953c1e0.0": - version: 9.1.0-nightly-release51-20240202-124852-30953c1e0.0 - resolution: "timeline-state-resolver@npm:9.1.0-nightly-release51-20240202-124852-30953c1e0.0" +"timeline-state-resolver@npm:9.1.0-nightly-release51-20240228-132329-b7ceb6950.0": + version: 9.1.0-nightly-release51-20240228-132329-b7ceb6950.0 + resolution: "timeline-state-resolver@npm:9.1.0-nightly-release51-20240228-132329-b7ceb6950.0" dependencies: "@tv2media/v-connection": ^7.3.2 - atem-connection: 3.4.0 + atem-connection: 3.5.0 atem-state: 1.2.0-nightly-master-20240202-124617-5b52568.0 - cacheable-lookup: ^5.0.3 - casparcg-connection: 6.2.0 + cacheable-lookup: ^5.0.4 + casparcg-connection: 6.2.1 casparcg-state: 3.0.3 debug: ^4.3.4 deepmerge: ^4.3.1 @@ -21629,25 +21640,25 @@ asn1@evs-broadcast/node-asn1: eventemitter3: ^4.0.7 got: ^11.8.6 hpagent: ^1.2.0 - hyperdeck-connection: 2.0.0-nightly-master-20231205-105655-157a483.0 + hyperdeck-connection: 2.0.0 klona: ^2.0.6 obs-websocket-js: ^5.0.4 osc: ^2.4.4 p-all: ^3.0.0 p-queue: ^6.6.2 p-timeout: ^3.2.0 - sprintf-js: ^1.1.2 + sprintf-js: ^1.1.3 superfly-timeline: ^9.0.0 threadedclass: ^1.2.1 - timeline-state-resolver-types: 9.1.0-nightly-release51-20240202-124852-30953c1e0.0 - tslib: ^2.5.1 + timeline-state-resolver-types: 9.1.0-nightly-release51-20240228-132329-b7ceb6950.0 + tslib: ^2.6.2 tv-automation-quantel-gateway-client: ^3.1.7 - type-fest: ^3.10.0 + type-fest: ^3.13.1 underscore: ^1.13.6 utf-8-validate: ^5.0.10 ws: ^7.5.9 xml-js: ^1.6.11 - checksum: c3bb968755cd547b498c6ec311ad321e7721f2d7c69eac68c1244060e3c1083971c4b43a2ec8a9c5e4f6c56e38fc45da355e0b1643634bbd1de13eaee7cd9d81 + checksum: 56df8dbe5f0303795ebc7d8d645bd5d2eb59e1e00f01f267b33ccf66268d21412276a46cfba8631e97bde15acc9aff68a52b7378fd8f9d192890e04cfebfaee6 languageName: node linkType: hard @@ -22009,7 +22020,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"tslib@npm:2.6.2, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.5.0, tslib@npm:^2.5.1, tslib@npm:^2.5.3, tslib@npm:^2.6.0, tslib@npm:^2.6.2": +"tslib@npm:2.6.2, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.5.0, tslib@npm:^2.5.3, tslib@npm:^2.6.0, tslib@npm:^2.6.2": version: 2.6.2 resolution: "tslib@npm:2.6.2" checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad @@ -22165,7 +22176,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"type-fest@npm:^3.1.0, type-fest@npm:^3.10.0, type-fest@npm:^3.11.0, type-fest@npm:^3.13.1": +"type-fest@npm:^3.1.0, type-fest@npm:^3.11.0, type-fest@npm:^3.13.1": version: 3.13.1 resolution: "type-fest@npm:3.13.1" checksum: c06b0901d54391dc46de3802375f5579868949d71f93b425ce564e19a428a0d411ae8d8cb0e300d330071d86152c3ea86e744c3f2860a42a79585b6ec2fdae8e From e4d45b6e5142e8ae71a75edf0fd5dcbeeec86f45 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 29 Feb 2024 16:50:28 +0100 Subject: [PATCH 253/479] fix: asyncapi generation --- .../api/schemas/activePlaylist.yaml | 2 +- .../live-status-gateway/api/schemas/root.yaml | 2 +- packages/live-status-gateway/package.json | 6 +- packages/yarn.lock | 1476 ++++++----------- 4 files changed, 467 insertions(+), 1019 deletions(-) diff --git a/packages/live-status-gateway/api/schemas/activePlaylist.yaml b/packages/live-status-gateway/api/schemas/activePlaylist.yaml index dfc9327e8a..5b373bfcc3 100644 --- a/packages/live-status-gateway/api/schemas/activePlaylist.yaml +++ b/packages/live-status-gateway/api/schemas/activePlaylist.yaml @@ -68,7 +68,7 @@ $defs: publicData: description: Optional arbitrary data required: [id, name, segmentId, pieces] - additionalProperties: false + # additionalProperties: false examples: - id: 'H5CBGYjThrMSmaYvRaa5FVKJIzk_' name: 'Intro' diff --git a/packages/live-status-gateway/api/schemas/root.yaml b/packages/live-status-gateway/api/schemas/root.yaml index e8a671cffc..488a5d3054 100644 --- a/packages/live-status-gateway/api/schemas/root.yaml +++ b/packages/live-status-gateway/api/schemas/root.yaml @@ -89,7 +89,7 @@ $defs: oneOf: - $ref: '#/$defs/subscriptionStatusError' - $ref: '#/$defs/subscriptionStatusSuccess' - additionalProperties: false + # additionalProperties: false examples: - $ref: '#/$defs/subscriptionStatusCommon/examples/0' subscriptionStatusError: diff --git a/packages/live-status-gateway/package.json b/packages/live-status-gateway/package.json index ab07c893d2..0f243e4255 100644 --- a/packages/live-status-gateway/package.json +++ b/packages/live-status-gateway/package.json @@ -66,9 +66,9 @@ "ws": "^8.16.0" }, "devDependencies": { - "@asyncapi/generator": "1.10.9", - "@asyncapi/html-template": "0.26.0", - "@asyncapi/nodejs-ws-template": "0.9.33", + "@asyncapi/generator": "^1.17.7", + "@asyncapi/html-template": "^2.1.7", + "@asyncapi/nodejs-ws-template": "^0.9.33", "type-fest": "^4.10.2" }, "lint-staged": { diff --git a/packages/yarn.lock b/packages/yarn.lock index cfc7476e32..90abe14add 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -227,26 +227,14 @@ __metadata: languageName: node linkType: hard -"@apidevtools/json-schema-ref-parser@npm:^9.0.6": - version: 9.1.2 - resolution: "@apidevtools/json-schema-ref-parser@npm:9.1.2" - dependencies: - "@jsdevtools/ono": ^7.1.3 - "@types/json-schema": ^7.0.6 - call-me-maybe: ^1.0.1 - js-yaml: ^4.1.0 - checksum: 5bd6885db0fd6633879bb4638b7a3aead6b061cb6422083c6be505ee6873be54e3376380df164c73edd8901d4145a9bfe9bc0b008a568fd8b0626b1df96fae8f - languageName: node - linkType: hard - -"@asyncapi/avro-schema-parser@npm:3.0.3, @asyncapi/avro-schema-parser@npm:^3.0.2": - version: 3.0.3 - resolution: "@asyncapi/avro-schema-parser@npm:3.0.3" +"@asyncapi/avro-schema-parser@npm:^3.0.15, @asyncapi/avro-schema-parser@npm:^3.0.3": + version: 3.0.15 + resolution: "@asyncapi/avro-schema-parser@npm:3.0.15" dependencies: - "@asyncapi/parser": ^2.1.0 + "@asyncapi/parser": ^3.0.7 "@types/json-schema": ^7.0.11 avsc: ^5.7.6 - checksum: 25c5388b2b661cb95d85d5175d3fb2eca5cda23dc01a36af61d8a351a8feb392586c2565577bbdcb29dd74b0b095d3841a70e8fbfeca18b1e5f75b75f31417c0 + checksum: 474189dfa61dba6bdd1a7ef4c6f88bd91458bebba0ef0f2238df917f12c2636115b6b50ee1a3e21307689e20cc4364ab02e7d87ab1915d031a9969a93fdc486d languageName: node linkType: hard @@ -270,11 +258,11 @@ __metadata: languageName: node linkType: hard -"@asyncapi/generator-react-sdk@npm:^0.2.23": - version: 0.2.25 - resolution: "@asyncapi/generator-react-sdk@npm:0.2.25" +"@asyncapi/generator-react-sdk@npm:^1.0.11, @asyncapi/generator-react-sdk@npm:^1.0.9": + version: 1.0.11 + resolution: "@asyncapi/generator-react-sdk@npm:1.0.11" dependencies: - "@asyncapi/parser": ^1.15.1 + "@asyncapi/parser": ^3.0.7 "@babel/core": 7.12.9 "@babel/preset-env": ^7.12.7 "@babel/preset-react": ^7.12.7 @@ -284,20 +272,18 @@ __metadata: react: ^17.0.1 rollup: ^2.60.1 source-map-support: ^0.5.19 - checksum: 56e477eafd64f064600266c0c2463bc44e70ea5a10d0ffacc19e5b273527c21a3ab785812ceae720af7795d4a1538e000c85f39406991886b67fd960ce02ebfb + checksum: b7201b8a95effa259f061fa45a7750a7527afb3969c2d23089241814ad4fcf925b8aa0b91057c2da793532f89156c880378e0d32229bcd1c7fdbc4f25544dabc languageName: node linkType: hard -"@asyncapi/generator@npm:1.10.9": - version: 1.10.9 - resolution: "@asyncapi/generator@npm:1.10.9" +"@asyncapi/generator@npm:^1.17.7": + version: 1.17.7 + resolution: "@asyncapi/generator@npm:1.17.7" dependencies: - "@asyncapi/avro-schema-parser": ^3.0.2 - "@asyncapi/generator-react-sdk": ^0.2.23 - "@asyncapi/openapi-schema-parser": ^3.0.3 - "@asyncapi/parser": ^2.0.3 - "@asyncapi/raml-dt-schema-parser": ^4.0.3 - "@npmcli/arborist": ^2.2.4 + "@asyncapi/generator-react-sdk": ^1.0.11 + "@asyncapi/parser": ^3.0.5 + "@npmcli/arborist": 5.6.3 + "@smoya/multi-parser": ^5.0.0 ajv: ^8.12.0 chokidar: ^3.4.0 commander: ^6.1.0 @@ -308,7 +294,6 @@ __metadata: js-yaml: ^3.13.1 levenshtein-edit-distance: ^2.0.5 loglevel: ^1.6.8 - markdown-it: ^12.3.2 minimatch: ^3.0.4 node-fetch: ^2.6.0 nunjucks: ^3.2.0 @@ -322,26 +307,27 @@ __metadata: bin: ag: cli.js asyncapi-generator: cli.js - checksum: f086bd583ae54c09999f3ce10434a2dafbc2103135e00c3d8dabb8cfee538d744fa39921a8a44c14500529a82f6976e6fafb7e730ed013d64d2b124e1eca03e9 + checksum: 4e916d409a2c431848481b29a77d5c5c00489712bef19b24ad0d00df94490596212f0e8e700b2a819d5381dc3ed913ab13efff2d713ed565c45bd58a738fceeb languageName: node linkType: hard -"@asyncapi/html-template@npm:0.26.0": - version: 0.26.0 - resolution: "@asyncapi/html-template@npm:0.26.0" +"@asyncapi/html-template@npm:^2.1.7": + version: 2.1.7 + resolution: "@asyncapi/html-template@npm:2.1.7" dependencies: - "@asyncapi/parser": ^1.17.0 - "@asyncapi/react-component": ^1.0.0-next.43 + "@asyncapi/generator-react-sdk": ^1.0.9 + "@asyncapi/parser": ^3.0.5 + "@asyncapi/react-component": ^1.2.13 highlight.js: 10.7.3 puppeteer: ^14.1.0 - react: ^17.0.2 react-dom: ^17.0.2 rimraf: ^3.0.2 - checksum: e8d74557f83219a5273ce9817ca3c7234897d4fd08de64593553e04bafa7d531fa5f6d0e589a7b7e162a9e0f13b1379c6d9d6c4db541b1dfb6274587db854c9b + sync-fetch: ^0.5.2 + checksum: b3cda2c6f4a9a4146f85f8a3f6f7a72720453daa5e642844433965b81fadb94d77139b4ce9291ab91c90f4859e0195f0818b6f6e281745a5dc3fbb16eb130c4c languageName: node linkType: hard -"@asyncapi/nodejs-ws-template@npm:0.9.33": +"@asyncapi/nodejs-ws-template@npm:^0.9.33": version: 0.9.33 resolution: "@asyncapi/nodejs-ws-template@npm:0.9.33" dependencies: @@ -351,71 +337,33 @@ __metadata: languageName: node linkType: hard -"@asyncapi/openapi-schema-parser@npm:3.0.4, @asyncapi/openapi-schema-parser@npm:^3.0.3": - version: 3.0.4 - resolution: "@asyncapi/openapi-schema-parser@npm:3.0.4" - dependencies: - "@asyncapi/parser": ^2.1.0 - "@openapi-contrib/openapi-schema-to-json-schema": ~3.2.0 - ajv: ^8.11.0 - ajv-errors: ^3.0.0 - ajv-formats: ^2.1.1 - checksum: c18bc4c0b2bad9945d09972f62193fb97485693ac1850b63196d1b694743e59595874a855d05b1296d9cd61a9c361fbc94b2b636794ef4e8c51db4804d409bb3 - languageName: node - linkType: hard - -"@asyncapi/parser@npm:^1.15.1, @asyncapi/parser@npm:^1.17.0": - version: 1.18.1 - resolution: "@asyncapi/parser@npm:1.18.1" - dependencies: - "@apidevtools/json-schema-ref-parser": ^9.0.6 - "@asyncapi/specs": ^4.1.1 - "@fmvilas/pseudo-yaml-ast": ^0.3.1 - ajv: ^6.10.1 - js-yaml: ^3.13.1 - json-to-ast: ^2.1.0 - lodash.clonedeep: ^4.5.0 - node-fetch: ^2.6.0 - tiny-merge-patch: ^0.1.2 - checksum: 6c2799d9338e9650001f2e6afaf1e7525ad9231243130da4adeb96b8af37f0ba2cac41181513aa903aacc08690e64defda8518c78f2b88007d67a7c71d58818e - languageName: node - linkType: hard - -"@asyncapi/parser@npm:^2.0.3, @asyncapi/parser@npm:^2.1.0": - version: 2.1.0 - resolution: "@asyncapi/parser@npm:2.1.0" +"@asyncapi/openapi-schema-parser@npm:^3.0.15, @asyncapi/openapi-schema-parser@npm:^3.0.4": + version: 3.0.15 + resolution: "@asyncapi/openapi-schema-parser@npm:3.0.15" dependencies: - "@asyncapi/specs": ^5.1.0 + "@asyncapi/parser": ^3.0.7 "@openapi-contrib/openapi-schema-to-json-schema": ~3.2.0 - "@stoplight/json-ref-resolver": ^3.1.5 - "@stoplight/spectral-core": ^1.16.1 - "@stoplight/spectral-functions": ^1.7.2 - "@stoplight/spectral-parsers": ^1.0.2 - "@types/json-schema": ^7.0.11 - "@types/urijs": ^1.19.19 ajv: ^8.11.0 ajv-errors: ^3.0.0 ajv-formats: ^2.1.1 - avsc: ^5.7.5 - js-yaml: ^4.1.0 - jsonpath-plus: ^7.2.0 - node-fetch: 2.6.7 - ramldt2jsonschema: ^1.2.3 - webapi-parser: ^0.5.0 - checksum: ed29f26851c0b391611229ecd084396bcd3e86cc171c06247a58c869f341d02c258389144c0e7db57f9bcd2597049c06064f279020011cd1e1a6dba80d7f7fc6 + checksum: 43800f303ddf3f0309befed2d085766726e49bc61208d7ce005ccae3235f3ca48c7e39b3133fc72778b4d2057a323d986552b894389d938e5349ad2e22e37626 languageName: node linkType: hard -"@asyncapi/parser@npm:^3.0.0-next-major-spec.1": - version: 3.0.0-next-major-spec.2 - resolution: "@asyncapi/parser@npm:3.0.0-next-major-spec.2" +"@asyncapi/parser@npm:^3.0.5, @asyncapi/parser@npm:^3.0.7, parserapiv3@npm:@asyncapi/parser@^3.0.7": + version: 3.0.7 + resolution: "@asyncapi/parser@npm:3.0.7" dependencies: - "@asyncapi/specs": ^6.0.0-next-major-spec.6 + "@asyncapi/specs": ^6.5.0 "@openapi-contrib/openapi-schema-to-json-schema": ~3.2.0 + "@stoplight/json": ^3.20.2 + "@stoplight/json-ref-readers": ^1.2.2 "@stoplight/json-ref-resolver": ^3.1.5 "@stoplight/spectral-core": ^1.16.1 "@stoplight/spectral-functions": ^1.7.2 "@stoplight/spectral-parsers": ^1.0.2 + "@stoplight/spectral-ref-resolver": ^1.0.3 + "@stoplight/types": ^13.12.0 "@types/json-schema": ^7.0.11 "@types/urijs": ^1.19.19 ajv: ^8.11.0 @@ -425,43 +373,41 @@ __metadata: js-yaml: ^4.1.0 jsonpath-plus: ^7.2.0 node-fetch: 2.6.7 - ramldt2jsonschema: ^1.2.3 - webapi-parser: ^0.5.0 - checksum: eb8cef4f65af9cfbd48b9699c580678e1a14eee0e8c63e2ec111476ddb6b60559eb211a528573c8ba01d598085bf71d1bb5275e9bf1ec1a5153aa31d9ab4deba + checksum: 8297f415c43c0e539cdeaaa5ef48c920f24e9af2efae8f7d14c8f1768238bce02075f7f649d29f953eeb9634e8d6692d08e831e3c2fab11bb6c8a98ccbd8f67a languageName: node linkType: hard -"@asyncapi/protobuf-schema-parser@npm:3.0.0": - version: 3.0.0 - resolution: "@asyncapi/protobuf-schema-parser@npm:3.0.0" +"@asyncapi/protobuf-schema-parser@npm:^3.0.0, @asyncapi/protobuf-schema-parser@npm:^3.2.4": + version: 3.2.4 + resolution: "@asyncapi/protobuf-schema-parser@npm:3.2.4" dependencies: - "@asyncapi/parser": ^2.1.0 + "@asyncapi/parser": ^3.0.7 "@types/protocol-buffers-schema": ^3.4.1 - protocol-buffers-schema: ^3.6.0 - checksum: a80bbc9bb5ef4e21d5eb0aa56f5aa1207afff482a72abcbf45faece6f295bdeaf81fff349aa1542ef4cf8674f1ff943e8bc29371b07154e615e097d80b15a7dd + protobufjs: ^7.2.6 + checksum: 461caaadc3a74eb7856d2535a7f8f6ad768d9e9343e873f180ed9a8cf30cb80bb3f7d96d2ab87e6e4f408908a969740446fe78d86ad6dc25977b8b81993e0242 languageName: node linkType: hard -"@asyncapi/raml-dt-schema-parser@npm:^4.0.3": - version: 4.0.4 - resolution: "@asyncapi/raml-dt-schema-parser@npm:4.0.4" +"@asyncapi/raml-dt-schema-parser@npm:^4.0.4": + version: 4.0.15 + resolution: "@asyncapi/raml-dt-schema-parser@npm:4.0.15" dependencies: - "@asyncapi/parser": ^2.1.0 + "@asyncapi/parser": ^3.0.7 js-yaml: ^4.1.0 ramldt2jsonschema: ^1.2.3 webapi-parser: ^0.5.0 - checksum: b3d8a31159d3fb9df9d839182c43482993311c5cf83307133b4c154a8001c10fe24c5800fe747ed35da113ece539dda12d77149b9e04cedd4ab0007d587d26d9 + checksum: c5cea6c723465cf0a4d41c203571636dc279e9a40eac0fba0798854bb489489f5c37e7c94b2118f768e4f69ab866937ebc3923ebc4402cfb75417e6c69ad9c0a languageName: node linkType: hard -"@asyncapi/react-component@npm:^1.0.0-next.43": - version: 1.0.0-next.54 - resolution: "@asyncapi/react-component@npm:1.0.0-next.54" +"@asyncapi/react-component@npm:^1.2.13": + version: 1.3.1 + resolution: "@asyncapi/react-component@npm:1.3.1" dependencies: - "@asyncapi/avro-schema-parser": 3.0.3 - "@asyncapi/openapi-schema-parser": 3.0.4 - "@asyncapi/parser": ^3.0.0-next-major-spec.1 - "@asyncapi/protobuf-schema-parser": 3.0.0 + "@asyncapi/avro-schema-parser": ^3.0.15 + "@asyncapi/openapi-schema-parser": ^3.0.15 + "@asyncapi/parser": ^3.0.7 + "@asyncapi/protobuf-schema-parser": ^3.2.4 highlight.js: ^10.7.2 isomorphic-dompurify: ^0.13.0 marked: ^4.0.14 @@ -470,16 +416,7 @@ __metadata: peerDependencies: react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: 36c08712901e07eb5a23b30a925b328d7d13cf22ced7dcbd5198a353167223bc9d549bea6dfaba9111a54d03f83c2ba51a6bec4fcc51a861358a235f4836784c - languageName: node - linkType: hard - -"@asyncapi/specs@npm:^4.1.1": - version: 4.3.1 - resolution: "@asyncapi/specs@npm:4.3.1" - dependencies: - "@types/json-schema": ^7.0.11 - checksum: 886f116550af884d1c0b73a35ec40ae18eb7169a9230658b7ddabf6e57bb1f148dedfbbf059e142354d6d8e2dd22839cc6990cae58f7f09d5c4d0d80c6c127a5 + checksum: d70992809223643e17bab39352d4fc31b188cd83f548e6ec16d9a638d448a96d1f52e4d9900c9e6a3070211ad6b9ddbb5e992e48445385dbf6cb4c5165d95dac languageName: node linkType: hard @@ -492,12 +429,12 @@ __metadata: languageName: node linkType: hard -"@asyncapi/specs@npm:^6.0.0-next-major-spec.6": - version: 6.0.0-next-major-spec.7 - resolution: "@asyncapi/specs@npm:6.0.0-next-major-spec.7" +"@asyncapi/specs@npm:^6.0.0-next-major-spec.9, @asyncapi/specs@npm:^6.5.0": + version: 6.5.0 + resolution: "@asyncapi/specs@npm:6.5.0" dependencies: "@types/json-schema": ^7.0.11 - checksum: 8d31bd97a867e5394ad1645e59f28646b5d8f991ce1face24b83d60c44cff40e08fe03fa55022119ef21a49edcb0dcd874384902aee5082e0fe2d411f1fc7c52 + checksum: 85c3719a9de8af1b0c5585e93be9967845a9c02fc3cc62dcce7f9ed5f31eb1bbab0b8488840da3a93da1e8e4dbb9a8e6a525b19fa6954f94c36b22971cab8788 languageName: node linkType: hard @@ -2672,16 +2609,7 @@ __metadata: languageName: node linkType: hard -"@fmvilas/pseudo-yaml-ast@npm:^0.3.1": - version: 0.3.1 - resolution: "@fmvilas/pseudo-yaml-ast@npm:0.3.1" - dependencies: - yaml-ast-parser: 0.0.43 - checksum: 94b825dcfe4295d3b3b275cccf756f4012f4c5260d2f8469f9004073635f9aefec204bb0c0dfa9f066a5f6998d32226cace293a6a4e78c2b3a1ba05457608647 - languageName: node - linkType: hard - -"@gar/promisify@npm:^1.0.1, @gar/promisify@npm:^1.1.3": +"@gar/promisify@npm:^1.1.3": version: 1.1.3 resolution: "@gar/promisify@npm:1.1.3" checksum: 4059f790e2d07bf3c3ff3e0fec0daa8144fe35c1f6e0111c9921bd32106adaa97a4ab096ad7dab1e28ee6a9060083c4d1a4ada42a7f5f3f7a96b8812e2b757c1 @@ -2750,7 +2678,7 @@ __metadata: languageName: node linkType: hard -"@isaacs/string-locale-compare@npm:^1.0.1, @isaacs/string-locale-compare@npm:^1.1.0": +"@isaacs/string-locale-compare@npm:^1.1.0": version: 1.1.0 resolution: "@isaacs/string-locale-compare@npm:1.1.0" checksum: 7287da5d11497b82c542d3c2abe534808015be4f4883e71c26853277b5456f6bbe4108535db847a29f385ad6dc9318ffb0f55ee79bb5f39993233d7dccf8751d @@ -3425,6 +3353,53 @@ __metadata: languageName: node linkType: hard +"@npmcli/arborist@npm:5.6.3": + version: 5.6.3 + resolution: "@npmcli/arborist@npm:5.6.3" + dependencies: + "@isaacs/string-locale-compare": ^1.1.0 + "@npmcli/installed-package-contents": ^1.0.7 + "@npmcli/map-workspaces": ^2.0.3 + "@npmcli/metavuln-calculator": ^3.0.1 + "@npmcli/move-file": ^2.0.0 + "@npmcli/name-from-folder": ^1.0.1 + "@npmcli/node-gyp": ^2.0.0 + "@npmcli/package-json": ^2.0.0 + "@npmcli/query": ^1.2.0 + "@npmcli/run-script": ^4.1.3 + bin-links: ^3.0.3 + cacache: ^16.1.3 + common-ancestor-path: ^1.0.1 + hosted-git-info: ^5.2.1 + json-parse-even-better-errors: ^2.3.1 + json-stringify-nice: ^1.1.4 + minimatch: ^5.1.0 + mkdirp: ^1.0.4 + mkdirp-infer-owner: ^2.0.0 + nopt: ^6.0.0 + npm-install-checks: ^5.0.0 + npm-package-arg: ^9.0.0 + npm-pick-manifest: ^7.0.2 + npm-registry-fetch: ^13.0.0 + npmlog: ^6.0.2 + pacote: ^13.6.1 + parse-conflict-json: ^2.0.1 + proc-log: ^2.0.0 + promise-all-reject-late: ^1.0.0 + promise-call-limit: ^1.0.1 + read-package-json-fast: ^2.0.2 + readdir-scoped-modules: ^1.1.0 + rimraf: ^3.0.2 + semver: ^7.3.7 + ssri: ^9.0.0 + treeverse: ^2.0.0 + walk-up-path: ^1.0.0 + bin: + arborist: bin/index.js + checksum: e0982108ca349d3d22d8072806941d6c7cef0cb73a4484befd6450271b35065f77178630510347b753e964e979081d306708e17a56558b386c6f7e307dd537e9 + languageName: node + linkType: hard + "@npmcli/arborist@npm:6.2.3": version: 6.2.3 resolution: "@npmcli/arborist@npm:6.2.3" @@ -3468,58 +3443,6 @@ __metadata: languageName: node linkType: hard -"@npmcli/arborist@npm:^2.2.4": - version: 2.10.0 - resolution: "@npmcli/arborist@npm:2.10.0" - dependencies: - "@isaacs/string-locale-compare": ^1.0.1 - "@npmcli/installed-package-contents": ^1.0.7 - "@npmcli/map-workspaces": ^1.0.2 - "@npmcli/metavuln-calculator": ^1.1.0 - "@npmcli/move-file": ^1.1.0 - "@npmcli/name-from-folder": ^1.0.1 - "@npmcli/node-gyp": ^1.0.1 - "@npmcli/package-json": ^1.0.1 - "@npmcli/run-script": ^1.8.2 - bin-links: ^2.2.1 - cacache: ^15.0.3 - common-ancestor-path: ^1.0.1 - json-parse-even-better-errors: ^2.3.1 - json-stringify-nice: ^1.1.4 - mkdirp: ^1.0.4 - mkdirp-infer-owner: ^2.0.0 - npm-install-checks: ^4.0.0 - npm-package-arg: ^8.1.5 - npm-pick-manifest: ^6.1.0 - npm-registry-fetch: ^11.0.0 - pacote: ^11.3.5 - parse-conflict-json: ^1.1.1 - proc-log: ^1.0.0 - promise-all-reject-late: ^1.0.0 - promise-call-limit: ^1.0.1 - read-package-json-fast: ^2.0.2 - readdir-scoped-modules: ^1.1.0 - rimraf: ^3.0.2 - semver: ^7.3.5 - ssri: ^8.0.1 - treeverse: ^1.0.4 - walk-up-path: ^1.0.0 - bin: - arborist: bin/index.js - checksum: dbb23048f75cf58ef00f1e0d034eb4c5ea70a9f8ad7a896d37d3c640c9c3e3181e1a2924c17c4d2f0d01bef2c25cb626935823acf31d4cf753aa8d320a94da7a - languageName: node - linkType: hard - -"@npmcli/fs@npm:^1.0.0": - version: 1.1.1 - resolution: "@npmcli/fs@npm:1.1.1" - dependencies: - "@gar/promisify": ^1.0.1 - semver: ^7.3.5 - checksum: f5ad92f157ed222e4e31c352333d0901df02c7c04311e42a81d8eb555d4ec4276ea9c635011757de20cc476755af33e91622838de573b17e52e2e7703f0a9965 - languageName: node - linkType: hard - "@npmcli/fs@npm:^2.1.0": version: 2.1.2 resolution: "@npmcli/fs@npm:2.1.2" @@ -3539,19 +3462,20 @@ __metadata: languageName: node linkType: hard -"@npmcli/git@npm:^2.1.0": - version: 2.1.0 - resolution: "@npmcli/git@npm:2.1.0" +"@npmcli/git@npm:^3.0.0": + version: 3.0.2 + resolution: "@npmcli/git@npm:3.0.2" dependencies: - "@npmcli/promise-spawn": ^1.3.2 - lru-cache: ^6.0.0 + "@npmcli/promise-spawn": ^3.0.0 + lru-cache: ^7.4.4 mkdirp: ^1.0.4 - npm-pick-manifest: ^6.1.1 + npm-pick-manifest: ^7.0.0 + proc-log: ^2.0.0 promise-inflight: ^1.0.1 promise-retry: ^2.0.1 semver: ^7.3.5 which: ^2.0.2 - checksum: 1f89752df7b836f378b8828423c6ae344fe59399915b9460acded19686e2d0626246251a3cd4cc411ed21c1be6fe7f0c2195c17f392e88748581262ee806dc33 + checksum: bdfd1229bb1113ad4883ef89b74b5dc442a2c96225d830491dd0dec4fa83d083b93cde92b6978d4956a8365521e61bc8dc1891fb905c7c693d5d6aa178f2ab44 languageName: node linkType: hard @@ -3571,7 +3495,7 @@ __metadata: languageName: node linkType: hard -"@npmcli/installed-package-contents@npm:^1.0.6, @npmcli/installed-package-contents@npm:^1.0.7": +"@npmcli/installed-package-contents@npm:^1.0.7": version: 1.0.7 resolution: "@npmcli/installed-package-contents@npm:1.0.7" dependencies: @@ -3595,15 +3519,15 @@ __metadata: languageName: node linkType: hard -"@npmcli/map-workspaces@npm:^1.0.2": - version: 1.0.4 - resolution: "@npmcli/map-workspaces@npm:1.0.4" +"@npmcli/map-workspaces@npm:^2.0.3": + version: 2.0.4 + resolution: "@npmcli/map-workspaces@npm:2.0.4" dependencies: "@npmcli/name-from-folder": ^1.0.1 - glob: ^7.1.6 - minimatch: ^3.0.4 - read-package-json-fast: ^2.0.1 - checksum: 395155a5cd4d6bd5dcce0a616bd4006e291f8eb50a264f143dbe9e4dc7bc37ae4e0d399e93df456758138d3877c465d54ed1e8cf17a9aa9f9f11540ac30e8ad4 + glob: ^8.0.1 + minimatch: ^5.0.1 + read-package-json-fast: ^2.0.3 + checksum: cc8d662ac5115ad9822742a11e11d2d32eda74214bd0f4efec30c9cd833975b5b4c8409fe54ddbb451b040b17a943f770976506cba0f26cfccd58d99b5880d6f languageName: node linkType: hard @@ -3619,14 +3543,15 @@ __metadata: languageName: node linkType: hard -"@npmcli/metavuln-calculator@npm:^1.1.0": - version: 1.1.1 - resolution: "@npmcli/metavuln-calculator@npm:1.1.1" +"@npmcli/metavuln-calculator@npm:^3.0.1": + version: 3.1.1 + resolution: "@npmcli/metavuln-calculator@npm:3.1.1" dependencies: - cacache: ^15.0.5 - pacote: ^11.1.11 - semver: ^7.3.2 - checksum: 63115796ab968e35351fa23accbcd1cf09f719c28565db3995989d9124aed44eafda09302b2e04396d414e3a683e4cb39c2830a3f898bad4d0747a512b154b5e + cacache: ^16.0.0 + json-parse-even-better-errors: ^2.3.1 + pacote: ^13.0.3 + semver: ^7.3.5 + checksum: dc9846fdb82a1f4274ff8943f81452c75615bd9bca523c862956ea2c32e18c5a4be5572e169104d3a0eb262b7ede72c8dbbc202a4ab3b3f4946fa55f226dcc64 languageName: node linkType: hard @@ -3642,16 +3567,6 @@ __metadata: languageName: node linkType: hard -"@npmcli/move-file@npm:^1.0.1, @npmcli/move-file@npm:^1.1.0": - version: 1.1.2 - resolution: "@npmcli/move-file@npm:1.1.2" - dependencies: - mkdirp: ^1.0.4 - rimraf: ^3.0.2 - checksum: c96381d4a37448ea280951e46233f7e541058cf57a57d4094dd4bdcaae43fa5872b5f2eb6bfb004591a68e29c5877abe3cdc210cb3588cbf20ab2877f31a7de7 - languageName: node - linkType: hard - "@npmcli/move-file@npm:^2.0.0": version: 2.0.1 resolution: "@npmcli/move-file@npm:2.0.1" @@ -3676,13 +3591,6 @@ __metadata: languageName: node linkType: hard -"@npmcli/node-gyp@npm:^1.0.1, @npmcli/node-gyp@npm:^1.0.2": - version: 1.0.3 - resolution: "@npmcli/node-gyp@npm:1.0.3" - checksum: 496d5eef2e90e34bb07e96adbcbbce3dba5370ae87e8c46ff5b28570848f35470c8e008b8f69e50863632783e0a9190e6f55b2e4b049c537142821153942d26a - languageName: node - linkType: hard - "@npmcli/node-gyp@npm:^2.0.0": version: 2.0.0 resolution: "@npmcli/node-gyp@npm:2.0.0" @@ -3697,12 +3605,12 @@ __metadata: languageName: node linkType: hard -"@npmcli/package-json@npm:^1.0.1": - version: 1.0.1 - resolution: "@npmcli/package-json@npm:1.0.1" +"@npmcli/package-json@npm:^2.0.0": + version: 2.0.0 + resolution: "@npmcli/package-json@npm:2.0.0" dependencies: json-parse-even-better-errors: ^2.3.1 - checksum: 08b66c8ddb1d6b678975a83006d2fe5070b3013bcb68ea9d54c0142538a614596ddfd1143183fbb8f82c5cecf477d98f3c4e473ef34df3bbf3814e97e37e18d3 + checksum: 7a598e42d2778654ec87438ebfafbcbafbe5a5f5e89ed2ca1db6ca3f94ef14655e304aa41f77632a2a3f5c66b6bd5960bd9370e0ceb4902ea09346720364f9e4 languageName: node linkType: hard @@ -3720,15 +3628,6 @@ __metadata: languageName: node linkType: hard -"@npmcli/promise-spawn@npm:^1.2.0, @npmcli/promise-spawn@npm:^1.3.2": - version: 1.3.2 - resolution: "@npmcli/promise-spawn@npm:1.3.2" - dependencies: - infer-owner: ^1.0.4 - checksum: 543b7c1e26230499b4100b10d45efa35b1077e8f25595050f34930ca3310abe9524f7387279fe4330139e0f28a0207595245503439276fd4b686cca2b6503080 - languageName: node - linkType: hard - "@npmcli/promise-spawn@npm:^3.0.0": version: 3.0.0 resolution: "@npmcli/promise-spawn@npm:3.0.0" @@ -3747,6 +3646,17 @@ __metadata: languageName: node linkType: hard +"@npmcli/query@npm:^1.2.0": + version: 1.2.0 + resolution: "@npmcli/query@npm:1.2.0" + dependencies: + npm-package-arg: ^9.1.0 + postcss-selector-parser: ^6.0.10 + semver: ^7.3.7 + checksum: 2fbefe864d5c942b169264eea3bac55746b8900443114bbca970b87f9e5d20073a66dfea87864e5c5198697086b0fb4af1d29829832a5ee2a995695b1934217c + languageName: node + linkType: hard + "@npmcli/query@npm:^3.0.0": version: 3.0.1 resolution: "@npmcli/query@npm:3.0.1" @@ -3769,15 +3679,16 @@ __metadata: languageName: node linkType: hard -"@npmcli/run-script@npm:^1.8.2": - version: 1.8.6 - resolution: "@npmcli/run-script@npm:1.8.6" +"@npmcli/run-script@npm:^4.1.0, @npmcli/run-script@npm:^4.1.3": + version: 4.2.1 + resolution: "@npmcli/run-script@npm:4.2.1" dependencies: - "@npmcli/node-gyp": ^1.0.2 - "@npmcli/promise-spawn": ^1.3.2 - node-gyp: ^7.1.0 - read-package-json-fast: ^2.0.1 - checksum: 41924e7925452ac8e78d78bef5d65b3d58f86eea4481a453e11e3a9099504bfbfcf1f65d7f75d92170b846fa347d05424e58e617fb9c17b3efd87db599a0f46e + "@npmcli/node-gyp": ^2.0.0 + "@npmcli/promise-spawn": ^3.0.0 + node-gyp: ^9.0.0 + read-package-json-fast: ^2.0.3 + which: ^2.0.2 + checksum: 7b8d6676353f157e68b26baf848e01e5d887bcf90ce81a52f23fc9a5d93e6ffb60057532d664cfd7aeeb76d464d0c8b0d314ee6cccb56943acb3b6c570b756c8 languageName: node linkType: hard @@ -4184,6 +4095,79 @@ __metadata: languageName: node linkType: hard +"@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2": + version: 1.1.2 + resolution: "@protobufjs/aspromise@npm:1.1.2" + checksum: 011fe7ef0826b0fd1a95935a033a3c0fd08483903e1aa8f8b4e0704e3233406abb9ee25350ec0c20bbecb2aad8da0dcea58b392bbd77d6690736f02c143865d2 + languageName: node + linkType: hard + +"@protobufjs/base64@npm:^1.1.2": + version: 1.1.2 + resolution: "@protobufjs/base64@npm:1.1.2" + checksum: 67173ac34de1e242c55da52c2f5bdc65505d82453893f9b51dc74af9fe4c065cf4a657a4538e91b0d4a1a1e0a0642215e31894c31650ff6e3831471061e1ee9e + languageName: node + linkType: hard + +"@protobufjs/codegen@npm:^2.0.4": + version: 2.0.4 + resolution: "@protobufjs/codegen@npm:2.0.4" + checksum: 59240c850b1d3d0b56d8f8098dd04787dcaec5c5bd8de186fa548de86b86076e1c50e80144b90335e705a044edf5bc8b0998548474c2a10a98c7e004a1547e4b + languageName: node + linkType: hard + +"@protobufjs/eventemitter@npm:^1.1.0": + version: 1.1.0 + resolution: "@protobufjs/eventemitter@npm:1.1.0" + checksum: 0369163a3d226851682f855f81413cbf166cd98f131edb94a0f67f79e75342d86e89df9d7a1df08ac28be2bc77e0a7f0200526bb6c2a407abbfee1f0262d5fd7 + languageName: node + linkType: hard + +"@protobufjs/fetch@npm:^1.1.0": + version: 1.1.0 + resolution: "@protobufjs/fetch@npm:1.1.0" + dependencies: + "@protobufjs/aspromise": ^1.1.1 + "@protobufjs/inquire": ^1.1.0 + checksum: 3fce7e09eb3f1171dd55a192066450f65324fd5f7cc01a431df01bb00d0a895e6bfb5b0c5561ce157ee1d886349c90703d10a4e11a1a256418ff591b969b3477 + languageName: node + linkType: hard + +"@protobufjs/float@npm:^1.0.2": + version: 1.0.2 + resolution: "@protobufjs/float@npm:1.0.2" + checksum: 5781e1241270b8bd1591d324ca9e3a3128d2f768077a446187a049e36505e91bc4156ed5ac3159c3ce3d2ba3743dbc757b051b2d723eea9cd367bfd54ab29b2f + languageName: node + linkType: hard + +"@protobufjs/inquire@npm:^1.1.0": + version: 1.1.0 + resolution: "@protobufjs/inquire@npm:1.1.0" + checksum: ca06f02eaf65ca36fb7498fc3492b7fc087bfcc85c702bac5b86fad34b692bdce4990e0ef444c1e2aea8c034227bd1f0484be02810d5d7e931c55445555646f4 + languageName: node + linkType: hard + +"@protobufjs/path@npm:^1.1.2": + version: 1.1.2 + resolution: "@protobufjs/path@npm:1.1.2" + checksum: 856eeb532b16a7aac071cacde5c5620df800db4c80cee6dbc56380524736205aae21e5ae47739114bf669ab5e8ba0e767a282ad894f3b5e124197cb9224445ee + languageName: node + linkType: hard + +"@protobufjs/pool@npm:^1.1.0": + version: 1.1.0 + resolution: "@protobufjs/pool@npm:1.1.0" + checksum: d6a34fbbd24f729e2a10ee915b74e1d77d52214de626b921b2d77288bd8f2386808da2315080f2905761527cceffe7ec34c7647bd21a5ae41a25e8212ff79451 + languageName: node + linkType: hard + +"@protobufjs/utf8@npm:^1.1.0": + version: 1.1.0 + resolution: "@protobufjs/utf8@npm:1.1.0" + checksum: f9bf3163d13aaa3b6f5e6fbf37a116e094ea021c0e1f2a7ccd0e12a29e2ce08dafba4e8b36e13f8ed7397e1591610ce880ed1289af4d66cf4ace8a36a9557278 + languageName: node + linkType: hard + "@rollup/plugin-babel@npm:^5.2.1": version: 5.3.1 resolution: "@rollup/plugin-babel@npm:5.3.1" @@ -4455,6 +4439,21 @@ __metadata: languageName: node linkType: hard +"@smoya/multi-parser@npm:^5.0.0": + version: 5.0.3 + resolution: "@smoya/multi-parser@npm:5.0.3" + dependencies: + "@asyncapi/avro-schema-parser": ^3.0.3 + "@asyncapi/openapi-schema-parser": ^3.0.4 + "@asyncapi/protobuf-schema-parser": ^3.0.0 + "@asyncapi/raml-dt-schema-parser": ^4.0.4 + parserapiv1: "npm:@asyncapi/parser@^2.1.0" + parserapiv2: "npm:@asyncapi/parser@3.0.0-next-major-spec.8" + parserapiv3: "npm:@asyncapi/parser@^3.0.7" + checksum: 3ee709e9accfb8105cf467de11cbd4f38bd3a65b7997788d00783b1e108f2d0f05e789edafc2cc01d1ae5cb7918ae4ca299e743e4d44884bc6a38dd10d76d7c3 + 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" @@ -4601,7 +4600,7 @@ __metadata: languageName: node linkType: hard -"@stoplight/json-ref-readers@npm:1.2.2": +"@stoplight/json-ref-readers@npm:1.2.2, @stoplight/json-ref-readers@npm:^1.2.2": version: 1.2.2 resolution: "@stoplight/json-ref-readers@npm:1.2.2" dependencies: @@ -4629,7 +4628,7 @@ __metadata: languageName: node linkType: hard -"@stoplight/json@npm:^3.17.0, @stoplight/json@npm:^3.17.1, @stoplight/json@npm:^3.21.0, @stoplight/json@npm:~3.21.0": +"@stoplight/json@npm:^3.17.0, @stoplight/json@npm:^3.17.1, @stoplight/json@npm:^3.20.2, @stoplight/json@npm:^3.21.0, @stoplight/json@npm:~3.21.0": version: 3.21.0 resolution: "@stoplight/json@npm:3.21.0" dependencies: @@ -4729,7 +4728,7 @@ __metadata: languageName: node linkType: hard -"@stoplight/spectral-ref-resolver@npm:^1.0.0": +"@stoplight/spectral-ref-resolver@npm:^1.0.0, @stoplight/spectral-ref-resolver@npm:^1.0.3": version: 1.0.4 resolution: "@stoplight/spectral-ref-resolver@npm:1.0.4" dependencies: @@ -4767,7 +4766,7 @@ __metadata: languageName: node linkType: hard -"@stoplight/types@npm:^12.3.0 || ^13.0.0, @stoplight/types@npm:^13.0.0, @stoplight/types@npm:^13.6.0": +"@stoplight/types@npm:^12.3.0 || ^13.0.0, @stoplight/types@npm:^13.0.0, @stoplight/types@npm:^13.12.0, @stoplight/types@npm:^13.6.0": version: 13.20.0 resolution: "@stoplight/types@npm:13.20.0" dependencies: @@ -5583,10 +5582,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=12.0.0": - version: 20.6.3 - resolution: "@types/node@npm:20.6.3" - checksum: 444a6f1f41cfa8d3e20ce0108e6e43960fb2ae0e481f233bb1c14d6252aa63a92e021de561cd317d9fdb411688f871065f40175a1f18763282dee2613a08f8a3 +"@types/node@npm:*, @types/node@npm:>=12.0.0, @types/node@npm:>=13.7.0": + version: 20.11.22 + resolution: "@types/node@npm:20.11.22" + dependencies: + undici-types: ~5.26.4 + checksum: ef8fd0b561c3c9810f3c23c990c856619232934e54308c84e79d4e39d44b66668eceb6eca89c64ebcbc78fb514446661ad58b0f8e6b5fa3d9ed9ff0983aac4ef languageName: node linkType: hard @@ -6376,7 +6377,7 @@ __metadata: languageName: node linkType: hard -"agentkeepalive@npm:^4.1.3, agentkeepalive@npm:^4.2.1": +"agentkeepalive@npm:^4.2.1": version: 4.5.0 resolution: "agentkeepalive@npm:4.5.0" dependencies: @@ -6474,7 +6475,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.10.1, ajv@npm:^6.11.0, ajv@npm:^6.12.2, ajv@npm:^6.12.3, ajv@npm:^6.12.4, ajv@npm:^6.12.5": +"ajv@npm:^6.11.0, ajv@npm:^6.12.2, ajv@npm:^6.12.4, ajv@npm:^6.12.5": version: 6.12.6 resolution: "ajv@npm:6.12.6" dependencies: @@ -6593,13 +6594,6 @@ __metadata: languageName: node linkType: hard -"ansi-regex@npm:^2.0.0": - version: 2.1.1 - resolution: "ansi-regex@npm:2.1.1" - checksum: 190abd03e4ff86794f338a31795d262c1dfe8c91f7e01d04f13f646f1dcb16c5800818f886047876f1272f065570ab86b24b99089f8b68a0e11ff19aed4ca8f1 - languageName: node - linkType: hard - "ansi-regex@npm:^5.0.1": version: 5.0.1 resolution: "ansi-regex@npm:5.0.1" @@ -6670,13 +6664,6 @@ __metadata: languageName: node linkType: hard -"aproba@npm:^1.0.3": - version: 1.2.0 - resolution: "aproba@npm:1.2.0" - checksum: 0fca141966559d195072ed047658b6e6c4fe92428c385dd38e288eacfc55807e7b4989322f030faff32c0f46bb0bc10f1e0ac32ec22d25315a1e5bbc0ebb76dc - languageName: node - linkType: hard - "aproba@npm:^1.0.3 || ^2.0.0, aproba@npm:^2.0.0": version: 2.0.0 resolution: "aproba@npm:2.0.0" @@ -6704,16 +6691,6 @@ __metadata: languageName: node linkType: hard -"are-we-there-yet@npm:~1.1.2": - version: 1.1.7 - resolution: "are-we-there-yet@npm:1.1.7" - dependencies: - delegates: ^1.0.0 - readable-stream: ^2.0.6 - checksum: 70d251719c969b2745bfe5ddf3ebaefa846a636e90a6d5212573676af5d6670e15457761d4725731e19cbebdce42c4ab0cbedf23ab047f2a08274985aa10a3c7 - languageName: node - linkType: hard - "arg@npm:^4.1.0": version: 4.1.3 resolution: "arg@npm:4.1.3" @@ -6855,22 +6832,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"asn1@npm:~0.2.3": - version: 0.2.6 - resolution: "asn1@npm:0.2.6" - dependencies: - safer-buffer: ~2.1.0 - checksum: 39f2ae343b03c15ad4f238ba561e626602a3de8d94ae536c46a4a93e69578826305366dc09fbb9b56aec39b4982a463682f259c38e59f6fa380cd72cd61e493d - languageName: node - linkType: hard - -"assert-plus@npm:1.0.0, assert-plus@npm:^1.0.0": - version: 1.0.0 - resolution: "assert-plus@npm:1.0.0" - checksum: 19b4340cb8f0e6a981c07225eacac0e9d52c2644c080198765d63398f0075f83bbc0c8e95474d54224e297555ad0d631c1dcd058adb1ddc2437b41a6b424ac64 - languageName: node - linkType: hard - "astring@npm:^1.8.1": version: 1.8.6 resolution: "astring@npm:1.8.6" @@ -6996,20 +6957,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"aws-sign2@npm:~0.7.0": - version: 0.7.0 - resolution: "aws-sign2@npm:0.7.0" - checksum: b148b0bb0778098ad8cf7e5fc619768bcb51236707ca1d3e5b49e41b171166d8be9fdc2ea2ae43d7decf02989d0aaa3a9c4caa6f320af95d684de9b548a71525 - languageName: node - linkType: hard - -"aws4@npm:^1.8.0": - version: 1.12.0 - resolution: "aws4@npm:1.12.0" - checksum: 68f79708ac7c335992730bf638286a3ee0a645cf12575d557860100767c500c08b30e24726b9f03265d74116417f628af78509e1333575e9f8d52a80edfe8cbc - languageName: node - linkType: hard - "axios@npm:1.6.5, axios@npm:^1.0.0": version: 1.6.5 resolution: "axios@npm:1.6.5" @@ -7256,15 +7203,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"bcrypt-pbkdf@npm:^1.0.0": - version: 1.0.2 - resolution: "bcrypt-pbkdf@npm:1.0.2" - dependencies: - tweetnacl: ^0.14.3 - checksum: 4edfc9fe7d07019609ccf797a2af28351736e9d012c8402a07120c4453a3b789a15f2ee1530dc49eee8f7eb9379331a8dd4b3766042b9e502f74a68e7f662291 - languageName: node - linkType: hard - "before-after-hook@npm:^2.2.0": version: 2.2.3 resolution: "before-after-hook@npm:2.2.3" @@ -7286,17 +7224,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"bin-links@npm:^2.2.1": - version: 2.3.0 - resolution: "bin-links@npm:2.3.0" +"bin-links@npm:^3.0.3": + version: 3.0.3 + resolution: "bin-links@npm:3.0.3" dependencies: - cmd-shim: ^4.0.1 + cmd-shim: ^5.0.0 mkdirp-infer-owner: ^2.0.0 - npm-normalize-package-bin: ^1.0.0 - read-cmd-shim: ^2.0.0 + npm-normalize-package-bin: ^2.0.0 + read-cmd-shim: ^3.0.0 rimraf: ^3.0.0 - write-file-atomic: ^3.0.3 - checksum: ec02b9b3fa50a8179baa656801de980023f25a71c1a39491fc5672277f0d76d2ae6330ecedf8f9c279ea3751664c46e5ed9a9e1be67c3c5792fa94b31000626f + write-file-atomic: ^4.0.0 + checksum: ea2dc6f91a6ef8b3840ceb48530bbeb8d6d1c6f7985fe1409b16d7e7db39432f0cb5ce15cc2788bb86d989abad6e2c7fba3500996a210a682eec18fb26a66e72 languageName: node linkType: hard @@ -7614,33 +7552,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"cacache@npm:^15.0.3, cacache@npm:^15.0.5, cacache@npm:^15.2.0": - version: 15.3.0 - resolution: "cacache@npm:15.3.0" - dependencies: - "@npmcli/fs": ^1.0.0 - "@npmcli/move-file": ^1.0.1 - chownr: ^2.0.0 - fs-minipass: ^2.0.0 - glob: ^7.1.4 - infer-owner: ^1.0.4 - lru-cache: ^6.0.0 - minipass: ^3.1.1 - minipass-collect: ^1.0.2 - minipass-flush: ^1.0.5 - minipass-pipeline: ^1.2.2 - mkdirp: ^1.0.3 - p-map: ^4.0.0 - promise-inflight: ^1.0.1 - rimraf: ^3.0.2 - ssri: ^8.0.1 - tar: ^6.0.2 - unique-filename: ^1.1.1 - checksum: a07327c27a4152c04eb0a831c63c00390d90f94d51bb80624a66f4e14a6b6360bbf02a84421267bd4d00ca73ac9773287d8d7169e8d2eafe378d2ce140579db8 - languageName: node - linkType: hard - -"cacache@npm:^16.1.0": +"cacache@npm:^16.0.0, cacache@npm:^16.1.0, cacache@npm:^16.1.3": version: 16.1.3 resolution: "cacache@npm:16.1.3" dependencies: @@ -7827,13 +7739,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"caseless@npm:~0.12.0": - version: 0.12.0 - resolution: "caseless@npm:0.12.0" - checksum: b43bd4c440aa1e8ee6baefee8063b4850fd0d7b378f6aabc796c9ec8cb26d27fb30b46885350777d9bd079c5256c0e1329ad0dc7c2817e0bb466810ebb353751 - languageName: node - linkType: hard - "casparcg-connection@npm:6.2.0": version: 6.2.0 resolution: "casparcg-connection@npm:6.2.0" @@ -8198,7 +8103,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"cmd-shim@npm:5.0.0": +"cmd-shim@npm:5.0.0, cmd-shim@npm:^5.0.0": version: 5.0.0 resolution: "cmd-shim@npm:5.0.0" dependencies: @@ -8207,15 +8112,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"cmd-shim@npm:^4.0.1": - version: 4.1.0 - resolution: "cmd-shim@npm:4.1.0" - dependencies: - mkdirp-infer-owner: ^2.0.0 - checksum: d25bb57a8accab681bcfc632e085573b9395cdc60aed8d0ce479f988f9ced16720c89732aef81020140e43fd223b6573c22402e5a1c0cbd0149443104df88d68 - languageName: node - linkType: hard - "cmd-shim@npm:^6.0.0": version: 6.0.1 resolution: "cmd-shim@npm:6.0.1" @@ -8241,20 +8137,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"code-error-fragment@npm:0.0.230": - version: 0.0.230 - resolution: "code-error-fragment@npm:0.0.230" - checksum: 6c5e800d6d70b30938cc85a2fc2c6069f028eadb58bceb65716b995ce6228c99906302f2c438ba50115fd81a1ee15dd95dc7d317b16a6c590e311ac7e50613f3 - languageName: node - linkType: hard - -"code-point-at@npm:^1.0.0": - version: 1.1.0 - resolution: "code-point-at@npm:1.1.0" - checksum: 17d5666611f9b16d64fdf48176d9b7fb1c7d1c1607a189f7e600040a11a6616982876af148230336adb7d8fe728a559f743a4e29db3747e3b1a32fa7f4529681 - languageName: node - linkType: hard - "collapse-white-space@npm:^1.0.2": version: 1.0.6 resolution: "collapse-white-space@npm:1.0.6" @@ -8371,7 +8253,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"combined-stream@npm:^1.0.6, combined-stream@npm:^1.0.8, combined-stream@npm:~1.0.6": +"combined-stream@npm:^1.0.6, combined-stream@npm:^1.0.8": version: 1.0.8 resolution: "combined-stream@npm:1.0.8" dependencies: @@ -8559,7 +8441,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"console-control-strings@npm:^1.0.0, console-control-strings@npm:^1.1.0, console-control-strings@npm:~1.1.0": +"console-control-strings@npm:^1.1.0": version: 1.1.0 resolution: "console-control-strings@npm:1.1.0" checksum: 8755d76787f94e6cf79ce4666f0c5519906d7f5b02d4b884cf41e11dcd759ed69c57da0670afd9236d229a46e0f9cf519db0cd829c6dca820bb5a5c3def584ed @@ -8799,13 +8681,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"core-util-is@npm:1.0.2": - version: 1.0.2 - resolution: "core-util-is@npm:1.0.2" - checksum: 7a4c925b497a2c91421e25bf76d6d8190f0b2359a9200dbeed136e63b2931d6294d3b1893eda378883ed363cd950f44a12a401384c609839ea616befb7927dab - languageName: node - linkType: hard - "core-util-is@npm:^1.0.2, core-util-is@npm:~1.0.0": version: 1.0.3 resolution: "core-util-is@npm:1.0.3" @@ -9222,15 +9097,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"dashdash@npm:^1.12.0": - version: 1.14.1 - resolution: "dashdash@npm:1.14.1" - dependencies: - assert-plus: ^1.0.0 - checksum: 3634c249570f7f34e3d34f866c93f866c5b417f0dd616275decae08147dcdf8fccfaa5947380ccfb0473998ea3a8057c0b4cd90c875740ee685d0624b2983598 - languageName: node - linkType: hard - "data-urls@npm:^2.0.0": version: 2.0.0 resolution: "data-urls@npm:2.0.0" @@ -9849,16 +9715,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"ecc-jsbn@npm:~0.1.1": - version: 0.1.2 - resolution: "ecc-jsbn@npm:0.1.2" - dependencies: - jsbn: ~0.1.0 - safer-buffer: ^2.1.0 - checksum: 22fef4b6203e5f31d425f5b711eb389e4c6c2723402e389af394f8411b76a488fa414d309d866e2b577ce3e8462d344205545c88a8143cc21752a5172818888a - languageName: node - linkType: hard - "ee-first@npm:1.1.1": version: 1.1.1 resolution: "ee-first@npm:1.1.1" @@ -10000,7 +9856,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"encoding@npm:^0.1.12, encoding@npm:^0.1.13": +"encoding@npm:^0.1.13": version: 0.1.13 resolution: "encoding@npm:0.1.13" dependencies: @@ -10065,13 +9921,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"entities@npm:~2.1.0": - version: 2.1.0 - resolution: "entities@npm:2.1.0" - checksum: a10a877e489586a3f6a691fe49bf3fc4e58f06c8e80522f08214a5150ba457e7017b447d4913a3fa041bda06ee4c92517baa4d8d75373eaa79369e9639225ffd - languageName: node - linkType: hard - "env-paths@npm:^2.2.0": version: 2.2.1 resolution: "env-paths@npm:2.2.1" @@ -10782,7 +10631,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"extend@npm:^3.0.0, extend@npm:~3.0.2": +"extend@npm:^3.0.0": version: 3.0.2 resolution: "extend@npm:3.0.2" checksum: a50a8309ca65ea5d426382ff09f33586527882cf532931cb08ca786ea3146c0553310bda688710ff61d7668eba9f96b923fe1420cdf56a2c3eaf30fcab87b515 @@ -10817,20 +10666,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"extsprintf@npm:1.3.0": - version: 1.3.0 - resolution: "extsprintf@npm:1.3.0" - checksum: cee7a4a1e34cffeeec18559109de92c27517e5641991ec6bab849aa64e3081022903dd53084f2080d0d2530803aa5ee84f1e9de642c365452f9e67be8f958ce2 - languageName: node - linkType: hard - -"extsprintf@npm:^1.2.0": - version: 1.4.1 - resolution: "extsprintf@npm:1.4.1" - checksum: a2f29b241914a8d2bad64363de684821b6b1609d06ae68d5b539e4de6b28659715b5bea94a7265201603713b7027d35399d10b0548f09071c5513e65e8323d33 - languageName: node - linkType: hard - "fast-clone@npm:^1.5.13": version: 1.5.13 resolution: "fast-clone@npm:1.5.13" @@ -11301,13 +11136,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"forever-agent@npm:~0.6.1": - version: 0.6.1 - resolution: "forever-agent@npm:0.6.1" - checksum: 766ae6e220f5fe23676bb4c6a99387cec5b7b62ceb99e10923376e27bfea72f3c3aeec2ba5f45f3f7ba65d6616965aa7c20b15002b6860833bb6e394dea546a8 - languageName: node - linkType: hard - "fork-ts-checker-webpack-plugin@npm:^6.5.0": version: 6.5.3 resolution: "fork-ts-checker-webpack-plugin@npm:6.5.3" @@ -11372,17 +11200,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"form-data@npm:~2.3.2": - version: 2.3.3 - resolution: "form-data@npm:2.3.3" - dependencies: - asynckit: ^0.4.0 - combined-stream: ^1.0.6 - mime-types: ^2.1.12 - checksum: 10c1780fa13dbe1ff3100114c2ce1f9307f8be10b14bf16e103815356ff567b6be39d70fc4a40f8990b9660012dc24b0f5e1dde1b6426166eb23a445ba068ca3 - languageName: node - linkType: hard - "forwarded-parse@npm:^2.1.0": version: 2.1.2 resolution: "forwarded-parse@npm:2.1.2" @@ -11584,22 +11401,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"gauge@npm:~2.7.3": - version: 2.7.4 - resolution: "gauge@npm:2.7.4" - dependencies: - aproba: ^1.0.3 - console-control-strings: ^1.0.0 - has-unicode: ^2.0.0 - object-assign: ^4.1.0 - signal-exit: ^3.0.0 - string-width: ^1.0.1 - strip-ansi: ^3.0.1 - wide-align: ^1.1.0 - checksum: a89b53cee65579b46832e050b5f3a79a832cc422c190de79c6b8e2e15296ab92faddde6ddf2d376875cbba2b043efa99b9e1ed8124e7365f61b04e3cee9d40ee - languageName: node - linkType: hard - "gensync@npm:^1.0.0-beta.1, gensync@npm:^1.0.0-beta.2": version: 1.0.0-beta.2 resolution: "gensync@npm:1.0.0-beta.2" @@ -11717,15 +11518,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"getpass@npm:^0.1.1": - version: 0.1.7 - resolution: "getpass@npm:0.1.7" - dependencies: - assert-plus: ^1.0.0 - checksum: ab18d55661db264e3eac6012c2d3daeafaab7a501c035ae0ccb193c3c23e9849c6e29b6ac762b9c2adae460266f925d55a3a2a3a3c8b94be2f222df94d70c046 - languageName: node - linkType: hard - "git-raw-commits@npm:^2.0.8": version: 2.0.11 resolution: "git-raw-commits@npm:2.0.11" @@ -12037,20 +11829,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.15, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.3, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": +"graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.15, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: ac85f94da92d8eb6b7f5a8b20ce65e43d66761c55ce85ac96df6865308390da45a8d3f0296dd3a663de65d30ba497bd46c696cc1e248c72b13d6d567138a4fc7 languageName: node linkType: hard -"grapheme-splitter@npm:^1.0.4": - version: 1.0.4 - resolution: "grapheme-splitter@npm:1.0.4" - checksum: 0c22ec54dee1b05cd480f78cf14f732cb5b108edc073572c4ec205df4cd63f30f8db8025afc5debc8835a8ddeacf648a1c7992fe3dcd6ad38f9a476d84906620 - languageName: node - linkType: hard - "graphemer@npm:^1.4.0": version: 1.4.0 resolution: "graphemer@npm:1.4.0" @@ -12104,23 +11889,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"har-schema@npm:^2.0.0": - version: 2.0.0 - resolution: "har-schema@npm:2.0.0" - checksum: d8946348f333fb09e2bf24cc4c67eabb47c8e1d1aa1c14184c7ffec1140a49ec8aa78aa93677ae452d71d5fc0fdeec20f0c8c1237291fc2bcb3f502a5d204f9b - languageName: node - linkType: hard - -"har-validator@npm:~5.1.3": - version: 5.1.5 - resolution: "har-validator@npm:5.1.5" - dependencies: - ajv: ^6.12.3 - har-schema: ^2.0.0 - checksum: b998a7269ca560d7f219eedc53e2c664cd87d487e428ae854a6af4573fc94f182fe9d2e3b92ab968249baec7ebaf9ead69cf975c931dc2ab282ec182ee988280 - languageName: node - linkType: hard - "hard-rejection@npm:^2.1.0": version: 2.1.0 resolution: "hard-rejection@npm:2.1.0" @@ -12181,7 +11949,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"has-unicode@npm:2.0.1, has-unicode@npm:^2.0.0, has-unicode@npm:^2.0.1": +"has-unicode@npm:2.0.1, has-unicode@npm:^2.0.1": version: 2.0.1 resolution: "has-unicode@npm:2.0.1" checksum: 1eab07a7436512db0be40a710b29b5dc21fa04880b7f63c9980b706683127e3c1b57cb80ea96d47991bdae2dfe479604f6a1ba410106ee1046a41d1bd0814400 @@ -12348,7 +12116,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"hosted-git-info@npm:^5.0.0": +"hosted-git-info@npm:^5.0.0, hosted-git-info@npm:^5.2.1": version: 5.2.1 resolution: "hosted-git-info@npm:5.2.1" dependencies: @@ -12584,17 +12352,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"http-signature@npm:~1.2.0": - version: 1.2.0 - resolution: "http-signature@npm:1.2.0" - dependencies: - assert-plus: ^1.0.0 - jsprim: ^1.2.2 - sshpk: ^1.7.0 - checksum: 3324598712266a9683585bb84a75dec4fd550567d5e0dd4a0fff6ff3f74348793404d3eeac4918fa0902c810eeee1a86419e4a2e92a164132dfe6b26743fb47c - languageName: node - linkType: hard - "http2-wrapper@npm:^1.0.0-beta.5.2": version: 1.0.3 resolution: "http2-wrapper@npm:1.0.3" @@ -12714,15 +12471,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"ignore-walk@npm:^3.0.3": - version: 3.0.4 - resolution: "ignore-walk@npm:3.0.4" - dependencies: - minimatch: ^3.0.4 - checksum: 9e9c5ef6c3e0ed7ef5d797991abb554dbb7e60d5fedf6cf05c7129819689eba2b462f625c6e3561e0fc79841904eb829565513eeeab1b44f4fbec4d3146b1a8d - languageName: node - linkType: hard - "ignore-walk@npm:^5.0.1": version: 5.0.1 resolution: "ignore-walk@npm:5.0.1" @@ -13169,15 +12917,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"is-fullwidth-code-point@npm:^1.0.0": - version: 1.0.0 - resolution: "is-fullwidth-code-point@npm:1.0.0" - dependencies: - number-is-nan: ^1.0.0 - checksum: 4d46a7465a66a8aebcc5340d3b63a56602133874af576a9ca42c6f0f4bd787a743605771c5f246db77da96605fefeffb65fc1dbe862dcc7328f4b4d03edf5a57 - languageName: node - linkType: hard - "is-fullwidth-code-point@npm:^3.0.0": version: 3.0.0 resolution: "is-fullwidth-code-point@npm:3.0.0" @@ -13491,7 +13230,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"is-typedarray@npm:^1.0.0, is-typedarray@npm:~1.0.0": +"is-typedarray@npm:^1.0.0": version: 1.0.0 resolution: "is-typedarray@npm:1.0.0" checksum: 3508c6cd0a9ee2e0df2fa2e9baabcdc89e911c7bd5cf64604586697212feec525aa21050e48affb5ffc3df20f0f5d2e2cf79b08caa64e1ccc9578e251763aef7 @@ -13599,13 +13338,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"isstream@npm:~0.1.2": - version: 0.1.2 - resolution: "isstream@npm:0.1.2" - checksum: 1eb2fe63a729f7bdd8a559ab552c69055f4f48eb5c2f03724430587c6f450783c8f1cd936c1c952d0a927925180fcc892ebd5b174236cf1065d4bd5bdb37e963 - languageName: node - linkType: hard - "istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0": version: 3.2.0 resolution: "istanbul-lib-coverage@npm:3.2.0" @@ -14226,13 +13958,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"jsbn@npm:~0.1.0": - version: 0.1.1 - resolution: "jsbn@npm:0.1.1" - checksum: e5ff29c1b8d965017ef3f9c219dacd6e40ad355c664e277d31246c90545a02e6047018c16c60a00f36d561b3647215c41894f5d869ada6908a2e0ce4200c88f2 - languageName: node - linkType: hard - "jsdom@npm:^16.5.2": version: 16.7.0 resolution: "jsdom@npm:16.7.0" @@ -14406,13 +14131,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"json-schema@npm:0.4.0": - version: 0.4.0 - resolution: "json-schema@npm:0.4.0" - checksum: 66389434c3469e698da0df2e7ac5a3281bcff75e797a5c127db7c5b56270e01ae13d9afa3c03344f76e32e81678337a8c912bdbb75101c62e487dc3778461d72 - languageName: node - linkType: hard - "json-stable-stringify-without-jsonify@npm:^1.0.1": version: 1.0.1 resolution: "json-stable-stringify-without-jsonify@npm:1.0.1" @@ -14427,23 +14145,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"json-stringify-safe@npm:^5.0.1, json-stringify-safe@npm:~5.0.1": +"json-stringify-safe@npm:^5.0.1": version: 5.0.1 resolution: "json-stringify-safe@npm:5.0.1" checksum: 48ec0adad5280b8a96bb93f4563aa1667fd7a36334f79149abd42446d0989f2ddc58274b479f4819f1f00617957e6344c886c55d05a4e15ebb4ab931e4a6a8ee languageName: node linkType: hard -"json-to-ast@npm:^2.1.0": - version: 2.1.0 - resolution: "json-to-ast@npm:2.1.0" - dependencies: - code-error-fragment: 0.0.230 - grapheme-splitter: ^1.0.4 - checksum: 1e9b051505b218573b39f3fec9054d75772413aefc2fee3e763d9033276664faa7eec26b945a71f70b9ce29685b2f13259df7dd3243e15eacf4672c62d5ba7ce - languageName: node - linkType: hard - "json5@npm:^2.1.2, json5@npm:^2.2.2, json5@npm:^2.2.3": version: 2.2.3 resolution: "json5@npm:2.2.3" @@ -14522,25 +14230,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"jsprim@npm:^1.2.2": - version: 1.4.2 - resolution: "jsprim@npm:1.4.2" - dependencies: - assert-plus: 1.0.0 - extsprintf: 1.3.0 - json-schema: 0.4.0 - verror: 1.10.0 - checksum: 2ad1b9fdcccae8b3d580fa6ced25de930eaa1ad154db21bbf8478a4d30bbbec7925b5f5ff29b933fba9412b16a17bd484a8da4fdb3663b5e27af95dd693bab2a - languageName: node - linkType: hard - -"just-diff-apply@npm:^3.0.0": - version: 3.1.2 - resolution: "just-diff-apply@npm:3.1.2" - checksum: 4368a2a3dd96665f0561b1b1b11861af2ef19b4f2371cb61f6d05ed4716cd4996fdde4dca401eb274fe09633b1ff16611f428ed7ffb00fe995723bab73e3fed8 - languageName: node - linkType: hard - "just-diff-apply@npm:^5.2.0": version: 5.5.0 resolution: "just-diff-apply@npm:5.5.0" @@ -14548,10 +14237,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"just-diff@npm:^3.0.1": - version: 3.1.1 - resolution: "just-diff@npm:3.1.1" - checksum: dc43480df5bfbc6bf33ae8cfbc01f6875a979712f766b80d5466b48377b59b16c912a4a778110fa14a2efef1f7a09434507138210533fd625669915b6841a03e +"just-diff@npm:^5.0.1": + version: 5.2.0 + resolution: "just-diff@npm:5.2.0" + checksum: 5527fb6d28a446185250fba501ad857370c049bac7aa5a34c9ec82a45e1380af1a96137be7df2f87252d9f75ef67be41d4c0267d481ed0235b2ceb3ee1f5f75d languageName: node linkType: hard @@ -14817,15 +14506,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"linkify-it@npm:^3.0.1": - version: 3.0.3 - resolution: "linkify-it@npm:3.0.3" - dependencies: - uc.micro: ^1.0.1 - checksum: 31367a4bb70c5bbc9703246236b504b0a8e049bcd4e0de4291fa50f0ebdebf235b5eb54db6493cb0b1319357c6eeafc4324c9f4aa34b0b943d9f2e11a1268fbc - languageName: node - linkType: hard - "lint-staged@npm:^13.2.2": version: 13.3.0 resolution: "lint-staged@npm:13.3.0" @@ -14869,9 +14549,9 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "live-status-gateway@workspace:live-status-gateway" dependencies: - "@asyncapi/generator": 1.10.9 - "@asyncapi/html-template": 0.26.0 - "@asyncapi/nodejs-ws-template": 0.9.33 + "@asyncapi/generator": ^1.17.7 + "@asyncapi/html-template": ^2.1.7 + "@asyncapi/nodejs-ws-template": ^0.9.33 "@sofie-automation/blueprints-integration": 1.51.0-in-development "@sofie-automation/corelib": 1.51.0-in-development "@sofie-automation/server-core-integration": 1.51.0-in-development @@ -14983,13 +14663,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"lodash.clonedeep@npm:^4.5.0": - version: 4.5.0 - resolution: "lodash.clonedeep@npm:4.5.0" - checksum: 92c46f094b064e876a23c97f57f81fbffd5d760bf2d8a1c61d85db6d1e488c66b0384c943abee4f6af7debf5ad4e4282e74ff83177c9e63d8ff081a4837c3489 - languageName: node - linkType: hard - "lodash.curry@npm:^4.0.1": version: 4.1.1 resolution: "lodash.curry@npm:4.1.1" @@ -15153,6 +14826,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"long@npm:^5.0.0": + version: 5.2.3 + resolution: "long@npm:5.2.3" + checksum: 885ede7c3de4facccbd2cacc6168bae3a02c3e836159ea4252c87b6e34d40af819824b2d4edce330bfb5c4d6e8ce3ec5864bdcf9473fa1f53a4f8225860e5897 + languageName: node + linkType: hard + "loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.2.0, loose-envify@npm:^1.3.1, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" @@ -15327,30 +15007,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"make-fetch-happen@npm:^9.0.1": - version: 9.1.0 - resolution: "make-fetch-happen@npm:9.1.0" - dependencies: - agentkeepalive: ^4.1.3 - cacache: ^15.2.0 - http-cache-semantics: ^4.1.0 - http-proxy-agent: ^4.0.1 - https-proxy-agent: ^5.0.0 - is-lambda: ^1.0.1 - lru-cache: ^6.0.0 - minipass: ^3.1.3 - minipass-collect: ^1.0.2 - minipass-fetch: ^1.3.2 - minipass-flush: ^1.0.5 - minipass-pipeline: ^1.2.4 - negotiator: ^0.6.2 - promise-retry: ^2.0.1 - socks-proxy-agent: ^6.0.0 - ssri: ^8.0.0 - checksum: 0eb371c85fdd0b1584fcfdf3dc3c62395761b3c14658be02620c310305a9a7ecf1617a5e6fb30c1d081c5c8aaf177fa133ee225024313afabb7aa6a10f1e3d04 - languageName: node - linkType: hard - "makeerror@npm:1.0.12": version: 1.0.12 resolution: "makeerror@npm:1.0.12" @@ -15403,21 +15059,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"markdown-it@npm:^12.3.2": - version: 12.3.2 - resolution: "markdown-it@npm:12.3.2" - dependencies: - argparse: ^2.0.1 - entities: ~2.1.0 - linkify-it: ^3.0.1 - mdurl: ^1.0.1 - uc.micro: ^1.0.5 - bin: - markdown-it: bin/markdown-it.js - checksum: 890555711c1c00fa03b936ca2b213001a3b9b37dea140d8445ae4130ce16628392aad24b12e2a0a9935336ca5951f2957a38f4e5309a2e38eab44e25ff32a41e - languageName: node - linkType: hard - "marked@npm:^4.0.14, marked@npm:^4.2.12": version: 4.3.0 resolution: "marked@npm:4.3.0" @@ -15650,7 +15291,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:~2.1.17, mime-types@npm:~2.1.19, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": +"mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:~2.1.17, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -15739,7 +15380,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"minimatch@npm:^5.0.1": +"minimatch@npm:^5.0.1, minimatch@npm:^5.1.0": version: 5.1.6 resolution: "minimatch@npm:5.1.6" dependencies: @@ -15818,21 +15459,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"minipass-fetch@npm:^1.3.0, minipass-fetch@npm:^1.3.2": - version: 1.4.1 - resolution: "minipass-fetch@npm:1.4.1" - dependencies: - encoding: ^0.1.12 - minipass: ^3.1.0 - minipass-sized: ^1.0.3 - minizlib: ^2.0.0 - dependenciesMeta: - encoding: - optional: true - checksum: ec93697bdb62129c4e6c0104138e681e30efef8c15d9429dd172f776f83898471bc76521b539ff913248cc2aa6d2b37b652c993504a51cc53282563640f29216 - languageName: node - linkType: hard - "minipass-fetch@npm:^2.0.3": version: 2.1.2 resolution: "minipass-fetch@npm:2.1.2" @@ -15882,7 +15508,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"minipass-pipeline@npm:^1.2.2, minipass-pipeline@npm:^1.2.4": +"minipass-pipeline@npm:^1.2.4": version: 1.2.4 resolution: "minipass-pipeline@npm:1.2.4" dependencies: @@ -15900,7 +15526,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"minipass@npm:^3.0.0, minipass@npm:^3.1.0, minipass@npm:^3.1.1, minipass@npm:^3.1.3, minipass@npm:^3.1.6": +"minipass@npm:^3.0.0, minipass@npm:^3.1.1, minipass@npm:^3.1.6": version: 3.3.6 resolution: "minipass@npm:3.3.6" dependencies: @@ -15930,7 +15556,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"minizlib@npm:^2.0.0, minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": +"minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": version: 2.1.2 resolution: "minizlib@npm:2.1.2" dependencies: @@ -16172,7 +15798,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"negotiator@npm:0.6.3, negotiator@npm:^0.6.2, negotiator@npm:^0.6.3": +"negotiator@npm:0.6.3, negotiator@npm:^0.6.3": version: 0.6.3 resolution: "negotiator@npm:0.6.3" checksum: b8ffeb1e262eff7968fc90a2b6767b04cfd9842582a9d0ece0af7049537266e7b2506dfb1d107a32f06dd849ab2aea834d5830f7f4d0e5cb7d36e1ae55d021d9 @@ -16302,26 +15928,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"node-gyp@npm:^7.1.0": - version: 7.1.2 - resolution: "node-gyp@npm:7.1.2" - dependencies: - env-paths: ^2.2.0 - glob: ^7.1.4 - graceful-fs: ^4.2.3 - nopt: ^5.0.0 - npmlog: ^4.1.2 - request: ^2.88.2 - rimraf: ^3.0.2 - semver: ^7.3.2 - tar: ^6.0.2 - which: ^2.0.2 - bin: - node-gyp: bin/node-gyp.js - checksum: 08582720f28f9a9bb64bc9cbe2f58b159c0258326a9c898e4e95d2f2d8002f44602338111ebf980e5aa47a3421e071525b758923b76855d780fab8cc03279ae0 - languageName: node - linkType: hard - "node-gyp@npm:^9.0.0, node-gyp@npm:latest": version: 9.4.0 resolution: "node-gyp@npm:9.4.0" @@ -16399,17 +16005,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"nopt@npm:^5.0.0": - version: 5.0.0 - resolution: "nopt@npm:5.0.0" - dependencies: - abbrev: 1 - bin: - nopt: bin/nopt.js - checksum: d35fdec187269503843924e0114c0c6533fb54bbf1620d0f28b4b60ba01712d6687f62565c55cc20a504eff0fbe5c63e22340c3fad549ad40469ffb611b04f2f - languageName: node - linkType: hard - "nopt@npm:^6.0.0": version: 6.0.0 resolution: "nopt@npm:6.0.0" @@ -16528,6 +16123,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"npm-bundled@npm:^2.0.0": + version: 2.0.1 + resolution: "npm-bundled@npm:2.0.1" + dependencies: + npm-normalize-package-bin: ^2.0.0 + checksum: 7747293985c48c5268871efe691545b03731cb80029692000cbdb0b3344b9617be5187aa36281cabbe6b938e3651b4e87236d1c31f9e645eef391a1a779413e6 + languageName: node + linkType: hard + "npm-bundled@npm:^3.0.0": version: 3.0.0 resolution: "npm-bundled@npm:3.0.0" @@ -16537,12 +16141,12 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"npm-install-checks@npm:^4.0.0": - version: 4.0.0 - resolution: "npm-install-checks@npm:4.0.0" +"npm-install-checks@npm:^5.0.0": + version: 5.0.0 + resolution: "npm-install-checks@npm:5.0.0" dependencies: semver: ^7.1.1 - checksum: 8308ff48e61e0863d7f148f62543e1f6c832525a7d8002ea742d5e478efa8b29bf65a87f9fb82786e15232e4b3d0362b126c45afdceed4c051c0d3c227dd0ace + checksum: 0e7d1aae52b1fe9d3a0fd4a008850c7047931722dd49ee908afd13fd0297ac5ddb10964d9c59afcdaaa2ca04b51d75af2788f668c729ae71fec0e4cdac590ffc languageName: node linkType: hard @@ -16599,18 +16203,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"npm-package-arg@npm:^8.0.0, npm-package-arg@npm:^8.0.1, npm-package-arg@npm:^8.1.2, npm-package-arg@npm:^8.1.5": - version: 8.1.5 - resolution: "npm-package-arg@npm:8.1.5" - dependencies: - hosted-git-info: ^4.0.1 - semver: ^7.3.4 - validate-npm-package-name: ^3.0.0 - checksum: ae76afbcebb4ea8d0b849b8b18ed1b0491030fb04a0af5d75f1b8390cc50bec186ced9fbe60f47d939eab630c7c0db0919d879ac56a87d3782267dfe8eec60d3 - languageName: node - linkType: hard - -"npm-package-arg@npm:^9.0.1": +"npm-package-arg@npm:^9.0.0, npm-package-arg@npm:^9.0.1, npm-package-arg@npm:^9.1.0": version: 9.1.2 resolution: "npm-package-arg@npm:9.1.2" dependencies: @@ -16636,17 +16229,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"npm-packlist@npm:^2.1.4": - version: 2.2.2 - resolution: "npm-packlist@npm:2.2.2" +"npm-packlist@npm:^5.1.0": + version: 5.1.3 + resolution: "npm-packlist@npm:5.1.3" dependencies: - glob: ^7.1.6 - ignore-walk: ^3.0.3 - npm-bundled: ^1.1.1 - npm-normalize-package-bin: ^1.0.1 + glob: ^8.0.1 + ignore-walk: ^5.0.1 + npm-bundled: ^2.0.0 + npm-normalize-package-bin: ^2.0.0 bin: npm-packlist: bin/index.js - checksum: 799ce94b077e4dc366a9a5bcc5f006669263bb1a48d6948161aed915fd2f11dea8a7cf516a63fc78e5df059915591dade5928f0738baadc99a8ab4685d8b58c3 + checksum: 94cc9c66740e8f80243301de85eb0a2cec5bbd570c3f26b6ad7af1a3eca155f7e810580dc7ea4448f12a8fd82f6db307e7132a5fe69e157eb45b325acadeb22a languageName: node linkType: hard @@ -16659,15 +16252,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"npm-pick-manifest@npm:^6.0.0, npm-pick-manifest@npm:^6.1.0, npm-pick-manifest@npm:^6.1.1": - version: 6.1.1 - resolution: "npm-pick-manifest@npm:6.1.1" +"npm-pick-manifest@npm:^7.0.0, npm-pick-manifest@npm:^7.0.2": + version: 7.0.2 + resolution: "npm-pick-manifest@npm:7.0.2" dependencies: - npm-install-checks: ^4.0.0 - npm-normalize-package-bin: ^1.0.1 - npm-package-arg: ^8.1.2 - semver: ^7.3.4 - checksum: 7a7b9475ae95cf903d37471229efbd12a829a9a7a1020ba36e75768aaa35da4c3a087fde3f06070baf81ec6b2ea2b660f022a1172644e6e7188199d7c1d2954b + npm-install-checks: ^5.0.0 + npm-normalize-package-bin: ^2.0.0 + npm-package-arg: ^9.0.0 + semver: ^7.3.5 + checksum: a93ec449c12219a2be8556837db9ac5332914f304a69469bb6f1f47717adc6e262aa318f79166f763512688abd9c4e4b6a2d83b2dd19753a7abe5f0360f2c8bc languageName: node linkType: hard @@ -16698,21 +16291,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"npm-registry-fetch@npm:^11.0.0": - version: 11.0.0 - resolution: "npm-registry-fetch@npm:11.0.0" - dependencies: - make-fetch-happen: ^9.0.1 - minipass: ^3.1.3 - minipass-fetch: ^1.3.0 - minipass-json-stream: ^1.0.1 - minizlib: ^2.0.0 - npm-package-arg: ^8.0.0 - checksum: dda149cd86f8ee73db1b0a0302fbf59983ef03ad180051caa9aad1de9f1e099aaa77adcda3ca2c3bd9d98958e9e6593bd56ee21d3f660746b0a65fafbf5ae161 - languageName: node - linkType: hard - -"npm-registry-fetch@npm:^13.0.0": +"npm-registry-fetch@npm:^13.0.0, npm-registry-fetch@npm:^13.0.1": version: 13.3.1 resolution: "npm-registry-fetch@npm:13.3.1" dependencies: @@ -16772,18 +16351,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"npmlog@npm:^4.1.2": - version: 4.1.2 - resolution: "npmlog@npm:4.1.2" - dependencies: - are-we-there-yet: ~1.1.2 - console-control-strings: ~1.1.0 - gauge: ~2.7.3 - set-blocking: ~2.0.0 - checksum: edbda9f95ec20957a892de1839afc6fb735054c3accf6fbefe767bac9a639fd5cea2baeac6bd2bcd50a85cb54924d57d9886c81c7fbc2332c2ddd19227504192 - languageName: node - linkType: hard - "npmlog@npm:^7.0.1": version: 7.0.1 resolution: "npmlog@npm:7.0.1" @@ -16821,13 +16388,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"number-is-nan@npm:^1.0.0": - version: 1.0.1 - resolution: "number-is-nan@npm:1.0.1" - checksum: 13656bc9aa771b96cef209ffca31c31a03b507ca6862ba7c3f638a283560620d723d52e626d57892c7fff475f4c36ac07f0600f14544692ff595abff214b9ffb - languageName: node - linkType: hard - "nunjucks@npm:^3.2.0": version: 3.2.4 resolution: "nunjucks@npm:3.2.4" @@ -16934,13 +16494,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"oauth-sign@npm:~0.9.0": - version: 0.9.0 - resolution: "oauth-sign@npm:0.9.0" - checksum: 8f5497a127967866a3c67094c21efd295e46013a94e6e828573c62220e9af568cc1d2d04b16865ba583e430510fa168baf821ea78f355146d8ed7e350fc44c64 - languageName: node - linkType: hard - "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" @@ -17540,32 +17093,34 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"pacote@npm:^11.1.11, pacote@npm:^11.3.5": - version: 11.3.5 - resolution: "pacote@npm:11.3.5" +"pacote@npm:^13.0.3, pacote@npm:^13.6.1": + version: 13.6.2 + resolution: "pacote@npm:13.6.2" dependencies: - "@npmcli/git": ^2.1.0 - "@npmcli/installed-package-contents": ^1.0.6 - "@npmcli/promise-spawn": ^1.2.0 - "@npmcli/run-script": ^1.8.2 - cacache: ^15.0.5 + "@npmcli/git": ^3.0.0 + "@npmcli/installed-package-contents": ^1.0.7 + "@npmcli/promise-spawn": ^3.0.0 + "@npmcli/run-script": ^4.1.0 + cacache: ^16.0.0 chownr: ^2.0.0 fs-minipass: ^2.1.0 infer-owner: ^1.0.4 - minipass: ^3.1.3 - mkdirp: ^1.0.3 - npm-package-arg: ^8.0.1 - npm-packlist: ^2.1.4 - npm-pick-manifest: ^6.0.0 - npm-registry-fetch: ^11.0.0 + minipass: ^3.1.6 + mkdirp: ^1.0.4 + npm-package-arg: ^9.0.0 + npm-packlist: ^5.1.0 + npm-pick-manifest: ^7.0.0 + npm-registry-fetch: ^13.0.1 + proc-log: ^2.0.0 promise-retry: ^2.0.1 - read-package-json-fast: ^2.0.1 + read-package-json: ^5.0.0 + read-package-json-fast: ^2.0.3 rimraf: ^3.0.2 - ssri: ^8.0.1 - tar: ^6.1.0 + ssri: ^9.0.0 + tar: ^6.1.11 bin: pacote: lib/bin.js - checksum: 4fae0b1429be77e69972402dad24775999c92198dadc20f1f7a418f24e268e8bf85faaffc3f778d94c21348645f99bb65ef519fb82776902b556eef934afd932 + checksum: a7b7f97094ab570a23e1c174537e9953a4d53176cc4b18bac77d7728bd89e2b9fa331d0f78fa463add03df79668a918bbdaa2750819504ee39242063abf53c6e languageName: node linkType: hard @@ -17616,14 +17171,14 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"parse-conflict-json@npm:^1.1.1": - version: 1.1.1 - resolution: "parse-conflict-json@npm:1.1.1" +"parse-conflict-json@npm:^2.0.1": + version: 2.0.2 + resolution: "parse-conflict-json@npm:2.0.2" dependencies: - json-parse-even-better-errors: ^2.3.0 - just-diff: ^3.0.1 - just-diff-apply: ^3.0.0 - checksum: 85de37e64bd6c616422aad08fb9e19dfe162a8eea47f9b29393e05d1238e4fa8d3fb8fb77d09c14f7828c96a7b73f0621615b7301955a144b7bee620eaa2bc9e + json-parse-even-better-errors: ^2.3.1 + just-diff: ^5.0.1 + just-diff-apply: ^5.2.0 + checksum: 076f65c958696586daefb153f59d575dfb59648be43116a21b74d5ff69ec63dd56f585a27cc2da56d8e64ca5abf0373d6619b8330c035131f8d1e990c8406378 languageName: node linkType: hard @@ -17725,6 +17280,58 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"parserapiv1@npm:@asyncapi/parser@^2.1.0": + version: 2.1.2 + resolution: "@asyncapi/parser@npm:2.1.2" + dependencies: + "@asyncapi/specs": ^5.1.0 + "@openapi-contrib/openapi-schema-to-json-schema": ~3.2.0 + "@stoplight/json": ^3.20.2 + "@stoplight/json-ref-readers": ^1.2.2 + "@stoplight/json-ref-resolver": ^3.1.5 + "@stoplight/spectral-core": ^1.16.1 + "@stoplight/spectral-functions": ^1.7.2 + "@stoplight/spectral-parsers": ^1.0.2 + "@stoplight/spectral-ref-resolver": ^1.0.3 + "@stoplight/types": ^13.12.0 + "@types/json-schema": ^7.0.11 + "@types/urijs": ^1.19.19 + ajv: ^8.11.0 + ajv-errors: ^3.0.0 + ajv-formats: ^2.1.1 + avsc: ^5.7.5 + js-yaml: ^4.1.0 + jsonpath-plus: ^7.2.0 + node-fetch: 2.6.7 + checksum: fb88d211455937bccd03a84addf546523c0ecd6f6564927227085f803d520e65915ae5aaf0e26dfad23d602f3444451abdb8d3001bc551f30d41e6b35986b307 + languageName: node + linkType: hard + +"parserapiv2@npm:@asyncapi/parser@3.0.0-next-major-spec.8": + version: 3.0.0-next-major-spec.8 + resolution: "@asyncapi/parser@npm:3.0.0-next-major-spec.8" + dependencies: + "@asyncapi/specs": ^6.0.0-next-major-spec.9 + "@openapi-contrib/openapi-schema-to-json-schema": ~3.2.0 + "@stoplight/json-ref-resolver": ^3.1.5 + "@stoplight/spectral-core": ^1.16.1 + "@stoplight/spectral-functions": ^1.7.2 + "@stoplight/spectral-parsers": ^1.0.2 + "@types/json-schema": ^7.0.11 + "@types/urijs": ^1.19.19 + ajv: ^8.11.0 + ajv-errors: ^3.0.0 + ajv-formats: ^2.1.1 + avsc: ^5.7.5 + js-yaml: ^4.1.0 + jsonpath-plus: ^7.2.0 + node-fetch: 2.6.7 + ramldt2jsonschema: ^1.2.3 + webapi-parser: ^0.5.0 + checksum: 13ab7d6410a8c14bbdccd8b74abb60f13966f93144eaafb1fc0f1cbe31a842090ae58678b26d390fc4b54f7483f59db44f2435d3d21ce128abcdc9d74ef9518a + languageName: node + linkType: hard + "parseurl@npm:~1.3.2, parseurl@npm:~1.3.3": version: 1.3.3 resolution: "parseurl@npm:1.3.3" @@ -17868,13 +17475,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"performance-now@npm:^2.1.0": - version: 2.1.0 - resolution: "performance-now@npm:2.1.0" - checksum: 534e641aa8f7cba160f0afec0599b6cecefbb516a2e837b512be0adbe6c1da5550e89c78059c7fabc5c9ffdf6627edabe23eb7c518c4500067a898fa65c2b550 - languageName: node - linkType: hard - "picocolors@npm:^1.0.0": version: 1.0.0 resolution: "picocolors@npm:1.0.0" @@ -18550,13 +18150,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"proc-log@npm:^1.0.0": - version: 1.0.0 - resolution: "proc-log@npm:1.0.0" - checksum: 249605d5b28bfa0499d70da24ab056ad1e082a301f0a46d0ace6e8049cf16aaa0e71d9ea5cab29b620ffb327c18af97f0e012d1db090673447e7c1d33239dd96 - languageName: node - linkType: hard - "proc-log@npm:^2.0.0, proc-log@npm:^2.0.1": version: 2.0.1 resolution: "proc-log@npm:2.0.1" @@ -18694,10 +18287,23 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"protocol-buffers-schema@npm:^3.6.0": - version: 3.6.0 - resolution: "protocol-buffers-schema@npm:3.6.0" - checksum: 8713b5770f6745ddbcdf3bbd03ee020624d506233bb567927a6615a6f69a5bd620a5f49597f34f4115792b853a4c9cb9e2d5d6b930a1c04bf198023e45c1c349 +"protobufjs@npm:^7.2.6": + version: 7.2.6 + resolution: "protobufjs@npm:7.2.6" + dependencies: + "@protobufjs/aspromise": ^1.1.2 + "@protobufjs/base64": ^1.1.2 + "@protobufjs/codegen": ^2.0.4 + "@protobufjs/eventemitter": ^1.1.0 + "@protobufjs/fetch": ^1.1.0 + "@protobufjs/float": ^1.0.2 + "@protobufjs/inquire": ^1.1.0 + "@protobufjs/path": ^1.1.2 + "@protobufjs/pool": ^1.1.0 + "@protobufjs/utf8": ^1.1.0 + "@types/node": ">=13.7.0" + long: ^5.0.0 + checksum: 3c62e48f7d50017ac3b0dcd2a58e617cf858f9fba56a488bd48b9aa3482893a75540052dbcb3c12dfbaab42b1d04964611175faf06bdadcd33a4ebac982a511e languageName: node linkType: hard @@ -18732,7 +18338,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"psl@npm:^1.1.28, psl@npm:^1.1.33": +"psl@npm:^1.1.33": version: 1.9.0 resolution: "psl@npm:1.9.0" checksum: 20c4277f640c93d393130673f392618e9a8044c6c7bf61c53917a0fddb4952790f5f362c6c730a9c32b124813e173733f9895add8d26f566ed0ea0654b2e711d @@ -18829,13 +18435,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"qs@npm:~6.5.2": - version: 6.5.3 - resolution: "qs@npm:6.5.3" - checksum: 6f20bf08cabd90c458e50855559539a28d00b2f2e7dddcb66082b16a43188418cb3cb77cbd09268bcef6022935650f0534357b8af9eeb29bf0f27ccb17655692 - languageName: node - linkType: hard - "querystringify@npm:^2.1.1": version: 2.2.0 resolution: "querystringify@npm:2.2.0" @@ -19164,10 +18763,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"read-cmd-shim@npm:^2.0.0": - version: 2.0.0 - resolution: "read-cmd-shim@npm:2.0.0" - checksum: 024f0a092d3630ad344af63eb0539bce90978883dd06a93e7bfbb26913168ab034473eae4a85685ea76a982eb31b0e8e16dee9c1138dabb3a925e7c4757952bc +"read-cmd-shim@npm:^3.0.0": + version: 3.0.1 + resolution: "read-cmd-shim@npm:3.0.1" + checksum: 79fe66aa78eddcca8dc196765ae3168b3a56e2b69ba54071525eb00a9eeee8cc83b3d5f784432c3d8ce868787fdc059b1a1e0b605246b5108c9003fc927ea263 languageName: node linkType: hard @@ -19196,7 +18795,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"read-package-json-fast@npm:^2.0.1, read-package-json-fast@npm:^2.0.2, read-package-json-fast@npm:^2.0.3": +"read-package-json-fast@npm:^2.0.2, read-package-json-fast@npm:^2.0.3": version: 2.0.3 resolution: "read-package-json-fast@npm:2.0.3" dependencies: @@ -19363,7 +18962,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"readable-stream@npm:^2.0.1, readable-stream@npm:^2.0.6, readable-stream@npm:~2.3.6": +"readable-stream@npm:^2.0.1, readable-stream@npm:~2.3.6": version: 2.3.8 resolution: "readable-stream@npm:2.3.8" dependencies: @@ -19679,34 +19278,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"request@npm:^2.88.2": - version: 2.88.2 - resolution: "request@npm:2.88.2" - dependencies: - aws-sign2: ~0.7.0 - aws4: ^1.8.0 - caseless: ~0.12.0 - combined-stream: ~1.0.6 - extend: ~3.0.2 - forever-agent: ~0.6.1 - form-data: ~2.3.2 - har-validator: ~5.1.3 - http-signature: ~1.2.0 - is-typedarray: ~1.0.0 - isstream: ~0.1.2 - json-stringify-safe: ~5.0.1 - mime-types: ~2.1.19 - oauth-sign: ~0.9.0 - performance-now: ^2.1.0 - qs: ~6.5.2 - safe-buffer: ^5.1.2 - tough-cookie: ~2.5.0 - tunnel-agent: ^0.6.0 - uuid: ^3.3.2 - checksum: 4e112c087f6eabe7327869da2417e9d28fcd0910419edd2eb17b6acfc4bfa1dad61954525949c228705805882d8a98a86a0ea12d7f739c01ee92af7062996983 - languageName: node - linkType: hard - "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -20019,7 +19590,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"safe-buffer@npm:5.2.1, safe-buffer@npm:>=5.1.0, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.2, safe-buffer@npm:~5.2.0": +"safe-buffer@npm:5.2.1, safe-buffer@npm:>=5.1.0, safe-buffer@npm:^5.1.0, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491 @@ -20051,7 +19622,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:^2.0.2, safer-buffer@npm:^2.1.0, safer-buffer@npm:~2.1.0": +"safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:~2.1.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" checksum: cab8f25ae6f1434abee8d80023d7e72b598cf1327164ddab31003c51215526801e40b66c5e65d658a0af1e9d6478cadcb4c745f4bd6751f97d8644786c0978b0 @@ -20308,7 +19879,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"set-blocking@npm:^2.0.0, set-blocking@npm:~2.0.0": +"set-blocking@npm:^2.0.0": version: 2.0.0 resolution: "set-blocking@npm:2.0.0" checksum: 6e65a05f7cf7ebdf8b7c75b101e18c0b7e3dff4940d480efed8aad3a36a4005140b660fa1d804cb8bce911cac290441dc728084a30504d3516ac2ff7ad607b02 @@ -20429,7 +20000,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"signal-exit@npm:3.0.7, signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": +"signal-exit@npm:3.0.7, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 @@ -20593,17 +20164,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"socks-proxy-agent@npm:^6.0.0": - version: 6.2.1 - resolution: "socks-proxy-agent@npm:6.2.1" - dependencies: - agent-base: ^6.0.2 - debug: ^4.3.3 - socks: ^2.6.2 - checksum: 9ca089d489e5ee84af06741135c4b0d2022977dad27ac8d649478a114cdce87849e8d82b7c22b51501a4116e231241592946fc7fae0afc93b65030ee57084f58 - languageName: node - linkType: hard - "socks-proxy-agent@npm:^7.0.0": version: 7.0.0 resolution: "socks-proxy-agent@npm:7.0.0" @@ -20870,27 +20430,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"sshpk@npm:^1.7.0": - version: 1.17.0 - resolution: "sshpk@npm:1.17.0" - dependencies: - asn1: ~0.2.3 - assert-plus: ^1.0.0 - bcrypt-pbkdf: ^1.0.0 - dashdash: ^1.12.0 - ecc-jsbn: ~0.1.1 - getpass: ^0.1.1 - jsbn: ~0.1.0 - safer-buffer: ^2.0.2 - tweetnacl: ~0.14.0 - bin: - sshpk-conv: bin/sshpk-conv - sshpk-sign: bin/sshpk-sign - sshpk-verify: bin/sshpk-verify - checksum: ba109f65c8e6c35133b8e6ed5576abeff8aa8d614824b7275ec3ca308f081fef483607c28d97780c1e235818b0f93ed8c8b56d0a5968d5a23fd6af57718c7597 - languageName: node - linkType: hard - "ssri@npm:9.0.1, ssri@npm:^9.0.0": version: 9.0.1 resolution: "ssri@npm:9.0.1" @@ -20909,15 +20448,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"ssri@npm:^8.0.0, ssri@npm:^8.0.1": - version: 8.0.1 - resolution: "ssri@npm:8.0.1" - dependencies: - minipass: ^3.1.1 - checksum: bc447f5af814fa9713aa201ec2522208ae0f4d8f3bda7a1f445a797c7b929a02720436ff7c478fb5edc4045adb02b1b88d2341b436a80798734e2494f1067b36 - languageName: node - linkType: hard - "stable@npm:^0.1.8": version: 0.1.8 resolution: "stable@npm:0.1.8" @@ -21020,17 +20550,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"string-width@npm:^1.0.1": - version: 1.0.2 - resolution: "string-width@npm:1.0.2" - dependencies: - code-point-at: ^1.0.0 - is-fullwidth-code-point: ^1.0.0 - strip-ansi: ^3.0.0 - checksum: 5c79439e95bc3bd7233a332c5f5926ab2ee90b23816ed4faa380ce3b2576d7800b0a5bb15ae88ed28737acc7ea06a518c2eef39142dd727adad0e45c776cd37e - languageName: node - linkType: hard - "string-width@npm:^5.0.0, string-width@npm:^5.0.1, string-width@npm:^5.1.2": version: 5.1.2 resolution: "string-width@npm:5.1.2" @@ -21120,15 +20639,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"strip-ansi@npm:^3.0.0, strip-ansi@npm:^3.0.1": - version: 3.0.1 - resolution: "strip-ansi@npm:3.0.1" - dependencies: - ansi-regex: ^2.0.0 - checksum: 9b974de611ce5075c70629c00fa98c46144043db92ae17748fb780f706f7a789e9989fd10597b7c2053ae8d1513fd707816a91f1879b2f71e6ac0b6a863db465 - languageName: node - linkType: hard - "strip-ansi@npm:^7.0.1": version: 7.1.0 resolution: "strip-ansi@npm:7.1.0" @@ -21355,6 +20865,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"sync-fetch@npm:^0.5.2": + version: 0.5.2 + resolution: "sync-fetch@npm:0.5.2" + dependencies: + node-fetch: ^2.6.1 + checksum: f5be31666bfec847c30c1ac1e806dbb4e2a87a9c84c315e227130c8425cfff81a6c9ee59e0ef970392b009ff2a1d5c6792985d2b15a4c07343a8ab24c6b7fe50 + languageName: node + linkType: hard + "tapable@npm:^1.0.0": version: 1.1.3 resolution: "tapable@npm:1.1.3" @@ -21408,7 +20927,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"tar@npm:^6.0.2, tar@npm:^6.1.0, tar@npm:^6.1.11, tar@npm:^6.1.2": +"tar@npm:^6.1.11, tar@npm:^6.1.2": version: 6.2.0 resolution: "tar@npm:6.2.0" dependencies: @@ -21679,13 +21198,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"tiny-merge-patch@npm:^0.1.2": - version: 0.1.2 - resolution: "tiny-merge-patch@npm:0.1.2" - checksum: 66b0cc5098adc8767459f2007f9d73f60b47a0e87fa1200076c73a7ebc41dfe4c3842fdc609716c5165d1b2ae0a4140a00d3c78256587f0b3a494e03774ac340 - languageName: node - linkType: hard - "tiny-warning@npm:^1.0.0": version: 1.0.3 resolution: "tiny-warning@npm:1.0.3" @@ -21804,16 +21316,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"tough-cookie@npm:~2.5.0": - version: 2.5.0 - resolution: "tough-cookie@npm:2.5.0" - dependencies: - psl: ^1.1.28 - punycode: ^2.1.1 - checksum: 16a8cd090224dd176eee23837cbe7573ca0fa297d7e468ab5e1c02d49a4e9a97bb05fef11320605eac516f91d54c57838a25864e8680e27b069a5231d8264977 - languageName: node - linkType: hard - "tr46@npm:^1.0.1": version: 1.0.1 resolution: "tr46@npm:1.0.1" @@ -21864,10 +21366,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"treeverse@npm:^1.0.4": - version: 1.0.4 - resolution: "treeverse@npm:1.0.4" - checksum: 712640acd811060ff552a3c761f700d18d22a4da544d31b4e290817ac4bbbfcfe33b58f85e7a5787e6ff7351d3a9100670721a289ca14eb87b36ad8a0c20ebd8 +"treeverse@npm:^2.0.0": + version: 2.0.0 + resolution: "treeverse@npm:2.0.0" + checksum: 3c6b2b890975a4d42c86b9a0f1eb932b4450db3fa874be5c301c4f5e306fd76330c6a490cf334b0937b3a44b049787ba5d98c88bc7b140f34fdb3ab1f83e5269 languageName: node linkType: hard @@ -22056,15 +21558,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"tunnel-agent@npm:^0.6.0": - version: 0.6.0 - resolution: "tunnel-agent@npm:0.6.0" - dependencies: - safe-buffer: ^5.0.1 - checksum: 05f6510358f8afc62a057b8b692f05d70c1782b70db86d6a1e0d5e28a32389e52fa6e7707b6c5ecccacc031462e4bc35af85ecfe4bbc341767917b7cf6965711 - languageName: node - linkType: hard - "tunnel@npm:0.0.6": version: 0.0.6 resolution: "tunnel@npm:0.0.6" @@ -22081,13 +21574,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"tweetnacl@npm:^0.14.3, tweetnacl@npm:~0.14.0": - version: 0.14.5 - resolution: "tweetnacl@npm:0.14.5" - checksum: 6061daba1724f59473d99a7bb82e13f211cdf6e31315510ae9656fefd4779851cb927adad90f3b488c8ed77c106adc0421ea8055f6f976ff21b27c5c4e918487 - languageName: node - linkType: hard - "twos-complement-buffer@npm:0.0.1": version: 0.0.1 resolution: "twos-complement-buffer@npm:0.0.1" @@ -22388,6 +21874,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"undici-types@npm:~5.26.4": + version: 5.26.5 + resolution: "undici-types@npm:5.26.5" + checksum: 3192ef6f3fd5df652f2dc1cd782b49d6ff14dc98e5dced492aa8a8c65425227da5da6aafe22523c67f035a272c599bb89cfe803c1db6311e44bed3042fc25487 + languageName: node + linkType: hard + "unherit@npm:^1.0.4": version: 1.1.3 resolution: "unherit@npm:1.1.3" @@ -22474,15 +21967,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"unique-filename@npm:^1.1.1": - version: 1.1.1 - resolution: "unique-filename@npm:1.1.1" - dependencies: - unique-slug: ^2.0.0 - checksum: cf4998c9228cc7647ba7814e255dec51be43673903897b1786eff2ac2d670f54d4d733357eb08dea969aa5e6875d0e1bd391d668fbdb5a179744e7c7551a6f80 - languageName: node - linkType: hard - "unique-filename@npm:^2.0.0": version: 2.0.1 resolution: "unique-filename@npm:2.0.1" @@ -22501,15 +21985,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"unique-slug@npm:^2.0.0": - version: 2.0.2 - resolution: "unique-slug@npm:2.0.2" - dependencies: - imurmurhash: ^0.1.4 - checksum: 5b6876a645da08d505dedb970d1571f6cebdf87044cb6b740c8dbb24f0d6e1dc8bdbf46825fd09f994d7cf50760e6f6e063cfa197d51c5902c00a861702eb75a - languageName: node - linkType: hard - "unique-slug@npm:^3.0.0": version: 3.0.0 resolution: "unique-slug@npm:3.0.0" @@ -22888,15 +22363,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"uuid@npm:^3.3.2": - version: 3.4.0 - resolution: "uuid@npm:3.4.0" - bin: - uuid: ./bin/uuid - checksum: 58de2feed61c59060b40f8203c0e4ed7fd6f99d42534a499f1741218a1dd0c129f4aa1de797bcf822c8ea5da7e4137aa3673431a96dae729047f7aca7b27866f - languageName: node - linkType: hard - "v8-compile-cache-lib@npm:^3.0.1": version: 3.0.1 resolution: "v8-compile-cache-lib@npm:3.0.1" @@ -22973,17 +22439,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"verror@npm:1.10.0": - version: 1.10.0 - resolution: "verror@npm:1.10.0" - dependencies: - assert-plus: ^1.0.0 - core-util-is: 1.0.2 - extsprintf: ^1.2.0 - checksum: c431df0bedf2088b227a4e051e0ff4ca54df2c114096b0c01e1cbaadb021c30a04d7dd5b41ab277bcd51246ca135bf931d4c4c796ecae7a4fef6d744ecef36ea - languageName: node - linkType: hard - "vfile-location@npm:^3.0.0, vfile-location@npm:^3.2.0": version: 3.2.0 resolution: "vfile-location@npm:3.2.0" @@ -23483,7 +22938,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"wide-align@npm:^1.1.0, wide-align@npm:^1.1.5": +"wide-align@npm:^1.1.5": version: 1.1.5 resolution: "wide-align@npm:1.1.5" dependencies: @@ -23622,7 +23077,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"write-file-atomic@npm:^3.0.0, write-file-atomic@npm:^3.0.3": +"write-file-atomic@npm:^3.0.0": version: 3.0.3 resolution: "write-file-atomic@npm:3.0.3" dependencies: @@ -23634,7 +23089,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"write-file-atomic@npm:^4.0.2": +"write-file-atomic@npm:^4.0.0, write-file-atomic@npm:^4.0.2": version: 4.0.2 resolution: "write-file-atomic@npm:4.0.2" dependencies: @@ -23830,13 +23285,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"yaml-ast-parser@npm:0.0.43": - version: 0.0.43 - resolution: "yaml-ast-parser@npm:0.0.43" - checksum: fb5df4c067b6ccbd00953a46faf6ff27f0e290d623c712dc41f330251118f110e22cfd184bbff498bd969cbcda3cd27e0f9d0adb9e6d90eb60ccafc0d8e28077 - languageName: node - linkType: hard - "yaml-eslint-parser@npm:^1.2.1": version: 1.2.2 resolution: "yaml-eslint-parser@npm:1.2.2" From 115ee830e9a04c494eed6c037464a22d3b305f18 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 29 Feb 2024 16:56:47 +0100 Subject: [PATCH 254/479] chore: add ci workflow to run asyncapi generator --- .github/workflows/node.yaml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index f2c1da1f84..fa152304b6 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -523,6 +523,34 @@ jobs: env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + asyncapi-generation: + name: AsyncAPI Generation + runs-on: ubuntu-latest + continue-on-error: true + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version-file: ".node-version" + - name: Prepare Environment + run: | + cd packages + yarn + env: + CI: true + - name: Run generator + run: | + cd packages/live-status-gateway + + yarn gendocs + yarn genserver + env: + CI: true + publish-docs: name: Publish Docs runs-on: ubuntu-latest From 87573f09806030e4664d499a6e7dc15f29e059f6 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 29 Feb 2024 16:57:54 +0100 Subject: [PATCH 255/479] chore: add ci workflow to run openapi generator --- .github/workflows/node.yaml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index fa152304b6..ea7ee42812 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -550,6 +550,34 @@ jobs: yarn genserver env: CI: true + openapi-generation: + name: OpenAPI Generation + runs-on: ubuntu-latest + continue-on-error: true + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version-file: ".node-version" + - name: Prepare Environment + run: | + cd packages + yarn + env: + CI: true + - name: Run generator + run: | + cd packages/openapi + + yarn gendocs + yarn genserver + yarn genclient:ts + env: + CI: true publish-docs: name: Publish Docs From 3eb0f4a11017a65cfd17266bb107e6047e7d1fe2 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 1 Mar 2024 09:48:27 +0100 Subject: [PATCH 256/479] chore: fix type errors --- meteor/server/publications/studio.ts | 9 +++------ packages/corelib/src/pubsub.ts | 5 +++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/meteor/server/publications/studio.ts b/meteor/server/publications/studio.ts index 4c556731a9..eaef1d5448 100644 --- a/meteor/server/publications/studio.ts +++ b/meteor/server/publications/studio.ts @@ -36,7 +36,6 @@ import { PeripheralDevicePubSub, PeripheralDevicePubSubCollectionsNames, } from '@sofie-automation/shared-lib/dist/pubsub/peripheralDevice' -import { PackageInfoDB } from '@sofie-automation/corelib/dist/dataModel/PackageInfos' meteorPublish(CorelibPubSub.studios, async function (studioIds: StudioId[] | null, token: string | undefined) { check(studioIds, Match.Maybe(Array)) @@ -118,13 +117,11 @@ meteorPublish( } ) -meteorPublish(CorelibPubSub.packageInfos, async function (deviceId, token) { +meteorPublish(CorelibPubSub.packageInfos, async function (deviceId: PeripheralDeviceId, token: string | undefined) { if (!deviceId) throw new Meteor.Error(400, 'deviceId argument missing') - const modifier: FindOptions = { - fields: {}, - } + if (await PeripheralDeviceReadAccess.peripheralDeviceContent(deviceId, { userId: this.userId, token })) { - return PackageInfos.findWithCursor({ deviceId }, modifier) + return PackageInfos.findWithCursor({ deviceId }) } return null }) diff --git a/packages/corelib/src/pubsub.ts b/packages/corelib/src/pubsub.ts index 512f0f025e..5a81f29c82 100644 --- a/packages/corelib/src/pubsub.ts +++ b/packages/corelib/src/pubsub.ts @@ -43,7 +43,7 @@ import { SegmentPlayoutId, ShowStyleVariantId, } from './dataModel/Ids' -import { PackageInfoDB } from '@sofie-automation/corelib/src/dataModel/PackageInfos' +import { PackageInfoDB } from './dataModel/PackageInfos' /** * Ids of possible DDP subscriptions for any the UI and gateways accessing the Rundown & RundownPlaylist model. @@ -333,7 +333,7 @@ export interface CorelibPubSubTypes { studioIds: StudioId[], token?: string ) => CollectionName.PackageContainerStatuses - [CorelibPubSub.packageInfos]: (deviceId: PeripheralDeviceId, token?: string) => PackageInfoDB + [CorelibPubSub.packageInfos]: (deviceId: PeripheralDeviceId, token?: string) => CollectionName.PackageInfos } export type CorelibPubSubCollections = { @@ -349,6 +349,7 @@ export type CorelibPubSubCollections = { [CollectionName.IngestDataCache]: IngestDataCacheObj [CollectionName.PartInstances]: DBPartInstance [CollectionName.PackageContainerStatuses]: PackageContainerStatusDB + [CollectionName.PackageInfos]: PackageInfoDB [CollectionName.Parts]: DBPart [CollectionName.PeripheralDevices]: PeripheralDevice [CollectionName.PieceInstances]: PieceInstance From c10d27d1c57d89d996a9f4addbb260318e7b763b Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 1 Mar 2024 09:56:41 +0100 Subject: [PATCH 257/479] fix: remove duplicated migration --- meteor/server/migration/X_X_X.ts | 101 ------------------------------- 1 file changed, 101 deletions(-) diff --git a/meteor/server/migration/X_X_X.ts b/meteor/server/migration/X_X_X.ts index 6f957523de..eb315e9ec4 100644 --- a/meteor/server/migration/X_X_X.ts +++ b/meteor/server/migration/X_X_X.ts @@ -1,18 +1,5 @@ import { addMigrationSteps } from './databaseMigration' import { CURRENT_SYSTEM_VERSION } from './currentSystemVersion' -import { AdLibActions, AdLibPieces, ExpectedPackages, Pieces } from '../collections' -import { ExpectedPackageDBType } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' -import _ from 'underscore' -import { - AdLibActionId, - PartId, - PieceId, - RundownBaselineAdLibActionId, -} from '@sofie-automation/corelib/dist/dataModel/Ids' -import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' -import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' -import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' -import { protectString } from '@sofie-automation/corelib/dist/protectedString' /* * ************************************************************************************** @@ -24,94 +11,6 @@ import { protectString } from '@sofie-automation/corelib/dist/protectedString' * ************************************************************************************** */ -const EXPECTED_PACKAGE_TYPES_ADDED_PART_ID = [ - ExpectedPackageDBType.PIECE, - ExpectedPackageDBType.ADLIB_PIECE, - ExpectedPackageDBType.ADLIB_ACTION, -] - export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [ // Add your migration here - - { - id: `ExpectedPackageDBFromAdLibAction and ExpectedPackageDBFromPiece add partId`, - canBeRunAutomatically: true, - validate: async () => { - const objectCount = await ExpectedPackages.countDocuments({ - fromPieceType: { $in: EXPECTED_PACKAGE_TYPES_ADDED_PART_ID }, - partId: { $exists: false }, - }) - - if (objectCount) { - return `object needs to be updated` - } - return false - }, - migrate: async () => { - const objects = await ExpectedPackages.findFetchAsync({ - fromPieceType: { $in: EXPECTED_PACKAGE_TYPES_ADDED_PART_ID }, - partId: { $exists: false }, - }) - - const neededPieceIds: Array = _.compact( - objects.map((obj) => obj.pieceId) - ) - const [pieces, adlibPieces, adlibActions] = await Promise.all([ - Pieces.findFetchAsync( - { - _id: { $in: neededPieceIds as PieceId[] }, - }, - { - projection: { - _id: 1, - startPartId: 1, - }, - } - ) as Promise[]>, - AdLibPieces.findFetchAsync( - { - _id: { $in: neededPieceIds as PieceId[] }, - }, - { - projection: { - _id: 1, - partId: 1, - }, - } - ) as Promise[]>, - AdLibActions.findFetchAsync( - { - _id: { $in: neededPieceIds as AdLibActionId[] }, - }, - { - projection: { - _id: 1, - partId: 1, - }, - } - ) as Promise[]>, - ]) - - const partIdLookup = new Map() - for (const piece of pieces) { - partIdLookup.set(piece._id, piece.startPartId) - } - for (const adlib of adlibPieces) { - if (adlib.partId) partIdLookup.set(adlib._id, adlib.partId) - } - for (const action of adlibActions) { - partIdLookup.set(action._id, action.partId) - } - - for (const expectedPackage of objects) { - if (!expectedPackage.pieceId) continue - - await ExpectedPackages.mutableCollection.updateAsync(expectedPackage._id, { - $set: { - partId: partIdLookup.get(expectedPackage.pieceId) ?? protectString(''), - }, - }) - } - }, - }, ]) From 1917daac71b33b20f262d342a72c34e1ba4d19a6 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 1 Feb 2024 17:18:57 +0000 Subject: [PATCH 258/479] chore: add tests for PlayoutSegmentModelImpl --- .../__tests__/PlayoutSegmentModelImpl.spec.ts | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 packages/job-worker/src/playout/model/implementation/__tests__/PlayoutSegmentModelImpl.spec.ts diff --git a/packages/job-worker/src/playout/model/implementation/__tests__/PlayoutSegmentModelImpl.spec.ts b/packages/job-worker/src/playout/model/implementation/__tests__/PlayoutSegmentModelImpl.spec.ts new file mode 100644 index 0000000000..ceacc7a5cf --- /dev/null +++ b/packages/job-worker/src/playout/model/implementation/__tests__/PlayoutSegmentModelImpl.spec.ts @@ -0,0 +1,111 @@ +import { DBSegment, SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' +import { PlayoutSegmentModelImpl } from '../PlayoutSegmentModelImpl' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' + +describe('PlayoutSegmentModelImpl', () => { + function createBasicDBSegment(): DBSegment { + return { + _id: protectString('abc'), + rundownId: protectString('rd0'), + externalId: 'ext1', + externalModified: 100000, + _rank: 1, + name: 'test segment', + } + } + + it('segment getter', async () => { + const segment = createBasicDBSegment() + const model = new PlayoutSegmentModelImpl(segment, []) + + expect(model.segment).toBe(segment) + }) + + describe('getPartIds', () => { + it('no parts', async () => { + const segment = createBasicDBSegment() + const model = new PlayoutSegmentModelImpl(segment, []) + + expect(model.getPartIds()).toEqual([]) + }) + it('with parts', async () => { + const fakePart: DBPart = { _id: protectString('part0'), _rank: 1 } as any + const fakePart2: DBPart = { _id: protectString('part1'), _rank: 2 } as any + const segment = createBasicDBSegment() + const model = new PlayoutSegmentModelImpl(segment, [fakePart, fakePart2]) + + expect(model.getPartIds()).toEqual([fakePart._id, fakePart2._id]) + }) + it('with parts ensuring order', async () => { + const fakePart: DBPart = { _id: protectString('part0'), _rank: 5 } as any + const fakePart2: DBPart = { _id: protectString('part1'), _rank: 2 } as any + const segment = createBasicDBSegment() + const model = new PlayoutSegmentModelImpl(segment, [fakePart, fakePart2]) + + expect(model.getPartIds()).toEqual([fakePart2._id, fakePart._id]) + }) + }) + + describe('getPart', () => { + it('no parts', async () => { + const segment = createBasicDBSegment() + const model = new PlayoutSegmentModelImpl(segment, []) + + expect(model.getPart(protectString('missing'))).toBeUndefined() + }) + it('with other parts', async () => { + const fakePart: DBPart = { _id: protectString('part0'), _rank: 1 } as any + const fakePart2: DBPart = { _id: protectString('part1'), _rank: 2 } as any + const segment = createBasicDBSegment() + const model = new PlayoutSegmentModelImpl(segment, [fakePart, fakePart2]) + + expect(model.getPart(protectString('missing'))).toBeUndefined() + }) + it('with found part', async () => { + const fakePart: DBPart = { _id: protectString('part0'), _rank: 1 } as any + const fakePart2: DBPart = { _id: protectString('part1'), _rank: 2 } as any + const segment = createBasicDBSegment() + const model = new PlayoutSegmentModelImpl(segment, [fakePart, fakePart2]) + + expect(model.getPart(fakePart._id)).toBe(fakePart) + }) + }) + + describe('setScratchpadRank', () => { + it('not scratchpad segment', async () => { + const segment = createBasicDBSegment() + const originalRank = segment._rank + const model = new PlayoutSegmentModelImpl(segment, []) + + expect(() => model.setScratchpadRank(originalRank + 1)).toThrow( + /setScratchpadRank can only be used on a SCRATCHPAD segment/ + ) + expect(model.segment._rank).toBe(originalRank) + }) + + it('is scratchpad segment', async () => { + const segment = createBasicDBSegment() + segment.orphaned = SegmentOrphanedReason.SCRATCHPAD + + const originalRank = segment._rank + const model = new PlayoutSegmentModelImpl(segment, []) + + model.setScratchpadRank(originalRank + 1) + expect(model.segment._rank).toBe(originalRank + 1) + }) + + it('not orphaned segment', async () => { + const segment = createBasicDBSegment() + segment.orphaned = SegmentOrphanedReason.DELETED + + const originalRank = segment._rank + const model = new PlayoutSegmentModelImpl(segment, []) + + expect(() => model.setScratchpadRank(originalRank + 1)).toThrow( + /setScratchpadRank can only be used on a SCRATCHPAD segment/ + ) + expect(model.segment._rank).toBe(originalRank) + }) + }) +}) From 6d18188d07a9f388480ed0f6aa86caceabc7aca3 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 4 Mar 2024 14:45:25 +0000 Subject: [PATCH 259/479] chore: add tests for PlayoutRundownModelImpl --- packages/job-worker/src/ingest/commit.ts | 12 +- .../src/playout/model/PlayoutRundownModel.ts | 6 +- .../implementation/PlayoutRundownModelImpl.ts | 21 +- .../__tests__/PlayoutRundownModelImpl.spec.ts | 342 ++++++++++++++++++ 4 files changed, 360 insertions(+), 21 deletions(-) create mode 100644 packages/job-worker/src/playout/model/implementation/__tests__/PlayoutRundownModelImpl.spec.ts diff --git a/packages/job-worker/src/ingest/commit.ts b/packages/job-worker/src/ingest/commit.ts index 0363596114..b609834493 100644 --- a/packages/job-worker/src/ingest/commit.ts +++ b/packages/job-worker/src/ingest/commit.ts @@ -729,16 +729,6 @@ async function removeSegments( async function validateScratchpad(_context: JobContext, playoutModel: PlayoutModel) { for (const rundown of playoutModel.rundowns) { - const scratchpadSegment = rundown.getScratchpadSegment() - - if (scratchpadSegment) { - // Ensure the _rank is just before the real content - const otherSegmentsInRundown = rundown.segments.filter( - (s) => s.segment.orphaned !== SegmentOrphanedReason.SCRATCHPAD - ) - const minNormalRank = Math.min(0, ...otherSegmentsInRundown.map((s) => s.segment._rank)) - - rundown.setScratchpadSegmentRank(minNormalRank - 1) - } + rundown.updateScratchpadSegmentRank() } } diff --git a/packages/job-worker/src/playout/model/PlayoutRundownModel.ts b/packages/job-worker/src/playout/model/PlayoutRundownModel.ts index 4ed16cb94c..35faf85b68 100644 --- a/packages/job-worker/src/playout/model/PlayoutRundownModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutRundownModel.ts @@ -63,9 +63,7 @@ export interface PlayoutRundownModel { */ getScratchpadSegment(): PlayoutSegmentModel | undefined /** - * Set the rank of the Scratchpad Segment in this Rundown - * Throws if the segment does not exists - * @param rank New rank + * Update the rank of the Scratchpad Segment in this Rundown, if it exists */ - setScratchpadSegmentRank(rank: number): void + updateScratchpadSegmentRank(): void } diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutRundownModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutRundownModelImpl.ts index ae41598f2e..1c967c6dac 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutRundownModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutRundownModelImpl.ts @@ -67,14 +67,12 @@ export class PlayoutRundownModelImpl implements PlayoutRundownModel { const existingSegment = this.segments.find((s) => s.segment.orphaned === SegmentOrphanedReason.SCRATCHPAD) if (existingSegment) throw UserError.create(UserErrorMessage.ScratchpadAlreadyActive) - const minSegmentRank = Math.min(0, ...this.segments.map((s) => s.segment._rank)) - const segmentId: SegmentId = getRandomId() this.#segments.unshift( new PlayoutSegmentModelImpl( { _id: segmentId, - _rank: minSegmentRank - 1, + _rank: calculateRankForScratchpadSegment(this.#segments), externalId: '__scratchpad__', externalModified: getCurrentTime(), rundownId: this.rundown._id, @@ -105,13 +103,24 @@ export class PlayoutRundownModelImpl implements PlayoutRundownModel { return this.#segments.find((s) => s.segment.orphaned === SegmentOrphanedReason.SCRATCHPAD) } - setScratchpadSegmentRank(rank: number): void { + updateScratchpadSegmentRank(): void { const segment = this.#segments.find((s) => s.segment.orphaned === SegmentOrphanedReason.SCRATCHPAD) - if (!segment) throw new Error('Scratchpad segment does not exist!') + if (!segment) return - segment.setScratchpadRank(rank) + segment.setScratchpadRank(calculateRankForScratchpadSegment(this.#segments)) this.#segments.sort((a, b) => a.segment._rank - b.segment._rank) this.#scratchPadSegmentHasChanged = true } } + +function calculateRankForScratchpadSegment(segments: readonly PlayoutSegmentModel[]) { + // Ensure the _rank is just before the real content + + return ( + Math.min( + 0, + ...segments.map((s) => (s.segment.orphaned === SegmentOrphanedReason.SCRATCHPAD ? 0 : s.segment._rank)) + ) - 1 + ) +} diff --git a/packages/job-worker/src/playout/model/implementation/__tests__/PlayoutRundownModelImpl.spec.ts b/packages/job-worker/src/playout/model/implementation/__tests__/PlayoutRundownModelImpl.spec.ts new file mode 100644 index 0000000000..64d094f724 --- /dev/null +++ b/packages/job-worker/src/playout/model/implementation/__tests__/PlayoutRundownModelImpl.spec.ts @@ -0,0 +1,342 @@ +import { DBSegment, SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' +import { PlayoutSegmentModelImpl } from '../PlayoutSegmentModelImpl' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { PlayoutRundownModelImpl } from '../PlayoutRundownModelImpl' +import { SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { ReadonlyDeep } from 'type-fest' +import { restartRandomId } from '../../../../__mocks__/nanoid' +import { UserErrorMessage } from '@sofie-automation/corelib/dist/error' + +describe('PlayoutRundownModelImpl', () => { + function createBasicDBRundown(): DBRundown { + return { + _id: protectString('rd0'), + organizationId: null, + studioId: protectString('studio0'), + showStyleBaseId: protectString('ssb0'), + showStyleVariantId: protectString('ssv0'), + created: 0, + modified: 0, + externalId: 'rd0', + name: `my rundown`, + importVersions: null as any, + timing: null as any, + externalNRCSName: 'FAKE', + playlistId: protectString('playlist0'), + } + } + + function createBasicDBSegment(id: string, rank: number): DBSegment { + return { + _id: protectString(id), + rundownId: protectString('rd0'), + externalId: id, + externalModified: 100000, + _rank: rank, + name: `${id} segment`, + } + } + + it('rundown getter', async () => { + const rundown = createBasicDBRundown() + const model = new PlayoutRundownModelImpl(rundown, [], []) + + expect(model.rundown).toBe(rundown) + }) + + it('getSegment', async () => { + const rundown = createBasicDBRundown() + + const segment = createBasicDBSegment('seg0', 0) + const segmentModel = new PlayoutSegmentModelImpl(segment, []) + + const segment2 = createBasicDBSegment('seg1', 5) + const segment2Model = new PlayoutSegmentModelImpl(segment2, []) + + const model = new PlayoutRundownModelImpl(rundown, [segmentModel, segment2Model], []) + + expect(model.getSegment(segment._id)).toBe(segmentModel) + expect(model.getSegment(segment2._id)).toBe(segment2Model) + + expect(model.getSegment(protectString('missing-id'))).toBeUndefined() + }) + + it('getSegmentIds', async () => { + const rundown = createBasicDBRundown() + + const segment = createBasicDBSegment('seg0', 0) + const segmentModel = new PlayoutSegmentModelImpl(segment, []) + + const segment2 = createBasicDBSegment('seg1', 5) + const segment2Model = new PlayoutSegmentModelImpl(segment2, []) + + const model = new PlayoutRundownModelImpl( + rundown, + [ + // Intentionally reverse the order + segment2Model, + segmentModel, + ], + [] + ) + + expect(model.getSegmentIds()).toEqual([segment._id, segment2._id]) + }) + + describe('insertScratchpadSegment', () => { + beforeEach(() => { + restartRandomId() + }) + + it('ok', async () => { + const rundown = createBasicDBRundown() + + const segment = createBasicDBSegment('seg0', 0) + const segmentModel = new PlayoutSegmentModelImpl(segment, []) + + const model = new PlayoutRundownModelImpl(rundown, [segmentModel], []) + expect(model.ScratchPadSegmentHasChanged).toBeFalsy() + + const expectedId: SegmentId = protectString('randomId9000') + expect(model.insertScratchpadSegment()).toEqual(expectedId) + + const createdSegment = model.getSegment(expectedId) as PlayoutSegmentModelImpl + expect(createdSegment).toBeTruthy() + + const fixedSegment: ReadonlyDeep = { + ...createdSegment.segment, + externalModified: 0, + } + + expect(fixedSegment).toEqual({ + _id: expectedId, + rundownId: protectString('rd0'), + externalId: '__scratchpad__', + externalModified: 0, + _rank: -1, + name: '', + orphaned: SegmentOrphanedReason.SCRATCHPAD, + } satisfies DBSegment) + + expect(model.ScratchPadSegmentHasChanged).toBeTruthy() + }) + + it('check rank - first segment higher', async () => { + const rundown = createBasicDBRundown() + const segmentModel = new PlayoutSegmentModelImpl(createBasicDBSegment('seg0', 10), []) + const model = new PlayoutRundownModelImpl(rundown, [segmentModel], []) + + const expectedId: SegmentId = protectString('randomId9000') + expect(model.insertScratchpadSegment()).toEqual(expectedId) + + const createdSegment = model.getSegment(expectedId) as PlayoutSegmentModelImpl + expect(createdSegment).toBeTruthy() + expect(createdSegment.segment._rank).toBe(-1) + }) + it('check rank - first segment lower', async () => { + const rundown = createBasicDBRundown() + const segmentModel = new PlayoutSegmentModelImpl(createBasicDBSegment('seg0', -5), []) + const model = new PlayoutRundownModelImpl(rundown, [segmentModel], []) + + const expectedId: SegmentId = protectString('randomId9000') + expect(model.insertScratchpadSegment()).toEqual(expectedId) + + const createdSegment = model.getSegment(expectedId) as PlayoutSegmentModelImpl + expect(createdSegment).toBeTruthy() + expect(createdSegment.segment._rank).toBe(-6) + }) + + it('calling twice fails', async () => { + const rundown = createBasicDBRundown() + + const segment = createBasicDBSegment('seg0', 0) + const segmentModel = new PlayoutSegmentModelImpl(segment, []) + + const model = new PlayoutRundownModelImpl(rundown, [segmentModel], []) + + const expectedId: SegmentId = protectString('randomId9000') + expect(model.insertScratchpadSegment()).toEqual(expectedId) + + const createdSegment = model.getSegment(expectedId) as PlayoutSegmentModelImpl + expect(createdSegment).toBeTruthy() + + model.clearScratchPadSegmentChangedFlag() + + // Expect a UserError + expect(() => model.insertScratchpadSegment()).toThrow( + expect.objectContaining({ key: UserErrorMessage.ScratchpadAlreadyActive }) + ) + + expect(model.ScratchPadSegmentHasChanged).toBeFalsy() + }) + + it('calling when predefined', async () => { + const rundown = createBasicDBRundown() + + const segment = createBasicDBSegment('seg0', 0) + segment.orphaned = SegmentOrphanedReason.SCRATCHPAD + const segmentModel = new PlayoutSegmentModelImpl(segment, []) + + const model = new PlayoutRundownModelImpl(rundown, [segmentModel], []) + + // Expect a UserError + expect(() => model.insertScratchpadSegment()).toThrow( + expect.objectContaining({ key: UserErrorMessage.ScratchpadAlreadyActive }) + ) + + expect(model.ScratchPadSegmentHasChanged).toBeFalsy() + }) + }) + + describe('removeScratchpadSegment', () => { + beforeEach(() => { + restartRandomId() + }) + + it('ok', async () => { + const rundown = createBasicDBRundown() + + const segment = createBasicDBSegment('seg0', 0) + segment.orphaned = SegmentOrphanedReason.SCRATCHPAD + const segmentModel = new PlayoutSegmentModelImpl(segment, []) + + const model = new PlayoutRundownModelImpl(rundown, [segmentModel], []) + expect(model.ScratchPadSegmentHasChanged).toBeFalsy() + + expect(model.removeScratchpadSegment()).toBeTruthy() + expect(model.ScratchPadSegmentHasChanged).toBeTruthy() + }) + + it('calling multiple times', async () => { + const rundown = createBasicDBRundown() + + const segment = createBasicDBSegment('seg0', 0) + segment.orphaned = SegmentOrphanedReason.SCRATCHPAD + const segmentModel = new PlayoutSegmentModelImpl(segment, []) + + const model = new PlayoutRundownModelImpl(rundown, [segmentModel], []) + expect(model.ScratchPadSegmentHasChanged).toBeFalsy() + + expect(model.removeScratchpadSegment()).toBeTruthy() + expect(model.ScratchPadSegmentHasChanged).toBeTruthy() + + // call again + expect(model.removeScratchpadSegment()).toBeFalsy() + expect(model.ScratchPadSegmentHasChanged).toBeTruthy() + + // once more, after clearing changed flag + model.clearScratchPadSegmentChangedFlag() + expect(model.removeScratchpadSegment()).toBeFalsy() + expect(model.ScratchPadSegmentHasChanged).toBeFalsy() + }) + + it('insert then remove', async () => { + const rundown = createBasicDBRundown() + + const segment = createBasicDBSegment('seg0', 0) + const segmentModel = new PlayoutSegmentModelImpl(segment, []) + + const model = new PlayoutRundownModelImpl(rundown, [segmentModel], []) + expect(model.ScratchPadSegmentHasChanged).toBeFalsy() + + const expectedId: SegmentId = protectString('randomId9000') + expect(model.insertScratchpadSegment()).toEqual(expectedId) + expect(model.ScratchPadSegmentHasChanged).toBeTruthy() + expect(model.getSegmentIds()).toEqual([expectedId, segment._id]) + + model.clearScratchPadSegmentChangedFlag() + expect(model.removeScratchpadSegment()).toBeTruthy() + expect(model.ScratchPadSegmentHasChanged).toBeTruthy() + expect(model.getSegmentIds()).toEqual([segment._id]) + }) + }) + + describe('getScratchpadSegment', () => { + beforeEach(() => { + restartRandomId() + }) + + it('pre-defined', async () => { + const rundown = createBasicDBRundown() + + const segment = createBasicDBSegment('seg0', 0) + segment.orphaned = SegmentOrphanedReason.SCRATCHPAD + const segmentModel = new PlayoutSegmentModelImpl(segment, []) + + const model = new PlayoutRundownModelImpl(rundown, [segmentModel], []) + + expect(model.getScratchpadSegment()).toBe(segmentModel) + }) + + it('after remove', async () => { + const rundown = createBasicDBRundown() + + const segment = createBasicDBSegment('seg0', 0) + segment.orphaned = SegmentOrphanedReason.SCRATCHPAD + const segmentModel = new PlayoutSegmentModelImpl(segment, []) + + const model = new PlayoutRundownModelImpl(rundown, [segmentModel], []) + + expect(model.removeScratchpadSegment()).toBeTruthy() + + expect(model.getScratchpadSegment()).toBe(undefined) + }) + + it('after insert', async () => { + const rundown = createBasicDBRundown() + + const segment = createBasicDBSegment('seg0', 0) + const segmentModel = new PlayoutSegmentModelImpl(segment, []) + + const model = new PlayoutRundownModelImpl(rundown, [segmentModel], []) + + const expectedId: SegmentId = protectString('randomId9000') + expect(model.insertScratchpadSegment()).toEqual(expectedId) + + expect(model.getScratchpadSegment()).toMatchObject({ + segment: { _id: expectedId }, + }) + }) + }) + + describe('setScratchpadSegmentRank', () => { + beforeEach(() => { + restartRandomId() + }) + + it('pre-defined', async () => { + const rundown = createBasicDBRundown() + + const segment = createBasicDBSegment('seg0', 99) + segment.orphaned = SegmentOrphanedReason.SCRATCHPAD + const segmentModel = new PlayoutSegmentModelImpl(segment, []) + + const model = new PlayoutRundownModelImpl(rundown, [segmentModel], []) + + expect(model.getScratchpadSegment()?.segment._rank).toBe(99) + + model.clearScratchPadSegmentChangedFlag() + model.updateScratchpadSegmentRank() + + expect(model.getScratchpadSegment()?.segment._rank).toBe(-1) + expect(model.ScratchPadSegmentHasChanged).toBeTruthy() + }) + + it('after remove', async () => { + const rundown = createBasicDBRundown() + + const segment = createBasicDBSegment('seg0', 0) + segment.orphaned = SegmentOrphanedReason.SCRATCHPAD + const segmentModel = new PlayoutSegmentModelImpl(segment, []) + + const model = new PlayoutRundownModelImpl(rundown, [segmentModel], []) + + expect(model.removeScratchpadSegment()).toBeTruthy() + + model.clearScratchPadSegmentChangedFlag() + model.updateScratchpadSegmentRank() + expect(model.ScratchPadSegmentHasChanged).toBeFalsy() + }) + }) +}) From 1a0fbcaa73c953f7f1071adf6d4ef81d872392ac Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 5 Mar 2024 15:56:24 +0000 Subject: [PATCH 260/479] chore: only save scratchpad segment when rank changes --- .../implementation/PlayoutRundownModelImpl.ts | 5 +++-- .../implementation/PlayoutSegmentModelImpl.ts | 5 ++++- .../__tests__/PlayoutRundownModelImpl.spec.ts | 16 ++++++++++++++++ .../__tests__/PlayoutSegmentModelImpl.spec.ts | 7 ++++++- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutRundownModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutRundownModelImpl.ts index 1c967c6dac..c43e38fb97 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutRundownModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutRundownModelImpl.ts @@ -107,9 +107,10 @@ export class PlayoutRundownModelImpl implements PlayoutRundownModel { const segment = this.#segments.find((s) => s.segment.orphaned === SegmentOrphanedReason.SCRATCHPAD) if (!segment) return - segment.setScratchpadRank(calculateRankForScratchpadSegment(this.#segments)) - this.#segments.sort((a, b) => a.segment._rank - b.segment._rank) + const changed = segment.setScratchpadRank(calculateRankForScratchpadSegment(this.#segments)) + if (!changed) return + this.#segments.sort((a, b) => a.segment._rank - b.segment._rank) this.#scratchPadSegmentHasChanged = true } } diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutSegmentModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutSegmentModelImpl.ts index 6753165086..519acaaa73 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutSegmentModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutSegmentModelImpl.ts @@ -32,10 +32,13 @@ export class PlayoutSegmentModelImpl implements PlayoutSegmentModel { * This segment belongs to Playout, so is allowed to be modified in this way * @param rank New rank for the segment */ - setScratchpadRank(rank: number): void { + setScratchpadRank(rank: number): boolean { if (this.#segment.orphaned !== SegmentOrphanedReason.SCRATCHPAD) throw new Error('setScratchpadRank can only be used on a SCRATCHPAD segment') + if (this.#segment._rank == rank) return false + this.#segment._rank = rank + return true } } diff --git a/packages/job-worker/src/playout/model/implementation/__tests__/PlayoutRundownModelImpl.spec.ts b/packages/job-worker/src/playout/model/implementation/__tests__/PlayoutRundownModelImpl.spec.ts index 64d094f724..41054565f1 100644 --- a/packages/job-worker/src/playout/model/implementation/__tests__/PlayoutRundownModelImpl.spec.ts +++ b/packages/job-worker/src/playout/model/implementation/__tests__/PlayoutRundownModelImpl.spec.ts @@ -323,6 +323,22 @@ describe('PlayoutRundownModelImpl', () => { expect(model.ScratchPadSegmentHasChanged).toBeTruthy() }) + it('pre-defined: no change', async () => { + const rundown = createBasicDBRundown() + + const segment = createBasicDBSegment('seg0', -1) + segment.orphaned = SegmentOrphanedReason.SCRATCHPAD + const segmentModel = new PlayoutSegmentModelImpl(segment, []) + + const model = new PlayoutRundownModelImpl(rundown, [segmentModel], []) + + model.clearScratchPadSegmentChangedFlag() + model.updateScratchpadSegmentRank() + + expect(model.getScratchpadSegment()?.segment._rank).toBe(-1) + expect(model.ScratchPadSegmentHasChanged).toBeFalsy() + }) + it('after remove', async () => { const rundown = createBasicDBRundown() diff --git a/packages/job-worker/src/playout/model/implementation/__tests__/PlayoutSegmentModelImpl.spec.ts b/packages/job-worker/src/playout/model/implementation/__tests__/PlayoutSegmentModelImpl.spec.ts index ceacc7a5cf..1cfbc84ccb 100644 --- a/packages/job-worker/src/playout/model/implementation/__tests__/PlayoutSegmentModelImpl.spec.ts +++ b/packages/job-worker/src/playout/model/implementation/__tests__/PlayoutSegmentModelImpl.spec.ts @@ -91,7 +91,12 @@ describe('PlayoutSegmentModelImpl', () => { const originalRank = segment._rank const model = new PlayoutSegmentModelImpl(segment, []) - model.setScratchpadRank(originalRank + 1) + // Set should report change + expect(model.setScratchpadRank(originalRank + 1)).toBeTruthy() + expect(model.segment._rank).toBe(originalRank + 1) + + // Set again should report no change + expect(model.setScratchpadRank(originalRank + 1)).toBeFalsy() expect(model.segment._rank).toBe(originalRank + 1) }) From d2f12b64f2027b07c764548753fa10e7616a7b4d Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 5 Mar 2024 16:26:05 +0000 Subject: [PATCH 261/479] chore: add tests for SavePlayoutModel --- .../job-worker/src/__mocks__/collection.ts | 14 +- .../__tests__/SavePlayoutModel.spec.ts | 395 ++++++++++++++++++ 2 files changed, 399 insertions(+), 10 deletions(-) create mode 100644 packages/job-worker/src/playout/model/implementation/__tests__/SavePlayoutModel.spec.ts diff --git a/packages/job-worker/src/__mocks__/collection.ts b/packages/job-worker/src/__mocks__/collection.ts index 3da3c0cd4c..932e06386e 100644 --- a/packages/job-worker/src/__mocks__/collection.ts +++ b/packages/job-worker/src/__mocks__/collection.ts @@ -175,9 +175,6 @@ export class MockMongoCollection }> imp async remove(selector: MongoQuery | TDoc['_id']): Promise { this.#ops.push({ type: 'remove', args: [selector] }) - return this.removeInner(selector) - } - private async removeInner(selector: MongoQuery | TDoc['_id']): Promise { const docs: Pick[] = await this.findFetchInner(selector, { projection: { _id: 1 } }) for (const doc of docs) { this.#documents.delete(doc._id) @@ -186,8 +183,6 @@ export class MockMongoCollection }> imp return docs.length } async update(selector: MongoQuery | TDoc['_id'], modifier: MongoModifier): Promise { - this.#ops.push({ type: 'update', args: [selector, modifier] }) - return this.updateInner(selector, modifier, false) } private async updateInner( @@ -195,6 +190,8 @@ export class MockMongoCollection }> imp modifier: MongoModifier, single: boolean ) { + this.#ops.push({ type: 'update', args: [selector, modifier] }) + const docs = await this.findFetchInner(selector) for (const doc of docs) { @@ -210,9 +207,6 @@ export class MockMongoCollection }> imp async replace(doc: TDoc | ReadonlyDeep): Promise { this.#ops.push({ type: 'replace', args: [doc._id] }) - return this.replaceInner(doc) - } - private async replaceInner(doc: TDoc | ReadonlyDeep): Promise { if (!doc._id) throw new Error(`replace requires document to have an _id`) const exists = this.#documents.has(doc._id) @@ -228,9 +222,9 @@ export class MockMongoCollection }> imp } else if ('updateOne' in op) { await this.updateInner(op.updateOne.filter, op.updateOne.update, true) } else if ('replaceOne' in op) { - await this.replaceInner(op.replaceOne.replacement as any) + await this.replace(op.replaceOne.replacement as any) } else if ('deleteMany' in op) { - await this.removeInner(op.deleteMany.filter) + await this.remove(op.deleteMany.filter) } else { // Note: implement more as we start using them throw new Error(`Unknown mongo Bulk Operation: ${JSON.stringify(op)}`) diff --git a/packages/job-worker/src/playout/model/implementation/__tests__/SavePlayoutModel.spec.ts b/packages/job-worker/src/playout/model/implementation/__tests__/SavePlayoutModel.spec.ts new file mode 100644 index 0000000000..5f4e8a4c68 --- /dev/null +++ b/packages/job-worker/src/playout/model/implementation/__tests__/SavePlayoutModel.spec.ts @@ -0,0 +1,395 @@ +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import { PlayoutSegmentModelImpl } from '../PlayoutSegmentModelImpl' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { PlayoutRundownModelImpl } from '../PlayoutRundownModelImpl' +import { setupDefaultJobEnvironment } from '../../../../__mocks__/context' +import { writePartInstancesAndPieceInstances, writeScratchpadSegments } from '../SavePlayoutModel' +import { PlayoutPartInstanceModelImpl } from '../PlayoutPartInstanceModelImpl' +import { PartInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' + +describe('SavePlayoutModel', () => { + function createRundownModel(segments?: DBSegment[]): PlayoutRundownModelImpl { + const rundown: DBRundown = { + _id: protectString('rd0'), + organizationId: null, + studioId: protectString('studio0'), + showStyleBaseId: protectString('ssb0'), + showStyleVariantId: protectString('ssv0'), + created: 0, + modified: 0, + externalId: 'rd0', + name: `my rundown`, + importVersions: null as any, + timing: null as any, + externalNRCSName: 'FAKE', + playlistId: protectString('playlist0'), + } + + const segmentModels = (segments ?? []).map((s) => new PlayoutSegmentModelImpl(s, [])) + return new PlayoutRundownModelImpl(rundown, segmentModels, []) + } + + describe('writeScratchpadSegments', () => { + it('no rundowns', async () => { + const context = setupDefaultJobEnvironment() + + await writeScratchpadSegments(context, []) + + expect(context.mockCollections.Segments.operations).toHaveLength(0) + }) + + it('no scratchpad segment', async () => { + const context = setupDefaultJobEnvironment() + + const rundown0 = createRundownModel() + const rundown1 = createRundownModel() + rundown1.insertScratchpadSegment() + rundown1.clearScratchPadSegmentChangedFlag() + + await writeScratchpadSegments(context, [rundown0, rundown1]) + + expect(context.mockCollections.Segments.operations).toHaveLength(0) + }) + + it('scratchpads with changes', async () => { + const context = setupDefaultJobEnvironment() + + // create a rundown with an inserted scratchpad + const rundown0 = createRundownModel() + rundown0.insertScratchpadSegment() + + // create a rundown with a removed scratchpad + const rundown1 = createRundownModel() + rundown1.insertScratchpadSegment() + rundown1.clearScratchPadSegmentChangedFlag() + rundown1.removeScratchpadSegment() + + // create a rundown with no changes + const rundown2 = createRundownModel() + rundown2.insertScratchpadSegment() + rundown2.clearScratchPadSegmentChangedFlag() + + await writeScratchpadSegments(context, [rundown0, rundown1, rundown2]) + + expect(context.mockCollections.Segments.operations).toMatchInlineSnapshot(` + [ + { + "args": [ + 3, + ], + "type": "bulkWrite", + }, + { + "args": [ + { + "_id": { + "$ne": "randomId9001", + }, + "orphaned": "scratchpad", + "rundownId": "rd0", + }, + ], + "type": "remove", + }, + { + "args": [ + "randomId9001", + ], + "type": "replace", + }, + { + "args": [ + { + "_id": { + "$ne": "", + }, + "orphaned": "scratchpad", + "rundownId": "rd0", + }, + ], + "type": "remove", + }, + ] + `) + }) + }) + + describe('writePartInstancesAndPieceInstances', () => { + it('no PartInstances', async () => { + const context = setupDefaultJobEnvironment() + + await Promise.all(writePartInstancesAndPieceInstances(context, new Map())) + + expect(context.mockCollections.PartInstances.operations).toHaveLength(0) + expect(context.mockCollections.PieceInstances.operations).toHaveLength(0) + }) + + it('delete PartInstances', async () => { + const context = setupDefaultJobEnvironment() + + const partInstances = new Map() + partInstances.set(protectString('id0'), null) + partInstances.set(protectString('id1'), null) + + await Promise.all(writePartInstancesAndPieceInstances(context, partInstances)) + + expect(context.mockCollections.PartInstances.operations).toHaveLength(2) + expect(context.mockCollections.PartInstances.operations).toMatchInlineSnapshot(` + [ + { + "args": [ + 1, + ], + "type": "bulkWrite", + }, + { + "args": [ + { + "_id": { + "$in": [ + "id0", + "id1", + ], + }, + }, + ], + "type": "remove", + }, + ] + `) + expect(context.mockCollections.PieceInstances.operations).toHaveLength(2) + expect(context.mockCollections.PieceInstances.operations).toMatchInlineSnapshot(` + [ + { + "args": [ + 1, + ], + "type": "bulkWrite", + }, + { + "args": [ + { + "partInstanceId": { + "$in": [ + "id0", + "id1", + ], + }, + }, + ], + "type": "remove", + }, + ] + `) + }) + + it('delete PieceInstances', async () => { + const context = setupDefaultJobEnvironment() + + const pieceInstance = { _id: 'test0' } as unknown as PieceInstance + const partInstanceModel = new PlayoutPartInstanceModelImpl(null as any, [pieceInstance], false) + expect(partInstanceModel.removePieceInstance(pieceInstance._id)).toBeTruthy() + + const partInstances = new Map() + partInstances.set(protectString('id0'), partInstanceModel) + + await Promise.all(writePartInstancesAndPieceInstances(context, partInstances)) + + expect(context.mockCollections.PartInstances.operations).toHaveLength(0) + + expect(context.mockCollections.PieceInstances.operations).toHaveLength(2) + expect(context.mockCollections.PieceInstances.operations).toMatchInlineSnapshot(` + [ + { + "args": [ + 1, + ], + "type": "bulkWrite", + }, + { + "args": [ + { + "_id": { + "$in": [ + "test0", + ], + }, + }, + ], + "type": "remove", + }, + ] + `) + }) + + it('update PartInstance', async () => { + const context = setupDefaultJobEnvironment() + + const partInstanceModel = new PlayoutPartInstanceModelImpl({ _id: 'id0' } as any, [], false) + expect(partInstanceModel.partInstance.blockTakeUntil).toBeUndefined() + partInstanceModel.blockTakeUntil(10000) + expect(partInstanceModel.partInstance.blockTakeUntil).toEqual(10000) + + const partInstances = new Map() + partInstances.set(protectString('id0'), partInstanceModel) + + await Promise.all(writePartInstancesAndPieceInstances(context, partInstances)) + + expect(context.mockCollections.PartInstances.operations).toHaveLength(2) + expect(context.mockCollections.PartInstances.operations).toMatchInlineSnapshot(` + [ + { + "args": [ + 1, + ], + "type": "bulkWrite", + }, + { + "args": [ + "id0", + ], + "type": "replace", + }, + ] + `) + }) + + it('update PieceInstance', async () => { + const context = setupDefaultJobEnvironment() + + const pieceInstance = { _id: 'test0' } as unknown as PieceInstance + const partInstanceModel = new PlayoutPartInstanceModelImpl(null as any, [pieceInstance], false) + expect( + partInstanceModel.mergeOrInsertPieceInstance({ + ...pieceInstance, + adLibSourceId: protectString('adlib0'), + }) + ).toBeTruthy() + + const partInstances = new Map() + partInstances.set(protectString('id0'), partInstanceModel) + + await Promise.all(writePartInstancesAndPieceInstances(context, partInstances)) + + expect(context.mockCollections.PartInstances.operations).toHaveLength(0) + + expect(context.mockCollections.PieceInstances.operations).toHaveLength(2) + expect(context.mockCollections.PieceInstances.operations).toMatchInlineSnapshot(` + [ + { + "args": [ + 1, + ], + "type": "bulkWrite", + }, + { + "args": [ + "test0", + ], + "type": "replace", + }, + ] + `) + }) + + it('combination of all ops', async () => { + const context = setupDefaultJobEnvironment() + + const pieceInstance = { _id: 'test0' } as unknown as PieceInstance + const pieceInstance2 = { _id: 'test1' } as unknown as PieceInstance + const partInstanceModel = new PlayoutPartInstanceModelImpl( + { _id: 'id0' } as any, + [pieceInstance, pieceInstance2], + false + ) + expect( + partInstanceModel.mergeOrInsertPieceInstance({ + ...pieceInstance, + adLibSourceId: protectString('adlib0'), + }) + ).toBeTruthy() + expect(partInstanceModel.removePieceInstance(pieceInstance2._id)).toBeTruthy() + partInstanceModel.blockTakeUntil(10000) + + const partInstances = new Map() + partInstances.set(protectString('id0'), partInstanceModel) + partInstances.set(protectString('id1'), null) + + await Promise.all(writePartInstancesAndPieceInstances(context, partInstances)) + + expect(context.mockCollections.PartInstances.operations).toHaveLength(3) + expect(context.mockCollections.PartInstances.operations).toMatchInlineSnapshot(` + [ + { + "args": [ + 2, + ], + "type": "bulkWrite", + }, + { + "args": [ + "id0", + ], + "type": "replace", + }, + { + "args": [ + { + "_id": { + "$in": [ + "id1", + ], + }, + }, + ], + "type": "remove", + }, + ] + `) + + expect(context.mockCollections.PieceInstances.operations).toHaveLength(4) + expect(context.mockCollections.PieceInstances.operations).toMatchInlineSnapshot(` + [ + { + "args": [ + 3, + ], + "type": "bulkWrite", + }, + { + "args": [ + "test0", + ], + "type": "replace", + }, + { + "args": [ + { + "partInstanceId": { + "$in": [ + "id1", + ], + }, + }, + ], + "type": "remove", + }, + { + "args": [ + { + "_id": { + "$in": [ + "test1", + ], + }, + }, + ], + "type": "remove", + }, + ] + `) + }) + }) +}) From ef918c2390c8af46caa1cd0ff08bab45af2f8380 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Fri, 8 Mar 2024 11:55:07 +0100 Subject: [PATCH 262/479] fix: add a way to take a heap snapshot. This is useful for troubleshooting memory leaks --- .../client/ui/Settings/SystemManagement.tsx | 49 +++++++++++++ meteor/server/api/heapSnapshot.ts | 69 +++++++++++++++++++ meteor/server/api/rest/api.ts | 6 ++ 3 files changed, 124 insertions(+) create mode 100644 meteor/server/api/heapSnapshot.ts diff --git a/meteor/client/ui/Settings/SystemManagement.tsx b/meteor/client/ui/Settings/SystemManagement.tsx index 9870cf9648..d5c0963839 100644 --- a/meteor/client/ui/Settings/SystemManagement.tsx +++ b/meteor/client/ui/Settings/SystemManagement.tsx @@ -47,6 +47,7 @@ export default function SystemManagement(): JSX.Element | null { +
) } @@ -522,3 +523,51 @@ export function checkForOldDataAndCleanUp(t: TFunction, retriesLeft = 0): void { }) .catch(catchError('system.cleanupOldData')) } +function SystemManagementHeapSnapshot() { + const { t } = useTranslation() + + const [displayWarning, setdisplayWarning] = React.useState(false) + const [active, setActive] = React.useState(false) + + const onAreYouSure = useCallback(() => { + setdisplayWarning(true) + }, []) + const onReset = useCallback(() => { + setdisplayWarning(false) + setActive(false) + }, []) + const onConfirm = useCallback(() => { + setActive(true) + setTimeout(() => setActive(false), 20000) + }, []) + return ( + <> +

{t('Memory troubleshooting')}

+
+ {active ? ( + {t('Preparing, please wait...')} + ) : displayWarning ? ( + <> +
{t(`Are you sure? This will cause the whole Sofie system to be unresponsive several seconds!`)}
+ + + {t(`Yes, Take and Download Memory Heap Snapshot`)} + + + + ) : ( + + )} +
+
+ + {t('To inspect the memory heap snapshot, use Chrome DevTools')} + +
+ + ) +} diff --git a/meteor/server/api/heapSnapshot.ts b/meteor/server/api/heapSnapshot.ts new file mode 100644 index 0000000000..e441c538e3 --- /dev/null +++ b/meteor/server/api/heapSnapshot.ts @@ -0,0 +1,69 @@ +import * as v8 from 'node:v8' +import { Readable } from 'stream' +import { Meteor } from 'meteor/meteor' +import Koa from 'koa' +import KoaRouter from '@koa/router' +import { fixValidPath } from '../../lib/lib' +import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' +import { logger } from '../logging' +import { Settings } from '../../lib/Settings' +import { Credentials } from '../security/lib/credentials' +import { SystemWriteAccess } from '../security/system' +import { sleep } from '@sofie-automation/corelib/dist/lib' + +async function retrieveHeapSnapshot(cred0: Credentials): Promise { + if (Settings.enableUserAccounts) { + await SystemWriteAccess.coreSystem(cred0) + } + logger.warn('Taking heap snapshot, expect system to be unresponsive for a few seconds..') + await sleep(100) // Allow the logger to catch up before continuing.. + + const stream = v8.getHeapSnapshot() + return stream +} + +export const heapSnapshotPrivateApiRouter = new KoaRouter() + +// Setup endpoints: +async function handleKoaResponse(ctx: Koa.ParameterizedContext, snapshotFcn: () => Promise) { + if (ctx.query.areYouSure !== 'yes') { + ctx.response.status = 403 + ctx.response.body = '?areYouSure=yes' + return + } + + try { + const stream = await snapshotFcn() + + ctx.response.type = 'application/octet-stream' + ctx.response.attachment(fixValidPath(`sofie-heap-snapshot-${new Date().toISOString()}.heapsnapshot`)) + ctx.response.status = 200 + ctx.body = stream + // ctx.response.body = JSON.stringify(snapshot, null, 4) + } catch (e) { + ctx.response.type = 'text/plain' + ctx.response.status = e instanceof Meteor.Error && typeof e.error === 'number' ? e.error : 500 + ctx.response.body = 'Error: ' + stringifyError(e) + + if (ctx.response.status !== 404) { + logger.error(stringifyError(e)) + } + } +} + +// For backwards compatibility: +if (!Settings.enableUserAccounts) { + // Retrieve heap snapshot: + heapSnapshotPrivateApiRouter.get('/retrieve', async (ctx) => { + return handleKoaResponse(ctx, async () => { + return retrieveHeapSnapshot({ userId: null }) + }) + }) +} + +// Retrieve heap snapshot: +heapSnapshotPrivateApiRouter.get('/:token/retrieve', async (ctx) => { + return handleKoaResponse(ctx, async () => { + return retrieveHeapSnapshot({ userId: null, token: ctx.params.token }) + }) +}) diff --git a/meteor/server/api/rest/api.ts b/meteor/server/api/rest/api.ts index af56e09d10..f7c2b7d076 100644 --- a/meteor/server/api/rest/api.ts +++ b/meteor/server/api/rest/api.ts @@ -10,6 +10,7 @@ import { actionTriggersRouter } from '../triggeredActions' import { peripheralDeviceRouter } from '../peripheralDevice' import { blueprintsRouter } from '../blueprints/http' import { createLegacyApiRouter } from './v0/index' +import { heapSnapshotPrivateApiRouter } from '../heapSnapshot' const LATEST_REST_API = 'v1.0' @@ -26,6 +27,11 @@ apiRouter.use('/private/shelfLayouts', shelfLayoutsRouter.routes(), shelfLayouts apiRouter.use('/private/actionTriggers', actionTriggersRouter.routes(), actionTriggersRouter.allowedMethods()) apiRouter.use('/private/peripheralDevices', peripheralDeviceRouter.routes(), peripheralDeviceRouter.allowedMethods()) apiRouter.use('/private/blueprints', blueprintsRouter.routes(), blueprintsRouter.allowedMethods()) +apiRouter.use( + '/private/heapSnapshot', + heapSnapshotPrivateApiRouter.routes(), + heapSnapshotPrivateApiRouter.allowedMethods() +) async function redirectToLatest(ctx: koa.ParameterizedContext, _next: koa.Next): Promise { ctx.redirect(`/api/${LATEST_REST_API}`) From 0f73bedbeb0490417c0643e521465a8112c2e444 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 8 Mar 2024 12:58:34 +0000 Subject: [PATCH 263/479] fix: build server-core image on alpine 3.19 (#1160) This is based on the meteor/node alpine variant, with an updated alpine and with minimal dependencies. This is a short-term solution until the version of node is updated, as the version of alpine offered by meteor has multiple CVEs --- meteor/Dockerfile | 40 ++++++++++++++++++++++++++++++++-------- meteor/Dockerfile.circle | 25 ++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/meteor/Dockerfile b/meteor/Dockerfile index 20bd960da0..62b62aac66 100644 --- a/meteor/Dockerfile +++ b/meteor/Dockerfile @@ -6,17 +6,23 @@ RUN curl "https://install.meteor.com/?release=2.13.3" | sh # Temporary change the NODE_ENV env variable, so that all libraries are installed: ENV NODE_ENV_TMP $NODE_ENV ENV NODE_ENV anythingButProduction + +# Prepare the packages +COPY package.json /opt/core/package.json COPY packages /opt/core/packages WORKDIR /opt/core/packages -RUN rm -R *-gateway documentation +RUN rm -R *-gateway documentation openapi +RUN corepack enable RUN yarn install && yarn build + +# Add the meteor source COPY meteor /opt/core/meteor COPY scripts /opt/core/scripts WORKDIR /opt/core/meteor # Force meteor to setup the runtime RUN meteor --version --allow-superuser -RUN meteor npm install -g yarn +RUN meteor corepack enable RUN meteor yarn install # Restore the NODE_ENV variable: @@ -25,14 +31,32 @@ RUN --mount=type=cache,target=/opt/core/meteor/.meteor/local NODE_OPTIONS="--max WORKDIR /opt/bundle/programs/server/ RUN npm install -# Install production dependencies for the worker -WORKDIR /opt/core/packages -RUN yarn workspaces focus --all --production - # DEPLOY IMAGE -FROM meteor/node:14.21.4-alpine3.17 +FROM alpine:3.19 + +ENV NODE_VERSION=14.21.4 +ENV NODE_URL="https://static.meteor.com/dev-bundle-node-os/unofficial-builds/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.gz" +ENV DIR_NODE=/usr/local + +RUN apk add --no-cache \ + libstdc++ \ + && apk add --no-cache --virtual .build-deps-full \ + binutils-gold \ + curl \ + gnupg \ + xz + +RUN echo $NODE_URL \ + && curl -sSL "$NODE_URL" | tar -xz -C /usr/local/ && mv $DIR_NODE/node-v${NODE_VERSION}-linux-x64 $DIR_NODE/v$NODE_VERSION + +# add node and npm to path so the commands are available +ENV NODE_PATH $DIR_NODE/v$NODE_VERSION/lib/node_modules +ENV PATH $DIR_NODE/v$NODE_VERSION/bin:$PATH + +# confirm installation +RUN node -v && npm -v + COPY --from=0 /opt/bundle /opt/core -COPY --from=0 /opt/core/packages /opt/packages COPY meteor/docker-entrypoint.sh /opt # Tell meteor where the worker code is located diff --git a/meteor/Dockerfile.circle b/meteor/Dockerfile.circle index 7a7dee94d9..9456265025 100644 --- a/meteor/Dockerfile.circle +++ b/meteor/Dockerfile.circle @@ -1,5 +1,28 @@ # DEPLOY IMAGE -FROM meteor/node:14.21.4-alpine3.17 +FROM alpine:3.19 + +ENV NODE_VERSION=14.21.4 +ENV NODE_URL="https://static.meteor.com/dev-bundle-node-os/unofficial-builds/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.gz" +ENV DIR_NODE=/usr/local + +RUN apk add --no-cache \ + libstdc++ \ + && apk add --no-cache --virtual .build-deps-full \ + binutils-gold \ + curl \ + gnupg \ + xz + +RUN echo $NODE_URL \ + && curl -sSL "$NODE_URL" | tar -xz -C /usr/local/ && mv $DIR_NODE/node-v${NODE_VERSION}-linux-x64 $DIR_NODE/v$NODE_VERSION + +# add node and npm to path so the commands are available +ENV NODE_PATH $DIR_NODE/v$NODE_VERSION/lib/node_modules +ENV PATH $DIR_NODE/v$NODE_VERSION/bin:$PATH + +# confirm installation +RUN node -v && npm -v + COPY meteor/bundle /opt/core COPY meteor/docker-entrypoint.sh /opt WORKDIR /opt/core/ From 0c25dbbc478b8177916921719aac52efc8036627 Mon Sep 17 00:00:00 2001 From: ianshade Date: Mon, 11 Mar 2024 10:54:52 +0100 Subject: [PATCH 264/479] refactor(live-status-gw): reassign log levels demotes many info messages to debug, some even to silly --- .../src/collections/adLibActionsHandler.ts | 6 +-- .../src/collections/adLibsHandler.ts | 6 +-- .../collections/globalAdLibActionsHandler.ts | 6 +-- .../src/collections/globalAdLibsHandler.ts | 6 +-- .../src/collections/partHandler.ts | 8 ++-- .../src/collections/partInstancesHandler.ts | 10 ++-- .../src/collections/partsHandler.ts | 6 +-- .../src/collections/pieceInstancesHandler.ts | 6 +-- .../src/collections/playlistHandler.ts | 12 ++--- .../src/collections/rundownHandler.ts | 7 +-- .../src/collections/rundownsHandler.ts | 11 ++--- .../src/collections/segmentHandler.ts | 6 +-- .../src/collections/segmentsHandler.ts | 11 ++--- .../src/collections/showStyleBaseHandler.ts | 6 +-- .../src/collections/studioHandler.ts | 2 +- .../live-status-gateway/src/coreHandler.ts | 6 +-- .../src/topics/activePlaylistTopic.ts | 18 +++++--- .../src/topics/adLibsTopic.ts | 12 ++--- .../live-status-gateway/src/topics/root.ts | 5 +- .../src/topics/segmentsTopic.ts | 6 +-- .../src/topics/studioTopic.ts | 4 +- packages/live-status-gateway/src/wsHandler.ts | 46 +++++++++++++++++-- 22 files changed, 119 insertions(+), 87 deletions(-) diff --git a/packages/live-status-gateway/src/collections/adLibActionsHandler.ts b/packages/live-status-gateway/src/collections/adLibActionsHandler.ts index f8ba845293..256d1fb571 100644 --- a/packages/live-status-gateway/src/collections/adLibActionsHandler.ts +++ b/packages/live-status-gateway/src/collections/adLibActionsHandler.ts @@ -22,7 +22,7 @@ export class AdLibActionsHandler } async changed(id: AdLibActionId, changeType: string): Promise { - this._logger.info(`${this._name} ${changeType} ${id}`) + this.logDocumentChange(id, changeType) if (!this._collectionName) return const col = this._core.getCollection(this._collectionName) if (!col) throw new Error(`collection '${this._collectionName}' not found!`) @@ -31,7 +31,7 @@ export class AdLibActionsHandler } async update(source: string, data: SelectedPartInstances | undefined): Promise { - this._logger.info(`${this._name} received partInstances update from ${source}`) + this.logUpdateReceived('partInstances', source) const prevRundownId = this._curRundownId this._curPartInstance = data ? data.current ?? data.next : undefined this._curRundownId = this._curPartInstance ? this._curPartInstance.rundownId : undefined @@ -69,7 +69,7 @@ export class AdLibActionsHandler // override notify to implement empty array handling async notify(data: AdLibAction[] | undefined): Promise { - this._logger.info(`${this._name} notifying update with ${data?.length} adLibActions`) + this.logNotifyingUpdate(data?.length) if (data !== undefined) { for (const observer of this._observers) { await observer.update(this._name, data) diff --git a/packages/live-status-gateway/src/collections/adLibsHandler.ts b/packages/live-status-gateway/src/collections/adLibsHandler.ts index 4195b94aaa..e34fbcb11f 100644 --- a/packages/live-status-gateway/src/collections/adLibsHandler.ts +++ b/packages/live-status-gateway/src/collections/adLibsHandler.ts @@ -23,7 +23,7 @@ export class AdLibsHandler } async changed(id: PieceId, changeType: string): Promise { - this._logger.info(`${this._name} ${changeType} ${id}`) + this.logDocumentChange(id, changeType) if (!this._collectionName) return const col = this._core.getCollection(this._collectionName) if (!col) throw new Error(`collection '${this._collectionName}' not found!`) @@ -32,7 +32,7 @@ export class AdLibsHandler } async update(source: string, data: SelectedPartInstances | undefined): Promise { - this._logger.info(`${this._name} received adLibs update from ${source}`) + this.logUpdateReceived('partInstances', source) const prevRundownId = this._currentRundownId this._currentPartInstance = data ? data.current ?? data.next : undefined this._currentRundownId = this._currentPartInstance?.rundownId @@ -70,7 +70,7 @@ export class AdLibsHandler // override notify to implement empty array handling async notify(data: AdLibPiece[] | undefined): Promise { - this._logger.info(`${this._name} notifying update with ${data?.length} adLibs`) + this.logNotifyingUpdate(data?.length) if (data !== undefined) { for (const observer of this._observers) { await observer.update(this._name, data) diff --git a/packages/live-status-gateway/src/collections/globalAdLibActionsHandler.ts b/packages/live-status-gateway/src/collections/globalAdLibActionsHandler.ts index 177d3b25c2..4ec34285b6 100644 --- a/packages/live-status-gateway/src/collections/globalAdLibActionsHandler.ts +++ b/packages/live-status-gateway/src/collections/globalAdLibActionsHandler.ts @@ -30,7 +30,7 @@ export class GlobalAdLibActionsHandler } async changed(id: RundownBaselineAdLibActionId, changeType: string): Promise { - this._logger.info(`${this._name} ${changeType} ${id}`) + this.logDocumentChange(id, changeType) if (!this._collectionName) return const col = this._core.getCollection(this._collectionName) if (!col) throw new Error(`collection '${this._collectionName}' not found!`) @@ -39,7 +39,7 @@ export class GlobalAdLibActionsHandler } async update(source: string, data: SelectedPartInstances | undefined): Promise { - this._logger.info(`${this._name} received partInstances update from ${source}`) + this.logUpdateReceived('partInstances', source) const prevRundownId = this._currentRundownId const partInstance = data ? data.current ?? data.next : undefined this._currentRundownId = partInstance?.rundownId @@ -75,7 +75,7 @@ export class GlobalAdLibActionsHandler // override notify to implement empty array handling async notify(data: RundownBaselineAdLibAction[] | undefined): Promise { - this._logger.info(`${this._name} notifying update with ${data?.length} globalAdLibActions`) + this.logNotifyingUpdate(data?.length) if (data !== undefined) { for (const observer of this._observers) { await observer.update(this._name, data) diff --git a/packages/live-status-gateway/src/collections/globalAdLibsHandler.ts b/packages/live-status-gateway/src/collections/globalAdLibsHandler.ts index 12c5838967..2f8f8fb662 100644 --- a/packages/live-status-gateway/src/collections/globalAdLibsHandler.ts +++ b/packages/live-status-gateway/src/collections/globalAdLibsHandler.ts @@ -30,7 +30,7 @@ export class GlobalAdLibsHandler } async changed(id: PieceId, changeType: string): Promise { - this._logger.info(`${this._name} ${changeType} ${id}`) + this.logDocumentChange(id, changeType) if (!this._collectionName) return const collection = this._core.getCollection(this._collectionName) if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) @@ -39,7 +39,7 @@ export class GlobalAdLibsHandler } async update(source: string, data: SelectedPartInstances | undefined): Promise { - this._logger.info(`${this._name} received globalAdLibs update from ${source}`) + this.logUpdateReceived('globalAdLibs', source) const prevRundownId = this._currentRundownId const partInstance = data ? data.current ?? data.next : undefined this._currentRundownId = partInstance?.rundownId @@ -75,7 +75,7 @@ export class GlobalAdLibsHandler // override notify to implement empty array handling async notify(data: RundownBaselineAdLibItem[] | undefined): Promise { - this._logger.info(`${this._name} notifying update with ${data?.length} globalAdLibs`) + this.logNotifyingUpdate(data?.length) if (data !== undefined) { for (const observer of this._observers) { await observer.update(this._name, data) diff --git a/packages/live-status-gateway/src/collections/partHandler.ts b/packages/live-status-gateway/src/collections/partHandler.ts index ef1301c1c4..c2df5416da 100644 --- a/packages/live-status-gateway/src/collections/partHandler.ts +++ b/packages/live-status-gateway/src/collections/partHandler.ts @@ -26,7 +26,7 @@ export class PartHandler } async changed(id: PartId, changeType: string): Promise { - this._logger.info(`${this._name} ${changeType} ${id}`) + this.logDocumentChange(id, changeType) if (!this._collectionName) return const collection = this._core.getCollection(this._collectionName) if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) @@ -46,11 +46,11 @@ export class PartHandler const partInstances = data as SelectedPartInstances switch (source) { case PlaylistHandler.name: - this._logger.info(`${this._name} received playlist update ${rundownPlaylist?._id}`) + this.logUpdateReceived('playlist', source, `rundownPlaylistId ${rundownPlaylist?._id}`) this._activePlaylist = rundownPlaylist break case PartInstancesHandler.name: - this._logger.info(`${this._name} received partInstances update from ${source}`) + this.logUpdateReceived('partInstances', source) this._currentPartInstance = partInstances.current break default: @@ -89,7 +89,7 @@ export class PartHandler await this._partsHandler.setParts(allParts) } if (prevCurPartInstance !== this._currentPartInstance) { - this._logger.info( + this._logger.debug( `${this._name} found updated partInstances with current part ${this._activePlaylist?.currentPartInfo?.partInstanceId}` ) if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) diff --git a/packages/live-status-gateway/src/collections/partInstancesHandler.ts b/packages/live-status-gateway/src/collections/partInstancesHandler.ts index d249f906d0..1ebd7fdec9 100644 --- a/packages/live-status-gateway/src/collections/partInstancesHandler.ts +++ b/packages/live-status-gateway/src/collections/partInstancesHandler.ts @@ -44,7 +44,7 @@ export class PartInstancesHandler } async changed(id: PartInstanceId, changeType: string): Promise { - this._logger.info(`${this._name} ${changeType} ${id}`) + this.logDocumentChange(id, changeType) if (!this._collectionName || this._subscriptionPending) return this._throttledUpdateAndNotify() @@ -101,10 +101,10 @@ export class PartInstancesHandler const prevRundownIds = this._rundownIds.map((rid) => rid) const prevActivationId = this._activationId - this._logger.info( - `${this._name} received playlist update ${data?._id}, active ${ - data?.activationId ? true : false - } from ${source}` + this.logUpdateReceived( + 'playlist', + source, + `rundownPlaylistId ${data?._id}, active ${data?.activationId ? true : false}` ) this._currentPlaylist = data if (!this._collectionName) return diff --git a/packages/live-status-gateway/src/collections/partsHandler.ts b/packages/live-status-gateway/src/collections/partsHandler.ts index 4cfd791e41..aece1f9c6d 100644 --- a/packages/live-status-gateway/src/collections/partsHandler.ts +++ b/packages/live-status-gateway/src/collections/partsHandler.ts @@ -22,15 +22,13 @@ export class PartsHandler } async setParts(parts: DBPart[]): Promise { - this._logger.info(`'${this._name}' handler received parts update with ${parts.length} parts`) + this.logUpdateReceived('parts', parts.length) this._collectionData = parts await this.throttledNotify(this._collectionData) } async notify(data: DBPart[] | undefined): Promise { - this._logger.info( - `${this._name} notifying all observers of an update with ${this._collectionData?.length} parts` - ) + this.logNotifyingUpdate(this._collectionData?.length) if (data !== undefined) { for (const observer of this._observers) { await observer.update(this._name, data) diff --git a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts index 75beae6c57..19bb5a3c08 100644 --- a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts +++ b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts @@ -56,7 +56,7 @@ export class PieceInstancesHandler } async changed(id: PieceInstanceId, changeType: string): Promise { - this._logger.info(`${this._name} ${changeType} ${id}`) + this.logDocumentChange(id, changeType) if (!this._collectionName || this._subscriptionPending) return this._throttledUpdateAndNotify() } @@ -111,9 +111,7 @@ export class PieceInstancesHandler const prevPartInstanceIds = this._partInstanceIds const prevActivationId = this._activationId - this._logger.info( - `${this._name} received playlist update ${data?._id}, active ${!!data?.activationId} from ${source}` - ) + this.logUpdateReceived('playlist', source, `rundownPlaylistId ${data?._id}, active ${!!data?.activationId}`) this._currentPlaylist = data if (!this._collectionName) return diff --git a/packages/live-status-gateway/src/collections/playlistHandler.ts b/packages/live-status-gateway/src/collections/playlistHandler.ts index 2f0f7d48c1..3c81b0818c 100644 --- a/packages/live-status-gateway/src/collections/playlistHandler.ts +++ b/packages/live-status-gateway/src/collections/playlistHandler.ts @@ -7,27 +7,25 @@ import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' export class PlaylistsHandler - extends CollectionBase + extends CollectionBase implements Collection { public observerName: string constructor(logger: Logger, coreHandler: CoreHandler) { - super(PlaylistsHandler.name, undefined, undefined, logger, coreHandler) + super(PlaylistsHandler.name, CollectionName.RundownPlaylists, undefined, logger, coreHandler) this.observerName = this._name } async setPlaylists(playlists: DBRundownPlaylist[]): Promise { - this._logger.info(`'${this._name}' handler received playlists update with ${playlists.length} playlists`) + this.logUpdateReceived('playlists', playlists.length) this._collectionData = playlists await this.notify(this._collectionData) } // override notify to implement empty array handling async notify(data: DBRundownPlaylist[] | undefined): Promise { - this._logger.info( - `${this._name} notifying all observers of an update with ${this._collectionData?.length} playlists` - ) + this.logNotifyingUpdate(this._collectionData?.length) if (data !== undefined) { for (const observer of this._observers) { await observer.update(this._name, data) @@ -78,7 +76,7 @@ export class PlaylistHandler } async changed(id: RundownPlaylistId, changeType: string): Promise { - this._logger.info(`${this._name} ${changeType} ${id}`) + this.logDocumentChange(id, changeType) if (!this._collectionName) return const collection = this._core.getCollection(this._collectionName) if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) diff --git a/packages/live-status-gateway/src/collections/rundownHandler.ts b/packages/live-status-gateway/src/collections/rundownHandler.ts index 2db173bad0..c59f077874 100644 --- a/packages/live-status-gateway/src/collections/rundownHandler.ts +++ b/packages/live-status-gateway/src/collections/rundownHandler.ts @@ -3,6 +3,7 @@ import { CoreHandler } from '../coreHandler' import { CollectionBase, Collection, CollectionObserver } from '../wsHandler' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { PartInstancesHandler, SelectedPartInstances } from './partInstancesHandler' import { RundownId, RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' @@ -24,7 +25,7 @@ export class RundownHandler } async changed(id: RundownId, changeType: string): Promise { - this._logger.info(`${this._name} ${changeType} ${id}`) + this.logDocumentChange(id, changeType) if (id !== this._currentRundownId) throw new Error(`${this._name} received change with unexpected id ${id} !== ${this._currentRundownId}`) if (!this._collectionName) return @@ -42,11 +43,11 @@ export class RundownHandler const partInstances = data as SelectedPartInstances switch (source) { case PlaylistHandler.name: - this._logger.info(`${this._name} received playlist update ${rundownPlaylist?._id}`) + this.logUpdateReceived('playlist', source, unprotectString(rundownPlaylist?._id)) this._currentPlaylistId = rundownPlaylist?._id break case PartInstancesHandler.name: - this._logger.info(`${this._name} received partInstances update from ${source}`) + this.logUpdateReceived('partInstances', source) this._currentRundownId = partInstances.current?.rundownId ?? partInstances.next?.rundownId break default: diff --git a/packages/live-status-gateway/src/collections/rundownsHandler.ts b/packages/live-status-gateway/src/collections/rundownsHandler.ts index 6f6367c1ff..0ffb07422c 100644 --- a/packages/live-status-gateway/src/collections/rundownsHandler.ts +++ b/packages/live-status-gateway/src/collections/rundownsHandler.ts @@ -2,29 +2,28 @@ import { Logger } from 'winston' import { CoreHandler } from '../coreHandler' import { CollectionBase, Collection } from '../wsHandler' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' export class RundownsHandler - extends CollectionBase + extends CollectionBase implements Collection { public observerName: string constructor(logger: Logger, coreHandler: CoreHandler) { - super(RundownsHandler.name, undefined, undefined, logger, coreHandler) + super(RundownsHandler.name, CollectionName.Rundowns, undefined, logger, coreHandler) this.observerName = this._name } async setRundowns(rundowns: DBRundown[]): Promise { - this._logger.info(`'${this._name}' handler received rundowns update with ${rundowns.length} rundowns`) + this.logUpdateReceived('rundowns', rundowns.length) this._collectionData = rundowns await this.notify(this._collectionData) } // override notify to implement empty array handling async notify(data: DBRundown[] | undefined): Promise { - this._logger.info( - `${this._name} notifying all observers of an update with ${this._collectionData?.length} rundowns` - ) + this.logNotifyingUpdate(this._collectionData?.length) if (data !== undefined) { for (const observer of this._observers) { await observer.update(this._name, data) diff --git a/packages/live-status-gateway/src/collections/segmentHandler.ts b/packages/live-status-gateway/src/collections/segmentHandler.ts index f58ab66c97..da4c85bcff 100644 --- a/packages/live-status-gateway/src/collections/segmentHandler.ts +++ b/packages/live-status-gateway/src/collections/segmentHandler.ts @@ -25,7 +25,7 @@ export class SegmentHandler } async changed(id: SegmentId, changeType: string): Promise { - this._logger.info(`${this._name} ${changeType} ${id}`) + this.logDocumentChange(id, changeType) if (!this._collectionName) return const collection = this._core.getCollection(this._collectionName) if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) @@ -43,13 +43,13 @@ export class SegmentHandler switch (source) { case PartInstancesHandler.name: { - this._logger.info(`${this._name} received update from ${source}`) + this.logUpdateReceived('partInstances', source) const partInstanceMap = data as SelectedPartInstances this._currentSegmentId = data ? partInstanceMap.current?.segmentId : undefined break } case PlaylistHandler.name: { - this._logger.info(`${this._name} received update from ${source}`) + this.logUpdateReceived('playlist', source) this._rundownIds = (data as DBRundownPlaylist | undefined)?.rundownIdsInOrder ?? [] break } diff --git a/packages/live-status-gateway/src/collections/segmentsHandler.ts b/packages/live-status-gateway/src/collections/segmentsHandler.ts index c0e68d97ac..401def1ead 100644 --- a/packages/live-status-gateway/src/collections/segmentsHandler.ts +++ b/packages/live-status-gateway/src/collections/segmentsHandler.ts @@ -3,33 +3,32 @@ import { CoreHandler } from '../coreHandler' import { CollectionBase, Collection } from '../wsHandler' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import * as _ from 'underscore' +import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' const THROTTLE_PERIOD_MS = 200 export class SegmentsHandler - extends CollectionBase + extends CollectionBase implements Collection { public observerName: string private throttledNotify: (data: DBSegment[]) => Promise constructor(logger: Logger, coreHandler: CoreHandler) { - super(SegmentsHandler.name, undefined, undefined, logger, coreHandler) + super(SegmentsHandler.name, CollectionName.Segments, undefined, logger, coreHandler) this.observerName = this._name this.throttledNotify = _.throttle(this.notify.bind(this), THROTTLE_PERIOD_MS, { leading: true, trailing: true }) } async setSegments(segments: DBSegment[]): Promise { - this._logger.info(`'${this._name}' handler received segments update with ${segments.length} segments`) + this.logUpdateReceived('segments', segments.length) this._collectionData = segments await this.throttledNotify(this._collectionData) } // override notify to implement empty array handling async notify(data: DBSegment[] | undefined): Promise { - this._logger.info( - `${this._name} notifying all observers of an update with ${this._collectionData?.length} segments` - ) + this.logNotifyingUpdate(this._collectionData?.length) if (data !== undefined) { for (const observer of this._observers) { await observer.update(this._name, data) diff --git a/packages/live-status-gateway/src/collections/showStyleBaseHandler.ts b/packages/live-status-gateway/src/collections/showStyleBaseHandler.ts index ad21ae80d2..c41e1e10f3 100644 --- a/packages/live-status-gateway/src/collections/showStyleBaseHandler.ts +++ b/packages/live-status-gateway/src/collections/showStyleBaseHandler.ts @@ -35,7 +35,7 @@ export class ShowStyleBaseHandler } async changed(id: ShowStyleBaseId, changeType: string): Promise { - this._logger.info(`${this._name} ${changeType} ${id}`) + this.logDocumentChange(id, changeType) if (!this._collectionName) return if (this._showStyleBaseId) { this.updateCollectionData() @@ -44,9 +44,7 @@ export class ShowStyleBaseHandler } async update(source: string, data: DBRundown | undefined): Promise { - this._logger.info( - `${this._name} received rundown update ${data?._id}, showStyleBaseId ${data?.showStyleBaseId} from ${source}` - ) + this.logUpdateReceived('rundown', source, `rundownId ${data?._id}, showStyleBaseId ${data?.showStyleBaseId}`) const prevShowStyleBaseId = this._showStyleBaseId this._showStyleBaseId = data?.showStyleBaseId diff --git a/packages/live-status-gateway/src/collections/studioHandler.ts b/packages/live-status-gateway/src/collections/studioHandler.ts index dece997e95..fb7be9b795 100644 --- a/packages/live-status-gateway/src/collections/studioHandler.ts +++ b/packages/live-status-gateway/src/collections/studioHandler.ts @@ -41,7 +41,7 @@ export class StudioHandler } async changed(id: StudioId, changeType: string): Promise { - this._logger.info(`${this._name} ${changeType} ${id}`) + this.logDocumentChange(id, changeType) if (!(id === this._studioId && this._collectionName)) return const collection = this._core.getCollection(this._collectionName) if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) diff --git a/packages/live-status-gateway/src/coreHandler.ts b/packages/live-status-gateway/src/coreHandler.ts index 419420d762..9250af4eb3 100644 --- a/packages/live-status-gateway/src/coreHandler.ts +++ b/packages/live-status-gateway/src/coreHandler.ts @@ -117,14 +117,14 @@ export class CoreHandler { collection: Key, ...params: ParametersOfFunctionOrNever ): Promise { - this.logger.info(`Core: Set up subscription for '${collection}'`) + this.logger.debug(`Core: Set up subscription for '${collection}'`) const subscriptionId = await this.core.autoSubscribe(collection, ...params) - this.logger.info(`Core: Subscription for '${collection}' set up with id ${subscriptionId}`) + this.logger.debug(`Core: Subscription for '${collection}' set up with id ${subscriptionId}`) return subscriptionId } unsubscribe(subscriptionId: SubscriptionId): void { - this.logger.info(`Core: Unsubscribing id '${subscriptionId}'`) + this.logger.debug(`Core: Unsubscribing id '${subscriptionId}'`) this.core.unsubscribe(subscriptionId) } diff --git a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts index f07d3e337f..e511a074df 100644 --- a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts +++ b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts @@ -183,8 +183,10 @@ export class ActivePlaylistTopic switch (source) { case PlaylistHandler.name: { const rundownPlaylist = data ? (data as DBRundownPlaylist) : undefined - this._logger.info( - `${this._name} received playlist update ${rundownPlaylist?._id}, activationId ${rundownPlaylist?.activationId}` + this.logUpdateReceived( + 'playlist', + source, + `rundownPlaylistId ${rundownPlaylist?._id}, activationId ${rundownPlaylist?.activationId}` ) this._activePlaylist = unprotectString(rundownPlaylist?.activationId) ? rundownPlaylist : undefined hasAnythingChanged = true @@ -192,15 +194,17 @@ export class ActivePlaylistTopic } case ShowStyleBaseHandler.name: { const showStyleBaseExt = data ? (data as ShowStyleBaseExt) : undefined - this._logger.info(`${this._name} received showStyleBase update from ${source}`) + this.logUpdateReceived('showStyleBase', source) this._showStyleBaseExt = showStyleBaseExt hasAnythingChanged = true break } case PartInstancesHandler.name: { const partInstances = data as SelectedPartInstances - this._logger.info( - `${this._name} received partInstances update from ${source} with ${partInstances.inCurrentSegment.length} instances in segment` + this.logUpdateReceived( + 'partInstances', + source, + `${partInstances.inCurrentSegment.length} instances in segment` ) this._currentPartInstance = partInstances.current this._nextPartInstance = partInstances.next @@ -211,13 +215,13 @@ export class ActivePlaylistTopic } case PartsHandler.name: { this._partsBySegmentId = _.groupBy(data as DBPart[], 'segmentId') - this._logger.info(`${this._name} received parts update from ${source}`) + this.logUpdateReceived('parts', source) hasAnythingChanged = true // TODO: can this be smarter? break } case PieceInstancesHandler.name: { const pieceInstances = data as SelectedPieceInstances - this._logger.info(`${this._name} received pieceInstances update from ${source}`) + this.logUpdateReceived('pieceInstances', source) if ( pieceInstances.currentPartInstance !== this._pieceInstancesInCurrentPartInstance || pieceInstances.nextPartInstance !== this._pieceInstancesInNextPartInstance diff --git a/packages/live-status-gateway/src/topics/adLibsTopic.ts b/packages/live-status-gateway/src/topics/adLibsTopic.ts index 4bf7c3cc3b..e6d1dad5df 100644 --- a/packages/live-status-gateway/src/topics/adLibsTopic.ts +++ b/packages/live-status-gateway/src/topics/adLibsTopic.ts @@ -203,37 +203,37 @@ export class AdLibsTopic case PlaylistHandler.name: { const previousPlaylistId = this._activePlaylist?._id this._activePlaylist = data as DBRundownPlaylist | undefined - this._logger.info(`${this._name} received playlist update from ${source}`) + this.logUpdateReceived('playlist', source) if (previousPlaylistId === this._activePlaylist?._id) return break } case AdLibActionsHandler.name: { const adLibActions = data ? (data as AdLibAction[]) : [] - this._logger.info(`${this._name} received adLibActions update from ${source}`) + this.logUpdateReceived('adLibActions', source) this._adLibActions = adLibActions break } case GlobalAdLibActionsHandler.name: { const globalAdLibActions = data ? (data as RundownBaselineAdLibAction[]) : [] - this._logger.info(`${this._name} received globalAdLibActions update from ${source}`) + this.logUpdateReceived('globalAdLibActions', source) this._globalAdLibActions = globalAdLibActions break } case AdLibsHandler.name: { const adLibs = data ? (data as AdLibPiece[]) : [] - this._logger.info(`${this._name} received adLibs update from ${source}`) + this.logUpdateReceived('adLibs', source) this._abLibs = adLibs break } case GlobalAdLibsHandler.name: { const globalAdLibs = data ? (data as RundownBaselineAdLibItem[]) : [] - this._logger.info(`${this._name} received globalAdLibs update from ${source}`) + this.logUpdateReceived('globalAdLibs', source) this._globalAdLibs = globalAdLibs break } case ShowStyleBaseHandler.name: { const showStyleBaseExt = data ? (data as ShowStyleBaseExt) : undefined - this._logger.info(`${this._name} received showStyleBase update from ${source}`) + this.logUpdateReceived('showStyleBase', source) this._sourceLayersMap = showStyleBaseExt?.sourceLayerNamesById ?? new Map() this._outputLayersMap = showStyleBaseExt?.outputLayerNamesById ?? new Map() break diff --git a/packages/live-status-gateway/src/topics/root.ts b/packages/live-status-gateway/src/topics/root.ts index 1097daf141..444ec7fe23 100644 --- a/packages/live-status-gateway/src/topics/root.ts +++ b/packages/live-status-gateway/src/topics/root.ts @@ -53,10 +53,7 @@ export class RootChannel extends WebSocketTopicBase implements WebSocketTopic { constructor(logger: Logger) { super('Root', logger) - this._heartbeat = setInterval( - () => this._subscribers.forEach((ws) => this.sendMessage(ws, { event: 'heartbeat' })), - 2000 - ) + this._heartbeat = setInterval(() => this._subscribers.forEach((ws) => this.sendHeartbeat(ws)), 2000) } close(): void { diff --git a/packages/live-status-gateway/src/topics/segmentsTopic.ts b/packages/live-status-gateway/src/topics/segmentsTopic.ts index e4d63da93c..22f2dd523b 100644 --- a/packages/live-status-gateway/src/topics/segmentsTopic.ts +++ b/packages/live-status-gateway/src/topics/segmentsTopic.ts @@ -88,17 +88,17 @@ export class SegmentsTopic switch (source) { case PlaylistHandler.name: { this._activePlaylist = data as DBRundownPlaylist | undefined - this._logger.info(`${this._name} received playlist update from ${source}`) + this.logUpdateReceived('playlist', source) break } case SegmentsHandler.name: { this._segments = data as DBSegment[] - this._logger.info(`${this._name} received segments update from ${source}`) + this.logUpdateReceived('segments', source) break } case PartsHandler.name: { this._partsBySegment = _.groupBy(data as DBPart[], 'segmentId') - this._logger.info(`${this._name} received parts update from ${source}`) + this.logUpdateReceived('parts', source) break } default: diff --git a/packages/live-status-gateway/src/topics/studioTopic.ts b/packages/live-status-gateway/src/topics/studioTopic.ts index 5b1ab82c9d..4998a1afd1 100644 --- a/packages/live-status-gateway/src/topics/studioTopic.ts +++ b/packages/live-status-gateway/src/topics/studioTopic.ts @@ -65,11 +65,11 @@ export class StudioTopic const studio = data ? (data as DBStudio) : undefined switch (source) { case StudioHandler.name: - this._logger.info(`${this._name} received studio update ${studio?._id}`) + this.logUpdateReceived('studio', source, `studioId ${studio?._id}`) this._studio = studio break case PlaylistsHandler.name: - this._logger.info(`${this._name} received playlists update from ${source}`) + this.logUpdateReceived('playlists', source) this._playlists = rundownPlaylists.map((p) => { let activationStatus: PlaylistActivationStatus = p.activationId === undefined ? 'deactivated' : 'activated' diff --git a/packages/live-status-gateway/src/wsHandler.ts b/packages/live-status-gateway/src/wsHandler.ts index f08ccfedca..136f6cf616 100644 --- a/packages/live-status-gateway/src/wsHandler.ts +++ b/packages/live-status-gateway/src/wsHandler.ts @@ -1,5 +1,5 @@ import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { CoreConnection, Observer, SubscriptionId } from '@sofie-automation/server-core-integration' +import { CoreConnection, Observer, ProtectedString, SubscriptionId } from '@sofie-automation/server-core-integration' import { Logger } from 'winston' import { WebSocket } from 'ws' import { CoreHandler } from './coreHandler' @@ -36,9 +36,23 @@ export abstract class WebSocketTopicBase { sendMessage(ws: WebSocket, msg: object): void { const msgStr = JSON.stringify(msg) - this._logger.info(`Send ${this._name} message '${msgStr}'`) + this._logger.debug(`Send ${this._name} message '${msgStr}'`) ws.send(msgStr) } + + sendHeartbeat(ws: WebSocket): void { + const msgStr = JSON.stringify({ event: 'heartbeat' }) + this._logger.silly(`Send ${this._name} message '${msgStr}'`) + ws.send(msgStr) + } + + protected logUpdateReceived(collectionName: string, source: string, extraInfo?: string): void { + let message = `${this._name} received ${collectionName} update from ${source}` + if (extraInfo) { + message += `, ${extraInfo}` + } + this._logger.debug(message) + } } export interface WebSocketTopic { @@ -56,7 +70,7 @@ export type ObserverForCollection = T extends keyof CorelibPubSubCollections export abstract class CollectionBase< T, TPubSub extends CorelibPubSub | undefined, - TCollection extends keyof CorelibPubSubCollections | undefined + TCollection extends keyof CorelibPubSubCollections > { protected _name: string protected _collectionName: TCollection @@ -111,6 +125,32 @@ export abstract class CollectionBase< await observer.update(this._name, data) } } + + protected logDocumentChange(documentId: string | ProtectedString, changeType: string): void { + this._logger.silly(`${this._name} ${changeType} ${documentId}`) + } + + protected logUpdateReceived(collectionName: string, updateCount: number | undefined): void + protected logUpdateReceived(collectionName: string, source: string, extraInfo?: string): void + protected logUpdateReceived( + collectionName: string, + sourceOrUpdateCount: string | number | undefined, + extraInfo?: string + ): void { + if (typeof sourceOrUpdateCount === 'string') { + let message = `${this._name} received ${collectionName} update from ${sourceOrUpdateCount}` + if (extraInfo) { + message += `, ${extraInfo}` + } + this._logger.debug(message) + } else { + this._logger.debug(`'${this._name}' handler received ${sourceOrUpdateCount} ${collectionName}`) + } + } + + protected logNotifyingUpdate(updateCount: number | undefined): void { + this._logger.debug(`${this._name} notifying update with ${updateCount} ${this._collectionName}`) + } } export interface Collection { From fec30c4eccc4fc419734691c460a5451de2cc47a Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Tue, 19 Mar 2024 08:12:12 +0100 Subject: [PATCH 265/479] chore: typo --- meteor/client/ui/Settings/SystemManagement.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/meteor/client/ui/Settings/SystemManagement.tsx b/meteor/client/ui/Settings/SystemManagement.tsx index d5c0963839..cdcb2e610b 100644 --- a/meteor/client/ui/Settings/SystemManagement.tsx +++ b/meteor/client/ui/Settings/SystemManagement.tsx @@ -526,14 +526,14 @@ export function checkForOldDataAndCleanUp(t: TFunction, retriesLeft = 0): void { function SystemManagementHeapSnapshot() { const { t } = useTranslation() - const [displayWarning, setdisplayWarning] = React.useState(false) + const [displayWarning, setDisplayWarning] = React.useState(false) const [active, setActive] = React.useState(false) const onAreYouSure = useCallback(() => { - setdisplayWarning(true) + setDisplayWarning(true) }, []) const onReset = useCallback(() => { - setdisplayWarning(false) + setDisplayWarning(false) setActive(false) }, []) const onConfirm = useCallback(() => { From ebb154d6b59369588da400d8d921a00e41b84dc8 Mon Sep 17 00:00:00 2001 From: ianshade Date: Fri, 5 Apr 2024 20:29:34 +0200 Subject: [PATCH 266/479] fix: allow replacement in replaceInfinitesFromPreviousPlayhead --- .../PlayoutPartInstanceModelImpl.ts | 3 +- .../PlayoutPartInstanceModelImpl.spec.ts | 110 ++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 packages/job-worker/src/playout/model/implementation/__tests__/PlayoutPartInstanceModelImpl.spec.ts diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts index 7332618717..4556335702 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts @@ -384,7 +384,8 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { } for (const pieceInstance of pieceInstances) { - if (this.pieceInstancesImpl.has(pieceInstance._id)) + const existingPieceInstance = this.pieceInstancesImpl.get(pieceInstance._id) + if (existingPieceInstance) throw new Error( `Cannot replace infinite PieceInstance "${pieceInstance._id}" as it replaces a non-infinite` ) diff --git a/packages/job-worker/src/playout/model/implementation/__tests__/PlayoutPartInstanceModelImpl.spec.ts b/packages/job-worker/src/playout/model/implementation/__tests__/PlayoutPartInstanceModelImpl.spec.ts new file mode 100644 index 0000000000..e90a967e90 --- /dev/null +++ b/packages/job-worker/src/playout/model/implementation/__tests__/PlayoutPartInstanceModelImpl.spec.ts @@ -0,0 +1,110 @@ +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' +import { getRandomId, literal } from '@sofie-automation/corelib/dist/lib' +import { PlayoutPartInstanceModelImpl } from '../PlayoutPartInstanceModelImpl' +import { PieceInstance, PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { IBlueprintPieceType, PieceLifespan } from '@sofie-automation/blueprints-integration' + +describe('PlayoutPartInstanceModelImpl', () => { + function createBasicDBPartInstance(): DBPartInstance { + return { + _id: getRandomId(), + rundownId: protectString(''), + segmentId: protectString(''), + playlistActivationId: protectString(''), + segmentPlayoutId: protectString(''), + rehearsal: false, + + takeCount: 0, + + part: { + _id: getRandomId(), + _rank: 0, + rundownId: protectString(''), + segmentId: protectString(''), + externalId: '', + title: '', + + expectedDurationWithPreroll: undefined, + }, + } + } + + function createPieceInstanceAsInfinite(id: string, fromPreviousPlayhead: boolean): PieceInstance { + return literal({ + _id: protectString(id), + rundownId: protectString(''), + partInstanceId: protectString(''), + playlistActivationId: protectString('active'), + piece: literal({ + _id: protectString(`${id}_p`), + externalId: '', + startPartId: protectString(''), + enable: { start: 0 }, + name: '', + lifespan: PieceLifespan.OutOnRundownChange, + sourceLayerId: '', + outputLayerId: '', + invalid: false, + content: {}, + timelineObjectsString: protectString(''), + pieceType: IBlueprintPieceType.Normal, + }), + infinite: { + infiniteInstanceId: protectString(`${id}_inf`), + infiniteInstanceIndex: 0, + infinitePieceId: protectString(`${id}_p`), + fromPreviousPart: false, + fromPreviousPlayhead, + }, + }) + } + + describe('replaceInfinitesFromPreviousPlayhead', () => { + it('works for an empty part', async () => { + const partInstance = createBasicDBPartInstance() + const model = new PlayoutPartInstanceModelImpl(partInstance, [], false) + + expect(() => model.replaceInfinitesFromPreviousPlayhead([])).not.toThrow() + expect(model.pieceInstances).toEqual([]) + }) + + it('keeps pieceInstance with fromPreviousPlayhead=false', async () => { + const partInstance = createBasicDBPartInstance() + const model = new PlayoutPartInstanceModelImpl( + partInstance, + [createPieceInstanceAsInfinite('p1', false)], + false + ) + + expect(() => model.replaceInfinitesFromPreviousPlayhead([])).not.toThrow() + expect(model.pieceInstances.map((p) => p.pieceInstance._id)).toEqual(['p1']) + }) + + it('deleted pieceInstance with fromPreviousPlayhead=true if no replacement provided', async () => { + const partInstance = createBasicDBPartInstance() + const model = new PlayoutPartInstanceModelImpl( + partInstance, + [createPieceInstanceAsInfinite('p1', true)], + false + ) + + expect(() => model.replaceInfinitesFromPreviousPlayhead([])).not.toThrow() + expect(model.pieceInstances.map((p) => p.pieceInstance._id)).toEqual([]) + }) + + it('replaces pieceInstance with fromPreviousPlayhead=true if replacement provided', async () => { + const partInstance = createBasicDBPartInstance() + const model = new PlayoutPartInstanceModelImpl( + partInstance, + [createPieceInstanceAsInfinite('p1', true)], + false + ) + + expect(() => + model.replaceInfinitesFromPreviousPlayhead([createPieceInstanceAsInfinite('p1', true)]) + ).not.toThrow() + expect(model.pieceInstances.map((p) => p.pieceInstance._id)).toEqual(['p1']) + }) + }) +}) From 7f5cff9a1a72c5ce98546c1d5337a3c57425576a Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 10 Apr 2024 16:19:31 +0200 Subject: [PATCH 267/479] chore(core): update threadedClass to 1.2.2 --- meteor/package.json | 2 +- meteor/yarn.lock | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/meteor/package.json b/meteor/package.json index 210487926d..d141140bc6 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -101,7 +101,7 @@ "react-timer-hoc": "^2.3.0", "semver": "^7.5.4", "superfly-timeline": "9.0.0", - "threadedclass": "^1.2.1", + "threadedclass": "^1.2.2", "timecode": "0.0.4", "type-fest": "^3.13.1", "underscore": "^1.13.6", diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 9ea0aabe5a..dfcc99da0f 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -2795,7 +2795,7 @@ __metadata: sinon: ^14.0.2 standard-version: ^9.5.0 superfly-timeline: 9.0.0 - threadedclass: ^1.2.1 + threadedclass: ^1.2.2 timecode: 0.0.4 ts-jest: ^29.1.2 type-fest: ^3.13.1 @@ -11715,6 +11715,18 @@ __metadata: languageName: node linkType: hard +"threadedclass@npm:^1.2.2": + version: 1.2.2 + resolution: "threadedclass@npm:1.2.2" + dependencies: + callsites: ^3.1.0 + eventemitter3: ^4.0.4 + is-running: ^2.1.0 + tslib: ^1.13.0 + checksum: 9bf8a0fab0105178885f7ad26b8f8a807935f66f59d348ab5c1bf5ac0bd19c1f4c64aebc3f017073a7f57d20658a53798bc8eae6f822d79ec071d3e0e009389d + languageName: node + linkType: hard + "through2@npm:^2.0.0, through2@npm:^2.0.1": version: 2.0.5 resolution: "through2@npm:2.0.5" From c36edcd1ec1d939c35d7fa54e70ac9c632075cbf Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Thu, 11 Apr 2024 10:22:34 +0200 Subject: [PATCH 268/479] chore(job-worker): update threadedClass dep --- meteor/yarn.lock | 14 +------------- packages/job-worker/package.json | 2 +- packages/yarn.lock | 14 +++++++++++++- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/meteor/yarn.lock b/meteor/yarn.lock index dfcc99da0f..ccb8895492 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1406,7 +1406,7 @@ __metadata: p-lazy: ^3.1.0 p-timeout: ^4.1.0 superfly-timeline: 9.0.0 - threadedclass: ^1.2.1 + threadedclass: ^1.2.2 tslib: ^2.6.2 type-fest: ^3.13.1 underscore: ^1.13.6 @@ -11703,18 +11703,6 @@ __metadata: languageName: node linkType: hard -"threadedclass@npm:^1.2.1": - version: 1.2.1 - resolution: "threadedclass@npm:1.2.1" - dependencies: - callsites: ^3.1.0 - eventemitter3: ^4.0.4 - is-running: ^2.1.0 - tslib: ^1.13.0 - checksum: 2f9cab0df9ed21865f6f874ff7e352bbc32c9970bb48f132ae9bd71d9203655089ad065fa8438bdb786e381c3e0284b43bf7f281ad607962ab365781ea9aef6f - languageName: node - linkType: hard - "threadedclass@npm:^1.2.2": version: 1.2.2 resolution: "threadedclass@npm:1.2.2" diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index 4732351ad1..cb481aae79 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -53,7 +53,7 @@ "p-lazy": "^3.1.0", "p-timeout": "^4.1.0", "superfly-timeline": "9.0.0", - "threadedclass": "^1.2.1", + "threadedclass": "^1.2.2", "tslib": "^2.6.2", "type-fest": "^3.13.1", "underscore": "^1.13.6" diff --git a/packages/yarn.lock b/packages/yarn.lock index 90abe14add..5ece406dde 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -4544,7 +4544,7 @@ __metadata: p-lazy: ^3.1.0 p-timeout: ^4.1.0 superfly-timeline: 9.0.0 - threadedclass: ^1.2.1 + threadedclass: ^1.2.2 tslib: ^2.6.2 type-fest: ^3.13.1 underscore: ^1.13.6 @@ -21094,6 +21094,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"threadedclass@npm:^1.2.2": + version: 1.2.2 + resolution: "threadedclass@npm:1.2.2" + dependencies: + callsites: ^3.1.0 + eventemitter3: ^4.0.4 + is-running: ^2.1.0 + tslib: ^1.13.0 + checksum: 9bf8a0fab0105178885f7ad26b8f8a807935f66f59d348ab5c1bf5ac0bd19c1f4c64aebc3f017073a7f57d20658a53798bc8eae6f822d79ec071d3e0e009389d + languageName: node + linkType: hard + "through2@npm:^2.0.0, through2@npm:^2.0.1": version: 2.0.5 resolution: "through2@npm:2.0.5" From c3fcc55ccb1d6a2c0792d2747fa78de8760b3145 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Mon, 15 Apr 2024 07:58:45 +0200 Subject: [PATCH 269/479] chore: update mos-connection to v4.1.9 --- meteor/package.json | 2 +- meteor/yarn.lock | 22 ++++++++++---------- packages/mos-gateway/package.json | 2 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 34 +++++++++++++++---------------- 5 files changed, 31 insertions(+), 31 deletions(-) diff --git a/meteor/package.json b/meteor/package.json index d141140bc6..263cd5f550 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -48,7 +48,7 @@ "@jstarpl/react-contextmenu": "^2.15.0", "@koa/cors": "^5.0.0", "@koa/router": "^12.0.1", - "@mos-connection/helper": "3.0.7", + "@mos-connection/helper": "^4.1.0", "@nrk/core-icons": "^9.6.0", "@popperjs/core": "^2.11.8", "@slack/webhook": "^6.1.0", diff --git a/meteor/yarn.lock b/meteor/yarn.lock index ccb8895492..b64f2c41bf 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1020,23 +1020,23 @@ __metadata: languageName: node linkType: hard -"@mos-connection/helper@npm:3.0.7": - version: 3.0.7 - resolution: "@mos-connection/helper@npm:3.0.7" +"@mos-connection/helper@npm:^4.1.0": + version: 4.1.0 + resolution: "@mos-connection/helper@npm:4.1.0" dependencies: - "@mos-connection/model": 3.0.7 + "@mos-connection/model": 4.1.0 iconv-lite: ^0.6.3 tslib: ^2.5.3 xml-js: ^1.6.11 xmlbuilder: ^15.1.1 - checksum: 802c95955328d89e62aa0addbaf79ce339c93bce499d96b6d314991cd77acbcbd573e4d6eb48408de234708f908d608b20941f846fceaa22813ddb2e6bdf6b4e + checksum: 46df4a6bb603da9d8759096f3c262d5353534f81978bfabeb3320a78af92242e86985b11bd00d407c1bf027b3fd3916eaa0f79ac9453959d7afaafbd185fba74 languageName: node linkType: hard -"@mos-connection/model@npm:3.0.7, @mos-connection/model@npm:^3.0.7": - version: 3.0.7 - resolution: "@mos-connection/model@npm:3.0.7" - checksum: 4f7390a40fc5152bfcdb4e99eee243e3a1df248bf1f77e0a63d7d29d8db88cb8c9df5f231ab5d18e7cb1147ba8d39af5c621877221f343db630906e000c5b089 +"@mos-connection/model@npm:4.1.0, @mos-connection/model@npm:^4.1.0": + version: 4.1.0 + resolution: "@mos-connection/model@npm:4.1.0" + checksum: b8e1f83d87c342959165956c19b4a8d03f2c7ae0b1caf02cd00451deb977c0c5ea4ad5b9c8915ba0cea199ce071efbbcfe2329b5e737f89f1725820740d12f60 languageName: node linkType: hard @@ -1417,7 +1417,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/shared-lib@portal:../packages/shared-lib::locator=automation-core%40workspace%3A." dependencies: - "@mos-connection/model": ^3.0.7 + "@mos-connection/model": ^4.1.0 timeline-state-resolver-types: 9.1.0-nightly-release51-20240228-132329-b7ceb6950.0 tslib: ^2.6.2 type-fest: ^3.13.1 @@ -2689,7 +2689,7 @@ __metadata: "@jstarpl/react-contextmenu": ^2.15.0 "@koa/cors": ^5.0.0 "@koa/router": ^12.0.1 - "@mos-connection/helper": 3.0.7 + "@mos-connection/helper": ^4.1.0 "@nrk/core-icons": ^9.6.0 "@popperjs/core": ^2.11.8 "@shopify/jest-koa-mocks": ^5.1.1 diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index 0adb46089a..7946085e7d 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -65,7 +65,7 @@ "production" ], "dependencies": { - "@mos-connection/connector": "^3.0.7", + "@mos-connection/connector": "^4.1.0", "@sofie-automation/server-core-integration": "1.51.0-in-development", "@sofie-automation/shared-lib": "1.51.0-in-development", "tslib": "^2.6.2", diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index addfaffbe9..8c2743d4d8 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -38,7 +38,7 @@ "/LICENSE" ], "dependencies": { - "@mos-connection/model": "^3.0.7", + "@mos-connection/model": "^4.1.0", "timeline-state-resolver-types": "9.1.0-nightly-release51-20240228-132329-b7ceb6950.0", "tslib": "^2.6.2", "type-fest": "^3.13.1" diff --git a/packages/yarn.lock b/packages/yarn.lock index 5ece406dde..0e974503be 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -3224,37 +3224,37 @@ __metadata: languageName: node linkType: hard -"@mos-connection/connector@npm:^3.0.7": - version: 3.0.7 - resolution: "@mos-connection/connector@npm:3.0.7" +"@mos-connection/connector@npm:^4.1.0": + version: 4.1.0 + resolution: "@mos-connection/connector@npm:4.1.0" dependencies: - "@mos-connection/helper": 3.0.7 - "@mos-connection/model": 3.0.7 + "@mos-connection/helper": 4.1.0 + "@mos-connection/model": 4.1.0 iconv-lite: ^0.6.3 tslib: ^2.5.3 xml-js: ^1.6.11 xmlbuilder: ^15.1.1 - checksum: 588a6c929eee17d272fe6cd15b8547b96bd36ad6e507cc6f0e6b879a479a63860e75c2e59287efe41fc78326bfa160b7a1e15a5dfbddb321f582a927cd64062f + checksum: 05a187b61aaa143b647afa51cd7e8632ae5a1843ecfd02ccce53f78647dfacd74267a836f4c47237e716c204df177e012461c366006b2c204973e81f3b2355e9 languageName: node linkType: hard -"@mos-connection/helper@npm:3.0.7": - version: 3.0.7 - resolution: "@mos-connection/helper@npm:3.0.7" +"@mos-connection/helper@npm:4.1.0": + version: 4.1.0 + resolution: "@mos-connection/helper@npm:4.1.0" dependencies: - "@mos-connection/model": 3.0.7 + "@mos-connection/model": 4.1.0 iconv-lite: ^0.6.3 tslib: ^2.5.3 xml-js: ^1.6.11 xmlbuilder: ^15.1.1 - checksum: 802c95955328d89e62aa0addbaf79ce339c93bce499d96b6d314991cd77acbcbd573e4d6eb48408de234708f908d608b20941f846fceaa22813ddb2e6bdf6b4e + checksum: 46df4a6bb603da9d8759096f3c262d5353534f81978bfabeb3320a78af92242e86985b11bd00d407c1bf027b3fd3916eaa0f79ac9453959d7afaafbd185fba74 languageName: node linkType: hard -"@mos-connection/model@npm:3.0.7, @mos-connection/model@npm:^3.0.7": - version: 3.0.7 - resolution: "@mos-connection/model@npm:3.0.7" - checksum: 4f7390a40fc5152bfcdb4e99eee243e3a1df248bf1f77e0a63d7d29d8db88cb8c9df5f231ab5d18e7cb1147ba8d39af5c621877221f343db630906e000c5b089 +"@mos-connection/model@npm:4.1.0, @mos-connection/model@npm:^4.1.0": + version: 4.1.0 + resolution: "@mos-connection/model@npm:4.1.0" + checksum: b8e1f83d87c342959165956c19b4a8d03f2c7ae0b1caf02cd00451deb977c0c5ea4ad5b9c8915ba0cea199ce071efbbcfe2329b5e737f89f1725820740d12f60 languageName: node linkType: hard @@ -4581,7 +4581,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: - "@mos-connection/model": ^3.0.7 + "@mos-connection/model": ^4.1.0 timeline-state-resolver-types: 9.1.0-nightly-release51-20240228-132329-b7ceb6950.0 tslib: ^2.6.2 type-fest: ^3.13.1 @@ -15678,7 +15678,7 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "mos-gateway@workspace:mos-gateway" dependencies: - "@mos-connection/connector": ^3.0.7 + "@mos-connection/connector": ^4.1.0 "@sofie-automation/server-core-integration": 1.51.0-in-development "@sofie-automation/shared-lib": 1.51.0-in-development tslib: ^2.6.2 From e8cec88f8b9d8c298f1b2ddec8d45f6f80fea9bf Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Mon, 15 Apr 2024 09:16:06 +0200 Subject: [PATCH 270/479] fix: create new mosTimes correctly --- packages/mos-gateway/src/CoreMosDeviceHandler.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/mos-gateway/src/CoreMosDeviceHandler.ts b/packages/mos-gateway/src/CoreMosDeviceHandler.ts index a5da2de82c..6063571da5 100644 --- a/packages/mos-gateway/src/CoreMosDeviceHandler.ts +++ b/packages/mos-gateway/src/CoreMosDeviceHandler.ts @@ -316,7 +316,7 @@ export class CoreMosDeviceHandler { const result = await this._mosDevice.sendRunningOrderStatus({ ID: this.mosTypes.mosString128.create(roId), Status: status, - Time: this.mosTypes.mosTime.create(undefined), + Time: this.mosTypes.mosTime.create(new Date()), }) // console.log('got result', result) @@ -328,7 +328,7 @@ export class CoreMosDeviceHandler { RunningOrderId: this.mosTypes.mosString128.create(roId), ID: this.mosTypes.mosString128.create(storyId), Status: status, - Time: this.mosTypes.mosTime.create(undefined), + Time: this.mosTypes.mosTime.create(new Date()), }) // console.log('got result', result) @@ -341,7 +341,7 @@ export class CoreMosDeviceHandler { StoryId: this.mosTypes.mosString128.create(storyId), ID: this.mosTypes.mosString128.create(itemId), Status: status, - Time: this.mosTypes.mosTime.create(undefined), + Time: this.mosTypes.mosTime.create(new Date()), }) // console.log('got result', result) From 869d3012bd3bbf891e9c701f23f42e31cb3e7ff0 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Thu, 18 Apr 2024 15:33:33 +0200 Subject: [PATCH 271/479] feat: new style for step GFX pill --- meteor/client/styles/rundownView.scss | 41 ++++-- .../PieceMultistepChevron.tsx | 44 +++++-- .../LinePartMainPiece/LinePartMainPiece.tsx | 13 +- .../Renderers/CustomLayerItemRenderer.tsx | 1 + .../Renderers/L3rdSourceRenderer.tsx | 121 ++++++++++++++---- 5 files changed, 162 insertions(+), 58 deletions(-) diff --git a/meteor/client/styles/rundownView.scss b/meteor/client/styles/rundownView.scss index fd7ce6fd3a..6b18b445a1 100644 --- a/meteor/client/styles/rundownView.scss +++ b/meteor/client/styles/rundownView.scss @@ -28,7 +28,8 @@ $segment-editorialline-color: rgba(255, 255, 255, 0.25); $rundown-divider-color: #000000; $rundown-divider-background-color: #dddddd; -$segment-piece-step-counter: #ffff00; +$segment-piece-step-counter: #ffffff; +$segment-piece-step-counter-background: #6f3010; $previewline-color: rgb(38, 137, 186); @@ -742,7 +743,6 @@ svg.icon { .segment-timeline__expectedTime { grid-column: duration-onAirIn-split / segment-group-controls; } - } .segment-timeline__switch-view-mode-button { @@ -2138,17 +2138,33 @@ svg.icon { margin: 0; } - .segment-timeline__piece__step-chevron { + &.with-steps { + > .segment-timeline__piece__label { + padding-right: 3em; + } + } + + .segment-timeline__piece__step { position: relative; vertical-align: top; margin-right: 0.2em; - background: rgba(0, 0, 0, 0.4); + margin-top: 0.1em; + background: $segment-piece-step-counter-background; display: inline-block; - padding: 0 1em 0 0.4em; + padding: 0 0.3em; color: $segment-piece-step-counter; text-shadow: 0 0 1px #000, 0.5px 0.5px 8px rgba(0, 0, 0, 0.8); - clip-path: polygon(0 0, calc(100% - 0.6em) 0, 100% 50%, calc(100% - 0.6em) 100%, 0 100%); - line-height: 1.4em; + line-height: 1.2em; + border-radius: 3px; + z-index: 3; + text-align: center; + + &.fixed-width { + padding: 0; + max-width: 3em; + min-width: 3em; + overflow: hidden; + } } > .segment-timeline__piece__label { @@ -2891,9 +2907,8 @@ svg.icon { position: absolute; top: 0; left: 0; - background: $segment-layer-background-graphics; - padding: 0 0.75em; - clip-path: polygon(0 0, calc(100% - 0.6em) 0, 100% 50%, calc(100% - 0.6em) 100%, 0 100%, 0.6em 50%); + background: $segment-piece-step-counter-background; + padding: 0 0.2em; font-weight: 400; text-shadow: 0 0 1px #000, 0.5px 0.5px 8px rgba(0, 0, 0, 0.8); color: $segment-piece-step-counter; @@ -3267,17 +3282,15 @@ svg.icon { .segment-storyboard__piece__step-chevron, .segment-opl__main-piece__label__step-chevron { margin-right: 0.2em; - background: rgba(0, 0, 0, 0.4); + background: $segment-piece-step-counter-background; display: inline-block; - padding: 0 1em 0 0.4em; + padding: 0 0.2em; color: $segment-piece-step-counter; text-shadow: 0 0 1px #000, 0.5px 0.5px 8px rgba(0, 0, 0, 0.8); - clip-path: polygon(0 0, calc(100% - 0.6em) 0, 100% 50%, calc(100% - 0.6em) 100%, 0 100%); } .segment-storyboard__piece__step-chevron { margin-left: -0.3em; - padding-left: 0.6em; } .segment-opl__main-piece__label__step-chevron { diff --git a/meteor/client/ui/SegmentContainer/PieceMultistepChevron.tsx b/meteor/client/ui/SegmentContainer/PieceMultistepChevron.tsx index b09472f752..1ef4ea61a4 100644 --- a/meteor/client/ui/SegmentContainer/PieceMultistepChevron.tsx +++ b/meteor/client/ui/SegmentContainer/PieceMultistepChevron.tsx @@ -2,13 +2,28 @@ import React from 'react' import { NoraContent, SourceLayerType } from '@sofie-automation/blueprints-integration' import { PieceExtended } from '../../../lib/Rundown' -export const PieceMultistepChevron = function PieceMultistepChevron({ - className, - piece, -}: { - className: string - piece: PieceExtended -}): JSX.Element | null { +export const PieceMultistepChevron = React.forwardRef< + HTMLSpanElement, + { + className: string + piece: PieceExtended + style?: React.CSSProperties + } +>(function PieceMultistepChevron({ className, piece, style }, ref): JSX.Element | null { + const hasStepChevron = usePieceSteps(piece) + + if (!hasStepChevron) return null + + const { currentStep, allSteps } = hasStepChevron + + return ( + + {currentStep}/{allSteps} + + ) +}) + +export function usePieceSteps(piece: PieceExtended): { currentStep: number; allSteps: number } | null { const noraContent = piece.instance.piece.content as NoraContent | undefined const hasStepChevron = @@ -17,11 +32,12 @@ export const PieceMultistepChevron = function PieceMultistepChevron({ if (!hasStepChevron) return null - return ( - - {noraContent?.payload?.step?.to === 'next' - ? (noraContent.payload.step?.from || 0) + 1 - : noraContent.payload.step?.to || 1} - - ) + const currentStep = + noraContent?.payload?.step?.to === 'next' + ? (noraContent.payload.step?.from || 0) + 1 + : noraContent.payload.step?.to || 1 + + const allSteps = (noraContent?.payload?.content?.steps as any[])?.length ?? 0 + + return { currentStep, allSteps } } diff --git a/meteor/client/ui/SegmentList/LinePartMainPiece/LinePartMainPiece.tsx b/meteor/client/ui/SegmentList/LinePartMainPiece/LinePartMainPiece.tsx index 8b95b27fb3..dbc1faa9c8 100644 --- a/meteor/client/ui/SegmentList/LinePartMainPiece/LinePartMainPiece.tsx +++ b/meteor/client/ui/SegmentList/LinePartMainPiece/LinePartMainPiece.tsx @@ -14,7 +14,7 @@ import { getNoticeLevelForPieceStatus } from '../../../../lib/notifications/noti import { PieceStatusIcon } from '../../../lib/ui/PieceStatusIcon' import { UIStudio } from '../../../../lib/api/studios' import classNames from 'classnames' -import { PieceMultistepChevron } from '../../SegmentContainer/PieceMultistepChevron' +import { PieceMultistepChevron, usePieceSteps } from '../../SegmentContainer/PieceMultistepChevron' import { LoopingPieceIcon } from '../../../lib/ui/icons/looping' interface IProps { @@ -163,10 +163,11 @@ export const LinePartMainPiece = withMediaObjectStatus()(function Li const noticeLevel = getNoticeLevelForPieceStatus(piece.contentStatus?.status) - const multistepChevron = PieceMultistepChevron({ - className: 'segment-opl__main-piece__label__step-chevron', - piece: piece, - }) + const hasStepChevron = usePieceSteps(piece) + + const multistepChevron = ( + + ) return ( ()(function Li {anomalies}
{noticeLevel !== null && } diff --git a/meteor/client/ui/SegmentTimeline/Renderers/CustomLayerItemRenderer.tsx b/meteor/client/ui/SegmentTimeline/Renderers/CustomLayerItemRenderer.tsx index b93035032d..9a05f42d16 100644 --- a/meteor/client/ui/SegmentTimeline/Renderers/CustomLayerItemRenderer.tsx +++ b/meteor/client/ui/SegmentTimeline/Renderers/CustomLayerItemRenderer.tsx @@ -26,6 +26,7 @@ export interface ICustomLayerItemProps { partDisplayDuration: number piece: PieceUi timeScale: number + scrollLeft: number onFollowLiveLine?: (state: boolean, event: any) => void relative?: boolean followLiveLine: boolean diff --git a/meteor/client/ui/SegmentTimeline/Renderers/L3rdSourceRenderer.tsx b/meteor/client/ui/SegmentTimeline/Renderers/L3rdSourceRenderer.tsx index cb678a70bd..328465c717 100644 --- a/meteor/client/ui/SegmentTimeline/Renderers/L3rdSourceRenderer.tsx +++ b/meteor/client/ui/SegmentTimeline/Renderers/L3rdSourceRenderer.tsx @@ -5,29 +5,64 @@ import { CustomLayerItemRenderer, ICustomLayerItemProps } from './CustomLayerIte import { NoraContent, SourceLayerType } from '@sofie-automation/blueprints-integration' import { L3rdFloatingInspector } from '../../FloatingInspectors/L3rdFloatingInspector' import { RundownUtils } from '../../../lib/rundown' +import { PieceMultistepChevron, usePieceSteps } from '../../SegmentContainer/PieceMultistepChevron' import classNames from 'classnames' -import { PieceMultistepChevron } from '../../SegmentContainer/PieceMultistepChevron' type IProps = ICustomLayerItemProps -interface IState {} +interface IState { + leftLabelWidth: number + rightLabelWidth: number + multistepPillWidth: number + stepsStyle: React.CSSProperties | null +} + export class L3rdSourceRenderer extends CustomLayerItemRenderer { leftLabel: HTMLElement | null = null rightLabel: HTMLElement | null = null lastOverflowTime: boolean | undefined + constructor(props: IProps) { + super(props) + this.state = { + leftLabelWidth: 0, + rightLabelWidth: 0, + multistepPillWidth: 0, + stepsStyle: null, + } + } + private updateAnchoredElsWidths = () => { const leftLabelWidth = this.leftLabel ? getElementWidth(this.leftLabel) : 0 const rightLabelWidth = this.rightLabel ? getElementWidth(this.rightLabel) : 0 this.setAnchoredElsWidths(leftLabelWidth, rightLabelWidth) + + this.setState({ + leftLabelWidth, + rightLabelWidth, + }) + } + + private setLeftLabelRef = (el: HTMLSpanElement) => { + this.leftLabel = el } - private setLeftLabelRef = (e: HTMLSpanElement) => { - this.leftLabel = e + private setRightLabelRef = (el: HTMLSpanElement) => { + this.rightLabel = el } - private setRightLabelRef = (e: HTMLSpanElement) => { - this.rightLabel = e + private setMultistepPillEl = (el: HTMLSpanElement | null) => { + if (!el) { + this.setState({ + multistepPillWidth: 0, + }) + return + } + + const { width } = el.getBoundingClientRect() + this.setState({ + multistepPillWidth: width, + }) } componentDidMount(): void { @@ -50,31 +85,75 @@ export class L3rdSourceRenderer extends CustomLayerItemRenderer } } + static getDerivedStateFromProps(props: Readonly, state: Readonly): Partial { + const { + scrollLeft, + timeScale, + isLiveLine, + liveLineHistorySize, + followLiveLine, + // livePosition, + isPreview, + relative, + partStartsAt, + piece, + } = props + const { leftLabelWidth, multistepPillWidth } = state + if (!isLiveLine || !followLiveLine || isPreview || relative) { + return { + stepsStyle: null, + } + } + + // const liveLineHistoryWithMargin = liveLineHistorySize + + const inPoint = piece.renderedInPoint || 0 + // const targetPos = Math.min( + // (scrollLeft - inPoint - partStartsAt) * timeScale, + // leftLabelWidth - multistepPillWidth * 2 + // ) + const targetPos = Math.min( + (scrollLeft - inPoint - partStartsAt) * timeScale - multistepPillWidth - 10, + leftLabelWidth - multistepPillWidth - liveLineHistorySize + ) + + return { + stepsStyle: { + transform: ` translate(${targetPos}px, 0) ` + ` translate(${liveLineHistorySize}px, 0) `, + // ' translate(-100%, 0)', + willChange: 'transform', + }, + } + } + render(): JSX.Element { - const innerPiece = this.props.piece.instance.piece + const { piece, isTooSmallForText, isLiveLine } = this.props + const innerPiece = piece.instance.piece const noraContent = innerPiece.content as NoraContent | undefined - const stepContent = noraContent?.payload?.step - const isMultiStep = stepContent?.enabled === true - - const multistepChevron = PieceMultistepChevron({ - className: 'segment-timeline__piece__step-chevron', - piece: this.props.piece, - }) + const hasStepChevron = usePieceSteps(piece) + const multistepPill = ( + + ) return ( - {!this.props.isTooSmallForText && ( + {!isTooSmallForText && ( <> - {!this.props.piece.hasOriginInPreceedingPart || this.props.isLiveLine ? ( + {!piece.hasOriginInPreceedingPart || isLiveLine ? ( - {multistepChevron} + {multistepPill} {innerPiece.name} ) : null} @@ -87,12 +166,6 @@ export class L3rdSourceRenderer extends CustomLayerItemRenderer {this.renderLoopIcon()} {this.renderOverflowTimeLabel()} - {isMultiStep ? ( - <> - - - - ) : null} )} Date: Thu, 18 Apr 2024 16:44:21 +0200 Subject: [PATCH 272/479] fix: clear Scratchpad when activating Rundown --- .../src/playout/activePlaylistActions.ts | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/job-worker/src/playout/activePlaylistActions.ts b/packages/job-worker/src/playout/activePlaylistActions.ts index 79e709c534..4f1fe905ff 100644 --- a/packages/job-worker/src/playout/activePlaylistActions.ts +++ b/packages/job-worker/src/playout/activePlaylistActions.ts @@ -1,17 +1,18 @@ import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' -import { getActiveRundownPlaylistsInStudioFromDb } from '../studio/lib' +import { ReadonlyDeep } from 'type-fest' +import { RundownActivationContext } from '../blueprints/context/RundownActivationContext' import { JobContext } from '../jobs' +import { getCurrentTime } from '../lib' import { logger } from '../logging' -import { PlayoutModel } from './model/PlayoutModel' +import { getActiveRundownPlaylistsInStudioFromDb } from '../studio/lib' +import { cleanTimelineDatastore } from './datastore' import { resetRundownPlaylist } from './lib' +import { PlayoutModel } from './model/PlayoutModel' import { selectNextPart } from './selectNextPart' import { setNextPart } from './setNext' import { updateStudioTimeline, updateTimeline } from './timeline/generate' -import { getCurrentTime } from '../lib' -import { cleanTimelineDatastore } from './datastore' -import { RundownActivationContext } from '../blueprints/context/RundownActivationContext' -import { ReadonlyDeep } from 'type-fest' export async function activateRundownPlaylist( context: JobContext, @@ -44,9 +45,21 @@ export async function activateRundownPlaylist( const newActivationId = playoutModel.activatePlaylist(rehearsal) + const currentPartInstance = playoutModel.currentPartInstance + if (currentPartInstance && !rehearsal) { + const rundownId = currentPartInstance.partInstance.rundownId + const rundown = playoutModel.getRundown(rundownId) + if (!rundown) throw new Error(`Could not find rundown "${rundownId}"`) + const currentSegment = playoutModel.findSegment(currentPartInstance.partInstance.segmentId) + if (!currentSegment) throw new Error(`Could not find segment "${currentPartInstance.partInstance.segmentId}"`) + if (currentSegment.segment.orphaned === SegmentOrphanedReason.SCRATCHPAD) { + currentPartInstance.markAsReset() + rundown.removeScratchpadSegment() + } + } + let rundown: ReadonlyDeep | undefined - const currentPartInstance = playoutModel.currentPartInstance if (!currentPartInstance || currentPartInstance.partInstance.reset) { playoutModel.clearSelectedPartInstances() From 2dbe0dd3e7cc7fc43d06690d608691c3c08c3b4b Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 19 Apr 2024 10:15:52 +0100 Subject: [PATCH 273/479] fix: refactor ingest.test.ts to remove dependencies between tests (#1172) --- packages/job-worker/src/__mocks__/context.ts | 10 + .../src/ingest/__tests__/ingest.test.ts | 2165 +++++++---------- 2 files changed, 902 insertions(+), 1273 deletions(-) diff --git a/packages/job-worker/src/__mocks__/context.ts b/packages/job-worker/src/__mocks__/context.ts index c04bfdffcd..6e3ffe303b 100644 --- a/packages/job-worker/src/__mocks__/context.ts +++ b/packages/job-worker/src/__mocks__/context.ts @@ -55,6 +55,7 @@ import { defaultStudio } from './defaultCollectionObjects' import { TimelineComplete } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { processShowStyleBase, processShowStyleVariant } from '../jobs/showStyle' import { JSONBlobStringify } from '@sofie-automation/shared-lib/dist/lib/JSONBlob' +import { removeRundownPlaylistFromDb } from '../ingest/__tests__/lib' export function setupDefaultJobEnvironment(studioId?: StudioId): MockJobContext { const { mockCollections, jobCollections } = getMockCollections() @@ -242,6 +243,15 @@ export class MockJobContext implements JobContext { ...blueprint, } } + + async clearAllRundownsAndPlaylists(): Promise { + // Cleanup any rundowns / playlists + const playlists = await this.mockCollections.RundownPlaylists.findFetch({}) + await removeRundownPlaylistFromDb( + this, + playlists.map((p) => p._id) + ) + } } const MockStudioBlueprint: () => StudioBlueprintManifest = () => ({ diff --git a/packages/job-worker/src/ingest/__tests__/ingest.test.ts b/packages/job-worker/src/ingest/__tests__/ingest.test.ts index 3bf5b38a82..1bb077c6f8 100644 --- a/packages/job-worker/src/ingest/__tests__/ingest.test.ts +++ b/packages/job-worker/src/ingest/__tests__/ingest.test.ts @@ -17,7 +17,7 @@ import { import { DBRundown, Rundown, RundownOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { DBSegment, SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' -import { getRandomString, literal } from '@sofie-automation/corelib/dist/lib' +import { clone, getRandomString, literal } from '@sofie-automation/corelib/dist/lib' import { sortPartsInSortedSegments, sortSegmentsInRundowns } from '@sofie-automation/corelib/dist/playout/playlist' import { MongoQuery } from '../../db' import { MockJobContext, setupDefaultJobEnvironment } from '../../__mocks__/context' @@ -38,18 +38,52 @@ import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartIns import { runJobWithPlayoutModel } from '../../playout/lock' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { insertQueuedPartWithPieces } from '../../playout/adlibUtils' -import { removeRundownPlaylistFromDb } from './lib' import { UserErrorMessage } from '@sofie-automation/corelib/dist/error' import { PlayoutPartInstanceModel } from '../../playout/model/PlayoutPartInstanceModel' -require('../../peripheralDevice.ts') // include in order to create the Meteor methods needed +const externalId = 'abcde' +const rundownData1: IngestRundown = { + externalId: externalId, + name: 'MyMockRundown', + type: 'mock', + segments: [ + { + externalId: 'segment0', + name: 'Segment 0', + rank: 0, + parts: [ + { + externalId: 'part0', + name: 'Part 0', + rank: 0, + }, + { + externalId: 'part1', + name: 'Part 1', + rank: 0, + }, + ], + }, + { + externalId: 'segment1', + name: 'Segment 1', + rank: 0, + parts: [ + { + externalId: 'part2', + name: 'Part 2', + rank: 0, + }, + ], + }, + ], +} describe('Test ingest actions for rundowns and segments', () => { let context: MockJobContext let device: PeripheralDevice let device2: PeripheralDevice - const externalId = 'abcde' const segExternalId = 'zyxwv' beforeAll(async () => { context = setupDefaultJobEnvironment() @@ -86,6 +120,8 @@ describe('Test ingest actions for rundowns and segments', () => { context.queueIngestJob = jest.fn(() => { throw new Error('Not implemented') }) + + await context.clearAllRundownsAndPlaylists() }) async function getRundownData(query?: MongoQuery) { const rundown = (await context.mockCollections.Rundowns.findOne(query)) as DBRundown @@ -109,52 +145,36 @@ describe('Test ingest actions for rundowns and segments', () => { } } + async function recreateRundown(data: IngestRundown): Promise { + await context.clearAllRundownsAndPlaylists() + + await handleUpdatedRundown(context, { + peripheralDeviceId: device._id, + rundownExternalId: data.externalId, + ingestRundown: data, + isCreateAction: true, + }) + + const rundown = (await context.mockCollections.Rundowns.findOne({ externalId: data.externalId })) as DBRundown + expect(rundown).toBeTruthy() + return rundown + } + + async function setRundownsOrphaned() { + await expect(context.mockCollections.Rundowns.findOne()).resolves.toBeTruthy() + + await context.mockCollections.Rundowns.update({}, { $set: { orphaned: RundownOrphanedReason.DELETED } }) + } + test('dataRundownCreate', async () => { // setLogLevel(LogLevel.DEBUG) await expect(context.mockCollections.Rundowns.findOne()).resolves.toBeFalsy() - const rundownData: IngestRundown = { - externalId: externalId, - name: 'MyMockRundown', - type: 'mock', - segments: [ - { - externalId: 'segment0', - name: 'Segment 0', - rank: 0, - parts: [ - { - externalId: 'part0', - name: 'Part 0', - rank: 0, - }, - { - externalId: 'part1', - name: 'Part 1', - rank: 0, - }, - ], - }, - { - externalId: 'segment1', - name: 'Segment 1', - rank: 0, - parts: [ - { - externalId: 'part2', - name: 'Part 2', - rank: 0, - }, - ], - }, - ], - } - await handleUpdatedRundown(context, { peripheralDeviceId: device._id, - rundownExternalId: rundownData.externalId, - ingestRundown: rundownData, + rundownExternalId: rundownData1.externalId, + ingestRundown: rundownData1, isCreateAction: true, }) @@ -165,7 +185,7 @@ describe('Test ingest actions for rundowns and segments', () => { }) expect(savedRundownData.rundown).toMatchObject({ - externalId: rundownData.externalId, + externalId: rundownData1.externalId, playlistId: savedRundownData.rundownPlaylist._id, }) @@ -179,43 +199,11 @@ describe('Test ingest actions for rundowns and segments', () => { }) test('dataRundownUpdate change name', async () => { - await expect(context.mockCollections.Rundowns.findOne()).resolves.toBeTruthy() + await recreateRundown(rundownData1) const rundownData: IngestRundown = { - externalId: externalId, + ...rundownData1, name: 'MyMockRundownRenamed', - type: 'mock', - segments: [ - { - externalId: 'segment0', - name: 'Segment 0', - rank: 0, - parts: [ - { - externalId: 'part0', - name: 'Part 0', - rank: 0, - }, - { - externalId: 'part1', - name: 'Part 1', - rank: 0, - }, - ], - }, - { - externalId: 'segment1', - name: 'Segment 1', - rank: 0, - parts: [ - { - externalId: 'part2', - name: 'Part 2', - rank: 0, - }, - ], - }, - ], } await handleUpdatedRundown(context, { @@ -251,55 +239,21 @@ describe('Test ingest actions for rundowns and segments', () => { }) test('dataRundownUpdate add a segment', async () => { - await expect(context.mockCollections.Rundowns.findOne()).resolves.toBeTruthy() - const rundownData: IngestRundown = { - externalId: externalId, - name: 'MyMockRundown', - type: 'mock', - segments: [ - { - externalId: 'segment0', - name: 'Segment 0', - rank: 0, - parts: [ - { - externalId: 'part0', - name: 'Part 0', - rank: 0, - }, - { - externalId: 'part1', - name: 'Part 1', - rank: 0, - }, - ], - }, - { - externalId: 'segment1', - name: 'Segment 1', - rank: 0, - parts: [ - { - externalId: 'part2', - name: 'Part 2', - rank: 0, - }, - ], - }, + await recreateRundown(rundownData1) + + const rundownData = clone(rundownData1) + rundownData.segments.push({ + externalId: 'segment2', + name: 'Segment 2', + rank: 0, + parts: [ { - externalId: 'segment2', - name: 'Segment 2', + externalId: 'part3', + name: 'Part 3', rank: 0, - parts: [ - { - externalId: 'part3', - name: 'Part 3', - rank: 0, - }, - ], }, ], - } + }) await handleUpdatedRundown(context, { peripheralDeviceId: device._id, @@ -329,60 +283,27 @@ describe('Test ingest actions for rundowns and segments', () => { }) test('dataRundownUpdate add a part', async () => { - await expect(context.mockCollections.Rundowns.findOne()).resolves.toBeTruthy() - const rundownData: IngestRundown = { - externalId: externalId, - name: 'MyMockRundown', - type: 'mock', - segments: [ - { - externalId: 'segment0', - name: 'Segment 0', - rank: 0, - parts: [ - { - externalId: 'part0', - name: 'Part 0', - rank: 0, - }, - { - externalId: 'part1', - name: 'Part 1', - rank: 0, - }, - ], - }, - { - externalId: 'segment1', - name: 'Segment 1', - rank: 0, - parts: [ - { - externalId: 'part2', - name: 'Part 2', - rank: 0, - }, - { - externalId: 'partZ', - name: 'Part Z', - rank: 0, - }, - ], - }, + await recreateRundown(rundownData1) + + const rundownData = clone(rundownData1) + rundownData.segments.push({ + externalId: 'segment2', + name: 'Segment 2', + rank: 0, + parts: [ { - externalId: 'segment2', - name: 'Segment 2', + externalId: 'part3', + name: 'Part 3', rank: 0, - parts: [ - { - externalId: 'part3', - name: 'Part 3', - rank: 0, - }, - ], }, ], - } + }) + rundownData.segments[1].parts.push({ + externalId: 'partZ', + name: 'Part Z', + rank: 0, + }) + await handleUpdatedRundown(context, { peripheralDeviceId: device._id, rundownExternalId: rundownData.externalId, @@ -416,47 +337,25 @@ describe('Test ingest actions for rundowns and segments', () => { }) test('dataRundownUpdate remove a segment', async () => { - await expect(context.mockCollections.Rundowns.findOne()).resolves.toBeTruthy() - const rundownData: IngestRundown = { - externalId: externalId, - name: 'MyMockRundown', - type: 'mock', - segments: [ - { - externalId: 'segment0', - name: 'Segment 0', - rank: 0, - parts: [ - { - externalId: 'part0', - name: 'Part 0', - rank: 0, - }, - { - externalId: 'part1', - name: 'Part 1', - rank: 0, - }, - ], - }, + const initialRundownData = clone(rundownData1) + initialRundownData.segments.push({ + externalId: 'segment2', + name: 'Segment 2', + rank: 0, + parts: [ { - externalId: 'segment2', - name: 'Segment 2', + externalId: 'part3', + name: 'Part 3', rank: 0, - parts: [ - { - externalId: 'part3', - name: 'Part 3', - rank: 0, - }, - ], }, ], - } + }) + await recreateRundown(initialRundownData) + await handleUpdatedRundown(context, { peripheralDeviceId: device._id, - rundownExternalId: rundownData.externalId, - ingestRundown: rundownData, + rundownExternalId: rundownData1.externalId, + ingestRundown: rundownData1, isCreateAction: false, }) @@ -469,7 +368,7 @@ describe('Test ingest actions for rundowns and segments', () => { externalId: savedRundownData.rundown._id, }) expect(savedRundownData.rundown).toMatchObject({ - externalId: rundownData.externalId, + externalId: rundownData1.externalId, }) expect(savedRundownData.segments).toHaveLength(2) @@ -482,38 +381,10 @@ describe('Test ingest actions for rundowns and segments', () => { }) test('dataRundownUpdate remove a part', async () => { - await expect(context.mockCollections.Rundowns.findOne()).resolves.toBeTruthy() - const rundownData: IngestRundown = { - externalId: externalId, - name: 'MyMockRundown', - type: 'mock', - segments: [ - { - externalId: 'segment0', - name: 'Segment 0', - rank: 0, - parts: [ - { - externalId: 'part1', - name: 'Part 1', - rank: 0, - }, - ], - }, - { - externalId: 'segment2', - name: 'Segment 2', - rank: 0, - parts: [ - { - externalId: 'part3', - name: 'Part 3', - rank: 0, - }, - ], - }, - ], - } + await recreateRundown(rundownData1) + + const rundownData = clone(rundownData1) + expect(rundownData.segments[0].parts.shift()).toBeTruthy() await handleUpdatedRundown(context, { peripheralDeviceId: device._id, @@ -544,7 +415,8 @@ describe('Test ingest actions for rundowns and segments', () => { }) test('dataRundownMetaDataUpdate change name, does not remove segments', async () => { - await expect(context.mockCollections.Rundowns.findOne()).resolves.toBeTruthy() + await recreateRundown(rundownData1) + const rundownData: Omit = { externalId: externalId, name: 'MyMockRundownRenamed', @@ -575,14 +447,14 @@ describe('Test ingest actions for rundowns and segments', () => { expect(savedRundownData.segments).toHaveLength(2) const parts0 = savedRundownData.parts.filter((p) => p.segmentId === savedRundownData.segments[0]._id) - expect(parts0).toHaveLength(1) + expect(parts0).toHaveLength(2) const parts1 = savedRundownData.parts.filter((p) => p.segmentId === savedRundownData.segments[1]._id) expect(parts1).toHaveLength(1) }) test('dataRundownDelete', async () => { - await expect(context.mockCollections.Rundowns.findOne()).resolves.toBeTruthy() + await recreateRundown(rundownData1) await handleRemovedRundown(context, { peripheralDeviceId: device._id, @@ -676,49 +548,8 @@ describe('Test ingest actions for rundowns and segments', () => { }) test('dataRundownUpdate fail when rundown is orphaned', async () => { - await expect(context.mockCollections.Rundowns.findOne()).resolves.toBeFalsy() - - const rundownData0: IngestRundown = { - externalId: externalId, - name: 'MyMockRundown', - type: 'mock', - segments: [ - { - externalId: 'segment0', - name: 'Segment 0', - rank: 0, - parts: [ - { - externalId: 'part1', - name: 'Part 1', - rank: 0, - }, - ], - }, - { - externalId: 'segment2', - name: 'Segment 2', - rank: 0, - parts: [ - { - externalId: 'part3', - name: 'Part 3', - rank: 0, - }, - ], - }, - ], - } - - await handleUpdatedRundown(context, { - peripheralDeviceId: device._id, - rundownExternalId: rundownData0.externalId, - ingestRundown: rundownData0, - isCreateAction: true, - }) - - await expect(context.mockCollections.Rundowns.findOne()).resolves.toBeTruthy() - await context.mockCollections.Rundowns.update({}, { $set: { orphaned: RundownOrphanedReason.DELETED } }) + await recreateRundown(rundownData1) + await setRundownsOrphaned() const rundown0 = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown expect(rundown0).toBeTruthy() @@ -761,46 +592,13 @@ describe('Test ingest actions for rundowns and segments', () => { }) test('dataRundownCreate replace orphaned rundown', async () => { - await expect( - context.mockCollections.Rundowns.findFetch({ orphaned: RundownOrphanedReason.DELETED }) - ).resolves.toHaveLength(1) - - const rundownData: IngestRundown = { - externalId: externalId, - name: 'MyMockRundown', - type: 'mock', - segments: [ - { - externalId: 'segment0', - name: 'Segment 0', - rank: 0, - parts: [ - { - externalId: 'part1', - name: 'Part 1', - rank: 0, - }, - ], - }, - { - externalId: 'segment2', - name: 'Segment 2', - rank: 0, - parts: [ - { - externalId: 'part3', - name: 'Part 3', - rank: 0, - }, - ], - }, - ], - } + await recreateRundown(rundownData1) + await setRundownsOrphaned() await handleUpdatedRundown(context, { peripheralDeviceId: device._id, - rundownExternalId: rundownData.externalId, - ingestRundown: rundownData, + rundownExternalId: rundownData1.externalId, + ingestRundown: rundownData1, isCreateAction: true, }) @@ -810,8 +608,8 @@ describe('Test ingest actions for rundowns and segments', () => { }) test('dataSegmentCreate in deleted rundown', async () => { - await expect(context.mockCollections.Rundowns.findOne()).resolves.toBeTruthy() - await context.mockCollections.Rundowns.update({}, { $set: { orphaned: RundownOrphanedReason.DELETED } }) + await recreateRundown(rundownData1) + await setRundownsOrphaned() const rundown0 = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown expect(rundown0).toBeTruthy() @@ -838,8 +636,7 @@ describe('Test ingest actions for rundowns and segments', () => { }) test('dataSegmentCreate', async () => { - await expect(context.mockCollections.Rundowns.findOne()).resolves.toBeTruthy() - await context.mockCollections.Rundowns.update({}, { $unset: { orphaned: 1 } }) + await recreateRundown(rundownData1) const rundown = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown expect(rundown).toBeTruthy() @@ -870,8 +667,9 @@ describe('Test ingest actions for rundowns and segments', () => { }) test('dataSegmentCreate replace deleted segment', async () => { - await expect(context.mockCollections.Rundowns.findOne()).resolves.toBeTruthy() - await context.mockCollections.Rundowns.update({}, { $unset: { orphaned: 1 } }) + await recreateRundown(rundownData1) + + const segExternalId = rundownData1.segments[0].externalId const segment0 = (await context.mockCollections.Segments.findOne({ externalId: segExternalId })) as DBSegment expect(segment0).toBeTruthy() @@ -881,7 +679,7 @@ describe('Test ingest actions for rundowns and segments', () => { const rundown = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown expect(rundown).toBeTruthy() - await expect(context.mockCollections.Segments.findFetch({ rundownId: rundown._id })).resolves.toHaveLength(3) + await expect(context.mockCollections.Segments.findFetch({ rundownId: rundown._id })).resolves.toHaveLength(2) const ingestSegment: IngestSegment = { externalId: segExternalId, @@ -905,10 +703,9 @@ describe('Test ingest actions for rundowns and segments', () => { }) test('dataSegmentUpdate add a part', async () => { - const rundown = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown - expect(rundown).toBeTruthy() - await context.mockCollections.Segments.update({ rundownId: rundown._id }, { $unset: { orphaned: 1 } }) - await expect(context.mockCollections.Segments.findFetch({ rundownId: rundown._id })).resolves.toHaveLength(3) + const rundown = await recreateRundown(rundownData1) + + const segExternalId = rundownData1.segments[0].externalId const ingestSegment: IngestSegment = { externalId: segExternalId, @@ -931,28 +728,28 @@ describe('Test ingest actions for rundowns and segments', () => { }) const segments = await context.mockCollections.Segments.findFetch({ rundownId: rundown._id }) - expect(segments).toHaveLength(3) + expect(segments).toHaveLength(2) - const parts3 = await context.mockCollections.Parts.findFetch({ + const parts0 = await context.mockCollections.Parts.findFetch({ rundownId: rundown._id, - segmentId: segments[2]._id, + segmentId: segments[0]._id, }) - expect(parts3).toHaveLength(1) - expect(parts3[0]).toMatchObject({ + expect(parts0).toHaveLength(1) + expect(parts0[0]).toMatchObject({ externalId: 'part42', title: 'Part 42', }) }) test('dataSegmentUpdate deleted segment', async () => { - const rundown = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown - expect(rundown).toBeTruthy() - await expect(context.mockCollections.Segments.findFetch({ rundownId: rundown._id })).resolves.toHaveLength(3) + const rundown = await recreateRundown(rundownData1) await context.mockCollections.Segments.update( { rundownId: rundown._id }, { $set: { orphaned: SegmentOrphanedReason.DELETED } } ) + const segExternalId = rundownData1.segments[0].externalId + const segmentBefore = (await context.mockCollections.Segments.findOne({ externalId: segExternalId, })) as DBSegment @@ -978,7 +775,7 @@ describe('Test ingest actions for rundowns and segments', () => { isCreateAction: false, }) - await expect(context.mockCollections.Segments.findFetch({ rundownId: rundown._id })).resolves.toHaveLength(3) + await expect(context.mockCollections.Segments.findFetch({ rundownId: rundown._id })).resolves.toHaveLength(2) // Ensure no changes const segmentAfter = (await context.mockCollections.Segments.findOne({ @@ -988,11 +785,10 @@ describe('Test ingest actions for rundowns and segments', () => { }) test('dataSegmentUpdate deleted rundown', async () => { - const rundown = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown - expect(rundown).toBeTruthy() - await context.mockCollections.Rundowns.update({}, { $set: { orphaned: RundownOrphanedReason.DELETED } }) - await context.mockCollections.Segments.update({ rundownId: rundown._id }, { $unset: { orphaned: 1 } }) - await expect(context.mockCollections.Segments.findFetch({ rundownId: rundown._id })).resolves.toHaveLength(3) + const rundown = await recreateRundown(rundownData1) + await setRundownsOrphaned() + + const segExternalId = rundownData1.segments[0].externalId const segmentBefore = (await context.mockCollections.Segments.findOne({ externalId: segExternalId, @@ -1019,7 +815,7 @@ describe('Test ingest actions for rundowns and segments', () => { isCreateAction: false, }) - await expect(context.mockCollections.Segments.findFetch({ rundownId: rundown._id })).resolves.toHaveLength(3) + await expect(context.mockCollections.Segments.findFetch({ rundownId: rundown._id })).resolves.toHaveLength(2) // Ensure no changes const segmentAfter = (await context.mockCollections.Segments.findOne({ @@ -1052,24 +848,17 @@ describe('Test ingest actions for rundowns and segments', () => { }) test('dataSegmentUpdate no change', async () => { - const rundown = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown - expect(rundown).toBeTruthy() - await context.mockCollections.Rundowns.update({}, { $unset: { orphaned: 1 } }) - await context.mockCollections.Segments.update({ rundownId: rundown._id }, { $unset: { orphaned: 1 } }) - await expect(context.mockCollections.Segments.findFetch({ rundownId: rundown._id })).resolves.toHaveLength(3) + const rundown = await recreateRundown(rundownData1) - const ingestSegment: IngestSegment = { - externalId: segExternalId, - name: 'MyMockSegment', - rank: 0, - parts: [ - { - externalId: 'part42', - name: 'Part 42', - rank: 0, - }, - ], - } + const segmentsBefore = await context.mockCollections.Segments.findFetch({ rundownId: rundown._id }) + expect(segmentsBefore).toHaveLength(2) + + const partsBefore = await context.mockCollections.Parts.findFetch({ + rundownId: rundown._id, + segmentId: segmentsBefore[0]._id, + }) + + const ingestSegment = rundownData1.segments[0] await handleUpdatedSegment(context, { peripheralDeviceId: device._id, @@ -1078,31 +867,30 @@ describe('Test ingest actions for rundowns and segments', () => { isCreateAction: false, }) - const segments = await context.mockCollections.Segments.findFetch({ rundownId: rundown._id }) - expect(segments).toHaveLength(3) + const segmentsAfter = await context.mockCollections.Segments.findFetch({ rundownId: rundown._id }) + expect(segmentsAfter).toHaveLength(2) - const parts3 = await context.mockCollections.Parts.findFetch({ + const partsAfter = await context.mockCollections.Parts.findFetch({ rundownId: rundown._id, - segmentId: segments[2]._id, - }) - expect(parts3).toHaveLength(1) - expect(parts3[0]).toMatchObject({ - externalId: 'part42', - title: 'Part 42', + segmentId: segmentsBefore[0]._id, }) + expect(partsAfter).toEqual(partsBefore) }) test('dataSegmentUpdate remove a part', async () => { - const rundown = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown - expect(rundown).toBeTruthy() - await expect(context.mockCollections.Segments.findFetch({ rundownId: rundown._id })).resolves.toHaveLength(3) + const rundown = await recreateRundown(rundownData1) - const ingestSegment: IngestSegment = { - externalId: segExternalId, - name: 'MyMockSegment', - rank: 0, - parts: [], - } + const ingestSegment = clone(rundownData1.segments[0]) + expect(ingestSegment.parts.pop()).toBeTruthy() + + const segmentsBefore = await context.mockCollections.Segments.findFetch({ rundownId: rundown._id }) + expect(segmentsBefore).toHaveLength(2) + + const partsBefore = await context.mockCollections.Parts.findFetch({ + rundownId: rundown._id, + segmentId: segmentsBefore[0]._id, + }) + expect(partsBefore).toHaveLength(2) await handleUpdatedSegment(context, { peripheralDeviceId: device._id, @@ -1111,20 +899,15 @@ describe('Test ingest actions for rundowns and segments', () => { isCreateAction: false, }) - const segments = await context.mockCollections.Segments.findFetch({ rundownId: rundown._id }) - expect(segments).toHaveLength(3) - - const parts3 = await context.mockCollections.Parts.findFetch({ + const partsAfter = await context.mockCollections.Parts.findFetch({ rundownId: rundown._id, - segmentId: segments[2]._id, + segmentId: segmentsBefore[0]._id, }) - expect(parts3).toHaveLength(0) + expect(partsAfter).toHaveLength(1) }) test('dataSegmentUpdate no external id', async () => { - const rundown = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown - expect(rundown).toBeTruthy() - await expect(context.mockCollections.Segments.findFetch({ rundownId: rundown._id })).resolves.toHaveLength(3) + await recreateRundown(rundownData1) const ingestSegment: IngestSegment = { externalId: '', @@ -1144,27 +927,22 @@ describe('Test ingest actions for rundowns and segments', () => { }) test('dataSegmentDelete already orphaned segment', async () => { - const rundown = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown - expect(rundown).toBeTruthy() - await expect( - context.mockCollections.Segments.findFetch({ rundownId: rundown._id, externalId: segExternalId }) - ).resolves.toHaveLength(1) + const rundown = await recreateRundown(rundownData1) + + const segExternalId = rundownData1.segments[0].externalId - await context.mockCollections.Rundowns.update({}, { $unset: { orphaned: 1 } }) await context.mockCollections.Segments.update( { rundownId: rundown._id, externalId: segExternalId }, { $set: { orphaned: SegmentOrphanedReason.DELETED } } ) - await expect(context.mockCollections.Segments.findFetch({ rundownId: rundown._id })).resolves.toHaveLength(3) - await handleRemovedSegment(context, { peripheralDeviceId: device._id, rundownExternalId: externalId, segmentExternalId: segExternalId, }) - await expect(context.mockCollections.Segments.findFetch({ rundownId: rundown._id })).resolves.toHaveLength(3) + await expect(context.mockCollections.Segments.findFetch({ rundownId: rundown._id })).resolves.toHaveLength(2) await expect(context.mockCollections.Segments.findOne({ externalId: segExternalId })).resolves.toBeTruthy() }) @@ -1243,64 +1021,9 @@ describe('Test ingest actions for rundowns and segments', () => { }) test('dataSegmentDelete', async () => { - // reset rundown - const rundownData: IngestRundown = { - externalId: externalId, - name: 'MyMockRundown', - type: 'mock', - segments: [ - { - externalId: 'segment0', - name: 'Segment 0', - rank: 0, - parts: [ - { - externalId: 'part1', - name: 'Part 1', - rank: 0, - }, - ], - }, - { - externalId: 'segment2', - name: 'Segment 2', - rank: 0, - parts: [ - { - externalId: 'part2', - name: 'Part 2', - rank: 0, - }, - ], - }, - { - externalId: segExternalId, - name: 'Segment 3', - rank: 0, - parts: [ - { - externalId: 'part3', - name: 'Part 3', - rank: 0, - }, - ], - }, - ], - } - - await handleUpdatedRundown(context, { - peripheralDeviceId: device._id, - rundownExternalId: externalId, - ingestRundown: rundownData, - isCreateAction: true, - }) + const rundown = await recreateRundown(rundownData1) - const rundown = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown - expect(rundown).toBeTruthy() - await expect(context.mockCollections.Segments.findFetch({ rundownId: rundown._id })).resolves.toHaveLength(3) - - await context.mockCollections.Rundowns.update({}, { $unset: { orphaned: 1 } }) - await context.mockCollections.Segments.update({ rundownId: rundown._id }, { $unset: { orphaned: 1 } }) + const segExternalId = rundownData1.segments[1].externalId await handleRemovedSegment(context, { peripheralDeviceId: device._id, @@ -1308,13 +1031,12 @@ describe('Test ingest actions for rundowns and segments', () => { segmentExternalId: segExternalId, }) - await expect(context.mockCollections.Segments.findFetch({ rundownId: rundown._id })).resolves.toHaveLength(2) + await expect(context.mockCollections.Segments.findFetch({ rundownId: rundown._id })).resolves.toHaveLength(1) await expect(context.mockCollections.Segments.findOne({ externalId: segExternalId })).resolves.toBeFalsy() }) - test('dataSegmentDelete for a second time', async () => { - const rundown = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown - expect(rundown).toBeTruthy() + test('dataSegmentDelete an unknown segment', async () => { + const rundown = await recreateRundown(rundownData1) await expect( context.mockCollections.Segments.findFetch({ rundownId: rundown._id, externalId: segExternalId }) ).resolves.toHaveLength(0) @@ -1331,9 +1053,8 @@ describe('Test ingest actions for rundowns and segments', () => { }) test('dataSegmentDelete from non-existant rundown', async () => { - const rundown = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown - expect(rundown).toBeTruthy() - await expect(context.mockCollections.Segments.findFetch({ rundownId: rundown._id })).resolves.toHaveLength(2) + const rundown = await context.mockCollections.Rundowns.findOne({}) + expect(rundown).toBeFalsy() await expect( handleRemovedSegment(context, { @@ -1345,8 +1066,8 @@ describe('Test ingest actions for rundowns and segments', () => { }) test('dataSegmentCreate non-existant rundown', async () => { - const rundown = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown - expect(rundown).toBeTruthy() + const rundown = (await context.mockCollections.Rundowns.findOne({})) as DBRundown + expect(rundown).toBeFalsy() const ingestSegment: IngestSegment = { externalId: segExternalId, @@ -1365,12 +1086,13 @@ describe('Test ingest actions for rundowns and segments', () => { }) test('dataPartCreate', async () => { - const rundown = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown - expect(rundown).toBeTruthy() - const segment = (await context.mockCollections.Segments.findOne({ externalId: 'segment0' })) as DBSegment + const rundown = await recreateRundown(rundownData1) + + const segmentExternalId = rundownData1.segments[0].externalId + const segment = (await context.mockCollections.Segments.findOne({ externalId: segmentExternalId })) as DBSegment await expect( context.mockCollections.Parts.findFetch({ rundownId: rundown._id, segmentId: segment._id }) - ).resolves.toHaveLength(1) + ).resolves.toHaveLength(2) const ingestPart: IngestPart = { externalId: 'party', @@ -1388,7 +1110,7 @@ describe('Test ingest actions for rundowns and segments', () => { await expect( context.mockCollections.Parts.findFetch({ rundownId: rundown._id, segmentId: segment._id }) - ).resolves.toHaveLength(2) + ).resolves.toHaveLength(3) const part = (await context.mockCollections.Parts.findOne({ externalId: 'party' })) as DBPart expect(part).toMatchObject({ @@ -1398,18 +1120,16 @@ describe('Test ingest actions for rundowns and segments', () => { }) test('dataPartUpdate', async () => { - const rundown = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown - expect(rundown).toBeTruthy() - const segment = (await context.mockCollections.Segments.findOne({ externalId: 'segment0' })) as DBSegment + const rundown = await recreateRundown(rundownData1) + + const segmentExternalId = rundownData1.segments[0].externalId + const segment = (await context.mockCollections.Segments.findOne({ externalId: segmentExternalId })) as DBSegment await expect( context.mockCollections.Parts.findFetch({ rundownId: rundown._id, segmentId: segment._id }) ).resolves.toHaveLength(2) - const ingestPart: IngestPart = { - externalId: 'party', - name: 'Part Z', - rank: 0, - } + const ingestPart = clone(rundownData1.segments[0].parts[0]) + ingestPart.name = 'My special part' await handleUpdatedPart(context, { peripheralDeviceId: device._id, @@ -1423,7 +1143,7 @@ describe('Test ingest actions for rundowns and segments', () => { context.mockCollections.Parts.findFetch({ rundownId: rundown._id, segmentId: segment._id }) ).resolves.toHaveLength(2) - const part = (await context.mockCollections.Parts.findOne({ externalId: 'party' })) as DBPart + const part = (await context.mockCollections.Parts.findOne({ externalId: ingestPart.externalId })) as DBPart expect(part).toMatchObject({ externalId: ingestPart.externalId, title: ingestPart.name, @@ -1431,24 +1151,31 @@ describe('Test ingest actions for rundowns and segments', () => { }) test('dataPartDelete', async () => { - const rundown = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown - expect(rundown).toBeTruthy() - const segment = (await context.mockCollections.Segments.findOne({ externalId: 'segment0' })) as DBSegment + const rundown = await recreateRundown(rundownData1) + + const segmentExternalId = rundownData1.segments[0].externalId + const partExternalId = rundownData1.segments[0].parts[0].externalId + + const segment = (await context.mockCollections.Segments.findOne({ externalId: segmentExternalId })) as DBSegment await expect( - context.mockCollections.Parts.findFetch({ rundownId: rundown._id, segmentId: segment._id }) - ).resolves.toHaveLength(2) + context.mockCollections.Parts.findFetch({ + rundownId: rundown._id, + segmentId: segment._id, + externalId: partExternalId, + }) + ).resolves.toHaveLength(1) await handleRemovedPart(context, { peripheralDeviceId: device._id, rundownExternalId: externalId, segmentExternalId: segment.externalId, - partExternalId: 'party', + partExternalId: partExternalId, }) await expect( context.mockCollections.Parts.findFetch({ rundownId: rundown._id, segmentId: segment._id }) ).resolves.toHaveLength(1) - await expect(context.mockCollections.Parts.findOne({ externalId: 'party' })).resolves.toBeFalsy() + await expect(context.mockCollections.Parts.findOne({ externalId: partExternalId })).resolves.toBeFalsy() }) // TODO Part tests are minimal/happy path only on the assumption the API gets little use @@ -1538,856 +1265,748 @@ describe('Test ingest actions for rundowns and segments', () => { }) test('unsyncing of rundown', async () => { - try { - { - // Cleanup any rundowns / playlists - const playlists = await context.mockCollections.RundownPlaylists.findFetch({}) - await removeRundownPlaylistFromDb( - context, - playlists.map((p) => p._id) - ) - } - - const rundownData: IngestRundown = { - externalId: externalId, - name: 'MyMockRundown', - type: 'mock', - segments: [ - { - externalId: 'segment0', - name: 'Segment 0', - rank: 0, - parts: [ - { - externalId: 'part0', - name: 'Part 0', - rank: 0, - }, - { - externalId: 'part1', - name: 'Part 1', - rank: 0, - }, - ], - }, - { - externalId: 'segment1', - name: 'Segment 1', - rank: 0, - parts: [ - { - externalId: 'part2', - name: 'Part 2', - rank: 0, - }, - ], - }, - ], - } + // Preparation: set up rundown + await expect(context.mockCollections.Rundowns.findOne()).resolves.toBeFalsy() + + await handleUpdatedRundown(context, { + peripheralDeviceId: device2._id, + rundownExternalId: rundownData1.externalId, + ingestRundown: rundownData1, + isCreateAction: true, + }) - // Preparation: set up rundown - await expect(context.mockCollections.Rundowns.findOne()).resolves.toBeFalsy() + const rundown = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown + expect(rundown).toBeTruthy() + expect(rundown).toMatchObject({ + externalId: rundownData1.externalId, + }) + const getRundownOrphaned = async () => { + const rd = (await context.mockCollections.Rundowns.findOne(rundown._id)) as DBRundown + return rd.orphaned + } + const getPlaylist = async () => + (await context.mockCollections.RundownPlaylists.findOne(rundown.playlistId)) as DBRundownPlaylist + const getSegmentOrphaned = async (id: SegmentId) => { + const segment = (await context.mockCollections.Segments.findOne(id)) as DBSegment + return segment.orphaned + } + + const resyncRundown = async () => { + // simulate a resync. we don't have a gateway to call out to, but this is how it will respond await handleUpdatedRundown(context, { peripheralDeviceId: device2._id, - rundownExternalId: rundownData.externalId, - ingestRundown: rundownData, + rundownExternalId: rundownData1.externalId, + ingestRundown: rundownData1, isCreateAction: true, }) + } - const rundown = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown - expect(rundown).toBeTruthy() - expect(rundown).toMatchObject({ - externalId: rundownData.externalId, - }) + const segments = await context.mockCollections.Segments.findFetch({ rundownId: rundown._id }) + const parts = await context.mockCollections.Parts.findFetch({ rundownId: rundown._id }) - const getRundownOrphaned = async () => { - const rd = (await context.mockCollections.Rundowns.findOne(rundown._id)) as DBRundown - return rd.orphaned - } - const getPlaylist = async () => - (await context.mockCollections.RundownPlaylists.findOne(rundown.playlistId)) as DBRundownPlaylist - const getSegmentOrphaned = async (id: SegmentId) => { - const segment = (await context.mockCollections.Segments.findOne(id)) as DBSegment - return segment.orphaned - } - - const resyncRundown = async () => { - // simulate a resync. we don't have a gateway to call out to, but this is how it will respond - await handleUpdatedRundown(context, { - peripheralDeviceId: device2._id, - rundownExternalId: rundownData.externalId, - ingestRundown: rundownData, - isCreateAction: true, - }) - } - - const segments = await context.mockCollections.Segments.findFetch({ rundownId: rundown._id }) - const parts = await context.mockCollections.Parts.findFetch({ rundownId: rundown._id }) + expect(segments).toHaveLength(2) + expect(parts).toHaveLength(3) - expect(segments).toHaveLength(2) - expect(parts).toHaveLength(3) + // Activate the rundown, make data updates and verify that it gets unsynced properly + await handleActivateRundownPlaylist(context, { + playlistId: rundown.playlistId, + rehearsal: true, + }) + await expect(getRundownOrphaned()).resolves.toBeUndefined() - // Activate the rundown, make data updates and verify that it gets unsynced properly - await handleActivateRundownPlaylist(context, { - playlistId: rundown.playlistId, - rehearsal: true, + await expect( + handleRemovedRundown(context, { + peripheralDeviceId: device2._id, + rundownExternalId: rundownData1.externalId, }) - await expect(getRundownOrphaned()).resolves.toBeUndefined() + ).rejects.toMatchUserError(UserErrorMessage.RundownRemoveWhileActive) + await expect(getRundownOrphaned()).resolves.toEqual(RundownOrphanedReason.DELETED) - await expect( - handleRemovedRundown(context, { - peripheralDeviceId: device2._id, - rundownExternalId: rundownData.externalId, - }) - ).rejects.toMatchUserError(UserErrorMessage.RundownRemoveWhileActive) - await expect(getRundownOrphaned()).resolves.toEqual(RundownOrphanedReason.DELETED) - - await resyncRundown() - await expect(getRundownOrphaned()).resolves.toBeUndefined() - - await handleTakeNextPart(context, { playlistId: rundown.playlistId, fromPartInstanceId: null }) - const partInstance = await context.mockCollections.PartInstances.findFetch({ 'part._id': parts[0]._id }) - expect(partInstance).toHaveLength(1) - await expect(getPlaylist()).resolves.toMatchObject({ - currentPartInfo: { partInstanceId: partInstance[0]._id }, - }) - expect(partInstance[0].segmentId).toEqual(segments[0]._id) + await resyncRundown() + await expect(getRundownOrphaned()).resolves.toBeUndefined() - await handleRemovedSegment(context, { - peripheralDeviceId: device2._id, - rundownExternalId: rundown.externalId, - segmentExternalId: segments[0].externalId, - }) - await expect(getRundownOrphaned()).resolves.toBeUndefined() - await expect(getSegmentOrphaned(segments[0]._id)).resolves.toEqual(SegmentOrphanedReason.DELETED) + await handleTakeNextPart(context, { playlistId: rundown.playlistId, fromPartInstanceId: null }) + const partInstance = await context.mockCollections.PartInstances.findFetch({ 'part._id': parts[0]._id }) + expect(partInstance).toHaveLength(1) + await expect(getPlaylist()).resolves.toMatchObject({ + currentPartInfo: { partInstanceId: partInstance[0]._id }, + }) + expect(partInstance[0].segmentId).toEqual(segments[0]._id) - await resyncRundown() - await expect(getRundownOrphaned()).resolves.toBeUndefined() - await expect(getSegmentOrphaned(segments[0]._id)).resolves.toBeUndefined() + await handleRemovedSegment(context, { + peripheralDeviceId: device2._id, + rundownExternalId: rundown.externalId, + segmentExternalId: segments[0].externalId, + }) + await expect(getRundownOrphaned()).resolves.toBeUndefined() + await expect(getSegmentOrphaned(segments[0]._id)).resolves.toEqual(SegmentOrphanedReason.DELETED) - await handleRemovedPart(context, { - peripheralDeviceId: device2._id, - rundownExternalId: rundown.externalId, - segmentExternalId: segments[0].externalId, - partExternalId: parts[0].externalId, - }) - await expect(getRundownOrphaned()).resolves.toBeUndefined() - await expect(getSegmentOrphaned(segments[0]._id)).resolves.toBeUndefined() - - await resyncRundown() - await expect(getRundownOrphaned()).resolves.toBeUndefined() - await expect(getSegmentOrphaned(segments[0]._id)).resolves.toBeUndefined() - } finally { - // forcefully 'deactivate' the playlist to allow for cleanup to happen - await context.mockCollections.RundownPlaylists.update({}, { $unset: { activationId: 1 } }) - } + await resyncRundown() + await expect(getRundownOrphaned()).resolves.toBeUndefined() + await expect(getSegmentOrphaned(segments[0]._id)).resolves.toBeUndefined() + + await handleRemovedPart(context, { + peripheralDeviceId: device2._id, + rundownExternalId: rundown.externalId, + segmentExternalId: segments[0].externalId, + partExternalId: parts[0].externalId, + }) + await expect(getRundownOrphaned()).resolves.toBeUndefined() + await expect(getSegmentOrphaned(segments[0]._id)).resolves.toBeUndefined() + + await resyncRundown() + await expect(getRundownOrphaned()).resolves.toBeUndefined() + await expect(getSegmentOrphaned(segments[0]._id)).resolves.toBeUndefined() }) test('replace the nexted part', async () => { - try { - { - // Cleanup any rundowns / playlists - const playlists = await context.mockCollections.RundownPlaylists.findFetch({}) - await removeRundownPlaylistFromDb( - context, - playlists.map((p) => p._id) - ) - } - - const rundownData: IngestRundown = { - externalId: externalId, - name: 'MyMockRundown', - type: 'mock', - segments: [ - { - externalId: 'segment0', - name: 'Segment 0', - rank: 0, - parts: [ - { - externalId: 'part0', - name: 'Part 0', - rank: 0, - payload: { - pieces: [ - literal({ - externalId: 'piece0', - name: '', - enable: { start: 0 }, - sourceLayerId: '', - outputLayerId: '', - lifespan: PieceLifespan.WithinPart, - content: { - timelineObjects: [], - }, - }), - ], - }, - }, - { - externalId: 'part1', - name: 'Part 1', - rank: 1, - payload: { - pieces: [ - literal({ - externalId: 'piece1', - name: '', - enable: { start: 0 }, - sourceLayerId: '', - outputLayerId: '', - lifespan: PieceLifespan.WithinPart, - content: { - timelineObjects: [], - }, - }), - ], - }, + const rundownData: IngestRundown = { + externalId: externalId, + name: 'MyMockRundown', + type: 'mock', + segments: [ + { + externalId: 'segment0', + name: 'Segment 0', + rank: 0, + parts: [ + { + externalId: 'part0', + name: 'Part 0', + rank: 0, + payload: { + pieces: [ + literal({ + externalId: 'piece0', + name: '', + enable: { start: 0 }, + sourceLayerId: '', + outputLayerId: '', + lifespan: PieceLifespan.WithinPart, + content: { + timelineObjects: [], + }, + }), + ], }, - ], - }, - { - externalId: 'segment1', - name: 'Segment 1', - rank: 1, - parts: [ - { - externalId: 'part2', - name: 'Part 2', - rank: 0, + }, + { + externalId: 'part1', + name: 'Part 1', + rank: 1, + payload: { + pieces: [ + literal({ + externalId: 'piece1', + name: '', + enable: { start: 0 }, + sourceLayerId: '', + outputLayerId: '', + lifespan: PieceLifespan.WithinPart, + content: { + timelineObjects: [], + }, + }), + ], }, - ], - }, - ], - } + }, + ], + }, + { + externalId: 'segment1', + name: 'Segment 1', + rank: 1, + parts: [ + { + externalId: 'part2', + name: 'Part 2', + rank: 0, + }, + ], + }, + ], + } - // Preparation: set up rundown - await expect(context.mockCollections.Rundowns.findOne()).resolves.toBeFalsy() + // Preparation: set up rundown + await expect(context.mockCollections.Rundowns.findOne()).resolves.toBeFalsy() - await handleUpdatedRundown(context, { - peripheralDeviceId: device2._id, - rundownExternalId: rundownData.externalId, - ingestRundown: rundownData, - isCreateAction: true, - }) + await handleUpdatedRundown(context, { + peripheralDeviceId: device2._id, + rundownExternalId: rundownData.externalId, + ingestRundown: rundownData, + isCreateAction: true, + }) - const rundown = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown - expect(rundown).toBeTruthy() - expect(rundown).toMatchObject({ - externalId: rundownData.externalId, - }) + const rundown = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown + expect(rundown).toBeTruthy() + expect(rundown).toMatchObject({ + externalId: rundownData.externalId, + }) - // const getRundown = () => Rundowns.findOne(rundown._id) as Rundown - const getPlaylist = async () => - (await context.mockCollections.RundownPlaylists.findOne(rundown.playlistId)) as DBRundownPlaylist + // const getRundown = () => Rundowns.findOne(rundown._id) as Rundown + const getPlaylist = async () => + (await context.mockCollections.RundownPlaylists.findOne(rundown.playlistId)) as DBRundownPlaylist - const segments = await context.mockCollections.Segments.findFetch({ rundownId: rundown._id }) - const parts = await context.mockCollections.Parts.findFetch({ rundownId: rundown._id }) + const segments = await context.mockCollections.Segments.findFetch({ rundownId: rundown._id }) + const parts = await context.mockCollections.Parts.findFetch({ rundownId: rundown._id }) - expect(segments).toHaveLength(2) - expect(parts).toHaveLength(3) + expect(segments).toHaveLength(2) + expect(parts).toHaveLength(3) + await expect(context.mockCollections.Pieces.findFetch({ startRundownId: rundown._id })).resolves.toHaveLength(2) + + // Activate the rundown, make data updates and verify that it gets unsynced properly + await handleActivateRundownPlaylist(context, { + playlistId: rundown.playlistId, + rehearsal: true, + }) + await expect(getPlaylist()).resolves.toMatchObject({ currentPartInfo: null }) + + // Take the first part + await handleTakeNextPart(context, { playlistId: rundown.playlistId, fromPartInstanceId: null }) + await expect(getPlaylist()).resolves.toMatchObject({ + currentPartInfo: { + partInstanceId: expect.stringContaining('random'), + }, + }) + + { + // Check which parts are current and next + const playlist = await getPlaylist() + const selectedInstances = await getSelectedPartInstances(context, playlist) + const currentPartInstance = selectedInstances.currentPartInstance as DBPartInstance + const nextPartInstance = selectedInstances.nextPartInstance as DBPartInstance + expect(currentPartInstance).toBeTruthy() + expect(currentPartInstance.part.externalId).toBe('part0') + expect(nextPartInstance).toBeTruthy() + expect(nextPartInstance.part.externalId).toBe('part1') + + // we should have some pieces await expect( - context.mockCollections.Pieces.findFetch({ startRundownId: rundown._id }) - ).resolves.toHaveLength(2) + context.mockCollections.PieceInstances.findFetch({ partInstanceId: nextPartInstance._id }) + ).resolves.not.toHaveLength(0) + } - // Activate the rundown, make data updates and verify that it gets unsynced properly - await handleActivateRundownPlaylist(context, { - playlistId: rundown.playlistId, - rehearsal: true, - }) - await expect(getPlaylist()).resolves.toMatchObject({ currentPartInfo: null }) + // Replace part1 with a new part + const updatedSegmentData: IngestSegment = rundownData.segments[0] + updatedSegmentData.parts[1].externalId = 'new-part' - // Take the first part - await handleTakeNextPart(context, { playlistId: rundown.playlistId, fromPartInstanceId: null }) - await expect(getPlaylist()).resolves.toMatchObject({ - currentPartInfo: { - partInstanceId: expect.stringContaining('random'), - }, - }) + await handleUpdatedSegment(context, { + peripheralDeviceId: device2._id, + rundownExternalId: rundownData.externalId, + ingestSegment: updatedSegmentData, + isCreateAction: false, + }) - { - // Check which parts are current and next - const playlist = await getPlaylist() - const selectedInstances = await getSelectedPartInstances(context, playlist) - const currentPartInstance = selectedInstances.currentPartInstance as DBPartInstance - const nextPartInstance = selectedInstances.nextPartInstance as DBPartInstance - expect(currentPartInstance).toBeTruthy() - expect(currentPartInstance.part.externalId).toBe('part0') - expect(nextPartInstance).toBeTruthy() - expect(nextPartInstance.part.externalId).toBe('part1') - - // we should have some pieces - await expect( - context.mockCollections.PieceInstances.findFetch({ partInstanceId: nextPartInstance._id }) - ).resolves.not.toHaveLength(0) - } - - // Replace part1 with a new part - const updatedSegmentData: IngestSegment = rundownData.segments[0] - updatedSegmentData.parts[1].externalId = 'new-part' - - await handleUpdatedSegment(context, { - peripheralDeviceId: device2._id, - rundownExternalId: rundownData.externalId, - ingestSegment: updatedSegmentData, - isCreateAction: false, - }) + { + const segments2 = await context.mockCollections.Segments.findFetch({ rundownId: rundown._id }) + const parts2 = await context.mockCollections.Parts.findFetch({ rundownId: rundown._id }) + + expect(segments2).toHaveLength(2) + expect(parts2).toHaveLength(3) - { - const segments2 = await context.mockCollections.Segments.findFetch({ rundownId: rundown._id }) - const parts2 = await context.mockCollections.Parts.findFetch({ rundownId: rundown._id }) - - expect(segments2).toHaveLength(2) - expect(parts2).toHaveLength(3) - - expect(parts2.find((p) => p.externalId === 'part1')).toBeFalsy() - const newPart = parts2.find((p) => p.externalId === 'new-part') as DBPart - expect(newPart).toBeTruthy() - - await expect( - context.mockCollections.Pieces.findFetch({ startRundownId: rundown._id }) - ).resolves.toHaveLength(2) - - // we need some pieces for the test to work - await expect( - context.mockCollections.Pieces.findFetch({ startPartId: newPart._id }) - ).resolves.not.toHaveLength(0) - } - - { - // Check if the partInstance was updated - const playlist = await getPlaylist() - const selectedInstances = await getSelectedPartInstances(context, playlist) - const currentPartInstance = selectedInstances.currentPartInstance as DBPartInstance - const nextPartInstance = selectedInstances.nextPartInstance as DBPartInstance - expect(currentPartInstance).toBeTruthy() - expect(currentPartInstance.part.externalId).toBe('part0') - expect(nextPartInstance).toBeTruthy() - expect(nextPartInstance.part.externalId).toBe('new-part') - - // the pieces should have been copied - await expect( - context.mockCollections.PieceInstances.findFetch({ partInstanceId: nextPartInstance._id }) - ).resolves.not.toHaveLength(0) - } - } finally { - // forcefully 'deactivate' the playlist to allow for cleanup to happen - await context.mockCollections.RundownPlaylists.update({}, { $unset: { activationId: 1 } }) + expect(parts2.find((p) => p.externalId === 'part1')).toBeFalsy() + const newPart = parts2.find((p) => p.externalId === 'new-part') as DBPart + expect(newPart).toBeTruthy() + + await expect( + context.mockCollections.Pieces.findFetch({ startRundownId: rundown._id }) + ).resolves.toHaveLength(2) + + // we need some pieces for the test to work + await expect( + context.mockCollections.Pieces.findFetch({ startPartId: newPart._id }) + ).resolves.not.toHaveLength(0) + } + + { + // Check if the partInstance was updated + const playlist = await getPlaylist() + const selectedInstances = await getSelectedPartInstances(context, playlist) + const currentPartInstance = selectedInstances.currentPartInstance as DBPartInstance + const nextPartInstance = selectedInstances.nextPartInstance as DBPartInstance + expect(currentPartInstance).toBeTruthy() + expect(currentPartInstance.part.externalId).toBe('part0') + expect(nextPartInstance).toBeTruthy() + expect(nextPartInstance.part.externalId).toBe('new-part') + + // the pieces should have been copied + await expect( + context.mockCollections.PieceInstances.findFetch({ partInstanceId: nextPartInstance._id }) + ).resolves.not.toHaveLength(0) } }) test('previous partinstance getting removed if an adlib part', async () => { - try { - { - // Cleanup any rundowns / playlists - const playlists = await context.mockCollections.RundownPlaylists.findFetch({}) - await removeRundownPlaylistFromDb( - context, - playlists.map((p) => p._id) - ) - } - - const rundownData: IngestRundown = { - externalId: externalId, - name: 'MyMockRundown', - type: 'mock', - segments: [ - { - externalId: 'segment0', - name: 'Segment 0', - rank: 0, - parts: [ - { - externalId: 'part0', - name: 'Part 0', - rank: 0, - payload: { - pieces: [ - literal({ - externalId: 'piece0', - name: '', - enable: { start: 0 }, - sourceLayerId: '', - outputLayerId: '', - lifespan: PieceLifespan.WithinPart, - content: { - timelineObjects: [], - }, - }), - ], - }, - }, - { - externalId: 'part1', - name: 'Part 1', - rank: 1, - payload: { - pieces: [ - literal({ - externalId: 'piece1', - name: '', - enable: { start: 0 }, - sourceLayerId: '', - outputLayerId: '', - lifespan: PieceLifespan.WithinPart, - content: { - timelineObjects: [], - }, - }), - ], - }, - }, - ], - }, - { - externalId: 'segment1', - name: 'Segment 1', - rank: 1, - parts: [ - { - externalId: 'part2', - name: 'Part 2', - rank: 0, + const rundownData: IngestRundown = { + externalId: externalId, + name: 'MyMockRundown', + type: 'mock', + segments: [ + { + externalId: 'segment0', + name: 'Segment 0', + rank: 0, + parts: [ + { + externalId: 'part0', + name: 'Part 0', + rank: 0, + payload: { + pieces: [ + literal({ + externalId: 'piece0', + name: '', + enable: { start: 0 }, + sourceLayerId: '', + outputLayerId: '', + lifespan: PieceLifespan.WithinPart, + content: { + timelineObjects: [], + }, + }), + ], }, - ], - }, - { - externalId: 'segment2', - name: 'Segment 2', - rank: 1, - parts: [ - { - externalId: 'part3', - name: 'Part 3', - rank: 0, + }, + { + externalId: 'part1', + name: 'Part 1', + rank: 1, + payload: { + pieces: [ + literal({ + externalId: 'piece1', + name: '', + enable: { start: 0 }, + sourceLayerId: '', + outputLayerId: '', + lifespan: PieceLifespan.WithinPart, + content: { + timelineObjects: [], + }, + }), + ], }, - ], - }, - ], - } + }, + ], + }, + { + externalId: 'segment1', + name: 'Segment 1', + rank: 1, + parts: [ + { + externalId: 'part2', + name: 'Part 2', + rank: 0, + }, + ], + }, + { + externalId: 'segment2', + name: 'Segment 2', + rank: 1, + parts: [ + { + externalId: 'part3', + name: 'Part 3', + rank: 0, + }, + ], + }, + ], + } - await handleUpdatedRundown(context, { - rundownExternalId: rundownData.externalId, - peripheralDeviceId: device2._id, - ingestRundown: rundownData, - isCreateAction: true, - }) + await handleUpdatedRundown(context, { + rundownExternalId: rundownData.externalId, + peripheralDeviceId: device2._id, + ingestRundown: rundownData, + isCreateAction: true, + }) + + const rundown = (await context.mockCollections.Rundowns.findOne()) as DBRundown + expect(rundown).toBeTruthy() + + // Take into first part + await handleActivateRundownPlaylist(context, { + playlistId: rundown.playlistId, + rehearsal: true, + }) + await handleTakeNextPart(context, { + playlistId: rundown.playlistId, + fromPartInstanceId: null, + }) - const rundown = (await context.mockCollections.Rundowns.findOne()) as DBRundown - expect(rundown).toBeTruthy() + const doQueuePart = async (): Promise => + runJobWithPlayoutModel( + context, + { + playlistId: rundown.playlistId, + }, + null, + async (playoutModel) => { + const rundown0 = playoutModel.rundowns[0] + expect(rundown0).toBeTruthy() + + const currentPartInstance = playoutModel.currentPartInstance as PlayoutPartInstanceModel + expect(currentPartInstance).toBeTruthy() + + // Simulate a queued part + const newPartInstance = await insertQueuedPartWithPieces( + context, + playoutModel, + rundown0, + currentPartInstance, + { + _id: protectString(`after_${currentPartInstance.partInstance._id}_part`), + _rank: 0, + externalId: `after_${currentPartInstance.partInstance._id}_externalId`, + title: 'New part', + expectedDurationWithPreroll: undefined, + }, + [], + undefined + ) - // Take into first part - await handleActivateRundownPlaylist(context, { + return newPartInstance.partInstance._id + } + ) + + // Queue and take an adlib-part + const partInstanceId0 = await doQueuePart() + { + const playlist = (await context.mockCollections.RundownPlaylists.findOne( + rundown.playlistId + )) as DBRundownPlaylist + expect(playlist).toBeTruthy() + + await handleTakeNextPart(context, { playlistId: rundown.playlistId, - rehearsal: true, + fromPartInstanceId: playlist.currentPartInfo?.partInstanceId ?? null, }) + } + + { + // Verify it was taken properly + const playlist = (await context.mockCollections.RundownPlaylists.findOne( + rundown.playlistId + )) as DBRundownPlaylist + expect(playlist).toBeTruthy() + expect(playlist.currentPartInfo?.partInstanceId).toBe(partInstanceId0) + + const currentPartInstance = (await getSelectedPartInstances(context, playlist)) + .currentPartInstance as DBPartInstance + expect(currentPartInstance).toBeTruthy() + expect(currentPartInstance.orphaned).toBe('adlib-part') + } + + // Ingest update should have no effect + const ingestSegment: IngestSegment = { + externalId: 'segment2', + name: 'Segment 2a', + rank: 1, + parts: [ + { + externalId: 'part3', + name: 'Part 3', + rank: 0, + }, + ], + } + + { + // Check props before + const segment2 = (await context.mockCollections.Segments.findOne({ + externalId: ingestSegment.externalId, + rundownId: rundown._id, + })) as DBSegment + expect(segment2).toBeTruthy() + expect(segment2.name).not.toBe(ingestSegment.name) + } + + await handleUpdatedSegment(context, { + peripheralDeviceId: device2._id, + rundownExternalId: rundownData.externalId, + ingestSegment: ingestSegment, + isCreateAction: false, + }) + + { + // Check props after + const segment2 = (await context.mockCollections.Segments.findOne({ + externalId: ingestSegment.externalId, + rundownId: rundown._id, + })) as DBSegment + expect(segment2).toBeTruthy() + expect(segment2.name).toBe(ingestSegment.name) + } + + { + // Verify the adlibbed part-instance didnt change + const playlist = (await context.mockCollections.RundownPlaylists.findOne( + rundown.playlistId + )) as DBRundownPlaylist + expect(playlist).toBeTruthy() + expect(playlist.currentPartInfo?.partInstanceId).toBe(partInstanceId0) + + const currentPartInstance = (await getSelectedPartInstances(context, playlist)) + .currentPartInstance as DBPartInstance + expect(currentPartInstance).toBeTruthy() + expect(currentPartInstance.orphaned).toBe('adlib-part') + } + + // Queue and take another adlib-part + const partInstanceId1 = await doQueuePart() + { + const playlist = (await context.mockCollections.RundownPlaylists.findOne( + rundown.playlistId + )) as DBRundownPlaylist + expect(playlist).toBeTruthy() + await handleTakeNextPart(context, { playlistId: rundown.playlistId, - fromPartInstanceId: null, + fromPartInstanceId: playlist.currentPartInfo?.partInstanceId ?? null, }) + } - const doQueuePart = async (): Promise => - runJobWithPlayoutModel( - context, - { - playlistId: rundown.playlistId, - }, - null, - async (playoutModel) => { - const rundown0 = playoutModel.rundowns[0] - expect(rundown0).toBeTruthy() - - const currentPartInstance = playoutModel.currentPartInstance as PlayoutPartInstanceModel - expect(currentPartInstance).toBeTruthy() - - // Simulate a queued part - const newPartInstance = await insertQueuedPartWithPieces( - context, - playoutModel, - rundown0, - currentPartInstance, - { - _id: protectString(`after_${currentPartInstance.partInstance._id}_part`), - _rank: 0, - externalId: `after_${currentPartInstance.partInstance._id}_externalId`, - title: 'New part', - expectedDurationWithPreroll: undefined, - }, - [], - undefined - ) - - return newPartInstance.partInstance._id - } - ) - - // Queue and take an adlib-part - const partInstanceId0 = await doQueuePart() - { - const playlist = (await context.mockCollections.RundownPlaylists.findOne( - rundown.playlistId - )) as DBRundownPlaylist - expect(playlist).toBeTruthy() - - await handleTakeNextPart(context, { - playlistId: rundown.playlistId, - fromPartInstanceId: playlist.currentPartInfo?.partInstanceId ?? null, - }) - } - - { - // Verify it was taken properly - const playlist = (await context.mockCollections.RundownPlaylists.findOne( - rundown.playlistId - )) as DBRundownPlaylist - expect(playlist).toBeTruthy() - expect(playlist.currentPartInfo?.partInstanceId).toBe(partInstanceId0) - - const currentPartInstance = (await getSelectedPartInstances(context, playlist)) - .currentPartInstance as DBPartInstance - expect(currentPartInstance).toBeTruthy() - expect(currentPartInstance.orphaned).toBe('adlib-part') - } - - // Ingest update should have no effect - const ingestSegment: IngestSegment = { - externalId: 'segment2', - name: 'Segment 2a', - rank: 1, - parts: [ - { - externalId: 'part3', - name: 'Part 3', - rank: 0, - }, - ], - } - - { - // Check props before - const segment2 = (await context.mockCollections.Segments.findOne({ - externalId: ingestSegment.externalId, - rundownId: rundown._id, - })) as DBSegment - expect(segment2).toBeTruthy() - expect(segment2.name).not.toBe(ingestSegment.name) - } - - await handleUpdatedSegment(context, { - peripheralDeviceId: device2._id, - rundownExternalId: rundownData.externalId, - ingestSegment: ingestSegment, - isCreateAction: false, - }) + { + // Verify the take was correct + const playlist = (await context.mockCollections.RundownPlaylists.findOne( + rundown.playlistId + )) as DBRundownPlaylist + expect(playlist).toBeTruthy() + expect(playlist.currentPartInfo?.partInstanceId).toBe(partInstanceId1) + expect(playlist.previousPartInfo?.partInstanceId).toBe(partInstanceId0) + + const currentPartInstance = (await getSelectedPartInstances(context, playlist)) + .currentPartInstance as DBPartInstance + expect(currentPartInstance).toBeTruthy() + expect(currentPartInstance.orphaned).toBe('adlib-part') + + const previousPartInstance = (await getSelectedPartInstances(context, playlist)) + .previousPartInstance as DBPartInstance + expect(previousPartInstance).toBeTruthy() + expect(previousPartInstance.orphaned).toBe('adlib-part') + } - { - // Check props after - const segment2 = (await context.mockCollections.Segments.findOne({ - externalId: ingestSegment.externalId, - rundownId: rundown._id, - })) as DBSegment - expect(segment2).toBeTruthy() - expect(segment2.name).toBe(ingestSegment.name) - } - - { - // Verify the adlibbed part-instance didnt change - const playlist = (await context.mockCollections.RundownPlaylists.findOne( - rundown.playlistId - )) as DBRundownPlaylist - expect(playlist).toBeTruthy() - expect(playlist.currentPartInfo?.partInstanceId).toBe(partInstanceId0) - - const currentPartInstance = (await getSelectedPartInstances(context, playlist)) - .currentPartInstance as DBPartInstance - expect(currentPartInstance).toBeTruthy() - expect(currentPartInstance.orphaned).toBe('adlib-part') - } - - // Queue and take another adlib-part - const partInstanceId1 = await doQueuePart() - { - const playlist = (await context.mockCollections.RundownPlaylists.findOne( - rundown.playlistId - )) as DBRundownPlaylist - expect(playlist).toBeTruthy() - - await handleTakeNextPart(context, { - playlistId: rundown.playlistId, - fromPartInstanceId: playlist.currentPartInfo?.partInstanceId ?? null, - }) - } - - { - // Verify the take was correct - const playlist = (await context.mockCollections.RundownPlaylists.findOne( - rundown.playlistId - )) as DBRundownPlaylist - expect(playlist).toBeTruthy() - expect(playlist.currentPartInfo?.partInstanceId).toBe(partInstanceId1) - expect(playlist.previousPartInfo?.partInstanceId).toBe(partInstanceId0) - - const currentPartInstance = (await getSelectedPartInstances(context, playlist)) - .currentPartInstance as DBPartInstance - expect(currentPartInstance).toBeTruthy() - expect(currentPartInstance.orphaned).toBe('adlib-part') - - const previousPartInstance = (await getSelectedPartInstances(context, playlist)) - .previousPartInstance as DBPartInstance - expect(previousPartInstance).toBeTruthy() - expect(previousPartInstance.orphaned).toBe('adlib-part') - } - - // Another ingest update - ingestSegment.name += '2' - - { - // Check props before - const segment2 = (await context.mockCollections.Segments.findOne({ - externalId: ingestSegment.externalId, - rundownId: rundown._id, - })) as DBSegment - expect(segment2).toBeTruthy() - expect(segment2.name).not.toBe(ingestSegment.name) - } - - await handleUpdatedSegment(context, { - peripheralDeviceId: device2._id, - rundownExternalId: rundownData.externalId, - ingestSegment: ingestSegment, - isCreateAction: false, - }) + // Another ingest update + ingestSegment.name += '2' + + { + // Check props before + const segment2 = (await context.mockCollections.Segments.findOne({ + externalId: ingestSegment.externalId, + rundownId: rundown._id, + })) as DBSegment + expect(segment2).toBeTruthy() + expect(segment2.name).not.toBe(ingestSegment.name) + } + + await handleUpdatedSegment(context, { + peripheralDeviceId: device2._id, + rundownExternalId: rundownData.externalId, + ingestSegment: ingestSegment, + isCreateAction: false, + }) + + { + // Check props after + const segment2 = (await context.mockCollections.Segments.findOne({ + externalId: ingestSegment.externalId, + rundownId: rundown._id, + })) as DBSegment + expect(segment2).toBeTruthy() + expect(segment2.name).toBe(ingestSegment.name) + } - { - // Check props after - const segment2 = (await context.mockCollections.Segments.findOne({ - externalId: ingestSegment.externalId, - rundownId: rundown._id, - })) as DBSegment - expect(segment2).toBeTruthy() - expect(segment2.name).toBe(ingestSegment.name) - } - - { - // Verify the part-instances havent changed - const playlist = (await context.mockCollections.RundownPlaylists.findOne( - rundown.playlistId - )) as DBRundownPlaylist - expect(playlist).toBeTruthy() - expect(playlist.currentPartInfo?.partInstanceId).toBe(partInstanceId1) - expect(playlist.previousPartInfo?.partInstanceId).toBe(partInstanceId0) - - const currentPartInstance = (await getSelectedPartInstances(context, playlist)) - .currentPartInstance as DBPartInstance - expect(currentPartInstance).toBeTruthy() - expect(currentPartInstance.orphaned).toBe('adlib-part') - - const previousPartInstance = (await getSelectedPartInstances(context, playlist)) - .previousPartInstance as DBPartInstance - expect(previousPartInstance).toBeTruthy() - expect(previousPartInstance.orphaned).toBe('adlib-part') - } - } finally { - // forcefully 'deactivate' the playlist to allow for cleanup to happen - await context.mockCollections.RundownPlaylists.update({}, { $unset: { activationId: 1 } }) + { + // Verify the part-instances havent changed + const playlist = (await context.mockCollections.RundownPlaylists.findOne( + rundown.playlistId + )) as DBRundownPlaylist + expect(playlist).toBeTruthy() + expect(playlist.currentPartInfo?.partInstanceId).toBe(partInstanceId1) + expect(playlist.previousPartInfo?.partInstanceId).toBe(partInstanceId0) + + const currentPartInstance = (await getSelectedPartInstances(context, playlist)) + .currentPartInstance as DBPartInstance + expect(currentPartInstance).toBeTruthy() + expect(currentPartInstance.orphaned).toBe('adlib-part') + + const previousPartInstance = (await getSelectedPartInstances(context, playlist)) + .previousPartInstance as DBPartInstance + expect(previousPartInstance).toBeTruthy() + expect(previousPartInstance.orphaned).toBe('adlib-part') } }) test('prevent hiding current segment when deleting segment onAir', async () => { - try { - // Cleanup any rundowns / playlists - { - // Cleanup any rundowns / playlists - const playlists = await context.mockCollections.RundownPlaylists.findFetch({}) - await removeRundownPlaylistFromDb( - context, - playlists.map((p) => p._id) - ) - } - - const rundownData: IngestRundown = { - externalId: externalId, - name: 'MyMockRundown', - type: 'mock', - segments: [ - { - externalId: 'segment0', - name: 'Segment 0', - rank: 0, - payload: {}, - parts: [ - { - externalId: 'part0', - name: 'Part 0', - rank: 0, - payload: { - pieces: [ - literal({ - externalId: 'piece0', - name: '', - enable: { start: 0 }, - sourceLayerId: '', - outputLayerId: '', - lifespan: PieceLifespan.WithinPart, - content: { timelineObjects: [] }, - }), - ], - }, - }, - { - externalId: 'part1', - name: 'Part 1', - rank: 1, - payload: { - pieces: [ - literal({ - externalId: 'piece1', - name: '', - enable: { start: 0 }, - sourceLayerId: '', - outputLayerId: '', - lifespan: PieceLifespan.WithinPart, - content: { timelineObjects: [] }, - }), - ], - }, + const rundownData: IngestRundown = { + externalId: externalId, + name: 'MyMockRundown', + type: 'mock', + segments: [ + { + externalId: 'segment0', + name: 'Segment 0', + rank: 0, + payload: {}, + parts: [ + { + externalId: 'part0', + name: 'Part 0', + rank: 0, + payload: { + pieces: [ + literal({ + externalId: 'piece0', + name: '', + enable: { start: 0 }, + sourceLayerId: '', + outputLayerId: '', + lifespan: PieceLifespan.WithinPart, + content: { timelineObjects: [] }, + }), + ], }, - ], - }, - { - externalId: 'segment1', - name: 'Segment 1', - rank: 1, - payload: {}, - parts: [ - { - externalId: 'part2', - name: 'Part 2', - rank: 0, + }, + { + externalId: 'part1', + name: 'Part 1', + rank: 1, + payload: { + pieces: [ + literal({ + externalId: 'piece1', + name: '', + enable: { start: 0 }, + sourceLayerId: '', + outputLayerId: '', + lifespan: PieceLifespan.WithinPart, + content: { timelineObjects: [] }, + }), + ], }, - ], - }, - ], - } - - // Preparation: set up rundown - await expect(context.mockCollections.Rundowns.findOne()).resolves.toBeFalsy() - await handleUpdatedRundown(context, { - peripheralDeviceId: device2._id, - rundownExternalId: rundownData.externalId, - ingestRundown: rundownData, - isCreateAction: true, - }) - const rundown = (await context.mockCollections.Rundowns.findOne()) as Rundown - expect(rundown).toMatchObject({ - externalId: rundownData.externalId, - }) + }, + ], + }, + { + externalId: 'segment1', + name: 'Segment 1', + rank: 1, + payload: {}, + parts: [ + { + externalId: 'part2', + name: 'Part 2', + rank: 0, + }, + ], + }, + ], + } - const getPlaylist = async () => - (await context.mockCollections.RundownPlaylists.findOne(rundown.playlistId)) as DBRundownPlaylist - const getCurrentPartInstanceId = async () => { - const playlist = await getPlaylist() - return playlist.currentPartInfo?.partInstanceId ?? null - } + // Preparation: set up rundown + await expect(context.mockCollections.Rundowns.findOne()).resolves.toBeFalsy() + await handleUpdatedRundown(context, { + peripheralDeviceId: device2._id, + rundownExternalId: rundownData.externalId, + ingestRundown: rundownData, + isCreateAction: true, + }) + const rundown = (await context.mockCollections.Rundowns.findOne()) as Rundown + expect(rundown).toMatchObject({ + externalId: rundownData.externalId, + }) + const getPlaylist = async () => + (await context.mockCollections.RundownPlaylists.findOne(rundown.playlistId)) as DBRundownPlaylist + const getCurrentPartInstanceId = async () => { const playlist = await getPlaylist() - expect(playlist).toBeTruthy() + return playlist.currentPartInfo?.partInstanceId ?? null + } + + const playlist = await getPlaylist() + expect(playlist).toBeTruthy() + + // const getRundown = async () => (await context.mockCollections.Rundowns.findOne(rundown._id)) as Rundown + + const { segments, parts } = await getRundownData({ _id: rundown._id }) + expect(segments).toHaveLength(2) + expect(parts).toHaveLength(3) + await expect(context.mockCollections.Pieces.findFetch({ startRundownId: rundown._id })).resolves.toHaveLength(2) + + // Activate the rundown + await handleActivateRundownPlaylist(context, { + playlistId: playlist._id, + rehearsal: true, + }) + await expect(getCurrentPartInstanceId()).resolves.toBeNull() + + // Take the first part + await handleTakeNextPart(context, { + playlistId: playlist._id, + fromPartInstanceId: null, + }) + await expect(getCurrentPartInstanceId()).resolves.not.toBeNull() - // const getRundown = async () => (await context.mockCollections.Rundowns.findOne(rundown._id)) as Rundown + { + // Check which part is current + const selectedInstances = await getSelectedPartInstances(context, await getPlaylist()) + const currentPartInstance = selectedInstances.currentPartInstance as DBPartInstance + expect(currentPartInstance).toBeTruthy() + expect(currentPartInstance.part.externalId).toBe('part0') + } + + // Delete segment 0, while on air + const segmentExternalId = rundownData.segments[0].externalId + await handleRemovedSegment(context, { + peripheralDeviceId: device2._id, + rundownExternalId: rundownData.externalId, + segmentExternalId: segmentExternalId, + }) + { const { segments, parts } = await getRundownData({ _id: rundown._id }) expect(segments).toHaveLength(2) - expect(parts).toHaveLength(3) - await expect( - context.mockCollections.Pieces.findFetch({ startRundownId: rundown._id }) - ).resolves.toHaveLength(2) - // Activate the rundown - await handleActivateRundownPlaylist(context, { - playlistId: playlist._id, - rehearsal: true, - }) - await expect(getCurrentPartInstanceId()).resolves.toBeNull() + const segment0 = segments.find((s) => s.externalId === segmentExternalId) as DBSegment + expect(segment0).toBeTruthy() + expect(segment0.orphaned).toBe(SegmentOrphanedReason.DELETED) + expect(segment0.isHidden).toBeFalsy() + + // Check that the PartInstance is still there + const selectedInstances = await getSelectedPartInstances(context, await getPlaylist()) + const currentPartInstance = selectedInstances.currentPartInstance as DBPartInstance + expect(currentPartInstance).toBeTruthy() + expect(currentPartInstance.part.externalId).toBe('part0') + expect(currentPartInstance.part.segmentId).toBe(segment0._id) + + // Check that the Parts have been removed + const parts0 = parts.filter((p) => p.segmentId === segment0._id) + expect(parts0).toHaveLength(0) // <- FAIL, length is 2 + } - // Take the first part - await handleTakeNextPart(context, { - playlistId: playlist._id, - fromPartInstanceId: null, - }) - await expect(getCurrentPartInstanceId()).resolves.not.toBeNull() - - { - // Check which part is current - const selectedInstances = await getSelectedPartInstances(context, await getPlaylist()) - const currentPartInstance = selectedInstances.currentPartInstance as DBPartInstance - expect(currentPartInstance).toBeTruthy() - expect(currentPartInstance.part.externalId).toBe('part0') - } - - // Delete segment 0, while on air - const segmentExternalId = rundownData.segments[0].externalId - await handleRemovedSegment(context, { - peripheralDeviceId: device2._id, - rundownExternalId: rundownData.externalId, - segmentExternalId: segmentExternalId, - }) + // Trigger an 'resync' of the rundown + rundownData.segments.splice(0, 1) + await handleUpdatedRundown(context, { + peripheralDeviceId: device2._id, + rundownExternalId: rundownData.externalId, + ingestRundown: rundownData, + isCreateAction: false, + }) - { - const { segments, parts } = await getRundownData({ _id: rundown._id }) - expect(segments).toHaveLength(2) - - const segment0 = segments.find((s) => s.externalId === segmentExternalId) as DBSegment - expect(segment0).toBeTruthy() - expect(segment0.orphaned).toBe(SegmentOrphanedReason.DELETED) - expect(segment0.isHidden).toBeFalsy() - - // Check that the PartInstance is still there - const selectedInstances = await getSelectedPartInstances(context, await getPlaylist()) - const currentPartInstance = selectedInstances.currentPartInstance as DBPartInstance - expect(currentPartInstance).toBeTruthy() - expect(currentPartInstance.part.externalId).toBe('part0') - expect(currentPartInstance.part.segmentId).toBe(segment0._id) - - // Check that the Parts have been removed - const parts0 = parts.filter((p) => p.segmentId === segment0._id) - expect(parts0).toHaveLength(0) // <- FAIL, length is 2 - } - - // Trigger an 'resync' of the rundown - rundownData.segments.splice(0, 1) - await handleUpdatedRundown(context, { - peripheralDeviceId: device2._id, - rundownExternalId: rundownData.externalId, - ingestRundown: rundownData, - isCreateAction: false, - }) + // Make sure segment0 is still deleted + { + const { segments } = await getRundownData({ _id: rundown._id }) + expect(segments).toHaveLength(2) - // Make sure segment0 is still deleted - { - const { segments } = await getRundownData({ _id: rundown._id }) - expect(segments).toHaveLength(2) - - const segment0 = segments.find((s) => s.externalId === segmentExternalId) as DBSegment - expect(segment0).toBeTruthy() - expect(segment0.orphaned).toBe(SegmentOrphanedReason.DELETED) - expect(segment0.isHidden).toBeFalsy() - } - } finally { - // forcefully 'deactivate' the playlist to allow for cleanup to happen - await context.mockCollections.RundownPlaylists.update({}, { $unset: { activationId: 1 } }) + const segment0 = segments.find((s) => s.externalId === segmentExternalId) as DBSegment + expect(segment0).toBeTruthy() + expect(segment0.orphaned).toBe(SegmentOrphanedReason.DELETED) + expect(segment0.isHidden).toBeFalsy() } }) test('ensure rundown can be deleted if it has bad showstyle ids', async () => { - // Cleanup any rundowns / playlists - { - // Cleanup any rundowns / playlists - const playlists = await context.mockCollections.RundownPlaylists.findFetch({}) - await removeRundownPlaylistFromDb( - context, - playlists.map((p) => p._id) - ) - } - const rundownData: IngestRundown = { externalId: externalId, name: 'MyMockRundown', From f9a66059c3d3b6a9a01b88ea6f87104fd12256f2 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Fri, 19 Apr 2024 14:53:12 +0200 Subject: [PATCH 274/479] chore(tests): add a scratchpad test --- packages/job-worker/src/__mocks__/context.ts | 66 ++++++++--------- .../src/__mocks__/defaultCollectionObjects.ts | 19 ++--- .../src/playout/__tests__/actions.test.ts | 72 +++++++++++++++++-- 3 files changed, 111 insertions(+), 46 deletions(-) diff --git a/packages/job-worker/src/__mocks__/context.ts b/packages/job-worker/src/__mocks__/context.ts index c04bfdffcd..0a271aefb4 100644 --- a/packages/job-worker/src/__mocks__/context.ts +++ b/packages/job-worker/src/__mocks__/context.ts @@ -1,60 +1,60 @@ import { - StudioId, + BlueprintManifestType, + BlueprintResultPart, + BlueprintResultRundown, + BlueprintResultSegment, + ExtendedIngestRundown, + IBlueprintActionManifest, + IBlueprintAdLibPiece, + IBlueprintPart, + IBlueprintPiece, + IBlueprintRundown, + IBlueprintSegment, + ISegmentUserContext, + IShowStyleContext, + IngestSegment, + PlaylistTimingType, + ShowStyleBlueprintManifest, + StudioBlueprintManifest, +} from '@sofie-automation/blueprints-integration' +import { + RundownId, RundownPlaylistId, ShowStyleBaseId, ShowStyleVariantId, - RundownId, + StudioId, } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { clone } from '@sofie-automation/corelib/dist/lib' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { EventsJobFunc } from '@sofie-automation/corelib/dist/worker/events' import { IngestJobFunc } from '@sofie-automation/corelib/dist/worker/ingest' import { StudioJobFunc } from '@sofie-automation/corelib/dist/worker/studio' -import { WrappedStudioBlueprint, WrappedShowStyleBlueprint } from '../blueprints/cache' +import { ReadonlyDeep } from 'type-fest' +import { WrappedShowStyleBlueprint, WrappedStudioBlueprint } from '../blueprints/cache' import { - ProcessedStudioConfig, ProcessedShowStyleConfig, - preprocessStudioConfig, + ProcessedStudioConfig, preprocessShowStyleConfig, + preprocessStudioConfig, } from '../blueprints/config' -import { BaseModel } from '../modelBase' -import { PlaylistLock, RundownLock } from '../jobs/lock' -import { ReadonlyDeep } from 'type-fest' +import { IDirectCollections } from '../db' import { ApmSpan, - ProcessedShowStyleBase, JobContext, + ProcessedShowStyleBase, ProcessedShowStyleCompound, ProcessedShowStyleVariant, } from '../jobs' +import { PlaylistLock, RundownLock } from '../jobs/lock' +import { BaseModel } from '../modelBase' import { createShowStyleCompound } from '../showStyles' import { IMockCollections, getMockCollections } from './collection' -import { clone } from '@sofie-automation/corelib/dist/lib' -import { IDirectCollections } from '../db' -import { - BlueprintManifestType, - BlueprintResultPart, - BlueprintResultRundown, - BlueprintResultSegment, - ExtendedIngestRundown, - IBlueprintActionManifest, - IBlueprintAdLibPiece, - IBlueprintPart, - IBlueprintPiece, - IBlueprintRundown, - IBlueprintSegment, - IngestSegment, - ISegmentUserContext, - IShowStyleContext, - PlaylistTimingType, - ShowStyleBlueprintManifest, - StudioBlueprintManifest, -} from '@sofie-automation/blueprints-integration' -import { protectString } from '@sofie-automation/corelib/dist/protectedString' // import _ = require('underscore') -import { defaultStudio } from './defaultCollectionObjects' import { TimelineComplete } from '@sofie-automation/corelib/dist/dataModel/Timeline' -import { processShowStyleBase, processShowStyleVariant } from '../jobs/showStyle' import { JSONBlobStringify } from '@sofie-automation/shared-lib/dist/lib/JSONBlob' +import { processShowStyleBase, processShowStyleVariant } from '../jobs/showStyle' +import { defaultStudio } from './defaultCollectionObjects' export function setupDefaultJobEnvironment(studioId?: StudioId): MockJobContext { const { mockCollections, jobCollections } = getMockCollections() diff --git a/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts b/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts index aadeec1fd1..076abb7b83 100644 --- a/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts +++ b/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts @@ -1,14 +1,15 @@ +import { IBlueprintPieceType, PieceLifespan, PlaylistTimingType } from '@sofie-automation/blueprints-integration' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { - RundownPlaylistId, - StudioId, + PartId, PeripheralDeviceId, + PieceId, + RundownId, + RundownPlaylistId, + SegmentId, ShowStyleBaseId, ShowStyleVariantId, - SegmentId, - RundownId, - PartId, - PieceId, + StudioId, } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { EmptyPieceTimelineObjectsBlob, Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' @@ -17,11 +18,10 @@ import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/Rund import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' -import { getRundownId } from '../ingest/lib' -import { getCurrentTime } from '../lib' -import { IBlueprintPieceType, PieceLifespan, PlaylistTimingType } from '@sofie-automation/blueprints-integration' import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants' +import { getRundownId } from '../ingest/lib' +import { getCurrentTime } from '../lib' export function defaultRundownPlaylist(_id: RundownPlaylistId, studioId: StudioId): DBRundownPlaylist { return { @@ -100,6 +100,7 @@ export function defaultStudio(_id: StudioId): DBStudio { frameRate: 25, mediaPreviewsUrl: '', minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, + allowScratchpad: true, }, routeSets: {}, routeSetExclusivityGroups: {}, diff --git a/packages/job-worker/src/playout/__tests__/actions.test.ts b/packages/job-worker/src/playout/__tests__/actions.test.ts index 810a750ca1..ce34e02fa8 100644 --- a/packages/job-worker/src/playout/__tests__/actions.test.ts +++ b/packages/job-worker/src/playout/__tests__/actions.test.ts @@ -1,14 +1,17 @@ +import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { MockJobContext, setupDefaultJobEnvironment } from '../../__mocks__/context' -import { removeRundownFromDb } from '../../rundownPlaylists' import { setupDefaultRundownPlaylist, setupMockShowStyleCompound } from '../../__mocks__/presetCollections' +import { runWithRundownLock } from '../../ingest/lock' +import { executePeripheralDeviceFunction } from '../../peripheralDevice' +import { removeRundownFromDb } from '../../rundownPlaylists' import { activateRundownPlaylist } from '../activePlaylistActions' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { runJobWithPlayoutModel } from '../lock' -import { runWithRundownLock } from '../../ingest/lock' +import { handleActivateScratchpad } from '../scratchpad' jest.mock('../../peripheralDevice') -import { executePeripheralDeviceFunction } from '../../peripheralDevice' type TexecutePeripheralDeviceFunction = jest.MockedFunction const executePeripheralDeviceFunctionMock = executePeripheralDeviceFunction as TexecutePeripheralDeviceFunction @@ -85,4 +88,65 @@ describe('Playout Actions', () => { expect(executePeripheralDeviceFunctionMock).toHaveBeenCalledTimes(0) }) + test('scratchpad', async () => { + const { playlistId: playlistId0, rundownId: rundownId0 } = await setupDefaultRundownPlaylist( + context, + undefined, + protectString('ro0') + ) + expect(playlistId0).toBeTruthy() + + const getFirstSegment = async () => + await context.directCollections.Segments.findOne( + { + rundownId: rundownId0, + }, + { + sort: { + _rank: 1, + }, + } + ) + + const getCurrentPartInstance = async (playlistId: RundownPlaylistId) => { + const playlist = await context.directCollections.RundownPlaylists.findOne(playlistId) + if (!playlist) throw new Error(`Playlist "${playlistId} not found`) + if (!playlist.currentPartInfo) throw new Error(`Playlist "${playlistId}" doesn't have any currentPartInfo`) + return context.directCollections.PartInstances.findOne(playlist.currentPartInfo?.partInstanceId) + } + + // Activating a rundown, to rehearsal + await runJobWithPlayoutModel(context, { playlistId: playlistId0 }, null, async (playoutModel) => + activateRundownPlaylist(context, playoutModel, true) + ) + + await expect(getFirstSegment()).resolves.toMatchObject({ + name: 'Segment 0', + }) + + await handleActivateScratchpad(context, { + playlistId: playlistId0, + rundownId: rundownId0, + }) + + // Scratchpad segment should be at the top + const topSegment = await getFirstSegment() + expect(topSegment).toMatchObject({ + orphaned: SegmentOrphanedReason.SCRATCHPAD, + }) + + await expect(getCurrentPartInstance(playlistId0)).resolves.toMatchObject({ + segmentId: topSegment?._id, + }) + + // Activating a rundown + await runJobWithPlayoutModel(context, { playlistId: playlistId0 }, null, async (playoutModel) => + activateRundownPlaylist(context, playoutModel, false) + ) + + // Scratchpad segment should be gone + await expect(getFirstSegment()).resolves.toMatchObject({ + name: 'Segment 0', + }) + }) }) From f0d5e6a79a73c9bf4434b1f0a24ba4b4e1c7d33f Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Fri, 19 Apr 2024 15:06:10 +0200 Subject: [PATCH 275/479] chore: rename usePieceSteps to getPieceSteps --- .../PieceMultistepChevron.tsx | 6 ++--- .../LinePartMainPiece/LinePartMainPiece.tsx | 26 +++++++++---------- .../Renderers/L3rdSourceRenderer.tsx | 10 +++---- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/meteor/client/ui/SegmentContainer/PieceMultistepChevron.tsx b/meteor/client/ui/SegmentContainer/PieceMultistepChevron.tsx index 1ef4ea61a4..29b1868fdc 100644 --- a/meteor/client/ui/SegmentContainer/PieceMultistepChevron.tsx +++ b/meteor/client/ui/SegmentContainer/PieceMultistepChevron.tsx @@ -1,5 +1,5 @@ -import React from 'react' import { NoraContent, SourceLayerType } from '@sofie-automation/blueprints-integration' +import React from 'react' import { PieceExtended } from '../../../lib/Rundown' export const PieceMultistepChevron = React.forwardRef< @@ -10,7 +10,7 @@ export const PieceMultistepChevron = React.forwardRef< style?: React.CSSProperties } >(function PieceMultistepChevron({ className, piece, style }, ref): JSX.Element | null { - const hasStepChevron = usePieceSteps(piece) + const hasStepChevron = getPieceSteps(piece) if (!hasStepChevron) return null @@ -23,7 +23,7 @@ export const PieceMultistepChevron = React.forwardRef< ) }) -export function usePieceSteps(piece: PieceExtended): { currentStep: number; allSteps: number } | null { +export function getPieceSteps(piece: PieceExtended): { currentStep: number; allSteps: number } | null { const noraContent = piece.instance.piece.content as NoraContent | undefined const hasStepChevron = diff --git a/meteor/client/ui/SegmentList/LinePartMainPiece/LinePartMainPiece.tsx b/meteor/client/ui/SegmentList/LinePartMainPiece/LinePartMainPiece.tsx index dbc1faa9c8..b4818d513b 100644 --- a/meteor/client/ui/SegmentList/LinePartMainPiece/LinePartMainPiece.tsx +++ b/meteor/client/ui/SegmentList/LinePartMainPiece/LinePartMainPiece.tsx @@ -1,21 +1,21 @@ -import React, { useMemo, useState, useRef } from 'react' import { EvsContent, SourceLayerType } from '@sofie-automation/blueprints-integration' +import React, { useMemo, useRef, useState } from 'react' import { PieceExtended } from '../../../../lib/Rundown' // TODO: Move to a shared lib file -import { getSplitItems } from '../../SegmentContainer/getSplitItems' -import { withMediaObjectStatus } from '../../SegmentTimeline/withMediaObjectStatus' -import { PieceUi } from '../../SegmentContainer/withResolvedSegment' -import { PieceElement } from '../../SegmentContainer/PieceElement' -import { getElementWidth } from '../../../utils/dimensions' -import { getElementDocumentOffset, OffsetPosition } from '../../../utils/positions' -import { PieceHoverInspector } from '../PieceHoverInspector' import { PartId, PartInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { getNoticeLevelForPieceStatus } from '../../../../lib/notifications/notifications' -import { PieceStatusIcon } from '../../../lib/ui/PieceStatusIcon' -import { UIStudio } from '../../../../lib/api/studios' import classNames from 'classnames' -import { PieceMultistepChevron, usePieceSteps } from '../../SegmentContainer/PieceMultistepChevron' +import { UIStudio } from '../../../../lib/api/studios' +import { getNoticeLevelForPieceStatus } from '../../../../lib/notifications/notifications' import { LoopingPieceIcon } from '../../../lib/ui/icons/looping' +import { PieceStatusIcon } from '../../../lib/ui/PieceStatusIcon' +import { getElementWidth } from '../../../utils/dimensions' +import { getElementDocumentOffset, OffsetPosition } from '../../../utils/positions' +import { getSplitItems } from '../../SegmentContainer/getSplitItems' +import { PieceElement } from '../../SegmentContainer/PieceElement' +import { getPieceSteps, PieceMultistepChevron } from '../../SegmentContainer/PieceMultistepChevron' +import { PieceUi } from '../../SegmentContainer/withResolvedSegment' +import { withMediaObjectStatus } from '../../SegmentTimeline/withMediaObjectStatus' +import { PieceHoverInspector } from '../PieceHoverInspector' interface IProps { partId: PartId @@ -163,7 +163,7 @@ export const LinePartMainPiece = withMediaObjectStatus()(function Li const noticeLevel = getNoticeLevelForPieceStatus(piece.contentStatus?.status) - const hasStepChevron = usePieceSteps(piece) + const hasStepChevron = getPieceSteps(piece) const multistepChevron = ( diff --git a/meteor/client/ui/SegmentTimeline/Renderers/L3rdSourceRenderer.tsx b/meteor/client/ui/SegmentTimeline/Renderers/L3rdSourceRenderer.tsx index 328465c717..7eaa425f83 100644 --- a/meteor/client/ui/SegmentTimeline/Renderers/L3rdSourceRenderer.tsx +++ b/meteor/client/ui/SegmentTimeline/Renderers/L3rdSourceRenderer.tsx @@ -1,12 +1,12 @@ import * as React from 'react' import { getElementWidth } from '../../../utils/dimensions' -import { CustomLayerItemRenderer, ICustomLayerItemProps } from './CustomLayerItemRenderer' import { NoraContent, SourceLayerType } from '@sofie-automation/blueprints-integration' -import { L3rdFloatingInspector } from '../../FloatingInspectors/L3rdFloatingInspector' -import { RundownUtils } from '../../../lib/rundown' -import { PieceMultistepChevron, usePieceSteps } from '../../SegmentContainer/PieceMultistepChevron' import classNames from 'classnames' +import { RundownUtils } from '../../../lib/rundown' +import { L3rdFloatingInspector } from '../../FloatingInspectors/L3rdFloatingInspector' +import { PieceMultistepChevron, getPieceSteps } from '../../SegmentContainer/PieceMultistepChevron' +import { CustomLayerItemRenderer, ICustomLayerItemProps } from './CustomLayerItemRenderer' type IProps = ICustomLayerItemProps interface IState { @@ -131,7 +131,7 @@ export class L3rdSourceRenderer extends CustomLayerItemRenderer const innerPiece = piece.instance.piece const noraContent = innerPiece.content as NoraContent | undefined - const hasStepChevron = usePieceSteps(piece) + const hasStepChevron = getPieceSteps(piece) const multistepPill = ( Date: Fri, 19 Apr 2024 15:07:17 +0200 Subject: [PATCH 276/479] chore: cleanup --- .../ui/SegmentTimeline/Renderers/L3rdSourceRenderer.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/meteor/client/ui/SegmentTimeline/Renderers/L3rdSourceRenderer.tsx b/meteor/client/ui/SegmentTimeline/Renderers/L3rdSourceRenderer.tsx index 7eaa425f83..bbefbcd606 100644 --- a/meteor/client/ui/SegmentTimeline/Renderers/L3rdSourceRenderer.tsx +++ b/meteor/client/ui/SegmentTimeline/Renderers/L3rdSourceRenderer.tsx @@ -92,7 +92,6 @@ export class L3rdSourceRenderer extends CustomLayerItemRenderer isLiveLine, liveLineHistorySize, followLiveLine, - // livePosition, isPreview, relative, partStartsAt, @@ -105,13 +104,7 @@ export class L3rdSourceRenderer extends CustomLayerItemRenderer } } - // const liveLineHistoryWithMargin = liveLineHistorySize - const inPoint = piece.renderedInPoint || 0 - // const targetPos = Math.min( - // (scrollLeft - inPoint - partStartsAt) * timeScale, - // leftLabelWidth - multistepPillWidth * 2 - // ) const targetPos = Math.min( (scrollLeft - inPoint - partStartsAt) * timeScale - multistepPillWidth - 10, leftLabelWidth - multistepPillWidth - liveLineHistorySize @@ -120,7 +113,6 @@ export class L3rdSourceRenderer extends CustomLayerItemRenderer return { stepsStyle: { transform: ` translate(${targetPos}px, 0) ` + ` translate(${liveLineHistorySize}px, 0) `, - // ' translate(-100%, 0)', willChange: 'transform', }, } From b025be4c43a7742745427d6da4306086e053f657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20St=C3=A6rk=C3=A6r?= Date: Mon, 22 Apr 2024 12:14:54 +0200 Subject: [PATCH 277/479] feat: upgrades docusaurus to 3.2.1 (cherry picked from commit 929be32b957a2b2879166d0bd29d8b41b02d0a20) --- .../ffmpeg-installation.md | 2 +- packages/documentation/docusaurus.config.js | 5 +- packages/documentation/package.json | 17 +- .../ffmpeg-installation.md | 2 +- .../ffmpeg-installation.md | 2 +- .../ffmpeg-installation.md | 2 +- .../ffmpeg-installation.md | 2 +- .../ffmpeg-installation.md | 2 +- .../ffmpeg-installation.md | 2 +- .../version-1.37.0-sidebars.json | 26 +- .../version-1.38.0-sidebars.json | 24 +- .../version-1.41.0-sidebars.json | 24 +- .../version-1.46.0-sidebars.json | 24 +- .../version-1.47.0-sidebars.json | 26 +- packages/yarn.lock | 5726 ++++++++++++----- 15 files changed, 4041 insertions(+), 1845 deletions(-) diff --git a/packages/documentation/docs/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md b/packages/documentation/docs/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md index 6d80150698..a0fd8d66a2 100644 --- a/packages/documentation/docs/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md +++ b/packages/documentation/docs/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md @@ -15,7 +15,7 @@ Some parts of Sofie (specifically the Package Manager) require that [`FFmpeg`](h ![System Properties screenshot](/img/docs/system_properties.png) -5. If you installed `FFmpeg` and `FFprobe` to a system-wide location such as `C:\Program Files\FFmpeg`, select and edit the `Path` variable under the "System variables" heading. Else, if you installed them to some place specific to your user account, edit the `Path` variable under the "User variables for " heading. +5. If you installed `FFmpeg` and `FFprobe` to a system-wide location such as `C:\Program Files\FFmpeg`, select and edit the `Path` variable under the "System variables" heading. Else, if you installed them to some place specific to your user account, edit the `Path` variable under the "User variables for \" heading. ![Environment Variables screenshot](/img/docs/environment_variables.png) diff --git a/packages/documentation/docusaurus.config.js b/packages/documentation/docusaurus.config.js index 682ee812e6..1d645225ae 100644 --- a/packages/documentation/docusaurus.config.js +++ b/packages/documentation/docusaurus.config.js @@ -1,5 +1,6 @@ -const lightCodeTheme = require('prism-react-renderer/themes/github') -const darkCodeTheme = require('prism-react-renderer/themes/dracula') +const { themes } = require('prism-react-renderer') +const lightCodeTheme = themes.github +const darkCodeTheme = themes.dracula /** @type {import('@docusaurus/types').DocusaurusConfig} */ module.exports = { diff --git a/packages/documentation/package.json b/packages/documentation/package.json index 3bbb794e45..887fda9b30 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -14,16 +14,21 @@ "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids" }, + "engines": { + "node": ">=18.0" + }, "devDependencies": { - "@docusaurus/core": "2.4.3", - "@docusaurus/preset-classic": "2.4.3", - "@mdx-js/react": "^1.6.22", + "@docusaurus/core": "3.2.1", + "@docusaurus/module-type-aliases": "3.2.1", + "@docusaurus/preset-classic": "3.2.1", + "@docusaurus/types": "3.2.1", + "@mdx-js/react": "^3.0.0", "@svgr/webpack": "^5.5.0", "clsx": "^1.2.1", "file-loader": "^6.2.0", - "prism-react-renderer": "^1.3.5", - "react": "^17.0.2", - "react-dom": "^17.0.2", + "prism-react-renderer": "^2.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", "url-loader": "^4.1.1" }, "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", diff --git a/packages/documentation/versioned_docs/version-1.37.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md b/packages/documentation/versioned_docs/version-1.37.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md index 6d80150698..a0fd8d66a2 100644 --- a/packages/documentation/versioned_docs/version-1.37.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md +++ b/packages/documentation/versioned_docs/version-1.37.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md @@ -15,7 +15,7 @@ Some parts of Sofie (specifically the Package Manager) require that [`FFmpeg`](h ![System Properties screenshot](/img/docs/system_properties.png) -5. If you installed `FFmpeg` and `FFprobe` to a system-wide location such as `C:\Program Files\FFmpeg`, select and edit the `Path` variable under the "System variables" heading. Else, if you installed them to some place specific to your user account, edit the `Path` variable under the "User variables for " heading. +5. If you installed `FFmpeg` and `FFprobe` to a system-wide location such as `C:\Program Files\FFmpeg`, select and edit the `Path` variable under the "System variables" heading. Else, if you installed them to some place specific to your user account, edit the `Path` variable under the "User variables for \" heading. ![Environment Variables screenshot](/img/docs/environment_variables.png) diff --git a/packages/documentation/versioned_docs/version-1.38.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md b/packages/documentation/versioned_docs/version-1.38.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md index 6d80150698..a0fd8d66a2 100644 --- a/packages/documentation/versioned_docs/version-1.38.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md +++ b/packages/documentation/versioned_docs/version-1.38.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md @@ -15,7 +15,7 @@ Some parts of Sofie (specifically the Package Manager) require that [`FFmpeg`](h ![System Properties screenshot](/img/docs/system_properties.png) -5. If you installed `FFmpeg` and `FFprobe` to a system-wide location such as `C:\Program Files\FFmpeg`, select and edit the `Path` variable under the "System variables" heading. Else, if you installed them to some place specific to your user account, edit the `Path` variable under the "User variables for " heading. +5. If you installed `FFmpeg` and `FFprobe` to a system-wide location such as `C:\Program Files\FFmpeg`, select and edit the `Path` variable under the "System variables" heading. Else, if you installed them to some place specific to your user account, edit the `Path` variable under the "User variables for \" heading. ![Environment Variables screenshot](/img/docs/environment_variables.png) diff --git a/packages/documentation/versioned_docs/version-1.41.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md b/packages/documentation/versioned_docs/version-1.41.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md index 6d80150698..a0fd8d66a2 100644 --- a/packages/documentation/versioned_docs/version-1.41.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md +++ b/packages/documentation/versioned_docs/version-1.41.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md @@ -15,7 +15,7 @@ Some parts of Sofie (specifically the Package Manager) require that [`FFmpeg`](h ![System Properties screenshot](/img/docs/system_properties.png) -5. If you installed `FFmpeg` and `FFprobe` to a system-wide location such as `C:\Program Files\FFmpeg`, select and edit the `Path` variable under the "System variables" heading. Else, if you installed them to some place specific to your user account, edit the `Path` variable under the "User variables for " heading. +5. If you installed `FFmpeg` and `FFprobe` to a system-wide location such as `C:\Program Files\FFmpeg`, select and edit the `Path` variable under the "System variables" heading. Else, if you installed them to some place specific to your user account, edit the `Path` variable under the "User variables for \" heading. ![Environment Variables screenshot](/img/docs/environment_variables.png) diff --git a/packages/documentation/versioned_docs/version-1.46.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md b/packages/documentation/versioned_docs/version-1.46.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md index 6d80150698..a0fd8d66a2 100644 --- a/packages/documentation/versioned_docs/version-1.46.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md +++ b/packages/documentation/versioned_docs/version-1.46.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md @@ -15,7 +15,7 @@ Some parts of Sofie (specifically the Package Manager) require that [`FFmpeg`](h ![System Properties screenshot](/img/docs/system_properties.png) -5. If you installed `FFmpeg` and `FFprobe` to a system-wide location such as `C:\Program Files\FFmpeg`, select and edit the `Path` variable under the "System variables" heading. Else, if you installed them to some place specific to your user account, edit the `Path` variable under the "User variables for " heading. +5. If you installed `FFmpeg` and `FFprobe` to a system-wide location such as `C:\Program Files\FFmpeg`, select and edit the `Path` variable under the "System variables" heading. Else, if you installed them to some place specific to your user account, edit the `Path` variable under the "User variables for \" heading. ![Environment Variables screenshot](/img/docs/environment_variables.png) diff --git a/packages/documentation/versioned_docs/version-1.47.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md b/packages/documentation/versioned_docs/version-1.47.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md index 6d80150698..a0fd8d66a2 100644 --- a/packages/documentation/versioned_docs/version-1.47.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md +++ b/packages/documentation/versioned_docs/version-1.47.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md @@ -15,7 +15,7 @@ Some parts of Sofie (specifically the Package Manager) require that [`FFmpeg`](h ![System Properties screenshot](/img/docs/system_properties.png) -5. If you installed `FFmpeg` and `FFprobe` to a system-wide location such as `C:\Program Files\FFmpeg`, select and edit the `Path` variable under the "System variables" heading. Else, if you installed them to some place specific to your user account, edit the `Path` variable under the "User variables for " heading. +5. If you installed `FFmpeg` and `FFprobe` to a system-wide location such as `C:\Program Files\FFmpeg`, select and edit the `Path` variable under the "System variables" heading. Else, if you installed them to some place specific to your user account, edit the `Path` variable under the "User variables for \" heading. ![Environment Variables screenshot](/img/docs/environment_variables.png) diff --git a/packages/documentation/versioned_docs/version-1.49.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md b/packages/documentation/versioned_docs/version-1.49.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md index 6d80150698..a0fd8d66a2 100644 --- a/packages/documentation/versioned_docs/version-1.49.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md +++ b/packages/documentation/versioned_docs/version-1.49.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md @@ -15,7 +15,7 @@ Some parts of Sofie (specifically the Package Manager) require that [`FFmpeg`](h ![System Properties screenshot](/img/docs/system_properties.png) -5. If you installed `FFmpeg` and `FFprobe` to a system-wide location such as `C:\Program Files\FFmpeg`, select and edit the `Path` variable under the "System variables" heading. Else, if you installed them to some place specific to your user account, edit the `Path` variable under the "User variables for " heading. +5. If you installed `FFmpeg` and `FFprobe` to a system-wide location such as `C:\Program Files\FFmpeg`, select and edit the `Path` variable under the "System variables" heading. Else, if you installed them to some place specific to your user account, edit the `Path` variable under the "User variables for \" heading. ![Environment Variables screenshot](/img/docs/environment_variables.png) diff --git a/packages/documentation/versioned_sidebars/version-1.37.0-sidebars.json b/packages/documentation/versioned_sidebars/version-1.37.0-sidebars.json index bdab5bf362..fd899824ae 100644 --- a/packages/documentation/versioned_sidebars/version-1.37.0-sidebars.json +++ b/packages/documentation/versioned_sidebars/version-1.37.0-sidebars.json @@ -1,14 +1,14 @@ { - "version-1.37.0/gettingStarted": [ - { - "type": "autogenerated", - "dirName": "user-guide" - } - ], - "version-1.37.0/forDevelopers": [ - { - "type": "autogenerated", - "dirName": "for-developers" - } - ] -} \ No newline at end of file + "gettingStarted": [ + { + "type": "autogenerated", + "dirName": "user-guide" + } + ], + "forDevelopers": [ + { + "type": "autogenerated", + "dirName": "for-developers" + } + ] +} diff --git a/packages/documentation/versioned_sidebars/version-1.38.0-sidebars.json b/packages/documentation/versioned_sidebars/version-1.38.0-sidebars.json index 26c9b3e48b..f9b82fa92b 100644 --- a/packages/documentation/versioned_sidebars/version-1.38.0-sidebars.json +++ b/packages/documentation/versioned_sidebars/version-1.38.0-sidebars.json @@ -1,14 +1,14 @@ { - "version-1.38.0/userGuide": [ - { - "type": "autogenerated", - "dirName": "user-guide" - } - ], - "version-1.38.0/forDevelopers": [ - { - "type": "autogenerated", - "dirName": "for-developers" - } - ] + "userGuide": [ + { + "type": "autogenerated", + "dirName": "user-guide" + } + ], + "forDevelopers": [ + { + "type": "autogenerated", + "dirName": "for-developers" + } + ] } diff --git a/packages/documentation/versioned_sidebars/version-1.41.0-sidebars.json b/packages/documentation/versioned_sidebars/version-1.41.0-sidebars.json index ab1b20c403..f9b82fa92b 100644 --- a/packages/documentation/versioned_sidebars/version-1.41.0-sidebars.json +++ b/packages/documentation/versioned_sidebars/version-1.41.0-sidebars.json @@ -1,14 +1,14 @@ { - "version-1.41.0/userGuide": [ - { - "type": "autogenerated", - "dirName": "user-guide" - } - ], - "version-1.41.0/forDevelopers": [ - { - "type": "autogenerated", - "dirName": "for-developers" - } - ] + "userGuide": [ + { + "type": "autogenerated", + "dirName": "user-guide" + } + ], + "forDevelopers": [ + { + "type": "autogenerated", + "dirName": "for-developers" + } + ] } diff --git a/packages/documentation/versioned_sidebars/version-1.46.0-sidebars.json b/packages/documentation/versioned_sidebars/version-1.46.0-sidebars.json index 893255e8df..f9b82fa92b 100644 --- a/packages/documentation/versioned_sidebars/version-1.46.0-sidebars.json +++ b/packages/documentation/versioned_sidebars/version-1.46.0-sidebars.json @@ -1,14 +1,14 @@ { - "version-1.45.0/userGuide": [ - { - "type": "autogenerated", - "dirName": "user-guide" - } - ], - "version-1.45.0/forDevelopers": [ - { - "type": "autogenerated", - "dirName": "for-developers" - } - ] + "userGuide": [ + { + "type": "autogenerated", + "dirName": "user-guide" + } + ], + "forDevelopers": [ + { + "type": "autogenerated", + "dirName": "for-developers" + } + ] } diff --git a/packages/documentation/versioned_sidebars/version-1.47.0-sidebars.json b/packages/documentation/versioned_sidebars/version-1.47.0-sidebars.json index 19073c3e3f..f9b82fa92b 100644 --- a/packages/documentation/versioned_sidebars/version-1.47.0-sidebars.json +++ b/packages/documentation/versioned_sidebars/version-1.47.0-sidebars.json @@ -1,14 +1,14 @@ { - "version-1.47.0/userGuide": [ - { - "type": "autogenerated", - "dirName": "user-guide" - } - ], - "version-1.47.0/forDevelopers": [ - { - "type": "autogenerated", - "dirName": "for-developers" - } - ] -} \ No newline at end of file + "userGuide": [ + { + "type": "autogenerated", + "dirName": "user-guide" + } + ], + "forDevelopers": [ + { + "type": "autogenerated", + "dirName": "for-developers" + } + ] +} diff --git a/packages/yarn.lock b/packages/yarn.lock index 5ece406dde..070491f4d9 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -75,6 +75,15 @@ __metadata: languageName: node linkType: hard +"@algolia/cache-browser-local-storage@npm:4.23.3": + version: 4.23.3 + resolution: "@algolia/cache-browser-local-storage@npm:4.23.3" + dependencies: + "@algolia/cache-common": 4.23.3 + checksum: bbce762cc69952d8e02a228bbc1b9795bd076e637fd374a6e52c4f117f44de465231731f00562dbdda72aca9c150d53a0efb22d5d9e5b0d57674c8f853bc5a85 + languageName: node + linkType: hard + "@algolia/cache-common@npm:4.20.0": version: 4.20.0 resolution: "@algolia/cache-common@npm:4.20.0" @@ -82,6 +91,13 @@ __metadata: languageName: node linkType: hard +"@algolia/cache-common@npm:4.23.3": + version: 4.23.3 + resolution: "@algolia/cache-common@npm:4.23.3" + checksum: c4502b9f188c451905d47c50e4706df3c188854615119b470a4d993d8c66d41ae1d9aec2464bc8a174c6ba2bfc939835b98cb7d4afddaa6c3ccb766231e1dbbc + languageName: node + linkType: hard + "@algolia/cache-in-memory@npm:4.20.0": version: 4.20.0 resolution: "@algolia/cache-in-memory@npm:4.20.0" @@ -91,6 +107,15 @@ __metadata: languageName: node linkType: hard +"@algolia/cache-in-memory@npm:4.23.3": + version: 4.23.3 + resolution: "@algolia/cache-in-memory@npm:4.23.3" + dependencies: + "@algolia/cache-common": 4.23.3 + checksum: 9a26f6213873ec99ab3fb1bc4ba3bb7c64fc433f46ac9365689921e7c1ddaae437ee78c42d85d4426fc18ef0410d8fc9b78824759000b16fc2da60aba490cb87 + languageName: node + linkType: hard + "@algolia/client-account@npm:4.20.0": version: 4.20.0 resolution: "@algolia/client-account@npm:4.20.0" @@ -102,6 +127,17 @@ __metadata: languageName: node linkType: hard +"@algolia/client-account@npm:4.23.3": + version: 4.23.3 + resolution: "@algolia/client-account@npm:4.23.3" + dependencies: + "@algolia/client-common": 4.23.3 + "@algolia/client-search": 4.23.3 + "@algolia/transporter": 4.23.3 + checksum: 56404a43dfe53eb0168e9be568482fb4b8b00adb73b978f7f5c02627d179f51eb273ea4880428d26aa692253f11cdd1d6b62796571f6e3ada1397c64f28fc591 + languageName: node + linkType: hard + "@algolia/client-analytics@npm:4.20.0": version: 4.20.0 resolution: "@algolia/client-analytics@npm:4.20.0" @@ -114,6 +150,18 @@ __metadata: languageName: node linkType: hard +"@algolia/client-analytics@npm:4.23.3": + version: 4.23.3 + resolution: "@algolia/client-analytics@npm:4.23.3" + dependencies: + "@algolia/client-common": 4.23.3 + "@algolia/client-search": 4.23.3 + "@algolia/requester-common": 4.23.3 + "@algolia/transporter": 4.23.3 + checksum: a108bdbad64eed6166bbce16ab4f9f10c46ad8d689142e7c48bc7743b34e5d0770b21745a87fab3d04131420b57a73baf0a2cd1a2c8baa547c899ff33a4051bd + languageName: node + linkType: hard + "@algolia/client-common@npm:4.20.0": version: 4.20.0 resolution: "@algolia/client-common@npm:4.20.0" @@ -124,6 +172,16 @@ __metadata: languageName: node linkType: hard +"@algolia/client-common@npm:4.23.3": + version: 4.23.3 + resolution: "@algolia/client-common@npm:4.23.3" + dependencies: + "@algolia/requester-common": 4.23.3 + "@algolia/transporter": 4.23.3 + checksum: 0767cd7a4f38abc0290a9c055d39730c5f507a0e9cd6657fbad749c15a9ba9cceb788c18fec0b5a25f49e6184fb40e8dd26c3e8b29824aa3df82822618399f08 + languageName: node + linkType: hard + "@algolia/client-personalization@npm:4.20.0": version: 4.20.0 resolution: "@algolia/client-personalization@npm:4.20.0" @@ -135,6 +193,17 @@ __metadata: languageName: node linkType: hard +"@algolia/client-personalization@npm:4.23.3": + version: 4.23.3 + resolution: "@algolia/client-personalization@npm:4.23.3" + dependencies: + "@algolia/client-common": 4.23.3 + "@algolia/requester-common": 4.23.3 + "@algolia/transporter": 4.23.3 + checksum: 393a6a2c53185090c141c50dfc4896baa7b93af836479e9e43ad29e71de1bcce00e1273bb51ba512376a996f75f10146ba6443c3d53d2e4acc50eef43b65582e + languageName: node + linkType: hard + "@algolia/client-search@npm:4.20.0": version: 4.20.0 resolution: "@algolia/client-search@npm:4.20.0" @@ -146,6 +215,17 @@ __metadata: languageName: node linkType: hard +"@algolia/client-search@npm:4.23.3": + version: 4.23.3 + resolution: "@algolia/client-search@npm:4.23.3" + dependencies: + "@algolia/client-common": 4.23.3 + "@algolia/requester-common": 4.23.3 + "@algolia/transporter": 4.23.3 + checksum: 0249aeeaffa94608948f047dabd25a1c452c52cfbf5ce3abaad4f41134e87344d55733f03b512f64ffd23d43ff78d4339a8abfb83887ea23ede1d2d6567bf421 + languageName: node + linkType: hard + "@algolia/events@npm:^4.0.1": version: 4.0.1 resolution: "@algolia/events@npm:4.0.1" @@ -160,6 +240,13 @@ __metadata: languageName: node linkType: hard +"@algolia/logger-common@npm:4.23.3": + version: 4.23.3 + resolution: "@algolia/logger-common@npm:4.23.3" + checksum: a6710ac3e790dc896d7f32eefc9e2967c765f0955fabd33291c14d61ad12d34259709370a18eb299518e36cc3b538c385ab1cc85b021b1acbf463315a61df67c + languageName: node + linkType: hard + "@algolia/logger-console@npm:4.20.0": version: 4.20.0 resolution: "@algolia/logger-console@npm:4.20.0" @@ -169,6 +256,34 @@ __metadata: languageName: node linkType: hard +"@algolia/logger-console@npm:4.23.3": + version: 4.23.3 + resolution: "@algolia/logger-console@npm:4.23.3" + dependencies: + "@algolia/logger-common": 4.23.3 + checksum: 881eab328986626deaa20f6b7e51b1a86b47678681869f20e89ec47cfdf4a0547081fa4315149ac8c5e2ed3cb16a9547e1265a48c14ed6b7d549ba7abc5a71e9 + languageName: node + linkType: hard + +"@algolia/recommend@npm:4.23.3": + version: 4.23.3 + resolution: "@algolia/recommend@npm:4.23.3" + dependencies: + "@algolia/cache-browser-local-storage": 4.23.3 + "@algolia/cache-common": 4.23.3 + "@algolia/cache-in-memory": 4.23.3 + "@algolia/client-common": 4.23.3 + "@algolia/client-search": 4.23.3 + "@algolia/logger-common": 4.23.3 + "@algolia/logger-console": 4.23.3 + "@algolia/requester-browser-xhr": 4.23.3 + "@algolia/requester-common": 4.23.3 + "@algolia/requester-node-http": 4.23.3 + "@algolia/transporter": 4.23.3 + checksum: b8030c85cd9b62aed42ae73931b0586f460d61f68265e292dd6ecad3a473d84abcaf56d9a5e444f9c6c196b1635d41825850cc330ccc78d436f679127039845c + languageName: node + linkType: hard + "@algolia/requester-browser-xhr@npm:4.20.0": version: 4.20.0 resolution: "@algolia/requester-browser-xhr@npm:4.20.0" @@ -178,6 +293,15 @@ __metadata: languageName: node linkType: hard +"@algolia/requester-browser-xhr@npm:4.23.3": + version: 4.23.3 + resolution: "@algolia/requester-browser-xhr@npm:4.23.3" + dependencies: + "@algolia/requester-common": 4.23.3 + checksum: afe1f81915d2386aa25c91c6d41d00a3958516a3567f1ec95a7d95eb976f87676cfb0dcc39e3fe7646e150c6cb5a8e3526c23be706cb09e56e0928a96da8eb6b + languageName: node + linkType: hard + "@algolia/requester-common@npm:4.20.0": version: 4.20.0 resolution: "@algolia/requester-common@npm:4.20.0" @@ -185,6 +309,13 @@ __metadata: languageName: node linkType: hard +"@algolia/requester-common@npm:4.23.3": + version: 4.23.3 + resolution: "@algolia/requester-common@npm:4.23.3" + checksum: b7b308e46dc6158fd8adad82c301f60e1dd759e585cb90514b9a0be6b67cfba3d9ff6ad87f6299657a5ab4b5e94a2d330fc14de6c447012f32f846219c9e6971 + languageName: node + linkType: hard + "@algolia/requester-node-http@npm:4.20.0": version: 4.20.0 resolution: "@algolia/requester-node-http@npm:4.20.0" @@ -194,6 +325,15 @@ __metadata: languageName: node linkType: hard +"@algolia/requester-node-http@npm:4.23.3": + version: 4.23.3 + resolution: "@algolia/requester-node-http@npm:4.23.3" + dependencies: + "@algolia/requester-common": 4.23.3 + checksum: 3d751c063e0f96e41a61d87a3428b2cb13b30aaa9e0ba3e70a3b92ad642afbb26c5095405dd1ed6dd16755d47faece0f42c5677f30673898658461ad51ec2235 + languageName: node + linkType: hard + "@algolia/transporter@npm:4.20.0": version: 4.20.0 resolution: "@algolia/transporter@npm:4.20.0" @@ -205,6 +345,17 @@ __metadata: languageName: node linkType: hard +"@algolia/transporter@npm:4.23.3": + version: 4.23.3 + resolution: "@algolia/transporter@npm:4.23.3" + dependencies: + "@algolia/cache-common": 4.23.3 + "@algolia/logger-common": 4.23.3 + "@algolia/requester-common": 4.23.3 + checksum: e2573d308d7f41aa74b47c4dc052186fc9eab350ca5fec7c830ff5ca34337eeef01a7168bdd10f2e13c0cb1283385be211e7dd0a896be0aabfd900c056aa3606 + languageName: node + linkType: hard + "@ampproject/remapping@npm:^2.2.0": version: 2.2.1 resolution: "@ampproject/remapping@npm:2.2.1" @@ -448,6 +599,16 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.24.1, @babel/code-frame@npm:^7.24.2": + version: 7.24.2 + resolution: "@babel/code-frame@npm:7.24.2" + dependencies: + "@babel/highlight": ^7.24.2 + picocolors: ^1.0.0 + checksum: 70e867340cfe09ca5488b2f36372c45cabf43c79a5b6426e6df5ef0611ff5dfa75a57dda841895693de6008f32c21a7c97027a8c7bcabd63a7d17416cbead6f8 + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.22.20, @babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.22.9, @babel/compat-data@npm:^7.23.5": version: 7.23.5 resolution: "@babel/compat-data@npm:7.23.5" @@ -455,6 +616,13 @@ __metadata: languageName: node linkType: hard +"@babel/compat-data@npm:^7.24.4": + version: 7.24.4 + resolution: "@babel/compat-data@npm:7.24.4" + checksum: 52ce371658dc7796c9447c9cb3b9c0659370d141b76997f21c5e0028cca4d026ca546b84bc8d157ce7ca30bd353d89f9238504eb8b7aefa9b1f178b4c100c2d4 + languageName: node + linkType: hard + "@babel/core@npm:7.12.9": version: 7.12.9 resolution: "@babel/core@npm:7.12.9" @@ -479,7 +647,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.18.6, @babel/core@npm:^7.19.6, @babel/core@npm:^7.23.9": +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.19.6, @babel/core@npm:^7.23.9": version: 7.23.9 resolution: "@babel/core@npm:7.23.9" dependencies: @@ -502,7 +670,30 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.12.5, @babel/generator@npm:^7.18.7, @babel/generator@npm:^7.23.6, @babel/generator@npm:^7.7.2": +"@babel/core@npm:^7.23.3": + version: 7.24.4 + resolution: "@babel/core@npm:7.24.4" + dependencies: + "@ampproject/remapping": ^2.2.0 + "@babel/code-frame": ^7.24.2 + "@babel/generator": ^7.24.4 + "@babel/helper-compilation-targets": ^7.23.6 + "@babel/helper-module-transforms": ^7.23.3 + "@babel/helpers": ^7.24.4 + "@babel/parser": ^7.24.4 + "@babel/template": ^7.24.0 + "@babel/traverse": ^7.24.1 + "@babel/types": ^7.24.0 + convert-source-map: ^2.0.0 + debug: ^4.1.0 + gensync: ^1.0.0-beta.2 + json5: ^2.2.3 + semver: ^6.3.1 + checksum: 15ecad7581f3329995956ba461961b1af7bed48901f14fe962ccd3217edca60049e9e6ad4ce48134618397e6c90230168c842e2c28e47ef1f16c97dbbf663c61 + languageName: node + linkType: hard + +"@babel/generator@npm:^7.12.5, @babel/generator@npm:^7.23.6, @babel/generator@npm:^7.7.2": version: 7.23.6 resolution: "@babel/generator@npm:7.23.6" dependencies: @@ -514,6 +705,18 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.23.3, @babel/generator@npm:^7.24.1, @babel/generator@npm:^7.24.4": + version: 7.24.4 + resolution: "@babel/generator@npm:7.24.4" + dependencies: + "@babel/types": ^7.24.0 + "@jridgewell/gen-mapping": ^0.3.5 + "@jridgewell/trace-mapping": ^0.3.25 + jsesc: ^2.5.1 + checksum: 1b6146c31386c9df3eb594a2c36b5c98da4f67f7c06edb3d68a442b92516b21bb5ba3ad7dbe0058fe76625ed24d66923e15c95b0df75ef1907d4068921a699b8 + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.22.5": version: 7.22.5 resolution: "@babel/helper-annotate-as-pure@npm:7.22.5" @@ -523,7 +726,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-builder-binary-assignment-operator-visitor@npm:^7.22.5": +"@babel/helper-builder-binary-assignment-operator-visitor@npm:^7.22.15, @babel/helper-builder-binary-assignment-operator-visitor@npm:^7.22.5": version: 7.22.15 resolution: "@babel/helper-builder-binary-assignment-operator-visitor@npm:7.22.15" dependencies: @@ -564,7 +767,26 @@ __metadata: languageName: node linkType: hard -"@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.22.5": +"@babel/helper-create-class-features-plugin@npm:^7.24.1, @babel/helper-create-class-features-plugin@npm:^7.24.4": + version: 7.24.4 + resolution: "@babel/helper-create-class-features-plugin@npm:7.24.4" + dependencies: + "@babel/helper-annotate-as-pure": ^7.22.5 + "@babel/helper-environment-visitor": ^7.22.20 + "@babel/helper-function-name": ^7.23.0 + "@babel/helper-member-expression-to-functions": ^7.23.0 + "@babel/helper-optimise-call-expression": ^7.22.5 + "@babel/helper-replace-supers": ^7.24.1 + "@babel/helper-skip-transparent-expression-wrappers": ^7.22.5 + "@babel/helper-split-export-declaration": ^7.22.6 + semver: ^6.3.1 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 75b0a51ae1f7232932559779b78711c271404d02d069156d1bd9a7982c165c5134058d2ec2d8b5f2e42026ee4f52ba2a30c86a7aa3bce6b5fd0991eb721abc8c + languageName: node + linkType: hard + +"@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.22.15, @babel/helper-create-regexp-features-plugin@npm:^7.22.5": version: 7.22.15 resolution: "@babel/helper-create-regexp-features-plugin@npm:7.22.15" dependencies: @@ -592,6 +814,21 @@ __metadata: languageName: node linkType: hard +"@babel/helper-define-polyfill-provider@npm:^0.6.1": + version: 0.6.1 + resolution: "@babel/helper-define-polyfill-provider@npm:0.6.1" + dependencies: + "@babel/helper-compilation-targets": ^7.22.6 + "@babel/helper-plugin-utils": ^7.22.5 + debug: ^4.1.1 + lodash.debounce: ^4.0.8 + resolve: ^1.14.2 + peerDependencies: + "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 + checksum: b45deb37ce1342d862422e81a3d25ff55f9c7ca52fe303405641e2add8db754091aaaa2119047a0f0b85072221fbddaa92adf53104274661d2795783b56bea2c + languageName: node + linkType: hard + "@babel/helper-environment-visitor@npm:^7.22.20, @babel/helper-environment-visitor@npm:^7.22.5": version: 7.22.20 resolution: "@babel/helper-environment-visitor@npm:7.22.20" @@ -627,6 +864,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-member-expression-to-functions@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/helper-member-expression-to-functions@npm:7.23.0" + dependencies: + "@babel/types": ^7.23.0 + checksum: 494659361370c979ada711ca685e2efe9460683c36db1b283b446122596602c901e291e09f2f980ecedfe6e0f2bd5386cb59768285446530df10c14df1024e75 + languageName: node + linkType: hard + "@babel/helper-module-imports@npm:^7.10.4, @babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.22.15, @babel/helper-module-imports@npm:^7.22.5": version: 7.22.15 resolution: "@babel/helper-module-imports@npm:7.22.15" @@ -636,6 +882,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-imports@npm:^7.24.1, @babel/helper-module-imports@npm:^7.24.3": + version: 7.24.3 + resolution: "@babel/helper-module-imports@npm:7.24.3" + dependencies: + "@babel/types": ^7.24.0 + checksum: c23492189ba97a1ec7d37012336a5661174e8b88194836b6bbf90d13c3b72c1db4626263c654454986f924c6da8be7ba7f9447876d709cd00bd6ffde6ec00796 + languageName: node + linkType: hard + "@babel/helper-module-transforms@npm:^7.12.1, @babel/helper-module-transforms@npm:^7.22.5, @babel/helper-module-transforms@npm:^7.22.9, @babel/helper-module-transforms@npm:^7.23.3": version: 7.23.3 resolution: "@babel/helper-module-transforms@npm:7.23.3" @@ -660,13 +915,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-plugin-utils@npm:7.10.4": - version: 7.10.4 - resolution: "@babel/helper-plugin-utils@npm:7.10.4" - checksum: 639ed8fc462b97a83226cee6bb081b1d77e7f73e8b033d2592ed107ee41d96601e321e5ea53a33e47469c7f1146b250a3dcda5ab873c7de162ab62120c341a41 - languageName: node - linkType: hard - "@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.8.0, @babel/helper-plugin-utils@npm:^7.8.3": version: 7.22.5 resolution: "@babel/helper-plugin-utils@npm:7.22.5" @@ -674,7 +922,14 @@ __metadata: languageName: node linkType: hard -"@babel/helper-remap-async-to-generator@npm:^7.22.5, @babel/helper-remap-async-to-generator@npm:^7.22.9": +"@babel/helper-plugin-utils@npm:^7.24.0": + version: 7.24.0 + resolution: "@babel/helper-plugin-utils@npm:7.24.0" + checksum: e2baa0eede34d2fa2265947042aa84d444aa48dc51e9feedea55b67fc1bc3ab051387e18b33ca7748285a6061390831ab82f8a2c767d08470b93500ec727e9b9 + languageName: node + linkType: hard + +"@babel/helper-remap-async-to-generator@npm:^7.22.20, @babel/helper-remap-async-to-generator@npm:^7.22.5, @babel/helper-remap-async-to-generator@npm:^7.22.9": version: 7.22.20 resolution: "@babel/helper-remap-async-to-generator@npm:7.22.20" dependencies: @@ -700,6 +955,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-replace-supers@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/helper-replace-supers@npm:7.24.1" + dependencies: + "@babel/helper-environment-visitor": ^7.22.20 + "@babel/helper-member-expression-to-functions": ^7.23.0 + "@babel/helper-optimise-call-expression": ^7.22.5 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: c04182c34a3195c6396de2f2945f86cb60daa94ca7392db09bd8b0d4e7a15b02fbe1947c70f6062c87eadaea6d7135207129efa35cf458ea0987bab8c0f02d5a + languageName: node + linkType: hard + "@babel/helper-simple-access@npm:^7.22.5": version: 7.22.5 resolution: "@babel/helper-simple-access@npm:7.22.5" @@ -770,6 +1038,17 @@ __metadata: languageName: node linkType: hard +"@babel/helpers@npm:^7.24.4": + version: 7.24.4 + resolution: "@babel/helpers@npm:7.24.4" + dependencies: + "@babel/template": ^7.24.0 + "@babel/traverse": ^7.24.1 + "@babel/types": ^7.24.0 + checksum: ecd2dc0b3b32e24b97fa3bcda432dd3235b77c2be1e16eafc35b8ef8f6c461faa99796a8bc2431a408c98b4aabfd572c160e2b67ecea4c5c9dd3a8314a97994a + languageName: node + linkType: hard + "@babel/highlight@npm:^7.23.4": version: 7.23.4 resolution: "@babel/highlight@npm:7.23.4" @@ -781,7 +1060,19 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.12.7, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.18.8, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9": +"@babel/highlight@npm:^7.24.2": + version: 7.24.2 + resolution: "@babel/highlight@npm:7.24.2" + dependencies: + "@babel/helper-validator-identifier": ^7.22.20 + chalk: ^2.4.2 + js-tokens: ^4.0.0 + picocolors: ^1.0.0 + checksum: 5f17b131cc3ebf3ab285a62cf98a404aef1bd71a6be045e748f8d5bf66d6a6e1aefd62f5972c84369472e8d9f22a614c58a89cd331eb60b7ba965b31b1bbeaf5 + languageName: node + linkType: hard + +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.12.7, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9": version: 7.23.9 resolution: "@babel/parser@npm:7.23.9" bin: @@ -790,6 +1081,27 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.24.0, @babel/parser@npm:^7.24.1, @babel/parser@npm:^7.24.4": + version: 7.24.4 + resolution: "@babel/parser@npm:7.24.4" + bin: + parser: ./bin/babel-parser.js + checksum: 94c9e3e592894cd6fc57c519f4e06b65463df9be5f01739bb0d0bfce7ffcf99b3c2fdadd44dc59cc858ba2739ce6e469813a941c2f2dfacf333a3b2c9c5c8465 + languageName: node + linkType: hard + +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.24.4": + version: 7.24.4 + resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.24.4" + dependencies: + "@babel/helper-environment-visitor": ^7.22.20 + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 0be3f41b1b865d7a4ed1a432337be48de67989d0b4e47def34a05097a804b6fc193115f97c954fd757339e0b80030ecf1d0a3d3fd6e7e91718644de0a5aae3d3 + languageName: node + linkType: hard + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.22.15": version: 7.22.15 resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.22.15" @@ -801,6 +1113,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: ec5fddc8db6de0e0082a883f21141d6f4f9f9f0bc190d662a732b5e9a506aae5d7d2337049a1bf055d7cb7add6f128036db6d4f47de5e9ac1be29e043c8b7ca8 + languageName: node + linkType: hard + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.22.15": version: 7.22.15 resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.22.15" @@ -814,16 +1137,28 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-proposal-object-rest-spread@npm:7.12.1": - version: 7.12.1 - resolution: "@babel/plugin-proposal-object-rest-spread@npm:7.12.1" +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": ^7.10.4 - "@babel/plugin-syntax-object-rest-spread": ^7.8.0 - "@babel/plugin-transform-parameters": ^7.12.1 + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/helper-skip-transparent-expression-wrappers": ^7.22.5 + "@babel/plugin-transform-optional-chaining": ^7.24.1 peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 221a41630c9a7162bf0416c71695b3f7f38482078a1d0d3af7abdc4f07ea1c9feed890399158d56c1d0278c971fe6f565ce822e9351e4481f7d98e9ff735dced + "@babel/core": ^7.13.0 + checksum: e18235463e716ac2443938aaec3c18b40c417a1746fba0fa4c26cf4d71326b76ef26c002081ab1b445abfae98e063d561519aa55672dddc1ef80b3940211ffbb + languageName: node + linkType: hard + +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:7.24.1" + dependencies: + "@babel/helper-environment-visitor": ^7.22.20 + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: b5e5889ce5ef51e813e3063cd548f55eb3c88e925c3c08913f334e15d62496861e538ae52a3974e0c56a3044ed8fd5033faea67a64814324af56edc9865b7359 languageName: node linkType: hard @@ -913,6 +1248,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-import-assertions@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-syntax-import-assertions@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 2a463928a63b62052e9fb8f8b0018aa11a926e94f32c168260ae012afe864875c6176c6eb361e13f300542c31316dad791b08a5b8ed92436a3095c7a0e4fce65 + languageName: node + linkType: hard + "@babel/plugin-syntax-import-attributes@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-syntax-import-attributes@npm:7.22.5" @@ -924,6 +1270,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-import-attributes@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-syntax-import-attributes@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 87c8aa4a5ef931313f956871b27f2c051556f627b97ed21e9a5890ca4906b222d89062a956cde459816f5e0dec185ff128d7243d3fdc389504522acb88f0464e + languageName: node + linkType: hard + "@babel/plugin-syntax-import-meta@npm:^7.10.4, @babel/plugin-syntax-import-meta@npm:^7.8.3": version: 7.10.4 resolution: "@babel/plugin-syntax-import-meta@npm:7.10.4" @@ -946,25 +1303,25 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-jsx@npm:7.12.1": - version: 7.12.1 - resolution: "@babel/plugin-syntax-jsx@npm:7.12.1" +"@babel/plugin-syntax-jsx@npm:^7.22.5, @babel/plugin-syntax-jsx@npm:^7.7.2": + version: 7.22.5 + resolution: "@babel/plugin-syntax-jsx@npm:7.22.5" dependencies: - "@babel/helper-plugin-utils": ^7.10.4 + "@babel/helper-plugin-utils": ^7.22.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: d4b9b589c484b2e0856799770f060dff34c67b24d7f4526f66309a0e0e9cf388a5c1f2c0da329d1973cc87d1b2cede8f3dc8facfac59e785d6393a003bcdd0f9 + checksum: 8829d30c2617ab31393d99cec2978e41f014f4ac6f01a1cecf4c4dd8320c3ec12fdc3ce121126b2d8d32f6887e99ca1a0bad53dedb1e6ad165640b92b24980ce languageName: node linkType: hard -"@babel/plugin-syntax-jsx@npm:^7.22.5, @babel/plugin-syntax-jsx@npm:^7.7.2": - version: 7.22.5 - resolution: "@babel/plugin-syntax-jsx@npm:7.22.5" +"@babel/plugin-syntax-jsx@npm:^7.23.3, @babel/plugin-syntax-jsx@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-syntax-jsx@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": ^7.22.5 + "@babel/helper-plugin-utils": ^7.24.0 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 8829d30c2617ab31393d99cec2978e41f014f4ac6f01a1cecf4c4dd8320c3ec12fdc3ce121126b2d8d32f6887e99ca1a0bad53dedb1e6ad165640b92b24980ce + checksum: 712f7e7918cb679f106769f57cfab0bc99b311032665c428b98f4c3e2e6d567601d45386a4f246df6a80d741e1f94192b3f008800d66c4f1daae3ad825c243f0 languageName: node linkType: hard @@ -1001,7 +1358,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-object-rest-spread@npm:7.8.3, @babel/plugin-syntax-object-rest-spread@npm:^7.8.0, @babel/plugin-syntax-object-rest-spread@npm:^7.8.3": +"@babel/plugin-syntax-object-rest-spread@npm:^7.8.3": version: 7.8.3 resolution: "@babel/plugin-syntax-object-rest-spread@npm:7.8.3" dependencies: @@ -1067,6 +1424,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-typescript@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-syntax-typescript@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: bf4bd70788d5456b5f75572e47a2e31435c7c4e43609bd4dffd2cc0c7a6cf90aabcf6cd389e351854de9a64412a07d30effef5373251fe8f6a4c9db0c0163bda + languageName: node + linkType: hard + "@babel/plugin-syntax-unicode-sets-regex@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-syntax-unicode-sets-regex@npm:7.18.6" @@ -1090,6 +1458,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-arrow-functions@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-arrow-functions@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 58f9aa9b0de8382f8cfa3f1f1d40b69d98cd2f52340e2391733d0af745fdddda650ba392e509bc056157c880a2f52834a38ab2c5aa5569af8c61bb6ecbf45f34 + languageName: node + linkType: hard + "@babel/plugin-transform-async-generator-functions@npm:^7.22.15": version: 7.22.15 resolution: "@babel/plugin-transform-async-generator-functions@npm:7.22.15" @@ -1104,6 +1483,20 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-async-generator-functions@npm:^7.24.3": + version: 7.24.3 + resolution: "@babel/plugin-transform-async-generator-functions@npm:7.24.3" + dependencies: + "@babel/helper-environment-visitor": ^7.22.20 + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/helper-remap-async-to-generator": ^7.22.20 + "@babel/plugin-syntax-async-generators": ^7.8.4 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 309af02610be65d937664435adb432a32d9b6eb42bb3d3232c377d27fbc57014774d931665a5bfdaff3d1841b72659e0ad7adcef84b709f251cb0b8444f19214 + languageName: node + linkType: hard + "@babel/plugin-transform-async-to-generator@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-async-to-generator@npm:7.22.5" @@ -1117,6 +1510,19 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-async-to-generator@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-async-to-generator@npm:7.24.1" + dependencies: + "@babel/helper-module-imports": ^7.24.1 + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/helper-remap-async-to-generator": ^7.22.20 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 429004a6596aa5c9e707b604156f49a146f8d029e31a3152b1649c0b56425264fda5fd38e5db1ddaeb33c3fe45c97dc8078d7abfafe3542a979b49f229801135 + languageName: node + linkType: hard + "@babel/plugin-transform-block-scoped-functions@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.22.5" @@ -1128,6 +1534,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-block-scoped-functions@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: d8e18bd57b156da1cd4d3c1780ab9ea03afed56c6824ca8e6e74f67959d7989a0e953ec370fe9b417759314f2eef30c8c437395ce63ada2e26c2f469e4704f82 + languageName: node + linkType: hard + "@babel/plugin-transform-block-scoping@npm:^7.22.15": version: 7.22.15 resolution: "@babel/plugin-transform-block-scoping@npm:7.22.15" @@ -1139,6 +1556,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-block-scoping@npm:^7.24.4": + version: 7.24.4 + resolution: "@babel/plugin-transform-block-scoping@npm:7.24.4" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 5229ffe1c55744b96f791521e2876b01ed05c81df67488a7453ce66c2faceb9d1d653089ce6f0abf512752e15e9acac0e75a797a860f24e05b4d36497c7c3183 + languageName: node + linkType: hard + "@babel/plugin-transform-class-properties@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-class-properties@npm:7.22.5" @@ -1151,6 +1579,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-class-properties@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-class-properties@npm:7.24.1" + dependencies: + "@babel/helper-create-class-features-plugin": ^7.24.1 + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 95779e9eef0c0638b9631c297d48aee53ffdbb2b1b5221bf40d7eccd566a8e34f859ff3571f8f20b9159b67f1bff7d7dc81da191c15d69fbae5a645197eae7e0 + languageName: node + linkType: hard + "@babel/plugin-transform-class-static-block@npm:^7.22.11": version: 7.22.11 resolution: "@babel/plugin-transform-class-static-block@npm:7.22.11" @@ -1164,6 +1604,19 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-class-static-block@npm:^7.24.4": + version: 7.24.4 + resolution: "@babel/plugin-transform-class-static-block@npm:7.24.4" + dependencies: + "@babel/helper-create-class-features-plugin": ^7.24.4 + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/plugin-syntax-class-static-block": ^7.14.5 + peerDependencies: + "@babel/core": ^7.12.0 + checksum: 3b1db3308b57ba21d47772a9f183804234c23fd64c9ca40915d2d65c5dc7a48b49a6de16b8b90b7a354eacbb51232a862f0fca3dbd23e27d34641f511decddab + languageName: node + linkType: hard + "@babel/plugin-transform-classes@npm:^7.22.15": version: 7.22.15 resolution: "@babel/plugin-transform-classes@npm:7.22.15" @@ -1183,16 +1636,46 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-computed-properties@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-computed-properties@npm:7.22.5" +"@babel/plugin-transform-classes@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-classes@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - "@babel/template": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: c2a77a0f94ec71efbc569109ec14ea2aa925b333289272ced8b33c6108bdbb02caf01830ffc7e49486b62dec51911924d13f3a76f1149f40daace1898009e131 - languageName: node + "@babel/helper-annotate-as-pure": ^7.22.5 + "@babel/helper-compilation-targets": ^7.23.6 + "@babel/helper-environment-visitor": ^7.22.20 + "@babel/helper-function-name": ^7.23.0 + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/helper-replace-supers": ^7.24.1 + "@babel/helper-split-export-declaration": ^7.22.6 + globals: ^11.1.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: e5337e707d731c9f4dcc107d09c9a99b90786bc0da6a250165919587ed818818f6cae2bbcceea880abef975c0411715c0c7f3f361ecd1526bf2eaca5ad26bb00 + languageName: node + linkType: hard + +"@babel/plugin-transform-computed-properties@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/plugin-transform-computed-properties@npm:7.22.5" + dependencies: + "@babel/helper-plugin-utils": ^7.22.5 + "@babel/template": ^7.22.5 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: c2a77a0f94ec71efbc569109ec14ea2aa925b333289272ced8b33c6108bdbb02caf01830ffc7e49486b62dec51911924d13f3a76f1149f40daace1898009e131 + languageName: node + linkType: hard + +"@babel/plugin-transform-computed-properties@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-computed-properties@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/template": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: f2832bcf100a70f348facbb395873318ef5b9ee4b0fb4104a420d9daaeb6003cc2ecc12fd8083dd2e4a7c2da873272ad73ff94de4497125a0cf473294ef9664e + languageName: node linkType: hard "@babel/plugin-transform-destructuring@npm:^7.22.15": @@ -1206,6 +1689,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-destructuring@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-destructuring@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 994fd3c513e40b8f1bdfdd7104ebdcef7c6a11a4e380086074496f586db3ac04cba0ae70babb820df6363b6700747b0556f6860783e046ace7c741a22f49ec5b + languageName: node + linkType: hard + "@babel/plugin-transform-dotall-regex@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-dotall-regex@npm:7.22.5" @@ -1218,6 +1712,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-dotall-regex@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-dotall-regex@npm:7.24.1" + dependencies: + "@babel/helper-create-regexp-features-plugin": ^7.22.15 + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 7f623d25b6f213b94ebc1754e9e31c1077c8e288626d8b7bfa76a97b067ce80ddcd0ede402a546706c65002c0ccf45cd5ec621511c2668eed31ebcabe8391d35 + languageName: node + linkType: hard + "@babel/plugin-transform-duplicate-keys@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-duplicate-keys@npm:7.22.5" @@ -1229,6 +1735,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-duplicate-keys@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-duplicate-keys@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: a3b07c07cee441e185858a9bb9739bb72643173c18bf5f9f949dd2d4784ca124e56b01d0a270790fb1ff0cf75d436075db0a2b643fb4285ff9a21df9e8dc6284 + languageName: node + linkType: hard + "@babel/plugin-transform-dynamic-import@npm:^7.22.11": version: 7.22.11 resolution: "@babel/plugin-transform-dynamic-import@npm:7.22.11" @@ -1241,6 +1758,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-dynamic-import@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-dynamic-import@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/plugin-syntax-dynamic-import": ^7.8.3 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 59fc561ee40b1a69f969c12c6c5fac206226d6642213985a569dd0f99f8e41c0f4eaedebd36936c255444a8335079842274c42a975a433beadb436d4c5abb79b + languageName: node + linkType: hard + "@babel/plugin-transform-exponentiation-operator@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.22.5" @@ -1253,6 +1782,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-exponentiation-operator@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.24.1" + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor": ^7.22.15 + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: f90841fe1a1e9f680b4209121d3e2992f923e85efcd322b26e5901c180ef44ff727fb89790803a23fac49af34c1ce2e480018027c22b4573b615512ac5b6fc50 + languageName: node + linkType: hard + "@babel/plugin-transform-export-namespace-from@npm:^7.22.11": version: 7.22.11 resolution: "@babel/plugin-transform-export-namespace-from@npm:7.22.11" @@ -1265,6 +1806,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-export-namespace-from@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-export-namespace-from@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/plugin-syntax-export-namespace-from": ^7.8.3 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: bc710ac231919df9555331885748385c11c5e695d7271824fe56fba51dd637d48d3e5cd52e1c69f2b1a384fbbb41552572bc1ca3a2285ee29571f002e9bb2421 + languageName: node + linkType: hard + "@babel/plugin-transform-for-of@npm:^7.22.15": version: 7.22.15 resolution: "@babel/plugin-transform-for-of@npm:7.22.15" @@ -1276,6 +1829,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-for-of@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-for-of@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/helper-skip-transparent-expression-wrappers": ^7.22.5 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 990adde96ea1766ed6008c006c7040127bef59066533bb2977b246ea4a596fe450a528d1881a0db5f894deaf1b81654dfb494b19ad405b369be942738aa9c364 + languageName: node + linkType: hard + "@babel/plugin-transform-function-name@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-function-name@npm:7.22.5" @@ -1289,6 +1854,19 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-function-name@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-function-name@npm:7.24.1" + dependencies: + "@babel/helper-compilation-targets": ^7.23.6 + "@babel/helper-function-name": ^7.23.0 + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 31eb3c75297dda7265f78eba627c446f2324e30ec0124a645ccc3e9f341254aaa40d6787bd62b2280d77c0a5c9fbfce1da2c200ef7c7f8e0a1b16a8eb3644c6f + languageName: node + linkType: hard + "@babel/plugin-transform-json-strings@npm:^7.22.11": version: 7.22.11 resolution: "@babel/plugin-transform-json-strings@npm:7.22.11" @@ -1301,6 +1879,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-json-strings@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-json-strings@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/plugin-syntax-json-strings": ^7.8.3 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: f42302d42fc81ac00d14e9e5d80405eb80477d7f9039d7208e712d6bcd486a4e3b32fdfa07b5f027d6c773723d8168193ee880f93b0e430c828e45f104fb82a4 + languageName: node + linkType: hard + "@babel/plugin-transform-literals@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-literals@npm:7.22.5" @@ -1312,6 +1902,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-literals@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-literals@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 2df94e9478571852483aca7588419e574d76bde97583e78551c286f498e01321e7dbb1d0ef67bee16e8f950688f79688809cfde370c5c4b84c14d841a3ef217a + languageName: node + linkType: hard + "@babel/plugin-transform-logical-assignment-operators@npm:^7.22.11": version: 7.22.11 resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.22.11" @@ -1324,6 +1925,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-logical-assignment-operators@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/plugin-syntax-logical-assignment-operators": ^7.10.4 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 895f2290adf457cbf327428bdb4fb90882a38a22f729bcf0629e8ad66b9b616d2721fbef488ac00411b647489d1dda1d20171bb3772d0796bb7ef5ecf057808a + languageName: node + linkType: hard + "@babel/plugin-transform-member-expression-literals@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-member-expression-literals@npm:7.22.5" @@ -1335,6 +1948,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-member-expression-literals@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-member-expression-literals@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 4ea641cc14a615f9084e45ad2319f95e2fee01c77ec9789685e7e11a6c286238a426a98f9c1ed91568a047d8ac834393e06e8c82d1ff01764b7aa61bee8e9023 + languageName: node + linkType: hard + "@babel/plugin-transform-modules-amd@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-modules-amd@npm:7.22.5" @@ -1347,6 +1971,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-modules-amd@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-modules-amd@npm:7.24.1" + dependencies: + "@babel/helper-module-transforms": ^7.23.3 + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 3d777c262f257e93f0405b13e178f9c4a0f31855b409f0191a76bb562a28c541326a027bfe6467fcb74752f3488c0333b5ff2de64feec1b3c4c6ace1747afa03 + languageName: node + linkType: hard + "@babel/plugin-transform-modules-commonjs@npm:^7.22.15, @babel/plugin-transform-modules-commonjs@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-modules-commonjs@npm:7.23.3" @@ -1360,6 +1996,19 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-modules-commonjs@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.24.1" + dependencies: + "@babel/helper-module-transforms": ^7.23.3 + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/helper-simple-access": ^7.22.5 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 11402b34c49f76aa921b43c2d76f3f129a32544a1dc4f0d1e48b310f9036ab75269a6d8684ed0198b7a0b07bd7898b12f0cacceb26fbb167999fd2a819aa0802 + languageName: node + linkType: hard + "@babel/plugin-transform-modules-systemjs@npm:^7.22.11": version: 7.22.11 resolution: "@babel/plugin-transform-modules-systemjs@npm:7.22.11" @@ -1374,6 +2023,20 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-modules-systemjs@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-modules-systemjs@npm:7.24.1" + dependencies: + "@babel/helper-hoist-variables": ^7.22.5 + "@babel/helper-module-transforms": ^7.23.3 + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/helper-validator-identifier": ^7.22.20 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 903766f6808f04278e887e4adec9b1efa741726279652dad255eaad0f5701df8f8ff0af25eb8541a00eb3c9eae2dccf337b085cfa011426ca33ed1f95d70bf75 + languageName: node + linkType: hard + "@babel/plugin-transform-modules-umd@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-modules-umd@npm:7.22.5" @@ -1386,6 +2049,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-modules-umd@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-modules-umd@npm:7.24.1" + dependencies: + "@babel/helper-module-transforms": ^7.23.3 + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 4922f5056d34de6fd59a1ab1c85bc3472afa706c776aceeb886289c9ac9117e6eb8e22d06c537eb5bc0ede6c30f6bd85210bdcc150dc0ae2d2373f8252df9364 + languageName: node + linkType: hard + "@babel/plugin-transform-named-capturing-groups-regex@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.22.5" @@ -1409,6 +2084,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-new-target@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-new-target@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: f56159ba56e8824840b8073f65073434e4bc4ef20e366bc03aa6cae9a4389365574fa72390e48aed76049edbc6eba1181eb810e58fae22c25946c62f9da13db4 + languageName: node + linkType: hard + "@babel/plugin-transform-nullish-coalescing-operator@npm:^7.22.11": version: 7.22.11 resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.22.11" @@ -1421,6 +2107,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 74025e191ceb7cefc619c15d33753aab81300a03d81b96ae249d9b599bc65878f962d608f452462d3aad5d6e334b7ab2b09a6bdcfe8d101fe77ac7aacca4261e + languageName: node + linkType: hard + "@babel/plugin-transform-numeric-separator@npm:^7.22.11": version: 7.22.11 resolution: "@babel/plugin-transform-numeric-separator@npm:7.22.11" @@ -1433,6 +2131,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-numeric-separator@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-numeric-separator@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/plugin-syntax-numeric-separator": ^7.10.4 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 3247bd7d409574fc06c59e0eb573ae7470d6d61ecf780df40b550102bb4406747d8f39dcbec57eb59406df6c565a86edd3b429e396ad02e4ce201ad92050832e + languageName: node + linkType: hard + "@babel/plugin-transform-object-rest-spread@npm:^7.22.15": version: 7.22.15 resolution: "@babel/plugin-transform-object-rest-spread@npm:7.22.15" @@ -1448,6 +2158,20 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-object-rest-spread@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-object-rest-spread@npm:7.24.1" + dependencies: + "@babel/helper-compilation-targets": ^7.23.6 + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/plugin-syntax-object-rest-spread": ^7.8.3 + "@babel/plugin-transform-parameters": ^7.24.1 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: d5d28b1f33c279a38299d34011421a4915e24b3846aa23a1aba947f1366ce673ddf8df09dd915e0f2c90c5327f798bf126dca013f8adff1fc8f09e18878b675a + languageName: node + linkType: hard + "@babel/plugin-transform-object-super@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-object-super@npm:7.22.5" @@ -1460,6 +2184,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-object-super@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-object-super@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/helper-replace-supers": ^7.24.1 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: d34d437456a54e2a5dcb26e9cf09ed4c55528f2a327c5edca92c93e9483c37176e228d00d6e0cf767f3d6fdbef45ae3a5d034a7c59337a009e20ae541c8220fa + languageName: node + linkType: hard + "@babel/plugin-transform-optional-catch-binding@npm:^7.22.11": version: 7.22.11 resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.22.11" @@ -1472,6 +2208,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-optional-catch-binding@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/plugin-syntax-optional-catch-binding": ^7.8.3 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: ff7c02449d32a6de41e003abb38537b4a1ad90b1eaa4c0b578cb1b55548201a677588a8c47f3e161c72738400ae811a6673ea7b8a734344755016ca0ac445dac + languageName: node + linkType: hard + "@babel/plugin-transform-optional-chaining@npm:^7.22.15": version: 7.22.15 resolution: "@babel/plugin-transform-optional-chaining@npm:7.22.15" @@ -1485,7 +2233,20 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-parameters@npm:^7.12.1, @babel/plugin-transform-parameters@npm:^7.22.15": +"@babel/plugin-transform-optional-chaining@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-optional-chaining@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/helper-skip-transparent-expression-wrappers": ^7.22.5 + "@babel/plugin-syntax-optional-chaining": ^7.8.3 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 0eb5f4abdeb1a101c0f67ef25eba4cce0978a74d8722f6222cdb179a28e60d21ab545eda231855f50169cd63d604ec8268cff44ae9370fd3a499a507c56c2bbd + languageName: node + linkType: hard + +"@babel/plugin-transform-parameters@npm:^7.22.15": version: 7.22.15 resolution: "@babel/plugin-transform-parameters@npm:7.22.15" dependencies: @@ -1496,6 +2257,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-parameters@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-parameters@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: d183008e67b1a13b86c92fb64327a75cd8e13c13eb80d0b6952e15806f1b0c4c456d18360e451c6af73485b2c8f543608b0a29e5126c64eb625a31e970b65f80 + languageName: node + linkType: hard + "@babel/plugin-transform-private-methods@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-private-methods@npm:7.22.5" @@ -1508,6 +2280,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-private-methods@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-private-methods@npm:7.24.1" + dependencies: + "@babel/helper-create-class-features-plugin": ^7.24.1 + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 7208c30bb3f3fbc73fb3a88bdcb78cd5cddaf6d523eb9d67c0c04e78f6fc6319ece89f4a5abc41777ceab16df55b3a13a4120e0efc9275ca6d2d89beaba80aa0 + languageName: node + linkType: hard + "@babel/plugin-transform-private-property-in-object@npm:^7.22.11": version: 7.22.11 resolution: "@babel/plugin-transform-private-property-in-object@npm:7.22.11" @@ -1522,6 +2306,20 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-private-property-in-object@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-private-property-in-object@npm:7.24.1" + dependencies: + "@babel/helper-annotate-as-pure": ^7.22.5 + "@babel/helper-create-class-features-plugin": ^7.24.1 + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/plugin-syntax-private-property-in-object": ^7.14.5 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 47c123ca9975f7f6b20e6fe8fe89f621cd04b622539faf5ec037e2be7c3d53ce2506f7c785b1930dcdea11994eff79094a02715795218c7d6a0bdc11f2fb3ac2 + languageName: node + linkType: hard + "@babel/plugin-transform-property-literals@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-property-literals@npm:7.22.5" @@ -1533,6 +2331,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-property-literals@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-property-literals@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: a73646d7ecd95b3931a3ead82c7d5efeb46e68ba362de63eb437d33531f294ec18bd31b6d24238cd3b6a3b919a6310c4a0ba4a2629927721d4d10b0518eb7715 + languageName: node + linkType: hard + "@babel/plugin-transform-react-constant-elements@npm:^7.12.1, @babel/plugin-transform-react-constant-elements@npm:^7.18.12": version: 7.22.5 resolution: "@babel/plugin-transform-react-constant-elements@npm:7.22.5" @@ -1555,6 +2364,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-react-display-name@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-react-display-name@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: d87ac36073f923a25de0ed3cffac067ec5abc4cde63f7f4366881388fbea6dcbced0e4fefd3b7e99edfe58a4ce32ea4d4c523a577d2b9f0515b872ed02b3d8c3 + languageName: node + linkType: hard + "@babel/plugin-transform-react-jsx-development@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-react-jsx-development@npm:7.22.5" @@ -1581,6 +2401,21 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-react-jsx@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-react-jsx@npm:7.23.4" + dependencies: + "@babel/helper-annotate-as-pure": ^7.22.5 + "@babel/helper-module-imports": ^7.22.15 + "@babel/helper-plugin-utils": ^7.22.5 + "@babel/plugin-syntax-jsx": ^7.23.3 + "@babel/types": ^7.23.4 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: d8b8c52e8e22e833bf77c8d1a53b0a57d1fd52ba9596a319d572de79446a8ed9d95521035bc1175c1589d1a6a34600d2e678fa81d81bac8fac121137097f1f0a + languageName: node + linkType: hard + "@babel/plugin-transform-react-pure-annotations@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-react-pure-annotations@npm:7.22.5" @@ -1593,6 +2428,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-react-pure-annotations@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-react-pure-annotations@npm:7.24.1" + dependencies: + "@babel/helper-annotate-as-pure": ^7.22.5 + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 06a6bfe80f1f36408d07dd80c48cf9f61177c8e5d814e80ddbe88cfad81a8b86b3110e1fe9d1ac943db77e74497daa7f874b5490c788707106ad26ecfbe44813 + languageName: node + linkType: hard + "@babel/plugin-transform-regenerator@npm:^7.22.10": version: 7.22.10 resolution: "@babel/plugin-transform-regenerator@npm:7.22.10" @@ -1605,6 +2452,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-regenerator@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-regenerator@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + regenerator-transform: ^0.15.2 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: a04319388a0a7931c3f8e15715d01444c32519692178b70deccc86d53304e74c0f589a4268f6c68578d86f75e934dd1fe6e6ed9071f54ee8379f356f88ef6e42 + languageName: node + linkType: hard + "@babel/plugin-transform-reserved-words@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-reserved-words@npm:7.22.5" @@ -1616,19 +2475,30 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-runtime@npm:^7.18.6": - version: 7.22.15 - resolution: "@babel/plugin-transform-runtime@npm:7.22.15" +"@babel/plugin-transform-reserved-words@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-reserved-words@npm:7.24.1" dependencies: - "@babel/helper-module-imports": ^7.22.15 - "@babel/helper-plugin-utils": ^7.22.5 - babel-plugin-polyfill-corejs2: ^0.4.5 - babel-plugin-polyfill-corejs3: ^0.8.3 - babel-plugin-polyfill-regenerator: ^0.5.2 + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 132c6040c65aabae2d98a39289efb5c51a8632546dc50d2ad032c8660aec307fbed74ef499856ea4f881fc8505905f49b48e0270585da2ea3d50b75e962afd89 + languageName: node + linkType: hard + +"@babel/plugin-transform-runtime@npm:^7.22.9": + version: 7.24.3 + resolution: "@babel/plugin-transform-runtime@npm:7.24.3" + dependencies: + "@babel/helper-module-imports": ^7.24.3 + "@babel/helper-plugin-utils": ^7.24.0 + babel-plugin-polyfill-corejs2: ^0.4.10 + babel-plugin-polyfill-corejs3: ^0.10.1 + babel-plugin-polyfill-regenerator: ^0.6.1 semver: ^6.3.1 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 7edf20b13d02f856276221624abf3b8084daa3f265a6e5c70ee0d0c63087fcf726dc8756a9c8bb3d25a1ce8697ab66ec8cdd15be992c21aed9971cb5bfe80a5b + checksum: 719112524e6fe3e665385ad4425530dadb2ddee839023381ed9d77edf5ce2748f32cc0e38dacda1990c56a7ae0af4de6cdca2413ffaf307e9f75f8d2200d09a2 languageName: node linkType: hard @@ -1643,8 +2513,19 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-spread@npm:^7.22.5": - version: 7.22.5 +"@babel/plugin-transform-shorthand-properties@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-shorthand-properties@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 006a2032d1c57dca76579ce6598c679c2f20525afef0a36e9d42affe3c8cf33c1427581ad696b519cc75dfee46c5e8ecdf0c6a29ffb14250caa3e16dd68cb424 + languageName: node + linkType: hard + +"@babel/plugin-transform-spread@npm:^7.22.5": + version: 7.22.5 resolution: "@babel/plugin-transform-spread@npm:7.22.5" dependencies: "@babel/helper-plugin-utils": ^7.22.5 @@ -1655,6 +2536,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-spread@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-spread@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/helper-skip-transparent-expression-wrappers": ^7.22.5 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 622ef507e2b5120a9010b25d3df5186c06102ecad8751724a38ec924df8d3527688198fa490c47064eabba14ef2f961b3069855bd22a8c0a1e51a23eed348d02 + languageName: node + linkType: hard + "@babel/plugin-transform-sticky-regex@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-sticky-regex@npm:7.22.5" @@ -1666,6 +2559,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-sticky-regex@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-sticky-regex@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: e326e96a9eeb6bb01dbc4d3362f989411490671b97f62edf378b8fb102c463a018b777f28da65344d41b22aa6efcdfa01ed43d2b11fdcf202046d3174be137c5 + languageName: node + linkType: hard + "@babel/plugin-transform-template-literals@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-template-literals@npm:7.22.5" @@ -1677,6 +2581,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-template-literals@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-template-literals@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 4c9009c72321caf20e3b6328bbe9d7057006c5ae57b794cf247a37ca34d87dfec5e27284169a16df5a6235a083bf0f3ab9e1bfcb005d1c8b75b04aed75652621 + languageName: node + linkType: hard + "@babel/plugin-transform-typeof-symbol@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-typeof-symbol@npm:7.22.5" @@ -1688,6 +2603,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-typeof-symbol@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-typeof-symbol@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 90251c02986aebe50937522a6e404cb83db1b1feda17c0244e97d6429ded1634340c8411536487d14c54495607e1b7c9dc4db4aed969d519f1ff1e363f9c2229 + languageName: node + linkType: hard + "@babel/plugin-transform-typescript@npm:^7.22.15": version: 7.22.15 resolution: "@babel/plugin-transform-typescript@npm:7.22.15" @@ -1702,6 +2628,20 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-typescript@npm:^7.24.1": + version: 7.24.4 + resolution: "@babel/plugin-transform-typescript@npm:7.24.4" + dependencies: + "@babel/helper-annotate-as-pure": ^7.22.5 + "@babel/helper-create-class-features-plugin": ^7.24.4 + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/plugin-syntax-typescript": ^7.24.1 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 57a9a776b1910c706d28972e4b056ced3af8fc59c29b2a6205c2bb2a408141ddb59a8f2f6041f8467a7b260942818767f4ecabb9f63adf7fddf2afa25e774dfc + languageName: node + linkType: hard + "@babel/plugin-transform-unicode-escapes@npm:^7.22.10": version: 7.22.10 resolution: "@babel/plugin-transform-unicode-escapes@npm:7.22.10" @@ -1713,6 +2653,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-unicode-escapes@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-unicode-escapes@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: d4d7cfea91af7be2768fb6bed902e00d6e3190bda738b5149c3a788d570e6cf48b974ec9548442850308ecd8fc9a67681f4ea8403129e7867bcb85adaf6ec238 + languageName: node + linkType: hard + "@babel/plugin-transform-unicode-property-regex@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-unicode-property-regex@npm:7.22.5" @@ -1725,6 +2676,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-unicode-property-regex@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-unicode-property-regex@npm:7.24.1" + dependencies: + "@babel/helper-create-regexp-features-plugin": ^7.22.15 + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 276099b4483e707f80b054e2d29bc519158bfe52461ef5ff76f70727d592df17e30b1597ef4d8a0f04d810f6cb5a8dd887bdc1d0540af3744751710ef280090f + languageName: node + linkType: hard + "@babel/plugin-transform-unicode-regex@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-unicode-regex@npm:7.22.5" @@ -1737,6 +2700,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-unicode-regex@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-unicode-regex@npm:7.24.1" + dependencies: + "@babel/helper-create-regexp-features-plugin": ^7.22.15 + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 400a0927bdb1425b4c0dc68a61b5b2d7d17c7d9f0e07317a1a6a373c080ef94be1dd65fdc4ac9a78fcdb58f89fd128450c7bc0d5b8ca0ae7eca3fbd98e50acba + languageName: node + linkType: hard + "@babel/plugin-transform-unicode-sets-regex@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-unicode-sets-regex@npm:7.22.5" @@ -1749,7 +2724,19 @@ __metadata: languageName: node linkType: hard -"@babel/preset-env@npm:^7.12.1, @babel/preset-env@npm:^7.12.7, @babel/preset-env@npm:^7.18.6, @babel/preset-env@npm:^7.19.4": +"@babel/plugin-transform-unicode-sets-regex@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-unicode-sets-regex@npm:7.24.1" + dependencies: + "@babel/helper-create-regexp-features-plugin": ^7.22.15 + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 364342fb8e382dfaa23628b88e6484dc1097e53fb7199f4d338f1e2cd71d839bb0a35a9b1380074f6a10adb2e98b79d53ca3ec78c0b8c557ca895ffff42180df + languageName: node + linkType: hard + +"@babel/preset-env@npm:^7.12.1, @babel/preset-env@npm:^7.12.7, @babel/preset-env@npm:^7.19.4": version: 7.22.20 resolution: "@babel/preset-env@npm:7.22.20" dependencies: @@ -1839,6 +2826,97 @@ __metadata: languageName: node linkType: hard +"@babel/preset-env@npm:^7.22.9": + version: 7.24.4 + resolution: "@babel/preset-env@npm:7.24.4" + dependencies: + "@babel/compat-data": ^7.24.4 + "@babel/helper-compilation-targets": ^7.23.6 + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/helper-validator-option": ^7.23.5 + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": ^7.24.4 + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ^7.24.1 + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ^7.24.1 + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": ^7.24.1 + "@babel/plugin-proposal-private-property-in-object": 7.21.0-placeholder-for-preset-env.2 + "@babel/plugin-syntax-async-generators": ^7.8.4 + "@babel/plugin-syntax-class-properties": ^7.12.13 + "@babel/plugin-syntax-class-static-block": ^7.14.5 + "@babel/plugin-syntax-dynamic-import": ^7.8.3 + "@babel/plugin-syntax-export-namespace-from": ^7.8.3 + "@babel/plugin-syntax-import-assertions": ^7.24.1 + "@babel/plugin-syntax-import-attributes": ^7.24.1 + "@babel/plugin-syntax-import-meta": ^7.10.4 + "@babel/plugin-syntax-json-strings": ^7.8.3 + "@babel/plugin-syntax-logical-assignment-operators": ^7.10.4 + "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3 + "@babel/plugin-syntax-numeric-separator": ^7.10.4 + "@babel/plugin-syntax-object-rest-spread": ^7.8.3 + "@babel/plugin-syntax-optional-catch-binding": ^7.8.3 + "@babel/plugin-syntax-optional-chaining": ^7.8.3 + "@babel/plugin-syntax-private-property-in-object": ^7.14.5 + "@babel/plugin-syntax-top-level-await": ^7.14.5 + "@babel/plugin-syntax-unicode-sets-regex": ^7.18.6 + "@babel/plugin-transform-arrow-functions": ^7.24.1 + "@babel/plugin-transform-async-generator-functions": ^7.24.3 + "@babel/plugin-transform-async-to-generator": ^7.24.1 + "@babel/plugin-transform-block-scoped-functions": ^7.24.1 + "@babel/plugin-transform-block-scoping": ^7.24.4 + "@babel/plugin-transform-class-properties": ^7.24.1 + "@babel/plugin-transform-class-static-block": ^7.24.4 + "@babel/plugin-transform-classes": ^7.24.1 + "@babel/plugin-transform-computed-properties": ^7.24.1 + "@babel/plugin-transform-destructuring": ^7.24.1 + "@babel/plugin-transform-dotall-regex": ^7.24.1 + "@babel/plugin-transform-duplicate-keys": ^7.24.1 + "@babel/plugin-transform-dynamic-import": ^7.24.1 + "@babel/plugin-transform-exponentiation-operator": ^7.24.1 + "@babel/plugin-transform-export-namespace-from": ^7.24.1 + "@babel/plugin-transform-for-of": ^7.24.1 + "@babel/plugin-transform-function-name": ^7.24.1 + "@babel/plugin-transform-json-strings": ^7.24.1 + "@babel/plugin-transform-literals": ^7.24.1 + "@babel/plugin-transform-logical-assignment-operators": ^7.24.1 + "@babel/plugin-transform-member-expression-literals": ^7.24.1 + "@babel/plugin-transform-modules-amd": ^7.24.1 + "@babel/plugin-transform-modules-commonjs": ^7.24.1 + "@babel/plugin-transform-modules-systemjs": ^7.24.1 + "@babel/plugin-transform-modules-umd": ^7.24.1 + "@babel/plugin-transform-named-capturing-groups-regex": ^7.22.5 + "@babel/plugin-transform-new-target": ^7.24.1 + "@babel/plugin-transform-nullish-coalescing-operator": ^7.24.1 + "@babel/plugin-transform-numeric-separator": ^7.24.1 + "@babel/plugin-transform-object-rest-spread": ^7.24.1 + "@babel/plugin-transform-object-super": ^7.24.1 + "@babel/plugin-transform-optional-catch-binding": ^7.24.1 + "@babel/plugin-transform-optional-chaining": ^7.24.1 + "@babel/plugin-transform-parameters": ^7.24.1 + "@babel/plugin-transform-private-methods": ^7.24.1 + "@babel/plugin-transform-private-property-in-object": ^7.24.1 + "@babel/plugin-transform-property-literals": ^7.24.1 + "@babel/plugin-transform-regenerator": ^7.24.1 + "@babel/plugin-transform-reserved-words": ^7.24.1 + "@babel/plugin-transform-shorthand-properties": ^7.24.1 + "@babel/plugin-transform-spread": ^7.24.1 + "@babel/plugin-transform-sticky-regex": ^7.24.1 + "@babel/plugin-transform-template-literals": ^7.24.1 + "@babel/plugin-transform-typeof-symbol": ^7.24.1 + "@babel/plugin-transform-unicode-escapes": ^7.24.1 + "@babel/plugin-transform-unicode-property-regex": ^7.24.1 + "@babel/plugin-transform-unicode-regex": ^7.24.1 + "@babel/plugin-transform-unicode-sets-regex": ^7.24.1 + "@babel/preset-modules": 0.1.6-no-external-plugins + babel-plugin-polyfill-corejs2: ^0.4.10 + babel-plugin-polyfill-corejs3: ^0.10.4 + babel-plugin-polyfill-regenerator: ^0.6.1 + core-js-compat: ^3.31.0 + semver: ^6.3.1 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 5a057a6463f92b02bfe66257d3f2c76878815bc7847f2a716b0539d9f547eae2a9d1f0fc62a5c0eff6ab0504bb52e815829ef893e4586b641f8dd6a609d114f3 + languageName: node + linkType: hard + "@babel/preset-modules@npm:0.1.6-no-external-plugins": version: 0.1.6-no-external-plugins resolution: "@babel/preset-modules@npm:0.1.6-no-external-plugins" @@ -1868,6 +2946,22 @@ __metadata: languageName: node linkType: hard +"@babel/preset-react@npm:^7.22.5": + version: 7.24.1 + resolution: "@babel/preset-react@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/helper-validator-option": ^7.23.5 + "@babel/plugin-transform-react-display-name": ^7.24.1 + "@babel/plugin-transform-react-jsx": ^7.23.4 + "@babel/plugin-transform-react-jsx-development": ^7.22.5 + "@babel/plugin-transform-react-pure-annotations": ^7.24.1 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 70e146a6de480cb4b6c5eb197003960a2d148d513e1f5b5d04ee954f255d68c935c2800da13e550267f47b894bd0214b2548181467b52a4bdc0a85020061b68c + languageName: node + linkType: hard + "@babel/preset-typescript@npm:^7.18.6": version: 7.22.15 resolution: "@babel/preset-typescript@npm:7.22.15" @@ -1883,6 +2977,21 @@ __metadata: languageName: node linkType: hard +"@babel/preset-typescript@npm:^7.22.5": + version: 7.24.1 + resolution: "@babel/preset-typescript@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/helper-validator-option": ^7.23.5 + "@babel/plugin-syntax-jsx": ^7.24.1 + "@babel/plugin-transform-modules-commonjs": ^7.24.1 + "@babel/plugin-transform-typescript": ^7.24.1 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: f3e0ff8c20dd5abc82614df2d7953f1549a98282b60809478f7dfb41c29be63720f2d1d7a51ef1f0d939b65e8666cb7d36e32bc4f8ac2b74c20664efd41e8bdd + languageName: node + linkType: hard + "@babel/regjsgen@npm:^0.8.0": version: 0.8.0 resolution: "@babel/regjsgen@npm:0.8.0" @@ -1890,17 +2999,17 @@ __metadata: languageName: node linkType: hard -"@babel/runtime-corejs3@npm:^7.18.6": - version: 7.22.15 - resolution: "@babel/runtime-corejs3@npm:7.22.15" +"@babel/runtime-corejs3@npm:^7.22.6": + version: 7.24.4 + resolution: "@babel/runtime-corejs3@npm:7.24.4" dependencies: core-js-pure: ^3.30.2 regenerator-runtime: ^0.14.0 - checksum: 6e27ca3890282612316aa87a9cd60fc19888ac26c802af926b4488ad67b3b06929086884974e9237fa550f48a5fe22c9c5e5be229bba9c4c017cfb2374835518 + checksum: 0c2e7c477de3dbf5cc6f2434cee3d78a34d87e8f1e2ea65840eb948d00f7d6968e0ef055449adf372a39d6214f8b9b2532506149b9d0e7ea3d09b1b84678ae6c languageName: node linkType: hard -"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.3, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.8.4": +"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.3, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.8.4": version: 7.22.15 resolution: "@babel/runtime@npm:7.22.15" dependencies: @@ -1909,6 +3018,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.22.6": + version: 7.24.4 + resolution: "@babel/runtime@npm:7.24.4" + dependencies: + regenerator-runtime: ^0.14.0 + checksum: 2f27d4c0ffac7ae7999ac0385e1106f2a06992a8bdcbf3da06adcac7413863cd08c198c2e4e970041bbea849e17f02e1df18875539b6afba76c781b6b59a07c3 + languageName: node + linkType: hard + "@babel/template@npm:^7.12.7, @babel/template@npm:^7.22.15, @babel/template@npm:^7.22.5, @babel/template@npm:^7.23.9, @babel/template@npm:^7.3.3": version: 7.23.9 resolution: "@babel/template@npm:7.23.9" @@ -1920,7 +3038,18 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.12.9, @babel/traverse@npm:^7.18.8, @babel/traverse@npm:^7.23.9": +"@babel/template@npm:^7.24.0": + version: 7.24.0 + resolution: "@babel/template@npm:7.24.0" + dependencies: + "@babel/code-frame": ^7.23.5 + "@babel/parser": ^7.24.0 + "@babel/types": ^7.24.0 + checksum: f257b003c071a0cecdbfceca74185f18fe62c055469ab5c1d481aab12abeebed328e67e0a19fd978a2a8de97b28953fa4bc3da6d038a7345fdf37923b9fcdec8 + languageName: node + linkType: hard + +"@babel/traverse@npm:^7.12.9, @babel/traverse@npm:^7.23.9": version: 7.23.9 resolution: "@babel/traverse@npm:7.23.9" dependencies: @@ -1938,6 +3067,24 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.22.8, @babel/traverse@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/traverse@npm:7.24.1" + dependencies: + "@babel/code-frame": ^7.24.1 + "@babel/generator": ^7.24.1 + "@babel/helper-environment-visitor": ^7.22.20 + "@babel/helper-function-name": ^7.23.0 + "@babel/helper-hoist-variables": ^7.22.5 + "@babel/helper-split-export-declaration": ^7.22.6 + "@babel/parser": ^7.24.1 + "@babel/types": ^7.24.0 + debug: ^4.3.1 + globals: ^11.1.0 + checksum: 92a5ca906abfba9df17666d2001ab23f18600035f706a687055a0e392a690ae48d6fec67c8bd4ef19ba18699a77a5b7f85727e36b83f7d110141608fe0c24fe9 + languageName: node + linkType: hard + "@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.6, @babel/types@npm:^7.12.7, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.6, @babel/types@npm:^7.23.9, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": version: 7.23.9 resolution: "@babel/types@npm:7.23.9" @@ -1949,6 +3096,17 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.23.4, @babel/types@npm:^7.24.0": + version: 7.24.0 + resolution: "@babel/types@npm:7.24.0" + dependencies: + "@babel/helper-string-parser": ^7.23.4 + "@babel/helper-validator-identifier": ^7.22.20 + to-fast-properties: ^2.0.0 + checksum: 4b574a37d490f621470ff36a5afaac6deca5546edcb9b5e316d39acbb20998e9c2be42f3fc0bf2b55906fc49ff2a5a6a097e8f5a726ee3f708a0b0ca93aed807 + languageName: node + linkType: hard + "@bcoe/v8-coverage@npm:^0.2.3": version: 0.2.3 resolution: "@bcoe/v8-coverage@npm:0.2.3" @@ -1997,20 +3155,20 @@ __metadata: languageName: node linkType: hard -"@docsearch/css@npm:3.5.2": - version: 3.5.2 - resolution: "@docsearch/css@npm:3.5.2" - checksum: d1d60dd230dd48f896755f21bd20b59583ba844212d7d336953ae48d389baaf868bdf83320fb734a4ed679c3f95b15d620cf3764cd538f6941cae239f8c9d35d +"@docsearch/css@npm:3.6.0": + version: 3.6.0 + resolution: "@docsearch/css@npm:3.6.0" + checksum: 6fa5d7a386f56dc90a2e060e3e368e075356709dd412df2a03bb7b4041c5c6dcf379078163c16d022c2a27fdd4c75596c33485d1bd6b37ad6fbac80f51704af1 languageName: node linkType: hard -"@docsearch/react@npm:^3.1.1": - version: 3.5.2 - resolution: "@docsearch/react@npm:3.5.2" +"@docsearch/react@npm:^3.5.2": + version: 3.6.0 + resolution: "@docsearch/react@npm:3.6.0" dependencies: "@algolia/autocomplete-core": 1.9.3 "@algolia/autocomplete-preset-algolia": 1.9.3 - "@docsearch/css": 3.5.2 + "@docsearch/css": 3.6.0 algoliasearch: ^4.19.1 peerDependencies: "@types/react": ">= 16.8.0 < 19.0.0" @@ -2026,150 +3184,156 @@ __metadata: optional: true search-insights: optional: true - checksum: 4b4584c2c73fc18cbd599047538896450974e134c2c74f19eb202db0ce8e6c3c49c6f65ed6ade61c796d476d3cbb55d6be58df62bc9568a0c72d88e42fca1d16 + checksum: 1025c6072661eb4427ffe561d9f6f4a8ca08b509a8e1bb64ff92eccad544d0dc1705c9cddbea74f9672e1d960dc3c94b76cfa8a8665346128aea2e19a3745a55 languageName: node linkType: hard -"@docusaurus/core@npm:2.4.3": - version: 2.4.3 - resolution: "@docusaurus/core@npm:2.4.3" +"@docusaurus/core@npm:3.2.1": + version: 3.2.1 + resolution: "@docusaurus/core@npm:3.2.1" dependencies: - "@babel/core": ^7.18.6 - "@babel/generator": ^7.18.7 + "@babel/core": ^7.23.3 + "@babel/generator": ^7.23.3 "@babel/plugin-syntax-dynamic-import": ^7.8.3 - "@babel/plugin-transform-runtime": ^7.18.6 - "@babel/preset-env": ^7.18.6 - "@babel/preset-react": ^7.18.6 - "@babel/preset-typescript": ^7.18.6 - "@babel/runtime": ^7.18.6 - "@babel/runtime-corejs3": ^7.18.6 - "@babel/traverse": ^7.18.8 - "@docusaurus/cssnano-preset": 2.4.3 - "@docusaurus/logger": 2.4.3 - "@docusaurus/mdx-loader": 2.4.3 + "@babel/plugin-transform-runtime": ^7.22.9 + "@babel/preset-env": ^7.22.9 + "@babel/preset-react": ^7.22.5 + "@babel/preset-typescript": ^7.22.5 + "@babel/runtime": ^7.22.6 + "@babel/runtime-corejs3": ^7.22.6 + "@babel/traverse": ^7.22.8 + "@docusaurus/cssnano-preset": 3.2.1 + "@docusaurus/logger": 3.2.1 + "@docusaurus/mdx-loader": 3.2.1 "@docusaurus/react-loadable": 5.5.2 - "@docusaurus/utils": 2.4.3 - "@docusaurus/utils-common": 2.4.3 - "@docusaurus/utils-validation": 2.4.3 - "@slorber/static-site-generator-webpack-plugin": ^4.0.7 - "@svgr/webpack": ^6.2.1 - autoprefixer: ^10.4.7 - babel-loader: ^8.2.5 + "@docusaurus/utils": 3.2.1 + "@docusaurus/utils-common": 3.2.1 + "@docusaurus/utils-validation": 3.2.1 + "@svgr/webpack": ^6.5.1 + autoprefixer: ^10.4.14 + babel-loader: ^9.1.3 babel-plugin-dynamic-import-node: ^2.3.3 boxen: ^6.2.1 chalk: ^4.1.2 chokidar: ^3.5.3 - clean-css: ^5.3.0 - cli-table3: ^0.6.2 + clean-css: ^5.3.2 + cli-table3: ^0.6.3 combine-promises: ^1.1.0 commander: ^5.1.0 copy-webpack-plugin: ^11.0.0 - core-js: ^3.23.3 - css-loader: ^6.7.1 - css-minimizer-webpack-plugin: ^4.0.0 - cssnano: ^5.1.12 + core-js: ^3.31.1 + css-loader: ^6.8.1 + css-minimizer-webpack-plugin: ^4.2.2 + cssnano: ^5.1.15 del: ^6.1.1 - detect-port: ^1.3.0 + detect-port: ^1.5.1 escape-html: ^1.0.3 - eta: ^2.0.0 + eta: ^2.2.0 + eval: ^0.1.8 file-loader: ^6.2.0 - fs-extra: ^10.1.0 - html-minifier-terser: ^6.1.0 - html-tags: ^3.2.0 - html-webpack-plugin: ^5.5.0 - import-fresh: ^3.3.0 + fs-extra: ^11.1.1 + html-minifier-terser: ^7.2.0 + html-tags: ^3.3.1 + html-webpack-plugin: ^5.5.3 leven: ^3.1.0 lodash: ^4.17.21 - mini-css-extract-plugin: ^2.6.1 - postcss: ^8.4.14 - postcss-loader: ^7.0.0 + mini-css-extract-plugin: ^2.7.6 + p-map: ^4.0.0 + postcss: ^8.4.26 + postcss-loader: ^7.3.3 prompts: ^2.4.2 react-dev-utils: ^12.0.1 react-helmet-async: ^1.3.0 react-loadable: "npm:@docusaurus/react-loadable@5.5.2" react-loadable-ssr-addon-v5-slorber: ^1.0.1 - react-router: ^5.3.3 + react-router: ^5.3.4 react-router-config: ^5.1.1 - react-router-dom: ^5.3.3 + react-router-dom: ^5.3.4 rtl-detect: ^1.0.4 - semver: ^7.3.7 - serve-handler: ^6.1.3 + semver: ^7.5.4 + serve-handler: ^6.1.5 shelljs: ^0.8.5 - terser-webpack-plugin: ^5.3.3 - tslib: ^2.4.0 - update-notifier: ^5.1.0 + terser-webpack-plugin: ^5.3.9 + tslib: ^2.6.0 + update-notifier: ^6.0.2 url-loader: ^4.1.1 - wait-on: ^6.0.1 - webpack: ^5.73.0 - webpack-bundle-analyzer: ^4.5.0 - webpack-dev-server: ^4.9.3 - webpack-merge: ^5.8.0 + webpack: ^5.88.1 + webpack-bundle-analyzer: ^4.9.0 + webpack-dev-server: ^4.15.1 + webpack-merge: ^5.9.0 webpackbar: ^5.0.2 peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 + react: ^18.0.0 + react-dom: ^18.0.0 bin: docusaurus: bin/docusaurus.mjs - checksum: cce7173ee131364857c16f70f94155ba0e1b044cde54045fb0cf62ad138f8d8ef093f5aba7c7617a9aa0545b3ee3930aec2e09f645daec015696968338963013 + checksum: 9267f08b41240cb9d399abbd8a41ff66e0082551284325db3f17fcce9643bef81d06564797a7cc4c528fe8bde2858c20666e74a0308f3ecc80f3be1dbee14bb5 languageName: node linkType: hard -"@docusaurus/cssnano-preset@npm:2.4.3": - version: 2.4.3 - resolution: "@docusaurus/cssnano-preset@npm:2.4.3" +"@docusaurus/cssnano-preset@npm:3.2.1": + version: 3.2.1 + resolution: "@docusaurus/cssnano-preset@npm:3.2.1" dependencies: - cssnano-preset-advanced: ^5.3.8 - postcss: ^8.4.14 - postcss-sort-media-queries: ^4.2.1 - tslib: ^2.4.0 - checksum: f4a4c60b075c23541da90e00ae26af2e7eaadf20d783b37b9110a5e34599e4e91947425e33bad58ba71abee81c85cca99f5d7d76575f53fbaf73617b55e39c62 + cssnano-preset-advanced: ^5.3.10 + postcss: ^8.4.26 + postcss-sort-media-queries: ^4.4.1 + tslib: ^2.6.0 + checksum: ee23a1229d23732d936fe1d68732d1305abf0132b43a398336fee500504a3e7566d3b0c6222f89f565e24e68e00e353765e0cbbab5611a3b35ecf88305558b6d languageName: node linkType: hard -"@docusaurus/logger@npm:2.4.3": - version: 2.4.3 - resolution: "@docusaurus/logger@npm:2.4.3" +"@docusaurus/logger@npm:3.2.1": + version: 3.2.1 + resolution: "@docusaurus/logger@npm:3.2.1" dependencies: chalk: ^4.1.2 - tslib: ^2.4.0 - checksum: f026a8233aa317f16ce5b25c6785a431f319c52fc07a1b9e26f4b3df2197974e75830a16b6140314f8f4ef02dc19242106ec2ae1599740b26d516cc34c56102f + tslib: ^2.6.0 + checksum: 9d5db5253eda98871563faddb5318bcb6b17ddf5882ababad4803d526917844819751e84ee8028e794fd5507646db6409f9041fd7f41b7f7971015df11cc6376 languageName: node linkType: hard -"@docusaurus/mdx-loader@npm:2.4.3": - version: 2.4.3 - resolution: "@docusaurus/mdx-loader@npm:2.4.3" +"@docusaurus/mdx-loader@npm:3.2.1": + version: 3.2.1 + resolution: "@docusaurus/mdx-loader@npm:3.2.1" dependencies: - "@babel/parser": ^7.18.8 - "@babel/traverse": ^7.18.8 - "@docusaurus/logger": 2.4.3 - "@docusaurus/utils": 2.4.3 - "@mdx-js/mdx": ^1.6.22 + "@docusaurus/logger": 3.2.1 + "@docusaurus/utils": 3.2.1 + "@docusaurus/utils-validation": 3.2.1 + "@mdx-js/mdx": ^3.0.0 + "@slorber/remark-comment": ^1.0.0 escape-html: ^1.0.3 + estree-util-value-to-estree: ^3.0.1 file-loader: ^6.2.0 - fs-extra: ^10.1.0 - image-size: ^1.0.1 - mdast-util-to-string: ^2.0.0 - remark-emoji: ^2.2.0 + fs-extra: ^11.1.1 + image-size: ^1.0.2 + mdast-util-mdx: ^3.0.0 + mdast-util-to-string: ^4.0.0 + rehype-raw: ^7.0.0 + remark-directive: ^3.0.0 + remark-emoji: ^4.0.0 + remark-frontmatter: ^5.0.0 + remark-gfm: ^4.0.0 stringify-object: ^3.3.0 - tslib: ^2.4.0 - unified: ^9.2.2 - unist-util-visit: ^2.0.3 + tslib: ^2.6.0 + unified: ^11.0.3 + unist-util-visit: ^5.0.0 url-loader: ^4.1.1 - webpack: ^5.73.0 + vfile: ^6.0.1 + webpack: ^5.88.1 peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - checksum: 5a774f7ea5f484e888b2bd1bf8b182279e3788afec779eb8920cf468b92ab8d83a1ae8be51925074241a4d1a38d989cfb366d2baf0f67ed6f063342395a7ca8e + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: 4609faf2d8b76085a3aa86ac5ca4ac3b3420e3cfd796f1b39c46f368c82b3db0db5b1308646cf35fdad0a1f6f088d367116eb0e2a8c3fa728ed886ee37516476 languageName: node linkType: hard -"@docusaurus/module-type-aliases@npm:2.4.3": - version: 2.4.3 - resolution: "@docusaurus/module-type-aliases@npm:2.4.3" +"@docusaurus/module-type-aliases@npm:3.2.1": + version: 3.2.1 + resolution: "@docusaurus/module-type-aliases@npm:3.2.1" dependencies: "@docusaurus/react-loadable": 5.5.2 - "@docusaurus/types": 2.4.3 + "@docusaurus/types": 3.2.1 "@types/history": ^4.7.11 "@types/react": "*" "@types/react-router-config": "*" @@ -2179,186 +3343,188 @@ __metadata: peerDependencies: react: "*" react-dom: "*" - checksum: 22ce1a6a20acc35cdd2ec57e55f29e65dbe0fb3a46aaa8c033ec78bf04cd3087f0523c816c744ed311095512dd686c83e0a8619cc1a2a937c27cd54527739c38 + checksum: 37b4a40f9afebbe76e350c10c857737b544c141a988462436904ae16993a52e4429018d406e2f55ad57a533e5a108dd7cdb903434abb84721deeec0d5f195d80 languageName: node linkType: hard -"@docusaurus/plugin-content-blog@npm:2.4.3": - version: 2.4.3 - resolution: "@docusaurus/plugin-content-blog@npm:2.4.3" - dependencies: - "@docusaurus/core": 2.4.3 - "@docusaurus/logger": 2.4.3 - "@docusaurus/mdx-loader": 2.4.3 - "@docusaurus/types": 2.4.3 - "@docusaurus/utils": 2.4.3 - "@docusaurus/utils-common": 2.4.3 - "@docusaurus/utils-validation": 2.4.3 +"@docusaurus/plugin-content-blog@npm:3.2.1": + version: 3.2.1 + resolution: "@docusaurus/plugin-content-blog@npm:3.2.1" + dependencies: + "@docusaurus/core": 3.2.1 + "@docusaurus/logger": 3.2.1 + "@docusaurus/mdx-loader": 3.2.1 + "@docusaurus/types": 3.2.1 + "@docusaurus/utils": 3.2.1 + "@docusaurus/utils-common": 3.2.1 + "@docusaurus/utils-validation": 3.2.1 cheerio: ^1.0.0-rc.12 feed: ^4.2.2 - fs-extra: ^10.1.0 + fs-extra: ^11.1.1 lodash: ^4.17.21 reading-time: ^1.5.0 - tslib: ^2.4.0 - unist-util-visit: ^2.0.3 + srcset: ^4.0.0 + tslib: ^2.6.0 + unist-util-visit: ^5.0.0 utility-types: ^3.10.0 - webpack: ^5.73.0 + webpack: ^5.88.1 peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - checksum: 9fd41331c609b9488eea363e617e3763a814c75f83eb1b858cef402a0f5b96f67a342e25ff8c333489e550eb4d379eae09a88b986a97c25170fe203662e2f1ae + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: d95147a28aad832cd2dc39af634e1902a8a36f958dd2ff5fa6eaa47b574b58df42609a64da823951826f647337ad35c1f1c8be8a0a085913e192936f38839413 languageName: node linkType: hard -"@docusaurus/plugin-content-docs@npm:2.4.3": - version: 2.4.3 - resolution: "@docusaurus/plugin-content-docs@npm:2.4.3" - dependencies: - "@docusaurus/core": 2.4.3 - "@docusaurus/logger": 2.4.3 - "@docusaurus/mdx-loader": 2.4.3 - "@docusaurus/module-type-aliases": 2.4.3 - "@docusaurus/types": 2.4.3 - "@docusaurus/utils": 2.4.3 - "@docusaurus/utils-validation": 2.4.3 - "@types/react-router-config": ^5.0.6 +"@docusaurus/plugin-content-docs@npm:3.2.1": + version: 3.2.1 + resolution: "@docusaurus/plugin-content-docs@npm:3.2.1" + dependencies: + "@docusaurus/core": 3.2.1 + "@docusaurus/logger": 3.2.1 + "@docusaurus/mdx-loader": 3.2.1 + "@docusaurus/module-type-aliases": 3.2.1 + "@docusaurus/types": 3.2.1 + "@docusaurus/utils": 3.2.1 + "@docusaurus/utils-common": 3.2.1 + "@docusaurus/utils-validation": 3.2.1 + "@types/react-router-config": ^5.0.7 combine-promises: ^1.1.0 - fs-extra: ^10.1.0 - import-fresh: ^3.3.0 + fs-extra: ^11.1.1 js-yaml: ^4.1.0 lodash: ^4.17.21 - tslib: ^2.4.0 + tslib: ^2.6.0 utility-types: ^3.10.0 - webpack: ^5.73.0 + webpack: ^5.88.1 peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - checksum: bc01201f64721131eb84f264e51c7497b8034d2a3d99d762169f5dc456c3d8882acfa01fdbaa8fdc6e2e220479b36e0c9e8e17397bf887884589535bdeaeb4bb + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: c182466c3ff513b36a8975a3899b07ffc4b227ab45ef69eacc0a77119d6f0cd6a0727a3e886cfcf4a56e4f522f64e1e6a2647ddc57eb8493b93c03240b1d9b39 languageName: node linkType: hard -"@docusaurus/plugin-content-pages@npm:2.4.3": - version: 2.4.3 - resolution: "@docusaurus/plugin-content-pages@npm:2.4.3" - dependencies: - "@docusaurus/core": 2.4.3 - "@docusaurus/mdx-loader": 2.4.3 - "@docusaurus/types": 2.4.3 - "@docusaurus/utils": 2.4.3 - "@docusaurus/utils-validation": 2.4.3 - fs-extra: ^10.1.0 - tslib: ^2.4.0 - webpack: ^5.73.0 +"@docusaurus/plugin-content-pages@npm:3.2.1": + version: 3.2.1 + resolution: "@docusaurus/plugin-content-pages@npm:3.2.1" + dependencies: + "@docusaurus/core": 3.2.1 + "@docusaurus/mdx-loader": 3.2.1 + "@docusaurus/types": 3.2.1 + "@docusaurus/utils": 3.2.1 + "@docusaurus/utils-validation": 3.2.1 + fs-extra: ^11.1.1 + tslib: ^2.6.0 + webpack: ^5.88.1 peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - checksum: 00439c2e1a1f345cd549739db13a3610b6d9f7ffa6cf7507ad6ac1f3c8d24041947acc2a446be7edf1a613cf354a50d1133aa28ddf64a0eff6ed8a31bf1a542f + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: 3cce99f8aa863b97cbb54a50b448073222a0678528b09f5bec2196e73ec4740f412f8675ed05d283ff672756a5d3005f7a1e4d8c8f882cd0d6d5691cbccb604c languageName: node linkType: hard -"@docusaurus/plugin-debug@npm:2.4.3": - version: 2.4.3 - resolution: "@docusaurus/plugin-debug@npm:2.4.3" +"@docusaurus/plugin-debug@npm:3.2.1": + version: 3.2.1 + resolution: "@docusaurus/plugin-debug@npm:3.2.1" dependencies: - "@docusaurus/core": 2.4.3 - "@docusaurus/types": 2.4.3 - "@docusaurus/utils": 2.4.3 - fs-extra: ^10.1.0 - react-json-view: ^1.21.3 - tslib: ^2.4.0 + "@docusaurus/core": 3.2.1 + "@docusaurus/types": 3.2.1 + "@docusaurus/utils": 3.2.1 + fs-extra: ^11.1.1 + react-json-view-lite: ^1.2.0 + tslib: ^2.6.0 peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - checksum: 88955828b72e463e04501cc6bedf802208e377ae0f4d72735625bcbb47918afc4f2588355c6914064cfdbe4945d3da6473ce76319aa1f66dd975b3b43c4c39b0 + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: b3fb1c8935463afb97f233042692c247d4147c03e18ef9fb37fbf0c46d4adaefa4af0d5c357025992dadfe7b83a9fd3754946b8947bfb8b9535dca390a3668d0 languageName: node linkType: hard -"@docusaurus/plugin-google-analytics@npm:2.4.3": - version: 2.4.3 - resolution: "@docusaurus/plugin-google-analytics@npm:2.4.3" +"@docusaurus/plugin-google-analytics@npm:3.2.1": + version: 3.2.1 + resolution: "@docusaurus/plugin-google-analytics@npm:3.2.1" dependencies: - "@docusaurus/core": 2.4.3 - "@docusaurus/types": 2.4.3 - "@docusaurus/utils-validation": 2.4.3 - tslib: ^2.4.0 + "@docusaurus/core": 3.2.1 + "@docusaurus/types": 3.2.1 + "@docusaurus/utils-validation": 3.2.1 + tslib: ^2.6.0 peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - checksum: 6e30de6b5c479493614a5552a295f07ffb9c83f3740a68c7d4dbac378b8288da7430f26cdc246d763855c6084ad86a6f87286e6c8b40f4817794bb1a04e109ea + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: e1e881fd6adbe408029257d526759b9217f7d70e5e068c7e9241a5f0c3050b0fa46acfeb4f8a23c3f36e1739d0a3d810642d69c6648ff6801ce13b646e44e6c1 languageName: node linkType: hard -"@docusaurus/plugin-google-gtag@npm:2.4.3": - version: 2.4.3 - resolution: "@docusaurus/plugin-google-gtag@npm:2.4.3" +"@docusaurus/plugin-google-gtag@npm:3.2.1": + version: 3.2.1 + resolution: "@docusaurus/plugin-google-gtag@npm:3.2.1" dependencies: - "@docusaurus/core": 2.4.3 - "@docusaurus/types": 2.4.3 - "@docusaurus/utils-validation": 2.4.3 - tslib: ^2.4.0 + "@docusaurus/core": 3.2.1 + "@docusaurus/types": 3.2.1 + "@docusaurus/utils-validation": 3.2.1 + "@types/gtag.js": ^0.0.12 + tslib: ^2.6.0 peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - checksum: 4aaac4d262b3bb7fc3f16620c5329b90db92bf28361ced54f2945fc0e4669483e2f36b076332e0ee9d11b6233cd2c81ca35c953119bad42171e62571c1692d6a + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: b7758289d8453e98baf95d41e754c1e4c8fd5b1c000ba444c4bdf13fc97674a3cddf3215b6406266729e23898641b5bae297c5422c5bd079ef04773fa5a15c1b languageName: node linkType: hard -"@docusaurus/plugin-google-tag-manager@npm:2.4.3": - version: 2.4.3 - resolution: "@docusaurus/plugin-google-tag-manager@npm:2.4.3" +"@docusaurus/plugin-google-tag-manager@npm:3.2.1": + version: 3.2.1 + resolution: "@docusaurus/plugin-google-tag-manager@npm:3.2.1" dependencies: - "@docusaurus/core": 2.4.3 - "@docusaurus/types": 2.4.3 - "@docusaurus/utils-validation": 2.4.3 - tslib: ^2.4.0 + "@docusaurus/core": 3.2.1 + "@docusaurus/types": 3.2.1 + "@docusaurus/utils-validation": 3.2.1 + tslib: ^2.6.0 peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - checksum: c3af89b4d41fab463d853cbfbe8f43d384f702dd09fd914fffcca01fdf94c282d1b98d762c9142fe21f6471f5dd643679e8d11344c95fdf6657aff0618c3c7a5 + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: 82355aed046b12ce0fead68339e24a3c6f2f517bc2b80c9c26c502cc49d86c1b6d0f797d5269d1d5e73ac78fd748c8a2f4528f7f3feee1137ae8e73876426426 languageName: node linkType: hard -"@docusaurus/plugin-sitemap@npm:2.4.3": - version: 2.4.3 - resolution: "@docusaurus/plugin-sitemap@npm:2.4.3" - dependencies: - "@docusaurus/core": 2.4.3 - "@docusaurus/logger": 2.4.3 - "@docusaurus/types": 2.4.3 - "@docusaurus/utils": 2.4.3 - "@docusaurus/utils-common": 2.4.3 - "@docusaurus/utils-validation": 2.4.3 - fs-extra: ^10.1.0 +"@docusaurus/plugin-sitemap@npm:3.2.1": + version: 3.2.1 + resolution: "@docusaurus/plugin-sitemap@npm:3.2.1" + dependencies: + "@docusaurus/core": 3.2.1 + "@docusaurus/logger": 3.2.1 + "@docusaurus/types": 3.2.1 + "@docusaurus/utils": 3.2.1 + "@docusaurus/utils-common": 3.2.1 + "@docusaurus/utils-validation": 3.2.1 + fs-extra: ^11.1.1 sitemap: ^7.1.1 - tslib: ^2.4.0 + tslib: ^2.6.0 peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - checksum: cf96b9f0e32cefa58e37a4bc2f0a112ea657f06faf47b780ec2ba39d5e2daca6486a73f3b376c56ad3bb42f3f0c3f70a783f1ce1964b74e2ba273e6f439e439b + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: b2e4c4fddd0fbdd4a6a4c93a0f9c16b1294162146eb9911ce378f33d70396f08dfa98d92aed133bba2a8df2b1710c257bf00c0657933ee6cd9c5edb36c8054dc languageName: node linkType: hard -"@docusaurus/preset-classic@npm:2.4.3": - version: 2.4.3 - resolution: "@docusaurus/preset-classic@npm:2.4.3" +"@docusaurus/preset-classic@npm:3.2.1": + version: 3.2.1 + resolution: "@docusaurus/preset-classic@npm:3.2.1" dependencies: - "@docusaurus/core": 2.4.3 - "@docusaurus/plugin-content-blog": 2.4.3 - "@docusaurus/plugin-content-docs": 2.4.3 - "@docusaurus/plugin-content-pages": 2.4.3 - "@docusaurus/plugin-debug": 2.4.3 - "@docusaurus/plugin-google-analytics": 2.4.3 - "@docusaurus/plugin-google-gtag": 2.4.3 - "@docusaurus/plugin-google-tag-manager": 2.4.3 - "@docusaurus/plugin-sitemap": 2.4.3 - "@docusaurus/theme-classic": 2.4.3 - "@docusaurus/theme-common": 2.4.3 - "@docusaurus/theme-search-algolia": 2.4.3 - "@docusaurus/types": 2.4.3 + "@docusaurus/core": 3.2.1 + "@docusaurus/plugin-content-blog": 3.2.1 + "@docusaurus/plugin-content-docs": 3.2.1 + "@docusaurus/plugin-content-pages": 3.2.1 + "@docusaurus/plugin-debug": 3.2.1 + "@docusaurus/plugin-google-analytics": 3.2.1 + "@docusaurus/plugin-google-gtag": 3.2.1 + "@docusaurus/plugin-google-tag-manager": 3.2.1 + "@docusaurus/plugin-sitemap": 3.2.1 + "@docusaurus/theme-classic": 3.2.1 + "@docusaurus/theme-common": 3.2.1 + "@docusaurus/theme-search-algolia": 3.2.1 + "@docusaurus/types": 3.2.1 peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - checksum: a321badc44696adf4ab2d4a5d6c93f595e8c17988aec9609d325928a1d60f5e0205b23fe849b28ddaed24f7935829e86c402f6b761d6e65db4224270b9dd443c + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: 343c896f22bffbda9db4af7d652588f353c5f60336e545eb07be0dfe9bc29ca04a3978d88d5a8b3fa7caafc56a48b341349ffd08006885fa0d4de216cfdc5401 languageName: node linkType: hard @@ -2374,178 +3540,182 @@ __metadata: languageName: node linkType: hard -"@docusaurus/theme-classic@npm:2.4.3": - version: 2.4.3 - resolution: "@docusaurus/theme-classic@npm:2.4.3" - dependencies: - "@docusaurus/core": 2.4.3 - "@docusaurus/mdx-loader": 2.4.3 - "@docusaurus/module-type-aliases": 2.4.3 - "@docusaurus/plugin-content-blog": 2.4.3 - "@docusaurus/plugin-content-docs": 2.4.3 - "@docusaurus/plugin-content-pages": 2.4.3 - "@docusaurus/theme-common": 2.4.3 - "@docusaurus/theme-translations": 2.4.3 - "@docusaurus/types": 2.4.3 - "@docusaurus/utils": 2.4.3 - "@docusaurus/utils-common": 2.4.3 - "@docusaurus/utils-validation": 2.4.3 - "@mdx-js/react": ^1.6.22 - clsx: ^1.2.1 - copy-text-to-clipboard: ^3.0.1 +"@docusaurus/theme-classic@npm:3.2.1": + version: 3.2.1 + resolution: "@docusaurus/theme-classic@npm:3.2.1" + dependencies: + "@docusaurus/core": 3.2.1 + "@docusaurus/mdx-loader": 3.2.1 + "@docusaurus/module-type-aliases": 3.2.1 + "@docusaurus/plugin-content-blog": 3.2.1 + "@docusaurus/plugin-content-docs": 3.2.1 + "@docusaurus/plugin-content-pages": 3.2.1 + "@docusaurus/theme-common": 3.2.1 + "@docusaurus/theme-translations": 3.2.1 + "@docusaurus/types": 3.2.1 + "@docusaurus/utils": 3.2.1 + "@docusaurus/utils-common": 3.2.1 + "@docusaurus/utils-validation": 3.2.1 + "@mdx-js/react": ^3.0.0 + clsx: ^2.0.0 + copy-text-to-clipboard: ^3.2.0 infima: 0.2.0-alpha.43 lodash: ^4.17.21 nprogress: ^0.2.0 - postcss: ^8.4.14 - prism-react-renderer: ^1.3.5 - prismjs: ^1.28.0 - react-router-dom: ^5.3.3 - rtlcss: ^3.5.0 - tslib: ^2.4.0 + postcss: ^8.4.26 + prism-react-renderer: ^2.3.0 + prismjs: ^1.29.0 + react-router-dom: ^5.3.4 + rtlcss: ^4.1.0 + tslib: ^2.6.0 utility-types: ^3.10.0 peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - checksum: 215b7fa416f40ce68773265a168af47fa770583ebe33ec7b34c7e082dfe7c79252b589a6b26532cb0ab7dd089611a9cd0e20c94df097be320a227b98e3b3fbb8 + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: 7b38e47e9334ba6ad84f6432ec9ae81caad7f6c630b2a332617b0f32f1559b0e56f3d8857c732da62d1d7213ad0f493853bf18b1707a2f8d8bcccef32f1d81a1 languageName: node linkType: hard -"@docusaurus/theme-common@npm:2.4.3": - version: 2.4.3 - resolution: "@docusaurus/theme-common@npm:2.4.3" - dependencies: - "@docusaurus/mdx-loader": 2.4.3 - "@docusaurus/module-type-aliases": 2.4.3 - "@docusaurus/plugin-content-blog": 2.4.3 - "@docusaurus/plugin-content-docs": 2.4.3 - "@docusaurus/plugin-content-pages": 2.4.3 - "@docusaurus/utils": 2.4.3 - "@docusaurus/utils-common": 2.4.3 +"@docusaurus/theme-common@npm:3.2.1": + version: 3.2.1 + resolution: "@docusaurus/theme-common@npm:3.2.1" + dependencies: + "@docusaurus/mdx-loader": 3.2.1 + "@docusaurus/module-type-aliases": 3.2.1 + "@docusaurus/plugin-content-blog": 3.2.1 + "@docusaurus/plugin-content-docs": 3.2.1 + "@docusaurus/plugin-content-pages": 3.2.1 + "@docusaurus/utils": 3.2.1 + "@docusaurus/utils-common": 3.2.1 "@types/history": ^4.7.11 "@types/react": "*" "@types/react-router-config": "*" - clsx: ^1.2.1 + clsx: ^2.0.0 parse-numeric-range: ^1.3.0 - prism-react-renderer: ^1.3.5 - tslib: ^2.4.0 - use-sync-external-store: ^1.2.0 + prism-react-renderer: ^2.3.0 + tslib: ^2.6.0 utility-types: ^3.10.0 peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - checksum: 76817f548705542124d708c804e724674ec9bf996a5cb2a5c9a2919416367567cca4a3fa6055589990c339f6e1fb9d3944e25ed30b79fabe191db00d6ef986ca + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: 13de70293476e05f1b52c2d99a1b26c73bf99ac92aba3c8ddc413b5336725d2b54c56c167d12244fdb0b518ee9cdecbbfb3258fb8cc91272e9b795361b131fbb languageName: node linkType: hard -"@docusaurus/theme-search-algolia@npm:2.4.3": - version: 2.4.3 - resolution: "@docusaurus/theme-search-algolia@npm:2.4.3" - dependencies: - "@docsearch/react": ^3.1.1 - "@docusaurus/core": 2.4.3 - "@docusaurus/logger": 2.4.3 - "@docusaurus/plugin-content-docs": 2.4.3 - "@docusaurus/theme-common": 2.4.3 - "@docusaurus/theme-translations": 2.4.3 - "@docusaurus/utils": 2.4.3 - "@docusaurus/utils-validation": 2.4.3 - algoliasearch: ^4.13.1 - algoliasearch-helper: ^3.10.0 - clsx: ^1.2.1 - eta: ^2.0.0 - fs-extra: ^10.1.0 +"@docusaurus/theme-search-algolia@npm:3.2.1": + version: 3.2.1 + resolution: "@docusaurus/theme-search-algolia@npm:3.2.1" + dependencies: + "@docsearch/react": ^3.5.2 + "@docusaurus/core": 3.2.1 + "@docusaurus/logger": 3.2.1 + "@docusaurus/plugin-content-docs": 3.2.1 + "@docusaurus/theme-common": 3.2.1 + "@docusaurus/theme-translations": 3.2.1 + "@docusaurus/utils": 3.2.1 + "@docusaurus/utils-validation": 3.2.1 + algoliasearch: ^4.18.0 + algoliasearch-helper: ^3.13.3 + clsx: ^2.0.0 + eta: ^2.2.0 + fs-extra: ^11.1.1 lodash: ^4.17.21 - tslib: ^2.4.0 + tslib: ^2.6.0 utility-types: ^3.10.0 peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - checksum: 665d244c25bff21dd45c983c9b85f9827d2dd58945b802d645370b5e7092820532faf488c0bc0ce88e8fc0088c7f56eb9abb96589cf3857372c1b61bba6cbed7 + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: befbb86bf309f2b770ae21bc1d5c91eb6e840a5a72858cdfd3b21dbabadd1738d6d427ada7745f9d3424bb1a6e01839e20bf35c15a4c13d59b63d259e52de5df languageName: node linkType: hard -"@docusaurus/theme-translations@npm:2.4.3": - version: 2.4.3 - resolution: "@docusaurus/theme-translations@npm:2.4.3" +"@docusaurus/theme-translations@npm:3.2.1": + version: 3.2.1 + resolution: "@docusaurus/theme-translations@npm:3.2.1" dependencies: - fs-extra: ^10.1.0 - tslib: ^2.4.0 - checksum: 8424583a130b0d32b6adf578dc5daeefaad199019c8a6a23fbd67577209be64923cde59d423ea9d41d6e7cfc2318e7fa6a17a665e8ae1c871ce0880525f9b8fd + fs-extra: ^11.1.1 + tslib: ^2.6.0 + checksum: 43bdb90d143576d2e8eb56bfe2c9daa0e4250cdb2dcfd10096b86466e6ee253548ac5ef2f9a4986a5bc9a573d118fe4695ee5004f0ef00b57b720dac7f124337 languageName: node linkType: hard -"@docusaurus/types@npm:2.4.3": - version: 2.4.3 - resolution: "@docusaurus/types@npm:2.4.3" +"@docusaurus/types@npm:3.2.1": + version: 3.2.1 + resolution: "@docusaurus/types@npm:3.2.1" dependencies: + "@mdx-js/mdx": ^3.0.0 "@types/history": ^4.7.11 "@types/react": "*" commander: ^5.1.0 - joi: ^17.6.0 + joi: ^17.9.2 react-helmet-async: ^1.3.0 utility-types: ^3.10.0 - webpack: ^5.73.0 - webpack-merge: ^5.8.0 + webpack: ^5.88.1 + webpack-merge: ^5.9.0 peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - checksum: c123c45630e885b588f808baa06a97f8408a3381906f65cb92ae75732aedfca6ab2cada94f969c08e043b885b95298616440326259b789010e0986cbcd7a960b + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: 4f19e162bff627675df160ae5c33c6063646050c4de5c9698018fbd9d198300b9ce7a7333e4d1b369b42cfa42296dc9fb36547e4e37664d594deb08639e6b620 languageName: node linkType: hard -"@docusaurus/utils-common@npm:2.4.3": - version: 2.4.3 - resolution: "@docusaurus/utils-common@npm:2.4.3" +"@docusaurus/utils-common@npm:3.2.1": + version: 3.2.1 + resolution: "@docusaurus/utils-common@npm:3.2.1" dependencies: - tslib: ^2.4.0 + tslib: ^2.6.0 peerDependencies: "@docusaurus/types": "*" peerDependenciesMeta: "@docusaurus/types": optional: true - checksum: 1ae315d8d8ce7a0163a698ffdca55b734d21f336512138c128bc0fa2a8d224edbaad0c8dbd7a3de2e8ef734dc2656c505d09066dee4fc84819d153593abb8984 + checksum: bc0b7e74bc29134dbdb7fbc2e8f9f39f0f460923a07d0ccd7f0542088e92c47faf06bdbd253b7ba2b9250b0869118a3b7bf3faa3a075a2a35f5f8545eb3345f2 languageName: node linkType: hard -"@docusaurus/utils-validation@npm:2.4.3": - version: 2.4.3 - resolution: "@docusaurus/utils-validation@npm:2.4.3" +"@docusaurus/utils-validation@npm:3.2.1": + version: 3.2.1 + resolution: "@docusaurus/utils-validation@npm:3.2.1" dependencies: - "@docusaurus/logger": 2.4.3 - "@docusaurus/utils": 2.4.3 - joi: ^17.6.0 + "@docusaurus/logger": 3.2.1 + "@docusaurus/utils": 3.2.1 + "@docusaurus/utils-common": 3.2.1 + joi: ^17.9.2 js-yaml: ^4.1.0 - tslib: ^2.4.0 - checksum: d3472b3f7a0a029c2cef1f00bc9db403d5f7e74e2091eccbc45d06f5776a84fd73bd1a18cf3a8a3cc0348ce49f753a1300deac670c2a82c56070cc40ca9df06e + tslib: ^2.6.0 + checksum: c7b5142083c8e4798c7f6aa1f7a06bc2e93e8e08a8a7a2c5eaf24aa6939e12e401f180f02164764805c40ec0f7179479e0ee98a935c2cb77037ca73ab33d80fd languageName: node linkType: hard -"@docusaurus/utils@npm:2.4.3": - version: 2.4.3 - resolution: "@docusaurus/utils@npm:2.4.3" +"@docusaurus/utils@npm:3.2.1": + version: 3.2.1 + resolution: "@docusaurus/utils@npm:3.2.1" dependencies: - "@docusaurus/logger": 2.4.3 - "@svgr/webpack": ^6.2.1 + "@docusaurus/logger": 3.2.1 + "@docusaurus/utils-common": 3.2.1 + "@svgr/webpack": ^6.5.1 escape-string-regexp: ^4.0.0 file-loader: ^6.2.0 - fs-extra: ^10.1.0 - github-slugger: ^1.4.0 + fs-extra: ^11.1.1 + github-slugger: ^1.5.0 globby: ^11.1.0 gray-matter: ^4.0.3 + jiti: ^1.20.0 js-yaml: ^4.1.0 lodash: ^4.17.21 micromatch: ^4.0.5 + prompts: ^2.4.2 resolve-pathname: ^3.0.0 shelljs: ^0.8.5 - tslib: ^2.4.0 + tslib: ^2.6.0 url-loader: ^4.1.1 - webpack: ^5.73.0 + webpack: ^5.88.1 peerDependencies: "@docusaurus/types": "*" peerDependenciesMeta: "@docusaurus/types": optional: true - checksum: dd1aa7688d1a4b2775e13a91d528608ceab33c57a921404d9a989867c31c8ef17fe3892e4f5680dfb4a783da7b9973e2077e907ff4ac172927433e606e8fa9b9 + checksum: ea862b178e303b49e644e77a663df6e42909632022918b77dc1ee69c4de46dde3f210052b1063e96a820e1443141f70e44aa51372f2bf9cfde65e080ea639889 languageName: node linkType: hard @@ -2616,14 +3786,14 @@ __metadata: languageName: node linkType: hard -"@hapi/hoek@npm:^9.0.0": +"@hapi/hoek@npm:^9.0.0, @hapi/hoek@npm:^9.3.0": version: 9.3.0 resolution: "@hapi/hoek@npm:9.3.0" checksum: 4771c7a776242c3c022b168046af4e324d116a9d2e1d60631ee64f474c6e38d1bb07092d898bf95c7bc5d334c5582798a1456321b2e53ca817d4e7c88bc25b43 languageName: node linkType: hard -"@hapi/topo@npm:^5.0.0": +"@hapi/topo@npm:^5.1.0": version: 5.1.0 resolution: "@hapi/topo@npm:5.1.0" dependencies: @@ -2946,6 +4116,17 @@ __metadata: languageName: node linkType: hard +"@jridgewell/gen-mapping@npm:^0.3.5": + version: 0.3.5 + resolution: "@jridgewell/gen-mapping@npm:0.3.5" + dependencies: + "@jridgewell/set-array": ^1.2.1 + "@jridgewell/sourcemap-codec": ^1.4.10 + "@jridgewell/trace-mapping": ^0.3.24 + checksum: ff7a1764ebd76a5e129c8890aa3e2f46045109dabde62b0b6c6a250152227647178ff2069ea234753a690d8f3c4ac8b5e7b267bbee272bffb7f3b0a370ab6e52 + languageName: node + linkType: hard + "@jridgewell/resolve-uri@npm:^3.0.3, @jridgewell/resolve-uri@npm:^3.1.0": version: 3.1.1 resolution: "@jridgewell/resolve-uri@npm:3.1.1" @@ -2960,6 +4141,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/set-array@npm:^1.2.1": + version: 1.2.1 + resolution: "@jridgewell/set-array@npm:1.2.1" + checksum: 832e513a85a588f8ed4f27d1279420d8547743cc37fcad5a5a76fc74bb895b013dfe614d0eed9cb860048e6546b798f8f2652020b4b2ba0561b05caa8c654b10 + languageName: node + linkType: hard + "@jridgewell/source-map@npm:^0.3.3": version: 0.3.5 resolution: "@jridgewell/source-map@npm:0.3.5" @@ -2997,6 +4185,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/trace-mapping@npm:^0.3.20, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": + version: 0.3.25 + resolution: "@jridgewell/trace-mapping@npm:0.3.25" + dependencies: + "@jridgewell/resolve-uri": ^3.1.0 + "@jridgewell/sourcemap-codec": ^1.4.14 + checksum: 9d3c40d225e139987b50c48988f8717a54a8c994d8a948ee42e1412e08988761d0754d7d10b803061cc3aebf35f92a5dbbab493bd0e1a9ef9e89a2130e83ba34 + languageName: node + linkType: hard + "@jsdevtools/ono@npm:^7.1.3": version: 7.1.3 resolution: "@jsdevtools/ono@npm:7.1.3" @@ -3172,46 +4370,46 @@ __metadata: languageName: node linkType: hard -"@mdx-js/mdx@npm:^1.6.22": - version: 1.6.22 - resolution: "@mdx-js/mdx@npm:1.6.22" +"@mdx-js/mdx@npm:^3.0.0": + version: 3.0.1 + resolution: "@mdx-js/mdx@npm:3.0.1" dependencies: - "@babel/core": 7.12.9 - "@babel/plugin-syntax-jsx": 7.12.1 - "@babel/plugin-syntax-object-rest-spread": 7.8.3 - "@mdx-js/util": 1.6.22 - babel-plugin-apply-mdx-type-prop: 1.6.22 - babel-plugin-extract-import-names: 1.6.22 - camelcase-css: 2.0.1 - detab: 2.0.4 - hast-util-raw: 6.0.1 - lodash.uniq: 4.5.0 - mdast-util-to-hast: 10.0.1 - remark-footnotes: 2.0.0 - remark-mdx: 1.6.22 - remark-parse: 8.0.3 - remark-squeeze-paragraphs: 4.0.0 - style-to-object: 0.3.0 - unified: 9.2.0 - unist-builder: 2.0.3 - unist-util-visit: 2.0.3 - checksum: 0839b4a3899416326ea6578fe9e470af319da559bc6d3669c60942e456b49a98eebeb3358c623007b4786a2175a450d2c51cd59df64639013c5a3d22366931a6 - languageName: node - linkType: hard - -"@mdx-js/react@npm:^1.6.22": - version: 1.6.22 - resolution: "@mdx-js/react@npm:1.6.22" - peerDependencies: - react: ^16.13.1 || ^17.0.0 - checksum: bc84bd514bc127f898819a0c6f1a6915d9541011bd8aefa1fcc1c9bea8939f31051409e546bdec92babfa5b56092a16d05ef6d318304ac029299df5181dc94c8 - languageName: node - linkType: hard - -"@mdx-js/util@npm:1.6.22": - version: 1.6.22 - resolution: "@mdx-js/util@npm:1.6.22" - checksum: 4b393907e39a1a75214f0314bf72a0adfa5e5adffd050dd5efe9c055b8549481a3cfc9f308c16dfb33311daf3ff63added7d5fd1fe52db614c004f886e0e559a + "@types/estree": ^1.0.0 + "@types/estree-jsx": ^1.0.0 + "@types/hast": ^3.0.0 + "@types/mdx": ^2.0.0 + collapse-white-space: ^2.0.0 + devlop: ^1.0.0 + estree-util-build-jsx: ^3.0.0 + estree-util-is-identifier-name: ^3.0.0 + estree-util-to-js: ^2.0.0 + estree-walker: ^3.0.0 + hast-util-to-estree: ^3.0.0 + hast-util-to-jsx-runtime: ^2.0.0 + markdown-extensions: ^2.0.0 + periscopic: ^3.0.0 + remark-mdx: ^3.0.0 + remark-parse: ^11.0.0 + remark-rehype: ^11.0.0 + source-map: ^0.7.0 + unified: ^11.0.0 + unist-util-position-from-estree: ^2.0.0 + unist-util-stringify-position: ^4.0.0 + unist-util-visit: ^5.0.0 + vfile: ^6.0.0 + checksum: 82221662279c39a755b88f63b031a30b9bc04365e5bfc3e45590f4fa7bf6bff12364f4caee31c768ae588145eed74fda10c327d53f9272b1a2cffbc8bd537ce6 + languageName: node + linkType: hard + +"@mdx-js/react@npm:^3.0.0": + version: 3.0.1 + resolution: "@mdx-js/react@npm:3.0.1" + dependencies: + "@types/mdx": ^2.0.0 + peerDependencies: + "@types/react": ">=16" + react: ">=16" + checksum: 1063a597264f6a8840aa13274a99beef8983a88dd45b0c5b8e48e6216bc23d33e247da8e2d95d6e1874483f8b4e0903b166ce5046874aa7ffa2b1333057dcddf languageName: node linkType: hard @@ -4088,6 +5286,33 @@ __metadata: languageName: node linkType: hard +"@pnpm/config.env-replace@npm:^1.1.0": + version: 1.1.0 + resolution: "@pnpm/config.env-replace@npm:1.1.0" + checksum: a3d2b57e35eec9543d9eb085854f6e33e8102dac99fdef2fad2eebdbbfc345e93299f0c20e8eb61c1b4c7aa123bfd47c175678626f161cda65dd147c2b6e1fa0 + languageName: node + linkType: hard + +"@pnpm/network.ca-file@npm:^1.0.1": + version: 1.0.2 + resolution: "@pnpm/network.ca-file@npm:1.0.2" + dependencies: + graceful-fs: 4.2.10 + checksum: d8d0884646500576bd5390464d13db1bb9a62e32a1069293e5bddb2ad8354b354b7e2d2a35e12850025651e795e6a80ce9e601c66312504667b7e3ee7b52becc + languageName: node + linkType: hard + +"@pnpm/npm-conf@npm:^2.1.0": + version: 2.2.2 + resolution: "@pnpm/npm-conf@npm:2.2.2" + dependencies: + "@pnpm/config.env-replace": ^1.1.0 + "@pnpm/network.ca-file": ^1.0.1 + config-chain: ^1.1.11 + checksum: d64aa4464be584caa855eafa8f109509390489997e36d602d6215784e2973b896bef3968426bb00896cf4ae7d440fed2cee7bb4e0dbc90362f024ea3f9e27ab1 + languageName: node + linkType: hard + "@polka/url@npm:^1.0.0-next.20": version: 1.0.0-next.23 resolution: "@polka/url@npm:1.0.0-next.23" @@ -4311,12 +5536,12 @@ __metadata: languageName: node linkType: hard -"@sideway/address@npm:^4.1.3": - version: 4.1.4 - resolution: "@sideway/address@npm:4.1.4" +"@sideway/address@npm:^4.1.5": + version: 4.1.5 + resolution: "@sideway/address@npm:4.1.5" dependencies: "@hapi/hoek": ^9.0.0 - checksum: b9fca2a93ac2c975ba12e0a6d97853832fb1f4fb02393015e012b47fa916a75ca95102d77214b2a29a2784740df2407951af8c5dde054824c65577fd293c4cdb + checksum: 3e3ea0f00b4765d86509282290368a4a5fd39a7995fdc6de42116ca19a96120858e56c2c995081def06e1c53e1f8bccc7d013f6326602bec9d56b72ee2772b9d languageName: node linkType: hard @@ -4378,20 +5603,20 @@ __metadata: languageName: node linkType: hard -"@sindresorhus/is@npm:^0.14.0": - version: 0.14.0 - resolution: "@sindresorhus/is@npm:0.14.0" - checksum: 971e0441dd44ba3909b467219a5e242da0fc584048db5324cfb8048148fa8dcc9d44d71e3948972c4f6121d24e5da402ef191420d1266a95f713bb6d6e59c98a - languageName: node - linkType: hard - -"@sindresorhus/is@npm:^4.0.0": +"@sindresorhus/is@npm:^4.0.0, @sindresorhus/is@npm:^4.6.0": version: 4.6.0 resolution: "@sindresorhus/is@npm:4.6.0" checksum: 83839f13da2c29d55c97abc3bc2c55b250d33a0447554997a85c539e058e57b8da092da396e252b11ec24a0279a0bed1f537fa26302209327060643e327f81d2 languageName: node linkType: hard +"@sindresorhus/is@npm:^5.2.0": + version: 5.6.0 + resolution: "@sindresorhus/is@npm:5.6.0" + checksum: 2e6e0c3acf188dcd9aea0f324ac1b6ad04c9fc672392a7b5a1218512fcde066965797eba8b9fe2108657a504388bd4a6664e6e6602555168e828a6df08b9f10e + languageName: node + linkType: hard + "@sinonjs/commons@npm:^3.0.0": version: 3.0.0 resolution: "@sinonjs/commons@npm:3.0.0" @@ -4428,14 +5653,14 @@ __metadata: languageName: node linkType: hard -"@slorber/static-site-generator-webpack-plugin@npm:^4.0.7": - version: 4.0.7 - resolution: "@slorber/static-site-generator-webpack-plugin@npm:4.0.7" +"@slorber/remark-comment@npm:^1.0.0": + version: 1.0.0 + resolution: "@slorber/remark-comment@npm:1.0.0" dependencies: - eval: ^0.1.8 - p-map: ^4.0.0 - webpack-sources: ^3.2.2 - checksum: a1e1d8b22dd51059524993f3fdd6861db10eb950debc389e5dd650702287fa2004eace03e6bc8f25b977bd7bc01d76a50aa271cbb73c58a8ec558945d728f307 + micromark-factory-space: ^1.0.0 + micromark-util-character: ^1.1.0 + micromark-util-symbol: ^1.0.1 + checksum: c96f1533d09913c57381859966f10a706afd8eb680923924af1c451f3b72f22c31e394028d7535131c10f8682d3c60206da95c50fb4f016fbbd04218c853cc88 languageName: node linkType: hard @@ -5076,7 +6301,7 @@ __metadata: languageName: node linkType: hard -"@svgr/webpack@npm:^6.2.1": +"@svgr/webpack@npm:^6.5.1": version: 6.5.1 resolution: "@svgr/webpack@npm:6.5.1" dependencies: @@ -5092,15 +6317,6 @@ __metadata: languageName: node linkType: hard -"@szmarczak/http-timer@npm:^1.1.2": - version: 1.1.2 - resolution: "@szmarczak/http-timer@npm:1.1.2" - dependencies: - defer-to-connect: ^1.0.1 - checksum: 4d9158061c5f397c57b4988cde33a163244e4f02df16364f103971957a32886beb104d6180902cbe8b38cb940e234d9f98a4e486200deca621923f62f50a06fe - languageName: node - linkType: hard - "@szmarczak/http-timer@npm:^4.0.5": version: 4.0.6 resolution: "@szmarczak/http-timer@npm:4.0.6" @@ -5110,6 +6326,15 @@ __metadata: languageName: node linkType: hard +"@szmarczak/http-timer@npm:^5.0.1": + version: 5.0.1 + resolution: "@szmarczak/http-timer@npm:5.0.1" + dependencies: + defer-to-connect: ^2.0.1 + checksum: fc9cb993e808806692e4a3337c90ece0ec00c89f4b67e3652a356b89730da98bc824273a6d67ca84d5f33cd85f317dcd5ce39d8cc0a2f060145a608a7cb8ce92 + languageName: node + linkType: hard + "@tokenizer/token@npm:^0.3.0": version: 0.3.0 resolution: "@tokenizer/token@npm:0.3.0" @@ -5195,6 +6420,15 @@ __metadata: languageName: node linkType: hard +"@types/acorn@npm:^4.0.0": + version: 4.0.6 + resolution: "@types/acorn@npm:4.0.6" + dependencies: + "@types/estree": "*" + checksum: 60e1fd28af18d6cb54a93a7231c7c18774a9a8739c9b179e9e8750dca631e10cbef2d82b02830ea3f557b1d121e6406441e9e1250bd492dc81d4b3456e76e4d4 + languageName: node + linkType: hard + "@types/amqplib@npm:^0.10.4": version: 0.10.4 resolution: "@types/amqplib@npm:0.10.4" @@ -5295,7 +6529,7 @@ __metadata: languageName: node linkType: hard -"@types/debug@npm:^4.1.12": +"@types/debug@npm:^4.0.0, @types/debug@npm:^4.1.12": version: 4.1.12 resolution: "@types/debug@npm:4.1.12" dependencies: @@ -5349,6 +6583,15 @@ __metadata: languageName: node linkType: hard +"@types/estree-jsx@npm:^1.0.0": + version: 1.0.5 + resolution: "@types/estree-jsx@npm:1.0.5" + dependencies: + "@types/estree": "*" + checksum: a028ab0cd7b2950168a05c6a86026eb3a36a54a4adfae57f13911d7b49dffe573d9c2b28421b2d029b49b3d02fcd686611be2622dc3dad6d9791166c083f6008 + languageName: node + linkType: hard + "@types/estree@npm:*, @types/estree@npm:^1.0.0": version: 1.0.1 resolution: "@types/estree@npm:1.0.1" @@ -5363,6 +6606,13 @@ __metadata: languageName: node linkType: hard +"@types/estree@npm:^1.0.5": + version: 1.0.5 + resolution: "@types/estree@npm:1.0.5" + checksum: dd8b5bed28e6213b7acd0fb665a84e693554d850b0df423ac8076cc3ad5823a6bc26b0251d080bdc545af83179ede51dd3f6fa78cad2c46ed1f29624ddf3e41a + languageName: node + linkType: hard + "@types/express-serve-static-core@npm:*, @types/express-serve-static-core@npm:^4.17.33": version: 4.17.36 resolution: "@types/express-serve-static-core@npm:4.17.36" @@ -5417,12 +6667,19 @@ __metadata: languageName: node linkType: hard -"@types/hast@npm:^2.0.0": - version: 2.3.6 - resolution: "@types/hast@npm:2.3.6" +"@types/gtag.js@npm:^0.0.12": + version: 0.0.12 + resolution: "@types/gtag.js@npm:0.0.12" + checksum: 34efc27fbfd0013255b8bfd4af38ded9d5a6ba761130c76f17fd3a9585d83acc88d8005aab667cfec4bdec0e7c7217f689739799a8f61aed0edb929be58b162e + languageName: node + linkType: hard + +"@types/hast@npm:^3.0.0": + version: 3.0.4 + resolution: "@types/hast@npm:3.0.4" dependencies: - "@types/unist": ^2 - checksum: c004372f6ab919ec92a2de43e4380707e27b76fe371c7d06ab26547c1e851dfba2a7c740c544218df8c7e0a94443458793c43730ad563a39e3fdc1a48904d7f5 + "@types/unist": "*" + checksum: 7a973e8d16fcdf3936090fa2280f408fb2b6a4f13b42edeb5fbd614efe042b82eac68e298e556d50f6b4ad585a3a93c353e9c826feccdc77af59de8dd400d044 languageName: node linkType: hard @@ -5447,6 +6704,13 @@ __metadata: languageName: node linkType: hard +"@types/http-cache-semantics@npm:^4.0.2": + version: 4.0.4 + resolution: "@types/http-cache-semantics@npm:4.0.4" + checksum: 7f4dd832e618bc1e271be49717d7b4066d77c2d4eed5b81198eb987e532bb3e1c7e02f45d77918185bad936f884b700c10cebe06305f50400f382ab75055f9e8 + languageName: node + linkType: hard + "@types/http-errors@npm:*": version: 2.0.2 resolution: "@types/http-errors@npm:2.0.2" @@ -5505,7 +6769,7 @@ __metadata: languageName: node linkType: hard -"@types/keyv@npm:^3.1.1, @types/keyv@npm:^3.1.4": +"@types/keyv@npm:^3.1.4": version: 3.1.4 resolution: "@types/keyv@npm:3.1.4" dependencies: @@ -5521,12 +6785,19 @@ __metadata: languageName: node linkType: hard -"@types/mdast@npm:^3.0.0": - version: 3.0.12 - resolution: "@types/mdast@npm:3.0.12" +"@types/mdast@npm:^4.0.0, @types/mdast@npm:^4.0.2": + version: 4.0.3 + resolution: "@types/mdast@npm:4.0.3" dependencies: - "@types/unist": ^2 - checksum: 83adb8679b9d139f69f63554d120af921e9f1289e9903a2c99e0554a327c8524a6c0beccdc0721e4fdbccc606e81964fecb0d390d53df0f74360938e22f1a469 + "@types/unist": "*" + checksum: 345c5a22fccf05f35239ea6313ee4aaf6ebed5927c03ac79744abccb69b9ba5e692f9b771e36a012b79e17429082cada30f579e9c43b8a54e0ffb365431498b6 + languageName: node + linkType: hard + +"@types/mdx@npm:^2.0.0": + version: 2.0.13 + resolution: "@types/mdx@npm:2.0.13" + checksum: 195137b548e75a85f0558bb1ca5088aff1c01ae0fc64454da06085b7513a043356d0bb51ed559d3cbc7ad724ccd8cef2a7d07d014b89a47a74dff8875ceb3b15 languageName: node linkType: hard @@ -5633,13 +6904,6 @@ __metadata: languageName: node linkType: hard -"@types/parse5@npm:^5.0.0": - version: 5.0.3 - resolution: "@types/parse5@npm:5.0.3" - checksum: d6b7495cb1850f9f2e9c5e103ede9f2d30a5320669707b105c403868adc9e4bf8d3a7ff314cc23f67826bbbbbc0e6147346ce9062ab429f099dba7a01f463919 - languageName: node - linkType: hard - "@types/prettier@npm:^2.1.5": version: 2.7.3 resolution: "@types/prettier@npm:2.7.3" @@ -5647,6 +6911,13 @@ __metadata: languageName: node linkType: hard +"@types/prismjs@npm:^1.26.0": + version: 1.26.3 + resolution: "@types/prismjs@npm:1.26.3" + checksum: c627fa9d9f4277ce413bb8347944152cddfc892702e34ff4b099dc1cf3f00c09514d36349c23529b903b0e57f3b2e0dc91ee66e98af07fbbe1e3fe8346b23370 + languageName: node + linkType: hard + "@types/prop-types@npm:*": version: 15.7.6 resolution: "@types/prop-types@npm:15.7.6" @@ -5684,7 +6955,7 @@ __metadata: languageName: node linkType: hard -"@types/react-router-config@npm:*, @types/react-router-config@npm:^5.0.6": +"@types/react-router-config@npm:*": version: 5.0.7 resolution: "@types/react-router-config@npm:5.0.7" dependencies: @@ -5695,6 +6966,17 @@ __metadata: languageName: node linkType: hard +"@types/react-router-config@npm:^5.0.7": + version: 5.0.11 + resolution: "@types/react-router-config@npm:5.0.11" + dependencies: + "@types/history": ^4.7.11 + "@types/react": "*" + "@types/react-router": ^5.1.0 + checksum: 4b72d9b71e0576e193c11e5085bbdac43f31debfa3b6ebc24666f3d646ef25c1f57f16c29b1ddd3051c881e85f8e0d4ab5a7bbd5fc215b9377f57675b210be7c + languageName: node + linkType: hard + "@types/react-router-dom@npm:*": version: 5.3.3 resolution: "@types/react-router-dom@npm:5.3.3" @@ -5849,7 +7131,14 @@ __metadata: languageName: node linkType: hard -"@types/unist@npm:^2, @types/unist@npm:^2.0.0, @types/unist@npm:^2.0.2, @types/unist@npm:^2.0.3": +"@types/unist@npm:*, @types/unist@npm:^3.0.0": + version: 3.0.2 + resolution: "@types/unist@npm:3.0.2" + checksum: 3d04d0be69316e5f14599a0d993a208606c12818cf631fd399243d1dc7a9bd8a3917d6066baa6abc290814afbd744621484756803c80cba892c39cd4b4a85616 + languageName: node + linkType: hard + +"@types/unist@npm:^2.0.0": version: 2.0.8 resolution: "@types/unist@npm:2.0.8" checksum: f4852d10a6752dc70df363917ef74453e5d2fd42824c0f6d09d19d530618e1402193977b1207366af4415aaec81d4e262c64d00345402020c4ca179216e553c7 @@ -6035,13 +7324,20 @@ __metadata: languageName: node linkType: hard -"@webassemblyjs/ast@npm:1.11.6, @webassemblyjs/ast@npm:^1.11.5": - version: 1.11.6 - resolution: "@webassemblyjs/ast@npm:1.11.6" +"@ungap/structured-clone@npm:^1.0.0": + version: 1.2.0 + resolution: "@ungap/structured-clone@npm:1.2.0" + checksum: 4f656b7b4672f2ce6e272f2427d8b0824ed11546a601d8d5412b9d7704e83db38a8d9f402ecdf2b9063fc164af842ad0ec4a55819f621ed7e7ea4d1efcc74524 + languageName: node + linkType: hard + +"@webassemblyjs/ast@npm:1.12.1, @webassemblyjs/ast@npm:^1.12.1": + version: 1.12.1 + resolution: "@webassemblyjs/ast@npm:1.12.1" dependencies: "@webassemblyjs/helper-numbers": 1.11.6 "@webassemblyjs/helper-wasm-bytecode": 1.11.6 - checksum: 38ef1b526ca47c210f30975b06df2faf1a8170b1636ce239fc5738fc231ce28389dd61ecedd1bacfc03cbe95b16d1af848c805652080cb60982836eb4ed2c6cf + checksum: 31bcc64147236bd7b1b6d29d1f419c1f5845c785e1e42dc9e3f8ca2e05a029e9393a271b84f3a5bff2a32d35f51ff59e2181a6e5f953fe88576acd6750506202 languageName: node linkType: hard @@ -6059,10 +7355,10 @@ __metadata: languageName: node linkType: hard -"@webassemblyjs/helper-buffer@npm:1.11.6": - version: 1.11.6 - resolution: "@webassemblyjs/helper-buffer@npm:1.11.6" - checksum: b14d0573bf680d22b2522e8a341ec451fddd645d1f9c6bd9012ccb7e587a2973b86ab7b89fe91e1c79939ba96095f503af04369a3b356c8023c13a5893221644 +"@webassemblyjs/helper-buffer@npm:1.12.1": + version: 1.12.1 + resolution: "@webassemblyjs/helper-buffer@npm:1.12.1" + checksum: c3ffb723024130308db608e86e2bdccd4868bbb62dffb0a9a1530606496f79c87f8565bd8e02805ce64912b71f1a70ee5fb00307258b0c082c3abf961d097eca languageName: node linkType: hard @@ -6084,15 +7380,15 @@ __metadata: languageName: node linkType: hard -"@webassemblyjs/helper-wasm-section@npm:1.11.6": - version: 1.11.6 - resolution: "@webassemblyjs/helper-wasm-section@npm:1.11.6" +"@webassemblyjs/helper-wasm-section@npm:1.12.1": + version: 1.12.1 + resolution: "@webassemblyjs/helper-wasm-section@npm:1.12.1" dependencies: - "@webassemblyjs/ast": 1.11.6 - "@webassemblyjs/helper-buffer": 1.11.6 + "@webassemblyjs/ast": 1.12.1 + "@webassemblyjs/helper-buffer": 1.12.1 "@webassemblyjs/helper-wasm-bytecode": 1.11.6 - "@webassemblyjs/wasm-gen": 1.11.6 - checksum: b2cf751bf4552b5b9999d27bbb7692d0aca75260140195cb58ea6374d7b9c2dc69b61e10b211a0e773f66209c3ddd612137ed66097e3684d7816f854997682e9 + "@webassemblyjs/wasm-gen": 1.12.1 + checksum: c19810cdd2c90ff574139b6d8c0dda254d42d168a9e5b3d353d1bc085f1d7164ccd1b3c05592a45a939c47f7e403dc8d03572bb686642f06a3d02932f6f0bc8f languageName: node linkType: hard @@ -6121,68 +7417,68 @@ __metadata: languageName: node linkType: hard -"@webassemblyjs/wasm-edit@npm:^1.11.5": - version: 1.11.6 - resolution: "@webassemblyjs/wasm-edit@npm:1.11.6" +"@webassemblyjs/wasm-edit@npm:^1.12.1": + version: 1.12.1 + resolution: "@webassemblyjs/wasm-edit@npm:1.12.1" dependencies: - "@webassemblyjs/ast": 1.11.6 - "@webassemblyjs/helper-buffer": 1.11.6 + "@webassemblyjs/ast": 1.12.1 + "@webassemblyjs/helper-buffer": 1.12.1 "@webassemblyjs/helper-wasm-bytecode": 1.11.6 - "@webassemblyjs/helper-wasm-section": 1.11.6 - "@webassemblyjs/wasm-gen": 1.11.6 - "@webassemblyjs/wasm-opt": 1.11.6 - "@webassemblyjs/wasm-parser": 1.11.6 - "@webassemblyjs/wast-printer": 1.11.6 - checksum: 29ce75870496d6fad864d815ebb072395a8a3a04dc9c3f4e1ffdc63fc5fa58b1f34304a1117296d8240054cfdbc38aca88e71fb51483cf29ffab0a61ef27b481 + "@webassemblyjs/helper-wasm-section": 1.12.1 + "@webassemblyjs/wasm-gen": 1.12.1 + "@webassemblyjs/wasm-opt": 1.12.1 + "@webassemblyjs/wasm-parser": 1.12.1 + "@webassemblyjs/wast-printer": 1.12.1 + checksum: ae23642303f030af888d30c4ef37b08dfec7eab6851a9575a616e65d1219f880d9223913a39056dd654e49049d76e97555b285d1f7e56935047abf578cce0692 languageName: node linkType: hard -"@webassemblyjs/wasm-gen@npm:1.11.6": - version: 1.11.6 - resolution: "@webassemblyjs/wasm-gen@npm:1.11.6" +"@webassemblyjs/wasm-gen@npm:1.12.1": + version: 1.12.1 + resolution: "@webassemblyjs/wasm-gen@npm:1.12.1" dependencies: - "@webassemblyjs/ast": 1.11.6 + "@webassemblyjs/ast": 1.12.1 "@webassemblyjs/helper-wasm-bytecode": 1.11.6 "@webassemblyjs/ieee754": 1.11.6 "@webassemblyjs/leb128": 1.11.6 "@webassemblyjs/utf8": 1.11.6 - checksum: a645a2eecbea24833c3260a249704a7f554ef4a94c6000984728e94bb2bc9140a68dfd6fd21d5e0bbb09f6dfc98e083a45760a83ae0417b41a0196ff6d45a23a + checksum: 5787626bb7f0b033044471ddd00ce0c9fe1ee4584e8b73e232051e3a4c99ba1a102700d75337151c8b6055bae77eefa4548960c610a5e4a504e356bd872138ff languageName: node linkType: hard -"@webassemblyjs/wasm-opt@npm:1.11.6": - version: 1.11.6 - resolution: "@webassemblyjs/wasm-opt@npm:1.11.6" +"@webassemblyjs/wasm-opt@npm:1.12.1": + version: 1.12.1 + resolution: "@webassemblyjs/wasm-opt@npm:1.12.1" dependencies: - "@webassemblyjs/ast": 1.11.6 - "@webassemblyjs/helper-buffer": 1.11.6 - "@webassemblyjs/wasm-gen": 1.11.6 - "@webassemblyjs/wasm-parser": 1.11.6 - checksum: b4557f195487f8e97336ddf79f7bef40d788239169aac707f6eaa2fa5fe243557c2d74e550a8e57f2788e70c7ae4e7d32f7be16101afe183d597b747a3bdd528 + "@webassemblyjs/ast": 1.12.1 + "@webassemblyjs/helper-buffer": 1.12.1 + "@webassemblyjs/wasm-gen": 1.12.1 + "@webassemblyjs/wasm-parser": 1.12.1 + checksum: 0e8fa8a0645304a1e18ff40d3db5a2e9233ebaa169b19fcc651d6fc9fe2cac0ce092ddee927318015ae735d9cd9c5d97c0cafb6a51dcd2932ac73587b62df991 languageName: node linkType: hard -"@webassemblyjs/wasm-parser@npm:1.11.6, @webassemblyjs/wasm-parser@npm:^1.11.5": - version: 1.11.6 - resolution: "@webassemblyjs/wasm-parser@npm:1.11.6" +"@webassemblyjs/wasm-parser@npm:1.12.1, @webassemblyjs/wasm-parser@npm:^1.12.1": + version: 1.12.1 + resolution: "@webassemblyjs/wasm-parser@npm:1.12.1" dependencies: - "@webassemblyjs/ast": 1.11.6 + "@webassemblyjs/ast": 1.12.1 "@webassemblyjs/helper-api-error": 1.11.6 "@webassemblyjs/helper-wasm-bytecode": 1.11.6 "@webassemblyjs/ieee754": 1.11.6 "@webassemblyjs/leb128": 1.11.6 "@webassemblyjs/utf8": 1.11.6 - checksum: 8200a8d77c15621724a23fdabe58d5571415cda98a7058f542e670ea965dd75499f5e34a48675184947c66f3df23adf55df060312e6d72d57908e3f049620d8a + checksum: 176015de3551ac068cd4505d837414f258d9ade7442bd71efb1232fa26c9f6d7d4e11a5c816caeed389943f409af7ebff6899289a992d7a70343cb47009d21a8 languageName: node linkType: hard -"@webassemblyjs/wast-printer@npm:1.11.6": - version: 1.11.6 - resolution: "@webassemblyjs/wast-printer@npm:1.11.6" +"@webassemblyjs/wast-printer@npm:1.12.1": + version: 1.12.1 + resolution: "@webassemblyjs/wast-printer@npm:1.12.1" dependencies: - "@webassemblyjs/ast": 1.11.6 + "@webassemblyjs/ast": 1.12.1 "@xtuc/long": 4.2.2 - checksum: d2fa6a4c427325ec81463e9c809aa6572af6d47f619f3091bf4c4a6fc34f1da3df7caddaac50b8e7a457f8784c62cd58c6311b6cb69b0162ccd8d4c072f79cf8 + checksum: 2974b5dda8d769145ba0efd886ea94a601e61fb37114c14f9a9a7606afc23456799af652ac3052f284909bd42edc3665a76bc9b50f95f0794c053a8a1757b713 languageName: node linkType: hard @@ -6306,7 +7602,7 @@ __metadata: languageName: node linkType: hard -"acorn-jsx@npm:^5.3.2": +"acorn-jsx@npm:^5.0.0, acorn-jsx@npm:^5.3.2": version: 5.3.2 resolution: "acorn-jsx@npm:5.3.2" peerDependencies: @@ -6338,6 +7634,15 @@ __metadata: languageName: node linkType: hard +"acorn@npm:^8.0.0": + version: 8.11.3 + resolution: "acorn@npm:8.11.3" + bin: + acorn: bin/acorn + checksum: 76d8e7d559512566b43ab4aadc374f11f563f0a9e21626dd59cb2888444e9445923ae9f3699972767f18af61df89cd89f5eaaf772d1327b055b45cb829b4a88c + languageName: node + linkType: hard + "acorn@npm:^8.0.4, acorn@npm:^8.2.4, acorn@npm:^8.4.1, acorn@npm:^8.7.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": version: 8.10.0 resolution: "acorn@npm:8.10.0" @@ -6506,18 +7811,41 @@ __metadata: languageName: node linkType: hard -"algoliasearch-helper@npm:^3.10.0": - version: 3.14.1 - resolution: "algoliasearch-helper@npm:3.14.1" +"algoliasearch-helper@npm:^3.13.3": + version: 3.18.0 + resolution: "algoliasearch-helper@npm:3.18.0" dependencies: "@algolia/events": ^4.0.1 peerDependencies: algoliasearch: ">= 3.1 < 6" - checksum: 30b0ebba46450cb44ec1625b1425d9c4f7bf0660d6d5dd0448f98d690418bd709c19ee6c1631293333922cdcc22cd5621d0bf48407dbd04273810c61affdc2f2 + checksum: 07b924b0aa3d134065865dc262439d87ec8cf1e1c80800931c6f0c12930bbb05a50f249f6b1d34762534cd05707868c9f4b9f5447756c660ad9c601029da9797 + languageName: node + linkType: hard + +"algoliasearch@npm:^4.18.0": + version: 4.23.3 + resolution: "algoliasearch@npm:4.23.3" + dependencies: + "@algolia/cache-browser-local-storage": 4.23.3 + "@algolia/cache-common": 4.23.3 + "@algolia/cache-in-memory": 4.23.3 + "@algolia/client-account": 4.23.3 + "@algolia/client-analytics": 4.23.3 + "@algolia/client-common": 4.23.3 + "@algolia/client-personalization": 4.23.3 + "@algolia/client-search": 4.23.3 + "@algolia/logger-common": 4.23.3 + "@algolia/logger-console": 4.23.3 + "@algolia/recommend": 4.23.3 + "@algolia/requester-browser-xhr": 4.23.3 + "@algolia/requester-common": 4.23.3 + "@algolia/requester-node-http": 4.23.3 + "@algolia/transporter": 4.23.3 + checksum: e5035b1234941b48821727feef38cb8438a0aab6343f23138392180f3de13769e0b3bc42f9fa34a7573c16c988a4e7897a5335be6e729803d749147dc04bf807 languageName: node linkType: hard -"algoliasearch@npm:^4.13.1, algoliasearch@npm:^4.19.1": +"algoliasearch@npm:^4.19.1": version: 4.20.0 resolution: "algoliasearch@npm:4.20.0" dependencies: @@ -6551,7 +7879,7 @@ __metadata: languageName: node linkType: hard -"ansi-align@npm:^3.0.0, ansi-align@npm:^3.0.1": +"ansi-align@npm:^3.0.1": version: 3.0.1 resolution: "ansi-align@npm:3.0.1" dependencies: @@ -6815,7 +8143,7 @@ __metadata: languageName: node linkType: hard -"asap@npm:^2.0.0, asap@npm:^2.0.3, asap@npm:~2.0.3": +"asap@npm:^2.0.0, asap@npm:^2.0.3": version: 2.0.6 resolution: "asap@npm:2.0.6" checksum: b296c92c4b969e973260e47523207cd5769abd27c245a68c26dc7a0fe8053c55bb04360237cb51cab1df52be939da77150ace99ad331fb7fb13b3423ed73ff3d @@ -6832,7 +8160,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"astring@npm:^1.8.1": +"astring@npm:^1.8.0, astring@npm:^1.8.1": version: 1.8.6 resolution: "astring@npm:1.8.6" bin: @@ -6925,7 +8253,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"autoprefixer@npm:^10.4.12, autoprefixer@npm:^10.4.7": +"autoprefixer@npm:^10.4.12": version: 10.4.15 resolution: "autoprefixer@npm:10.4.15" dependencies: @@ -6943,6 +8271,24 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"autoprefixer@npm:^10.4.14": + version: 10.4.19 + resolution: "autoprefixer@npm:10.4.19" + dependencies: + browserslist: ^4.23.0 + caniuse-lite: ^1.0.30001599 + fraction.js: ^4.3.7 + normalize-range: ^0.1.2 + picocolors: ^1.0.0 + postcss-value-parser: ^4.2.0 + peerDependencies: + postcss: ^8.1.0 + bin: + autoprefixer: bin/autoprefixer + checksum: 3a4bc5bace05e057396dca2b306503efc175e90e8f2abf5472d3130b72da1d54d97c0ee05df21bf04fe66a7df93fd8c8ec0f1aca72a165f4701a02531abcbf11 + languageName: node + linkType: hard + "available-typed-arrays@npm:^1.0.5": version: 1.0.5 resolution: "available-typed-arrays@npm:1.0.5" @@ -6977,15 +8323,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"axios@npm:^0.25.0": - version: 0.25.0 - resolution: "axios@npm:0.25.0" - dependencies: - follow-redirects: ^1.14.7 - checksum: 2a8a3787c05f2a0c9c3878f49782357e2a9f38945b93018fb0c4fd788171c43dceefbb577988628e09fea53952744d1ecebde234b561f1e703aa43e0a598a3ad - languageName: node - linkType: hard - "babel-jest@npm:^29.7.0": version: 29.7.0 resolution: "babel-jest@npm:29.7.0" @@ -7003,30 +8340,16 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"babel-loader@npm:^8.2.5": - version: 8.3.0 - resolution: "babel-loader@npm:8.3.0" - dependencies: - find-cache-dir: ^3.3.1 - loader-utils: ^2.0.0 - make-dir: ^3.1.0 - schema-utils: ^2.6.5 - peerDependencies: - "@babel/core": ^7.0.0 - webpack: ">=2" - checksum: d48bcf9e030e598656ad3ff5fb85967db2eaaf38af5b4a4b99d25618a2057f9f100e6b231af2a46c1913206db506115ca7a8cbdf52c9c73d767070dae4352ab5 - languageName: node - linkType: hard - -"babel-plugin-apply-mdx-type-prop@npm:1.6.22": - version: 1.6.22 - resolution: "babel-plugin-apply-mdx-type-prop@npm:1.6.22" +"babel-loader@npm:^9.1.3": + version: 9.1.3 + resolution: "babel-loader@npm:9.1.3" dependencies: - "@babel/helper-plugin-utils": 7.10.4 - "@mdx-js/util": 1.6.22 + find-cache-dir: ^4.0.0 + schema-utils: ^4.0.0 peerDependencies: - "@babel/core": ^7.11.6 - checksum: 43e2100164a8f3e46fddd76afcbfb1f02cbebd5612cfe63f3d344a740b0afbdc4d2bf5659cffe9323dd2554c7b86b23ebedae9dadcec353b6594f4292a1a28e2 + "@babel/core": ^7.12.0 + webpack: ">=5" + checksum: b168dde5b8cf11206513371a79f86bb3faa7c714e6ec9fffd420876b61f3d7f5f4b976431095ef6a14bc4d324505126deb91045fd41e312ba49f4deaa166fe28 languageName: node linkType: hard @@ -7039,15 +8362,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"babel-plugin-extract-import-names@npm:1.6.22": - version: 1.6.22 - resolution: "babel-plugin-extract-import-names@npm:1.6.22" - dependencies: - "@babel/helper-plugin-utils": 7.10.4 - checksum: 145ccf09c96d36411d340e78086555f8d4d5924ea39fcb0eca461c066cfa98bc4344982bb35eb85d054ef88f8d4dfc0205ba27370c1d8fcc78191b02908d044d - languageName: node - linkType: hard - "babel-plugin-istanbul@npm:^6.1.1": version: 6.1.1 resolution: "babel-plugin-istanbul@npm:6.1.1" @@ -7073,6 +8387,19 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"babel-plugin-polyfill-corejs2@npm:^0.4.10": + version: 0.4.10 + resolution: "babel-plugin-polyfill-corejs2@npm:0.4.10" + dependencies: + "@babel/compat-data": ^7.22.6 + "@babel/helper-define-polyfill-provider": ^0.6.1 + semver: ^6.3.1 + peerDependencies: + "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 + checksum: 2c0e4868789152f50db306f4957fa7934876cefb51d5d86436595f0b091539e45ce0e9c0125b5db2d71f913b29cd48ae76b8e942ba28fcf2273e084f54664a1c + languageName: node + linkType: hard + "babel-plugin-polyfill-corejs2@npm:^0.4.5": version: 0.4.5 resolution: "babel-plugin-polyfill-corejs2@npm:0.4.5" @@ -7086,6 +8413,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"babel-plugin-polyfill-corejs3@npm:^0.10.1, babel-plugin-polyfill-corejs3@npm:^0.10.4": + version: 0.10.4 + resolution: "babel-plugin-polyfill-corejs3@npm:0.10.4" + dependencies: + "@babel/helper-define-polyfill-provider": ^0.6.1 + core-js-compat: ^3.36.1 + peerDependencies: + "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 + checksum: b96a54495f7cc8b3797251c8c15f5ed015edddc3110fc122f6b32c94bec33af1e8bc56fa99091808f500bde0cccaaa266889cdc5935d9e6e9cf09898214f02dd + languageName: node + linkType: hard + "babel-plugin-polyfill-corejs3@npm:^0.8.3": version: 0.8.3 resolution: "babel-plugin-polyfill-corejs3@npm:0.8.3" @@ -7109,6 +8448,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"babel-plugin-polyfill-regenerator@npm:^0.6.1": + version: 0.6.1 + resolution: "babel-plugin-polyfill-regenerator@npm:0.6.1" + dependencies: + "@babel/helper-define-polyfill-provider": ^0.6.1 + peerDependencies: + "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 + checksum: 9df4a8e9939dd419fed3d9ea26594b4479f2968f37c225e1b2aa463001d7721f5537740e6622909d2a570b61cec23256924a1701404fc9d6fd4474d3e845cedb + languageName: node + linkType: hard + "babel-plugin-source-map-support@npm:^2.1.3": version: 2.2.0 resolution: "babel-plugin-source-map-support@npm:2.2.0" @@ -7152,10 +8502,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"bail@npm:^1.0.0": - version: 1.0.5 - resolution: "bail@npm:1.0.5" - checksum: 6c334940d7eaa4e656a12fb12407b6555649b6deb6df04270fa806e0da82684ebe4a4e47815b271c794b40f8d6fa286e0c248b14ddbabb324a917fab09b7301a +"bail@npm:^2.0.0": + version: 2.0.2 + resolution: "bail@npm:2.0.2" + checksum: aab4e8ccdc8d762bf3fdfce8e706601695620c0c2eda256dd85088dc0be3cfd7ff126f6e99c2bee1f24f5d418414aacf09d7f9702f16d6963df2fa488cda8824 languageName: node linkType: hard @@ -7166,13 +8516,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"base16@npm:^1.0.0": - version: 1.0.0 - resolution: "base16@npm:1.0.0" - checksum: 0cd449a2db0f0f957e4b6b57e33bc43c9e20d4f1dd744065db94b5da35e8e71fa4dc4bc7a901e59a84d5f8b6936e3c520e2471787f667fc155fb0f50d8540f5d - languageName: node - linkType: hard - "base64-arraybuffer-es6@npm:^0.3.1": version: 0.3.1 resolution: "base64-arraybuffer-es6@npm:0.3.1" @@ -7328,22 +8671,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"boxen@npm:^5.0.0": - version: 5.1.2 - resolution: "boxen@npm:5.1.2" - dependencies: - ansi-align: ^3.0.0 - camelcase: ^6.2.0 - chalk: ^4.1.0 - cli-boxes: ^2.2.1 - string-width: ^4.2.2 - type-fest: ^0.20.2 - widest-line: ^3.1.0 - wrap-ansi: ^7.0.0 - checksum: 82d03e42a72576ff235123f17b7c505372fe05c83f75f61e7d4fa4bcb393897ec95ce766fecb8f26b915f0f7a7227d66e5ec7cef43f5b2bd9d3aeed47ec55877 - languageName: node - linkType: hard - "boxen@npm:^6.2.1": version: 6.2.1 resolution: "boxen@npm:6.2.1" @@ -7360,6 +8687,22 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"boxen@npm:^7.0.0": + version: 7.1.1 + resolution: "boxen@npm:7.1.1" + dependencies: + ansi-align: ^3.0.1 + camelcase: ^7.0.1 + chalk: ^5.2.0 + cli-boxes: ^3.0.0 + string-width: ^5.1.2 + type-fest: ^2.13.0 + widest-line: ^4.0.1 + wrap-ansi: ^8.1.0 + checksum: ad8833d5f2845b0a728fdf8a0bc1505dff0c518edcb0fd56979a08774b1f26cf48b71e66532179ccdfb9ed95b64aa008689cca26f7776f93f002b8000a683d76 + languageName: node + linkType: hard + "bplist-parser@npm:^0.2.0": version: 0.2.0 resolution: "bplist-parser@npm:0.2.0" @@ -7413,7 +8756,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"browserslist@npm:^4.0.0, browserslist@npm:^4.14.5, browserslist@npm:^4.18.1, browserslist@npm:^4.21.10, browserslist@npm:^4.21.4, browserslist@npm:^4.22.2": +"browserslist@npm:^4.0.0, browserslist@npm:^4.18.1, browserslist@npm:^4.21.10, browserslist@npm:^4.21.4, browserslist@npm:^4.22.2": version: 4.22.3 resolution: "browserslist@npm:4.22.3" dependencies: @@ -7427,6 +8770,20 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"browserslist@npm:^4.23.0": + version: 4.23.0 + resolution: "browserslist@npm:4.23.0" + dependencies: + caniuse-lite: ^1.0.30001587 + electron-to-chromium: ^1.4.668 + node-releases: ^2.0.14 + update-browserslist-db: ^1.0.13 + bin: + browserslist: cli.js + checksum: 436f49e796782ca751ebab7edc010cfc9c29f68536f387666cd70ea22f7105563f04dd62c6ff89cb24cc3254d17cba385f979eeeb3484d43e012412ff7e75def + languageName: node + linkType: hard + "bs-logger@npm:0.x": version: 0.2.6 resolution: "bs-logger@npm:0.2.6" @@ -7605,18 +8962,25 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"cacheable-request@npm:^6.0.0": - version: 6.1.0 - resolution: "cacheable-request@npm:6.1.0" +"cacheable-lookup@npm:^7.0.0": + version: 7.0.0 + resolution: "cacheable-lookup@npm:7.0.0" + checksum: 9e2856763fc0a7347ab34d704c010440b819d4bb5e3593b664381b7433e942dd22e67ee5581f12256f908e79b82d30b86ebbacf40a081bfe10ee93fbfbc2d6a9 + languageName: node + linkType: hard + +"cacheable-request@npm:^10.2.8": + version: 10.2.14 + resolution: "cacheable-request@npm:10.2.14" dependencies: - clone-response: ^1.0.2 - get-stream: ^5.1.0 - http-cache-semantics: ^4.0.0 - keyv: ^3.0.0 - lowercase-keys: ^2.0.0 - normalize-url: ^4.1.0 - responselike: ^1.0.2 - checksum: b510b237b18d17e89942e9ee2d2a077cb38db03f12167fd100932dfa8fc963424bfae0bfa1598df4ae16c944a5484e43e03df8f32105b04395ee9495e9e4e9f1 + "@types/http-cache-semantics": ^4.0.2 + get-stream: ^6.0.1 + http-cache-semantics: ^4.1.1 + keyv: ^4.5.3 + mimic-response: ^4.0.0 + normalize-url: ^8.0.0 + responselike: ^3.0.0 + checksum: 56f2b8e1c497c91f8391f0b099d19907a7dde25e71087e622b23e45fc8061736c2a6964ef121b16f377c3c61079cf8dc17320ab54004209d1343e4d26aba7015 languageName: node linkType: hard @@ -7669,13 +9033,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"camelcase-css@npm:2.0.1": - version: 2.0.1 - resolution: "camelcase-css@npm:2.0.1" - checksum: 1cec2b3b3dcb5026688a470b00299a8db7d904c4802845c353dbd12d9d248d3346949a814d83bfd988d4d2e5b9904c07efe76fecd195a1d4f05b543e7c0b56b1 - languageName: node - linkType: hard - "camelcase-keys@npm:^6.2.2": version: 6.2.2 resolution: "camelcase-keys@npm:6.2.2" @@ -7713,7 +9070,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"camelcase@npm:^7.0.0": +"camelcase@npm:^7.0.0, camelcase@npm:^7.0.1": version: 7.0.1 resolution: "camelcase@npm:7.0.1" checksum: 86ab8f3ebf08bcdbe605a211a242f00ed30d8bfb77dab4ebb744dd36efbc84432d1c4adb28975ba87a1b8be40a80fbd1e60e2f06565315918fa7350011a26d3d @@ -7739,6 +9096,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"caniuse-lite@npm:^1.0.30001587, caniuse-lite@npm:^1.0.30001599": + version: 1.0.30001612 + resolution: "caniuse-lite@npm:1.0.30001612" + checksum: 2b6ab6a19c72bdf8dccac824944e828a2a1fae52c6dfeb2d64ccecfd60d0466d2e5a392e996da2150d92850188a5034666dceed34a38d978177f6934e0bf106d + languageName: node + linkType: hard + "casparcg-connection@npm:6.2.0": version: 6.2.0 resolution: "casparcg-connection@npm:6.2.0" @@ -7772,10 +9136,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"ccount@npm:^1.0.0": - version: 1.1.0 - resolution: "ccount@npm:1.1.0" - checksum: b335a79d0aa4308919cf7507babcfa04ac63d389ebed49dbf26990d4607c8a4713cde93cc83e707d84571ddfe1e7615dad248be9bc422ae4c188210f71b08b78 +"ccount@npm:^2.0.0": + version: 2.0.1 + resolution: "ccount@npm:2.0.1" + checksum: 48193dada54c9e260e0acf57fc16171a225305548f9ad20d5471e0f7a8c026aedd8747091dccb0d900cde7df4e4ddbd235df0d8de4a64c71b12f0d3303eeafd4 languageName: node linkType: hard @@ -7799,7 +9163,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"chalk@npm:5.3.0": +"chalk@npm:5.3.0, chalk@npm:^5.0.1, chalk@npm:^5.2.0": version: 5.3.0 resolution: "chalk@npm:5.3.0" checksum: 623922e077b7d1e9dedaea6f8b9e9352921f8ae3afe739132e0e00c275971bdd331268183b2628cf4ab1727c45ea1f28d7e24ac23ce1db1eb653c414ca8a5a80 @@ -7824,24 +9188,31 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"character-entities-legacy@npm:^1.0.0": - version: 1.1.4 - resolution: "character-entities-legacy@npm:1.1.4" - checksum: fe03a82c154414da3a0c8ab3188e4237ec68006cbcd681cf23c7cfb9502a0e76cd30ab69a2e50857ca10d984d57de3b307680fff5328ccd427f400e559c3a811 +"character-entities-html4@npm:^2.0.0": + version: 2.1.0 + resolution: "character-entities-html4@npm:2.1.0" + checksum: 7034aa7c7fa90309667f6dd50499c8a760c3d3a6fb159adb4e0bada0107d194551cdbad0714302f62d06ce4ed68565c8c2e15fdef2e8f8764eb63fa92b34b11d languageName: node linkType: hard -"character-entities@npm:^1.0.0": - version: 1.2.4 - resolution: "character-entities@npm:1.2.4" - checksum: e1545716571ead57beac008433c1ff69517cd8ca5b336889321c5b8ff4a99c29b65589a701e9c086cda8a5e346a67295e2684f6c7ea96819fe85cbf49bf8686d +"character-entities-legacy@npm:^3.0.0": + version: 3.0.0 + resolution: "character-entities-legacy@npm:3.0.0" + checksum: 7582af055cb488b626d364b7d7a4e46b06abd526fb63c0e4eb35bcb9c9799cc4f76b39f34fdccef2d1174ac95e53e9ab355aae83227c1a2505877893fce77731 languageName: node linkType: hard -"character-reference-invalid@npm:^1.0.0": - version: 1.1.4 - resolution: "character-reference-invalid@npm:1.1.4" - checksum: 20274574c70e05e2f81135f3b93285536bc8ff70f37f0809b0d17791a832838f1e49938382899ed4cb444e5bbd4314ca1415231344ba29f4222ce2ccf24fea0b +"character-entities@npm:^2.0.0": + version: 2.0.2 + resolution: "character-entities@npm:2.0.2" + checksum: cf1643814023697f725e47328fcec17923b8f1799102a8a79c1514e894815651794a2bffd84bb1b3a4b124b050154e4529ed6e81f7c8068a734aecf07a6d3def + languageName: node + linkType: hard + +"character-reference-invalid@npm:^2.0.0": + version: 2.0.1 + resolution: "character-reference-invalid@npm:2.0.1" + checksum: 98d3b1a52ae510b7329e6ee7f6210df14f1e318c5415975d4c9e7ee0ef4c07875d47c6e74230c64551f12f556b4a8ccc24d9f3691a2aa197019e72a95e9297ee languageName: node linkType: hard @@ -7942,7 +9313,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"clean-css@npm:^5.2.2, clean-css@npm:^5.3.0": +"clean-css@npm:^5.2.2": version: 5.3.2 resolution: "clean-css@npm:5.3.2" dependencies: @@ -7951,6 +9322,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"clean-css@npm:^5.3.2, clean-css@npm:~5.3.2": + version: 5.3.3 + resolution: "clean-css@npm:5.3.3" + dependencies: + source-map: ~0.6.0 + checksum: 941987c14860dd7d346d5cf121a82fd2caf8344160b1565c5387f7ccca4bbcaf885bace961be37c4f4713ce2d8c488dd89483c1add47bb779790edbfdcc79cbc + languageName: node + linkType: hard + "clean-stack@npm:^2.0.0": version: 2.2.0 resolution: "clean-stack@npm:2.2.0" @@ -7958,13 +9338,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"cli-boxes@npm:^2.2.1": - version: 2.2.1 - resolution: "cli-boxes@npm:2.2.1" - checksum: be79f8ec23a558b49e01311b39a1ea01243ecee30539c880cf14bf518a12e223ef40c57ead0cb44f509bffdffc5c129c746cd50d863ab879385370112af4f585 - languageName: node - linkType: hard - "cli-boxes@npm:^3.0.0": version: 3.0.0 resolution: "cli-boxes@npm:3.0.0" @@ -8017,16 +9390,16 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"cli-table3@npm:^0.6.2": - version: 0.6.3 - resolution: "cli-table3@npm:0.6.3" +"cli-table3@npm:^0.6.3": + version: 0.6.4 + resolution: "cli-table3@npm:0.6.4" dependencies: "@colors/colors": 1.5.0 string-width: ^4.2.0 dependenciesMeta: "@colors/colors": optional: true - checksum: 09897f68467973f827c04e7eaadf13b55f8aec49ecd6647cc276386ea660059322e2dd8020a8b6b84d422dbdd619597046fa89cbbbdc95b2cea149a2df7c096c + checksum: 0942d9977c05b31e9c7e0172276246b3ac2124c2929451851c01dbf5fc9b3d40cc4e1c9d468ff26dd3cfd18617963fe227b4cfeeae2881b70f302d69d792b5bb languageName: node linkType: hard @@ -8103,6 +9476,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"clsx@npm:^2.0.0": + version: 2.1.0 + resolution: "clsx@npm:2.1.0" + checksum: 43fefc29b6b49c9476fbce4f8b1cc75c27b67747738e598e6651dd40d63692135dc60b18fa1c5b78a2a9ba8ae6fd2055a068924b94e20b42039bd53b78b98e1d + languageName: node + linkType: hard + "cmd-shim@npm:5.0.0, cmd-shim@npm:^5.0.0": version: 5.0.0 resolution: "cmd-shim@npm:5.0.0" @@ -8137,10 +9517,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"collapse-white-space@npm:^1.0.2": - version: 1.0.6 - resolution: "collapse-white-space@npm:1.0.6" - checksum: 9673fb797952c5c888341435596c69388b22cd5560c8cd3f40edb72734a9c820f56a7c9525166bcb7068b5d5805372e6fd0c4b9f2869782ad070cb5d3faf26e7 +"collapse-white-space@npm:^2.0.0": + version: 2.1.0 + resolution: "collapse-white-space@npm:2.1.0" + checksum: c8978b1f4e7d68bf846cfdba6c6689ce8910511df7d331eb6e6757e51ceffb52768d59a28db26186c91dcf9594955b59be9f8ccd473c485790f5d8b90dc6726f languageName: node linkType: hard @@ -8262,10 +9642,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"comma-separated-tokens@npm:^1.0.0": - version: 1.0.8 - resolution: "comma-separated-tokens@npm:1.0.8" - checksum: 0adcb07174fa4d08cf0f5c8e3aec40a36b5ff0c2c720e5e23f50fe02e6789d1d00a67036c80e0c1e1539f41d3e7f0101b074039dd833b4e4a59031b659d6ca0d +"comma-separated-tokens@npm:^2.0.0": + version: 2.0.3 + resolution: "comma-separated-tokens@npm:2.0.3" + checksum: e3bf9e0332a5c45f49b90e79bcdb4a7a85f28d6a6f0876a94f1bb9b2bfbdbbb9292aac50e1e742d8c0db1e62a0229a106f57917e2d067fca951d81737651700d languageName: node linkType: hard @@ -8283,6 +9663,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"commander@npm:^10.0.0": + version: 10.0.1 + resolution: "commander@npm:10.0.1" + checksum: 436901d64a818295803c1996cd856621a74f30b9f9e28a588e726b2b1670665bccd7c1a77007ebf328729f0139838a88a19265858a0fa7a8728c4656796db948 + languageName: node + linkType: hard + "commander@npm:^2.20.0": version: 2.20.3 resolution: "commander@npm:2.20.3" @@ -8318,10 +9705,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"commondir@npm:^1.0.1": - version: 1.0.1 - resolution: "commondir@npm:1.0.1" - checksum: 59715f2fc456a73f68826285718503340b9f0dd89bfffc42749906c5cf3d4277ef11ef1cca0350d0e79204f00f1f6d83851ececc9095dc88512a697ac0b9bdcb +"common-path-prefix@npm:^3.0.0": + version: 3.0.0 + resolution: "common-path-prefix@npm:3.0.0" + checksum: fdb3c4f54e51e70d417ccd950c07f757582de800c0678ca388aedefefc84982039f346f9fd9a1252d08d2da9e9ef4019f580a1d1d3a10da031e4bb3c924c5818 languageName: node linkType: hard @@ -8413,17 +9800,26 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"configstore@npm:^5.0.1": - version: 5.0.1 - resolution: "configstore@npm:5.0.1" +"config-chain@npm:^1.1.11": + version: 1.1.13 + resolution: "config-chain@npm:1.1.13" dependencies: - dot-prop: ^5.2.0 - graceful-fs: ^4.1.2 - make-dir: ^3.0.0 - unique-string: ^2.0.0 - write-file-atomic: ^3.0.0 - xdg-basedir: ^4.0.0 - checksum: 60ef65d493b63f96e14b11ba7ec072fdbf3d40110a94fb7199d1c287761bdea5c5244e76b2596325f30c1b652213aa75de96ea20afd4a5f82065e61ea090988e + ini: ^1.3.4 + proto-list: ~1.2.1 + checksum: 828137a28e7c2fc4b7fb229bd0cd6c1397bcf83434de54347e608154008f411749041ee392cbe42fab6307e02de4c12480260bf769b7d44b778fdea3839eafab + languageName: node + linkType: hard + +"configstore@npm:^6.0.0": + version: 6.0.0 + resolution: "configstore@npm:6.0.0" + dependencies: + dot-prop: ^6.0.1 + graceful-fs: ^4.2.6 + unique-string: ^3.0.0 + write-file-atomic: ^3.0.3 + xdg-basedir: ^5.0.1 + checksum: 81995351c10bc04c58507f17748477aeac6f47465109d20e3534cebc881d22e927cfd29e73dd852c46c55f62c2b7be4cd1fe6eb3a93ba51f7f9813c218f9bae0 languageName: node linkType: hard @@ -8617,7 +10013,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"copy-text-to-clipboard@npm:^3.0.1": +"copy-text-to-clipboard@npm:^3.2.0": version: 3.2.0 resolution: "copy-text-to-clipboard@npm:3.2.0" checksum: df7115c197a166d51f59e4e20ab2a68a855ae8746d25ff149b5465c694d9a405c7e6684b73a9f87ba8d653070164e229c15dfdb9fd77c30be1ff0da569661060 @@ -8667,6 +10063,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"core-js-compat@npm:^3.36.1": + version: 3.37.0 + resolution: "core-js-compat@npm:3.37.0" + dependencies: + browserslist: ^4.23.0 + checksum: cab5078e98625f889fd9bbbb19e84cb408f31c87e68302d380db0d26ae8e35c1b38cde084358ff345d4aa461af5f3c60d8a913a5b30bff3a83b4b7859374db36 + languageName: node + linkType: hard + "core-js-pure@npm:^3.30.2": version: 3.32.2 resolution: "core-js-pure@npm:3.32.2" @@ -8674,10 +10079,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"core-js@npm:^3.23.3": - version: 3.32.2 - resolution: "core-js@npm:3.32.2" - checksum: d6fac7e8eb054eefc211c76cd0a0ff07447a917122757d085f469f046ec888d122409c7db1a9601c3eb5fa767608ed380bcd219eace02bdf973da155680edeec +"core-js@npm:^3.31.1": + version: 3.37.0 + resolution: "core-js@npm:3.37.0" + checksum: 212c3e9b3fc277dbb63739ef58a61c5709ccd0b36f09c3ce6946aa91fa180c60f57f976d4a5fdb9cda0c6cb55417379ba5a008fc3a1384ec94ec8ec61826469d languageName: node linkType: hard @@ -8727,7 +10132,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"cosmiconfig@npm:^8.2.0": +"cosmiconfig@npm:^8.3.5": version: 8.3.6 resolution: "cosmiconfig@npm:8.3.6" dependencies: @@ -8777,15 +10182,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"cross-fetch@npm:^3.1.5": - version: 3.1.8 - resolution: "cross-fetch@npm:3.1.8" - dependencies: - node-fetch: ^2.6.12 - checksum: 78f993fa099eaaa041122ab037fe9503ecbbcb9daef234d1d2e0b9230a983f64d645d088c464e21a247b825a08dc444a6e7064adfa93536d3a9454b4745b3632 - languageName: node - linkType: hard - "cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": version: 7.0.3 resolution: "cross-spawn@npm:7.0.3" @@ -8829,25 +10225,31 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"css-loader@npm:^6.7.1": - version: 6.8.1 - resolution: "css-loader@npm:6.8.1" +"css-loader@npm:^6.8.1": + version: 6.11.0 + resolution: "css-loader@npm:6.11.0" dependencies: icss-utils: ^5.1.0 - postcss: ^8.4.21 - postcss-modules-extract-imports: ^3.0.0 - postcss-modules-local-by-default: ^4.0.3 - postcss-modules-scope: ^3.0.0 + postcss: ^8.4.33 + postcss-modules-extract-imports: ^3.1.0 + postcss-modules-local-by-default: ^4.0.5 + postcss-modules-scope: ^3.2.0 postcss-modules-values: ^4.0.0 postcss-value-parser: ^4.2.0 - semver: ^7.3.8 + semver: ^7.5.4 peerDependencies: + "@rspack/core": 0.x || 1.x webpack: ^5.0.0 - checksum: 7c1784247bdbe76dc5c55fb1ac84f1d4177a74c47259942c9cfdb7a8e6baef11967a0bc85ac285f26bd26d5059decb848af8154a03fdb4f4894f41212f45eef3 + peerDependenciesMeta: + "@rspack/core": + optional: true + webpack: + optional: true + checksum: 5c8d35975a7121334905394e88e28f05df72f037dbed2fb8fec4be5f0b313ae73a13894ba791867d4a4190c35896da84a7fd0c54fb426db55d85ba5e714edbe3 languageName: node linkType: hard -"css-minimizer-webpack-plugin@npm:^4.0.0": +"css-minimizer-webpack-plugin@npm:^4.2.2": version: 4.2.2 resolution: "css-minimizer-webpack-plugin@npm:4.2.2" dependencies: @@ -8964,7 +10366,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"cssnano-preset-advanced@npm:^5.3.8": +"cssnano-preset-advanced@npm:^5.3.10": version: 5.3.10 resolution: "cssnano-preset-advanced@npm:5.3.10" dependencies: @@ -9028,7 +10430,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"cssnano@npm:^5.1.12, cssnano@npm:^5.1.8": +"cssnano@npm:^5.1.15, cssnano@npm:^5.1.8": version: 5.1.15 resolution: "cssnano@npm:5.1.15" dependencies: @@ -9124,6 +10526,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"debounce@npm:^1.2.1": + version: 1.2.1 + resolution: "debounce@npm:1.2.1" + checksum: 682a89506d9e54fb109526f4da255c5546102fbb8e3ae75eef3b04effaf5d4853756aee97475cd4650641869794e44f410eeb20ace2b18ea592287ab2038519e + languageName: node + linkType: hard + "debug@npm:2.6.9, debug@npm:^2.6.0": version: 2.6.9 resolution: "debug@npm:2.6.9" @@ -9133,7 +10542,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.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.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -9192,12 +10601,12 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"decompress-response@npm:^3.3.0": - version: 3.3.0 - resolution: "decompress-response@npm:3.3.0" +"decode-named-character-reference@npm:^1.0.0": + version: 1.0.2 + resolution: "decode-named-character-reference@npm:1.0.2" dependencies: - mimic-response: ^1.0.0 - checksum: 952552ac3bd7de2fc18015086b09468645c9638d98a551305e485230ada278c039c91116e946d07894b39ee53c0f0d5b6473f25a224029344354513b412d7380 + character-entities: ^2.0.0 + checksum: f4c71d3b93105f20076052f9cb1523a22a9c796b8296cd35eef1ca54239c78d182c136a848b83ff8da2071e3ae2b1d300bf29d00650a6d6e675438cc31b11d78 languageName: node linkType: hard @@ -9290,14 +10699,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"defer-to-connect@npm:^1.0.1": - version: 1.1.3 - resolution: "defer-to-connect@npm:1.1.3" - checksum: 9491b301dcfa04956f989481ba7a43c2231044206269eb4ab64a52d6639ee15b1252262a789eb4239fb46ab63e44d4e408641bae8e0793d640aee55398cb3930 - languageName: node - linkType: hard - -"defer-to-connect@npm:^2.0.0": +"defer-to-connect@npm:^2.0.0, defer-to-connect@npm:^2.0.1": version: 2.0.1 resolution: "defer-to-connect@npm:2.0.1" checksum: 8a9b50d2f25446c0bfefb55a48e90afd58f85b21bcf78e9207cd7b804354f6409032a1705c2491686e202e64fc05f147aa5aa45f9aa82627563f045937f5791b @@ -9398,6 +10800,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"dequal@npm:^2.0.0": + version: 2.0.3 + resolution: "dequal@npm:2.0.3" + checksum: 8679b850e1a3d0ebbc46ee780d5df7b478c23f335887464023a631d1b9af051ad4a6595a44220f9ff8ff95a8ddccf019b5ad778a976fd7bbf77383d36f412f90 + languageName: node + linkType: hard + "destroy@npm:1.2.0": version: 1.2.0 resolution: "destroy@npm:1.2.0" @@ -9405,15 +10814,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"detab@npm:2.0.4": - version: 2.0.4 - resolution: "detab@npm:2.0.4" - dependencies: - repeat-string: ^1.5.4 - checksum: 34b077521ecd4c6357d32ff7923be644d34aa6f6b7d717d40ec4a9168243eefaea2b512a75a460a6f70c31b0bbc31ff90f820a891803b4ddaf99e9d04d0d389d - languageName: node - linkType: hard - "detect-indent@npm:^5.0.0": version: 5.0.0 resolution: "detect-indent@npm:5.0.0" @@ -9448,7 +10848,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"detect-port@npm:^1.3.0": +"detect-port@npm:^1.5.1": version: 1.5.1 resolution: "detect-port@npm:1.5.1" dependencies: @@ -9461,6 +10861,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"devlop@npm:^1.0.0, devlop@npm:^1.1.0": + version: 1.1.0 + resolution: "devlop@npm:1.1.0" + dependencies: + dequal: ^2.0.0 + checksum: d2ff650bac0bb6ef08c48f3ba98640bb5fec5cce81e9957eb620408d1bab1204d382a45b785c6b3314dc867bb0684936b84c6867820da6db97cbb5d3c15dd185 + languageName: node + linkType: hard + "devtools-protocol@npm:0.0.1001819": version: 0.0.1001819 resolution: "devtools-protocol@npm:0.0.1001819" @@ -9657,7 +11066,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"dot-prop@npm:6.0.1": +"dot-prop@npm:6.0.1, dot-prop@npm:^6.0.1": version: 6.0.1 resolution: "dot-prop@npm:6.0.1" dependencies: @@ -9666,7 +11075,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"dot-prop@npm:^5.1.0, dot-prop@npm:^5.2.0": +"dot-prop@npm:^5.1.0": version: 5.3.0 resolution: "dot-prop@npm:5.3.0" dependencies: @@ -9682,13 +11091,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"duplexer3@npm:^0.1.4": - version: 0.1.5 - resolution: "duplexer3@npm:0.1.5" - checksum: e677cb4c48f031ca728601d6a20bf6aed4c629d69ef9643cb89c67583d673c4ec9317cc6427501f38bd8c368d3a18f173987cc02bd99d8cf8fe3d94259a22a20 - languageName: node - linkType: hard - "duplexer@npm:^0.1.1, duplexer@npm:^0.1.2": version: 0.1.2 resolution: "duplexer@npm:0.1.2" @@ -9793,6 +11195,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"electron-to-chromium@npm:^1.4.668": + version: 1.4.745 + resolution: "electron-to-chromium@npm:1.4.745" + checksum: f73b576108863cad160deb22b8e8c6754a8b16b22cda90cfce038a755f886be9c03fb8360bbd7c9d28ddd184800d0d6bd430a11f9289316145f0b28321dfe71d + languageName: node + linkType: hard + "emberplus-connection@npm:^0.2.1": version: 0.2.1 resolution: "emberplus-connection@npm:0.2.1" @@ -9828,6 +11237,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"emojilib@npm:^2.4.0": + version: 2.4.0 + resolution: "emojilib@npm:2.4.0" + checksum: ea241c342abda5a86ffd3a15d8f4871a616d485f700e03daea38c6ce38205847cea9f6ff8d5e962c00516b004949cc96c6e37b05559ea71a0a496faba53b56da + languageName: node + linkType: hard + "emojis-list@npm:^3.0.0": version: 3.0.0 resolution: "emojis-list@npm:3.0.0" @@ -9835,10 +11251,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"emoticon@npm:^3.2.0": - version: 3.2.0 - resolution: "emoticon@npm:3.2.0" - checksum: f30649d18b672ab3139e95cb04f77b2442feb95c99dc59372ff80fbfd639b2bf4018bc68ab0b549bd765aecf8230d7899c43f86cfcc7b6370c06c3232783e24f +"emoticon@npm:^4.0.1": + version: 4.0.1 + resolution: "emoticon@npm:4.0.1" + checksum: 991ab6421927601af4eb44036b60e3125759a4d81f32d2ad96b66e3491e2fdb6a026eeb6bffbfa66724592dca95235570785963607d16961ea73a62ecce715e2 languageName: node linkType: hard @@ -9881,13 +11297,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"enhanced-resolve@npm:^5.15.0": - version: 5.15.0 - resolution: "enhanced-resolve@npm:5.15.0" +"enhanced-resolve@npm:^5.16.0": + version: 5.16.0 + resolution: "enhanced-resolve@npm:5.16.0" dependencies: graceful-fs: ^4.2.4 tapable: ^2.2.0 - checksum: fbd8cdc9263be71cc737aa8a7d6c57b43d6aa38f6cc75dde6fcd3598a130cc465f979d2f4d01bb3bf475acb43817749c79f8eef9be048683602ca91ab52e4f11 + checksum: ccfd01850ecf2aa51e8554d539973319ff7d8a539ef1e0ba3460a0ccad6223c4ef6e19165ee64161b459cd8a48df10f52af4434c60023c65fde6afa32d475f7e languageName: node linkType: hard @@ -10119,10 +11535,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"escape-goat@npm:^2.0.0": - version: 2.1.1 - resolution: "escape-goat@npm:2.1.1" - checksum: ce05c70c20dd7007b60d2d644b625da5412325fdb57acf671ba06cb2ab3cd6789e2087026921a05b665b0a03fadee2955e7fc0b9a67da15a6551a980b260eba7 +"escape-goat@npm:^4.0.0": + version: 4.0.0 + resolution: "escape-goat@npm:4.0.0" + checksum: 7034e0025eec7b751074b837f10312c5b768493265bdad046347c0aadbc1e652776f7e5df94766473fecb5d3681169cc188fe9ccc1e22be53318c18be1671cc0 languageName: node linkType: hard @@ -10154,6 +11570,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"escape-string-regexp@npm:^5.0.0": + version: 5.0.0 + resolution: "escape-string-regexp@npm:5.0.0" + checksum: 20daabe197f3cb198ec28546deebcf24b3dbb1a5a269184381b3116d12f0532e06007f4bc8da25669d6a7f8efb68db0758df4cd981f57bc5b57f521a3e12c59e + languageName: node + linkType: hard + "escodegen@npm:^2.0.0": version: 2.1.0 resolution: "escodegen@npm:2.1.0" @@ -10413,6 +11836,65 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"estree-util-attach-comments@npm:^3.0.0": + version: 3.0.0 + resolution: "estree-util-attach-comments@npm:3.0.0" + dependencies: + "@types/estree": ^1.0.0 + checksum: 56254eaef39659e6351919ebc2ae53a37a09290a14571c19e373e9d5fad343a3403d9ad0c23ae465d6e7d08c3e572fd56fb8c793efe6434a261bf1489932dbd5 + languageName: node + linkType: hard + +"estree-util-build-jsx@npm:^3.0.0": + version: 3.0.1 + resolution: "estree-util-build-jsx@npm:3.0.1" + dependencies: + "@types/estree-jsx": ^1.0.0 + devlop: ^1.0.0 + estree-util-is-identifier-name: ^3.0.0 + estree-walker: ^3.0.0 + checksum: 185eff060eda2ba32cecd15904db4f5ba0681159fbdf54f0f6586cd9411e77e733861a833d0aee3415e1d1fd4b17edf08bc9e9872cee98e6ec7b0800e1a85064 + languageName: node + linkType: hard + +"estree-util-is-identifier-name@npm:^3.0.0": + version: 3.0.0 + resolution: "estree-util-is-identifier-name@npm:3.0.0" + checksum: ea3909f0188ea164af0aadeca87c087e3e5da78d76da5ae9c7954ff1340ea3e4679c4653bbf4299ffb70caa9b322218cc1128db2541f3d2976eb9704f9857787 + languageName: node + linkType: hard + +"estree-util-to-js@npm:^2.0.0": + version: 2.0.0 + resolution: "estree-util-to-js@npm:2.0.0" + dependencies: + "@types/estree-jsx": ^1.0.0 + astring: ^1.8.0 + source-map: ^0.7.0 + checksum: 833edc94ab9978e0918f90261e0a3361bf4564fec4901f326d2237a9235d3f5fc6482da3be5acc545e702c8c7cb8bc5de5c7c71ba3b080eb1975bcfdf3923d79 + languageName: node + linkType: hard + +"estree-util-value-to-estree@npm:^3.0.1": + version: 3.1.1 + resolution: "estree-util-value-to-estree@npm:3.1.1" + dependencies: + "@types/estree": ^1.0.0 + is-plain-obj: ^4.0.0 + checksum: 80e1d227ac80fab0b148c40427af31ad4dd37a3a4a0e0894d7975370284ea39566fe7df132f3454cf0e47adcc79b47ae0737464a85a413bce6f8d159336f8a37 + languageName: node + linkType: hard + +"estree-util-visit@npm:^2.0.0": + version: 2.0.0 + resolution: "estree-util-visit@npm:2.0.0" + dependencies: + "@types/estree-jsx": ^1.0.0 + "@types/unist": ^3.0.0 + checksum: 6444b38f224322945a6d19ea81a8828a0eec64aefb2bf1ea791fe20df496f7b7c543408d637df899e6a8e318b638f66226f16378a33c4c2b192ba5c3f891121f + languageName: node + linkType: hard + "estree-walker@npm:^1.0.1": version: 1.0.1 resolution: "estree-walker@npm:1.0.1" @@ -10420,6 +11902,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"estree-walker@npm:^3.0.0": + version: 3.0.3 + resolution: "estree-walker@npm:3.0.3" + dependencies: + "@types/estree": ^1.0.0 + checksum: a65728d5727b71de172c5df323385755a16c0fdab8234dc756c3854cfee343261ddfbb72a809a5660fac8c75d960bb3e21aa898c2d7e9b19bb298482ca58a3af + languageName: node + linkType: hard + "esutils@npm:^2.0.2": version: 2.0.3 resolution: "esutils@npm:2.0.3" @@ -10427,7 +11918,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"eta@npm:^2.0.0": +"eta@npm:^2.2.0": version: 2.2.0 resolution: "eta@npm:2.2.0" checksum: 6a09631481d4f26a9662a1eb736a65cc1cbc48e24935e6ff5d83a83b0cb509ea56d588d66d7c087d590601dc59bdabdac2356936b1b789d020eb0cf2d8304d54 @@ -10801,6 +12292,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"fault@npm:^2.0.0": + version: 2.0.1 + resolution: "fault@npm:2.0.1" + dependencies: + format: ^0.2.0 + checksum: c9b30f47d95769177130a9409976a899ed31eb598450fbad5b0d39f2f5f56d5f4a9ff9257e0bee8407cb0fc3ce37165657888c6aa6d78472e403893104329b72 + languageName: node + linkType: hard + "faye-websocket@npm:^0.11.3, faye-websocket@npm:^0.11.4": version: 0.11.4 resolution: "faye-websocket@npm:0.11.4" @@ -10819,37 +12319,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"fbemitter@npm:^3.0.0": - version: 3.0.0 - resolution: "fbemitter@npm:3.0.0" - dependencies: - fbjs: ^3.0.0 - checksum: 069690b8cdff3521ade3c9beb92ba0a38d818a86ef36dff8690e66749aef58809db4ac0d6938eb1cacea2dbef5f2a508952d455669590264cdc146bbe839f605 - languageName: node - linkType: hard - -"fbjs-css-vars@npm:^1.0.0": - version: 1.0.2 - resolution: "fbjs-css-vars@npm:1.0.2" - checksum: 72baf6d22c45b75109118b4daecb6c8016d4c83c8c0f23f683f22e9d7c21f32fff6201d288df46eb561e3c7d4bb4489b8ad140b7f56444c453ba407e8bd28511 - languageName: node - linkType: hard - -"fbjs@npm:^3.0.0, fbjs@npm:^3.0.1": - version: 3.0.5 - resolution: "fbjs@npm:3.0.5" - dependencies: - cross-fetch: ^3.1.5 - fbjs-css-vars: ^1.0.0 - loose-envify: ^1.0.0 - object-assign: ^4.1.0 - promise: ^7.1.1 - setimmediate: ^1.0.5 - ua-parser-js: ^1.0.35 - checksum: e609b5b64686bc96495a5c67728ed9b2710b9b3d695c5759c5f5e47c9483d1c323543ac777a86459e3694efc5712c6ce7212e944feb19752867d699568bb0e54 - languageName: node - linkType: hard - "fd-slicer@npm:~1.1.0": version: 1.1.0 resolution: "fd-slicer@npm:1.1.0" @@ -10981,14 +12450,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"find-cache-dir@npm:^3.3.1": - version: 3.3.2 - resolution: "find-cache-dir@npm:3.3.2" +"find-cache-dir@npm:^4.0.0": + version: 4.0.0 + resolution: "find-cache-dir@npm:4.0.0" dependencies: - commondir: ^1.0.1 - make-dir: ^3.0.2 - pkg-dir: ^4.1.0 - checksum: 1e61c2e64f5c0b1c535bd85939ae73b0e5773142713273818cc0b393ee3555fb0fd44e1a5b161b8b6c3e03e98c2fcc9c227d784850a13a90a8ab576869576817 + common-path-prefix: ^3.0.0 + pkg-dir: ^7.0.0 + checksum: 52a456a80deeb27daa3af6e06059b63bdb9cc4af4d845fc6d6229887e505ba913cd56000349caa60bc3aa59dacdb5b4c37903d4ba34c75102d83cab330b70d2f languageName: node linkType: hard @@ -11074,18 +12542,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"flux@npm:^4.0.1": - version: 4.0.4 - resolution: "flux@npm:4.0.4" - dependencies: - fbemitter: ^3.0.0 - fbjs: ^3.0.1 - peerDependencies: - react: ^15.0.2 || ^16.0.0 || ^17.0.0 - checksum: 8fa5c2f9322258de3e331f67c6f1078a7f91c4dec9dbe8a54c4b8a80eed19a4f91889028b768668af4a796e8f2ee75e461e1571b8615432a3920ae95cc4ff794 - languageName: node - linkType: hard - "fn.name@npm:1.x.x": version: 1.1.0 resolution: "fn.name@npm:1.1.0" @@ -11093,7 +12549,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.14.0, follow-redirects@npm:^1.14.7, follow-redirects@npm:^1.15.4": +"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.14.0, follow-redirects@npm:^1.15.4": version: 1.15.5 resolution: "follow-redirects@npm:1.15.5" peerDependenciesMeta: @@ -11167,6 +12623,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"form-data-encoder@npm:^2.1.2": + version: 2.1.4 + resolution: "form-data-encoder@npm:2.1.4" + checksum: e0b3e5950fb69b3f32c273944620f9861f1933df9d3e42066e038e26dfb343d0f4465de9f27e0ead1a09d9df20bc2eed06a63c2ca2f8f00949e7202bae9e29dd + languageName: node + linkType: hard + "form-data@npm:^2.5.0": version: 2.5.1 resolution: "form-data@npm:2.5.1" @@ -11200,6 +12663,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"format@npm:^0.2.0": + version: 0.2.2 + resolution: "format@npm:0.2.2" + checksum: 646a60e1336250d802509cf24fb801e43bd4a70a07510c816fa133aa42cdbc9c21e66e9cc0801bb183c5b031c9d68be62e7fbb6877756e52357850f92aa28799 + languageName: node + linkType: hard + "forwarded-parse@npm:^2.1.0": version: 2.1.2 resolution: "forwarded-parse@npm:2.1.2" @@ -11221,6 +12691,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"fraction.js@npm:^4.3.7": + version: 4.3.7 + resolution: "fraction.js@npm:4.3.7" + checksum: e1553ae3f08e3ba0e8c06e43a3ab20b319966dfb7ddb96fd9b5d0ee11a66571af7f993229c88ebbb0d4a816eb813a24ed48207b140d442a8f76f33763b8d1f3f + languageName: node + linkType: hard + "fresh@npm:0.5.2": version: 0.5.2 resolution: "fresh@npm:0.5.2" @@ -11235,7 +12712,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"fs-extra@npm:10.1.0, fs-extra@npm:^10.1.0": +"fs-extra@npm:10.1.0": version: 10.1.0 resolution: "fs-extra@npm:10.1.0" dependencies: @@ -11269,6 +12746,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"fs-extra@npm:^11.1.1": + version: 11.2.0 + resolution: "fs-extra@npm:11.2.0" + dependencies: + graceful-fs: ^4.2.0 + jsonfile: ^6.0.1 + universalify: ^2.0.0 + checksum: b12e42fa40ba47104202f57b8480dd098aa931c2724565e5e70779ab87605665594e76ee5fb00545f772ab9ace167fe06d2ab009c416dc8c842c5ae6df7aa7e8 + languageName: node + linkType: hard + "fs-extra@npm:~0.6.1": version: 0.6.4 resolution: "fs-extra@npm:0.6.4" @@ -11483,15 +12971,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"get-stream@npm:^4.1.0": - version: 4.1.0 - resolution: "get-stream@npm:4.1.0" - dependencies: - pump: ^3.0.0 - checksum: 443e1914170c15bd52ff8ea6eff6dfc6d712b031303e36302d2778e3de2506af9ee964d6124010f7818736dcfde05c04ba7ca6cc26883106e084357a17ae7d73 - languageName: node - linkType: hard - "get-stream@npm:^5.1.0": version: 5.2.0 resolution: "get-stream@npm:5.2.0" @@ -11583,7 +13062,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"github-slugger@npm:^1.4.0": +"github-slugger@npm:^1.5.0": version: 1.5.0 resolution: "github-slugger@npm:1.5.0" checksum: c70988224578b3bdaa25df65973ffc8c24594a77a28550c3636e495e49d17aef5cdb04c04fa3f1744babef98c61eecc6a43299a13ea7f3cc33d680bf9053ffbe @@ -11803,22 +13282,22 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"got@npm:^9.6.0": - version: 9.6.0 - resolution: "got@npm:9.6.0" +"got@npm:^12.1.0": + version: 12.6.1 + resolution: "got@npm:12.6.1" dependencies: - "@sindresorhus/is": ^0.14.0 - "@szmarczak/http-timer": ^1.1.2 - cacheable-request: ^6.0.0 - decompress-response: ^3.3.0 - duplexer3: ^0.1.4 - get-stream: ^4.1.0 - lowercase-keys: ^1.0.1 - mimic-response: ^1.0.1 - p-cancelable: ^1.0.0 - to-readable-stream: ^1.0.0 - url-parse-lax: ^3.0.0 - checksum: 941807bd9704bacf5eb401f0cc1212ffa1f67c6642f2d028fd75900471c221b1da2b8527f4553d2558f3faeda62ea1cf31665f8b002c6137f5de8732f07370b0 + "@sindresorhus/is": ^5.2.0 + "@szmarczak/http-timer": ^5.0.1 + cacheable-lookup: ^7.0.0 + cacheable-request: ^10.2.8 + decompress-response: ^6.0.0 + form-data-encoder: ^2.1.2 + get-stream: ^6.0.1 + http2-wrapper: ^2.1.10 + lowercase-keys: ^3.0.0 + p-cancelable: ^3.0.0 + responselike: ^3.0.0 + checksum: 3c37f5d858aca2859f9932e7609d35881d07e7f2d44c039d189396f0656896af6c77c22f2c51c563f8918be483f60ff41e219de742ab4642d4b106711baccbd5 languageName: node linkType: hard @@ -11829,7 +13308,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.15, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": +"graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.15, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: ac85f94da92d8eb6b7f5a8b20ce65e43d66761c55ce85ac96df6865308390da45a8d3f0296dd3a663de65d30ba497bd46c696cc1e248c72b13d6d567138a4fc7 @@ -11956,10 +13435,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"has-yarn@npm:^2.1.0": - version: 2.1.0 - resolution: "has-yarn@npm:2.1.0" - checksum: 5eb1d0bb8518103d7da24532bdbc7124ffc6d367b5d3c10840b508116f2f1bcbcf10fd3ba843ff6e2e991bdf9969fd862d42b2ed58aade88343326c950b7e7f7 +"has-yarn@npm:^3.0.0": + version: 3.0.0 + resolution: "has-yarn@npm:3.0.0" + checksum: b9e14e78e0a37bc070550c862b201534287bc10e62a86ec9c1f455ffb082db42817ce9aed914bd73f1d589bbf268520e194629ff2f62ff6b98a482c4bd2dcbfb languageName: node linkType: hard @@ -11972,83 +13451,133 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"hast-to-hyperscript@npm:^9.0.0": - version: 9.0.1 - resolution: "hast-to-hyperscript@npm:9.0.1" +"hast-util-from-parse5@npm:^8.0.0": + version: 8.0.1 + resolution: "hast-util-from-parse5@npm:8.0.1" dependencies: - "@types/unist": ^2.0.3 - comma-separated-tokens: ^1.0.0 - property-information: ^5.3.0 - space-separated-tokens: ^1.0.0 - style-to-object: ^0.3.0 - unist-util-is: ^4.0.0 - web-namespaces: ^1.0.0 - checksum: de570d789853018fff2fd38fc096549b9814e366b298f60c90c159a57018230eefc44d46a246027b0e2426ed9e99f2e270050bc183d5bdfe4c9487c320b392cd + "@types/hast": ^3.0.0 + "@types/unist": ^3.0.0 + devlop: ^1.0.0 + hastscript: ^8.0.0 + property-information: ^6.0.0 + vfile: ^6.0.0 + vfile-location: ^5.0.0 + web-namespaces: ^2.0.0 + checksum: fdd1ab8b03af13778ecb94ef9a58b1e3528410cdfceb3d6bb7600508967d0d836b451bc7bc3baf66efb7c730d3d395eea4bb1b30352b0162823d9f0de976774b languageName: node linkType: hard -"hast-util-from-parse5@npm:^6.0.0": - version: 6.0.1 - resolution: "hast-util-from-parse5@npm:6.0.1" +"hast-util-parse-selector@npm:^4.0.0": + version: 4.0.0 + resolution: "hast-util-parse-selector@npm:4.0.0" dependencies: - "@types/parse5": ^5.0.0 - hastscript: ^6.0.0 - property-information: ^5.0.0 - vfile: ^4.0.0 - vfile-location: ^3.2.0 - web-namespaces: ^1.0.0 - checksum: 4daa78201468af7779161e7caa2513c329830778e0528481ab16b3e1bcef4b831f6285b526aacdddbee802f3bd9d64df55f80f010591ea1916da535e3a923b83 + "@types/hast": ^3.0.0 + checksum: 76087670d3b0b50b23a6cb70bca53a6176d6608307ccdbb3ed18b650b82e7c3513bfc40348f1389dc0c5ae872b9a768851f4335f44654abd7deafd6974c52402 languageName: node linkType: hard -"hast-util-parse-selector@npm:^2.0.0": - version: 2.2.5 - resolution: "hast-util-parse-selector@npm:2.2.5" - checksum: 22ee4afbd11754562144cb3c4f3ec52524dafba4d90ee52512902d17cf11066d83b38f7bdf6ca571bbc2541f07ba30db0d234657b6ecb8ca4631587466459605 +"hast-util-raw@npm:^9.0.0": + version: 9.0.2 + resolution: "hast-util-raw@npm:9.0.2" + dependencies: + "@types/hast": ^3.0.0 + "@types/unist": ^3.0.0 + "@ungap/structured-clone": ^1.0.0 + hast-util-from-parse5: ^8.0.0 + hast-util-to-parse5: ^8.0.0 + html-void-elements: ^3.0.0 + mdast-util-to-hast: ^13.0.0 + parse5: ^7.0.0 + unist-util-position: ^5.0.0 + unist-util-visit: ^5.0.0 + vfile: ^6.0.0 + web-namespaces: ^2.0.0 + zwitch: ^2.0.0 + checksum: 27fd7c723b3b1e06481cd85ca20b447d58d340c53abd2bd61f4a502982109d16aa17b3d71db2ef7c9d24bd627e306ad81cbcaf98c146a3641ba150db731e644c languageName: node linkType: hard -"hast-util-raw@npm:6.0.1": - version: 6.0.1 - resolution: "hast-util-raw@npm:6.0.1" +"hast-util-to-estree@npm:^3.0.0": + version: 3.1.0 + resolution: "hast-util-to-estree@npm:3.1.0" + dependencies: + "@types/estree": ^1.0.0 + "@types/estree-jsx": ^1.0.0 + "@types/hast": ^3.0.0 + comma-separated-tokens: ^2.0.0 + devlop: ^1.0.0 + estree-util-attach-comments: ^3.0.0 + estree-util-is-identifier-name: ^3.0.0 + hast-util-whitespace: ^3.0.0 + mdast-util-mdx-expression: ^2.0.0 + mdast-util-mdx-jsx: ^3.0.0 + mdast-util-mdxjs-esm: ^2.0.0 + property-information: ^6.0.0 + space-separated-tokens: ^2.0.0 + style-to-object: ^0.4.0 + unist-util-position: ^5.0.0 + zwitch: ^2.0.0 + checksum: 61272f7c18c9d2a5e34df7cfd2c97cbf12f6e9d05114d60e4dedd64e5576565eb1e35c78b9213c909bb8f984f0f8e9c49b568f04bdb444b83d0bca9159e14f3c + languageName: node + linkType: hard + +"hast-util-to-jsx-runtime@npm:^2.0.0": + version: 2.3.0 + resolution: "hast-util-to-jsx-runtime@npm:2.3.0" dependencies: - "@types/hast": ^2.0.0 - hast-util-from-parse5: ^6.0.0 - hast-util-to-parse5: ^6.0.0 - html-void-elements: ^1.0.0 - parse5: ^6.0.0 - unist-util-position: ^3.0.0 - vfile: ^4.0.0 - web-namespaces: ^1.0.0 - xtend: ^4.0.0 - zwitch: ^1.0.0 - checksum: f6d960644f9fbbe0b92d0227b20a24d659cce021d5f9fd218e077154931b4524ee920217b7fd5a45ec2736ec1dee53de9209fe449f6f89454c01d225ff0e7851 + "@types/estree": ^1.0.0 + "@types/hast": ^3.0.0 + "@types/unist": ^3.0.0 + comma-separated-tokens: ^2.0.0 + devlop: ^1.0.0 + estree-util-is-identifier-name: ^3.0.0 + hast-util-whitespace: ^3.0.0 + mdast-util-mdx-expression: ^2.0.0 + mdast-util-mdx-jsx: ^3.0.0 + mdast-util-mdxjs-esm: ^2.0.0 + property-information: ^6.0.0 + space-separated-tokens: ^2.0.0 + style-to-object: ^1.0.0 + unist-util-position: ^5.0.0 + vfile-message: ^4.0.0 + checksum: 599a97c6ec61c1430776813d7fb42e6f96032bf4a04dfcbb8eceef3bc8d1845ecf242387a4426b9d3f52320dbbfa26450643b81124b3d6a0b9bbb0fff4d0ba83 + languageName: node + linkType: hard + +"hast-util-to-parse5@npm:^8.0.0": + version: 8.0.0 + resolution: "hast-util-to-parse5@npm:8.0.0" + dependencies: + "@types/hast": ^3.0.0 + comma-separated-tokens: ^2.0.0 + devlop: ^1.0.0 + property-information: ^6.0.0 + space-separated-tokens: ^2.0.0 + web-namespaces: ^2.0.0 + zwitch: ^2.0.0 + checksum: 137469209cb2b32b57387928878dc85310fbd5afa4807a8da69529199bb1d19044bfc95b50c3dc68d4fb2b6cb8bf99b899285597ab6ab318f50422eefd5599dd languageName: node linkType: hard -"hast-util-to-parse5@npm:^6.0.0": - version: 6.0.0 - resolution: "hast-util-to-parse5@npm:6.0.0" +"hast-util-whitespace@npm:^3.0.0": + version: 3.0.0 + resolution: "hast-util-whitespace@npm:3.0.0" dependencies: - hast-to-hyperscript: ^9.0.0 - property-information: ^5.0.0 - web-namespaces: ^1.0.0 - xtend: ^4.0.0 - zwitch: ^1.0.0 - checksum: 91a36244e37df1d63c8b7e865ab0c0a25bb7396155602be005cf71d95c348e709568f80e0f891681a3711d733ad896e70642dc41a05b574eddf2e07d285408a8 + "@types/hast": ^3.0.0 + checksum: 41d93ccce218ba935dc3c12acdf586193c35069489c8c8f50c2aa824c00dec94a3c78b03d1db40fa75381942a189161922e4b7bca700b3a2cc779634c351a1e4 languageName: node linkType: hard -"hastscript@npm:^6.0.0": - version: 6.0.0 - resolution: "hastscript@npm:6.0.0" +"hastscript@npm:^8.0.0": + version: 8.0.0 + resolution: "hastscript@npm:8.0.0" dependencies: - "@types/hast": ^2.0.0 - comma-separated-tokens: ^1.0.0 - hast-util-parse-selector: ^2.0.0 - property-information: ^5.0.0 - space-separated-tokens: ^1.0.0 - checksum: 5e50b85af0d2cb7c17979cb1ddca75d6b96b53019dd999b39e7833192c9004201c3cee6445065620ea05d0087d9ae147a4844e582d64868be5bc6b0232dfe52d + "@types/hast": ^3.0.0 + comma-separated-tokens: ^2.0.0 + hast-util-parse-selector: ^4.0.0 + property-information: ^6.0.0 + space-separated-tokens: ^2.0.0 + checksum: ae3c20223e7b847320c0f98b6fb3c763ebe1bf3913c5805fbc176cf84553a9db1117ca34cf842a5235890b4b9ae0e94501bfdc9a9b870a5dbf5fc52426db1097 languageName: node linkType: hard @@ -12169,14 +13698,14 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"html-escaper@npm:^2.0.0": +"html-escaper@npm:^2.0.0, html-escaper@npm:^2.0.2": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" checksum: d2df2da3ad40ca9ee3a39c5cc6475ef67c8f83c234475f24d8e9ce0dc80a2c82df8e1d6fa78ddd1e9022a586ea1bd247a615e80a5cd9273d90111ddda7d9e974 languageName: node linkType: hard -"html-minifier-terser@npm:^6.0.2, html-minifier-terser@npm:^6.1.0": +"html-minifier-terser@npm:^6.0.2": version: 6.1.0 resolution: "html-minifier-terser@npm:6.1.0" dependencies: @@ -12193,23 +13722,40 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"html-tags@npm:^3.2.0": +"html-minifier-terser@npm:^7.2.0": + version: 7.2.0 + resolution: "html-minifier-terser@npm:7.2.0" + dependencies: + camel-case: ^4.1.2 + clean-css: ~5.3.2 + commander: ^10.0.0 + entities: ^4.4.0 + param-case: ^3.0.4 + relateurl: ^0.2.7 + terser: ^5.15.1 + bin: + html-minifier-terser: cli.js + checksum: 39feed354b5a8aafc8e910977d68cfd961d6db330a8e1a5b16a528c86b8ee7745d8945134822cf00acf7bf0d0135bf1abad650bf308bee4ea73adb003f5b8656 + languageName: node + linkType: hard + +"html-tags@npm:^3.3.1": version: 3.3.1 resolution: "html-tags@npm:3.3.1" checksum: b4ef1d5a76b678e43cce46e3783d563607b1d550cab30b4f511211564574770aa8c658a400b100e588bc60b8234e59b35ff72c7851cc28f3b5403b13a2c6cbce languageName: node linkType: hard -"html-void-elements@npm:^1.0.0": - version: 1.0.5 - resolution: "html-void-elements@npm:1.0.5" - checksum: 1a56f4f6cfbeb994c21701ff72b4b7f556fe784a70e5e554d1566ff775af83b91ea93f10664f039a67802d9f7b40d4a7f1ed20312bab47bd88d89bd792ea84ca +"html-void-elements@npm:^3.0.0": + version: 3.0.0 + resolution: "html-void-elements@npm:3.0.0" + checksum: 59be397525465a7489028afa064c55763d9cccd1d7d9f630cca47137317f0e897a9ca26cef7e745e7cff1abc44260cfa407742b243a54261dfacd42230e94fce languageName: node linkType: hard -"html-webpack-plugin@npm:^5.5.0": - version: 5.5.3 - resolution: "html-webpack-plugin@npm:5.5.3" +"html-webpack-plugin@npm:^5.5.3": + version: 5.6.0 + resolution: "html-webpack-plugin@npm:5.6.0" dependencies: "@types/html-minifier-terser": ^6.0.0 html-minifier-terser: ^6.0.2 @@ -12217,8 +13763,14 @@ asn1@evs-broadcast/node-asn1: pretty-error: ^4.0.0 tapable: ^2.0.0 peerDependencies: + "@rspack/core": 0.x || 1.x webpack: ^5.20.0 - checksum: ccf685195739c372ad641bbd0c9100a847904f34eedc7aff3ece7856cd6c78fd3746d2d615af1bb71e5727993fe711b89e9b744f033ed3fde646540bf5d5e954 + peerDependenciesMeta: + "@rspack/core": + optional: true + webpack: + optional: true + checksum: 32a6e41da538e798fd0be476637d7611a5e8a98a3508f031996e9eb27804dcdc282cb01f847cf5d066f21b49cfb8e21627fcf977ffd0c9bea81cf80e5a65070d languageName: node linkType: hard @@ -12362,6 +13914,16 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"http2-wrapper@npm:^2.1.10": + version: 2.2.1 + resolution: "http2-wrapper@npm:2.2.1" + dependencies: + quick-lru: ^5.1.1 + resolve-alpn: ^1.2.0 + checksum: e95e55e22c6fd61182ce81fecb9b7da3af680d479febe8ad870d05f7ebbc9f076e455193766f4e7934e50913bf1d8da3ba121fb5cd2928892390b58cf9d5c509 + languageName: node + linkType: hard + "https-proxy-agent@npm:5.0.1, https-proxy-agent@npm:^5.0.0": version: 5.0.1 resolution: "https-proxy-agent@npm:5.0.1" @@ -12503,14 +14065,14 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"image-size@npm:^1.0.1": - version: 1.0.2 - resolution: "image-size@npm:1.0.2" +"image-size@npm:^1.0.2": + version: 1.1.1 + resolution: "image-size@npm:1.1.1" dependencies: queue: 6.0.2 bin: image-size: bin/image-size.js - checksum: 01745fdb47f87cecf538e69c63f9adc5bfab30a345345c2de91105f3afbd1bfcfba1256af02bf3323077b33b0004469a837e077bf0cbb9c907e9c1e9e7547585 + checksum: 23b3a515dded89e7f967d52b885b430d6a5a903da954fce703130bfb6069d738d80e6588efd29acfaf5b6933424a56535aa7bf06867e4ebd0250c2ee51f19a4a languageName: node linkType: hard @@ -12543,10 +14105,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"import-lazy@npm:^2.1.0": - version: 2.1.0 - resolution: "import-lazy@npm:2.1.0" - checksum: 05294f3b9dd4971d3a996f0d2f176410fb6745d491d6e73376429189f5c1c3d290548116b2960a7cf3e89c20cdf11431739d1d2d8c54b84061980795010e803a +"import-lazy@npm:^4.0.0": + version: 4.0.0 + resolution: "import-lazy@npm:4.0.0" + checksum: 22f5e51702134aef78890156738454f620e5fe7044b204ebc057c614888a1dd6fdf2ede0fdcca44d5c173fd64f65c985f19a51775b06967ef58cc3d26898df07 languageName: node linkType: hard @@ -12614,7 +14176,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.0, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.1, inherits@npm:~2.0.3": +"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.1, inherits@npm:~2.0.3": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 4a48a733847879d6cf6691860a6b1e3f0f4754176e4d71494c41f3475553768b10f84b5ce1d40fbd0e34e6bfbb864ee35858ad4dd2cf31e02fc4a154b724d7f1 @@ -12664,6 +14226,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"inline-style-parser@npm:0.2.3": + version: 0.2.3 + resolution: "inline-style-parser@npm:0.2.3" + checksum: ed6454de80759e7faef511f51b5716b33c40a6b05b8a8f5383dc01e8a087c6fd5df877446d05e8e3961ae0751e028e25e180f5cffc192a5ce7822edef6810ade + languageName: node + linkType: hard + "inquirer@npm:8.2.4": version: 8.2.4 resolution: "inquirer@npm:8.2.4" @@ -12758,20 +14327,20 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"is-alphabetical@npm:1.0.4, is-alphabetical@npm:^1.0.0": - version: 1.0.4 - resolution: "is-alphabetical@npm:1.0.4" - checksum: 6508cce44fd348f06705d377b260974f4ce68c74000e7da4045f0d919e568226dc3ce9685c5a2af272195384df6930f748ce9213fc9f399b5d31b362c66312cb +"is-alphabetical@npm:^2.0.0": + version: 2.0.1 + resolution: "is-alphabetical@npm:2.0.1" + checksum: 56207db8d9de0850f0cd30f4966bf731eb82cedfe496cbc2e97e7c3bacaf66fc54a972d2d08c0d93bb679cb84976a05d24c5ad63de56fabbfc60aadae312edaa languageName: node linkType: hard -"is-alphanumerical@npm:^1.0.0": - version: 1.0.4 - resolution: "is-alphanumerical@npm:1.0.4" +"is-alphanumerical@npm:^2.0.0": + version: 2.0.1 + resolution: "is-alphanumerical@npm:2.0.1" dependencies: - is-alphabetical: ^1.0.0 - is-decimal: ^1.0.0 - checksum: e2e491acc16fcf5b363f7c726f666a9538dba0a043665740feb45bba1652457a73441e7c5179c6768a638ed396db3437e9905f403644ec7c468fb41f4813d03f + is-alphabetical: ^2.0.0 + is-decimal: ^2.0.0 + checksum: 87acc068008d4c9c4e9f5bd5e251041d42e7a50995c77b1499cf6ed248f971aadeddb11f239cabf09f7975ee58cac7a48ffc170b7890076d8d227b24a68663c9 languageName: node linkType: hard @@ -12828,13 +14397,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"is-buffer@npm:^2.0.0": - version: 2.0.5 - resolution: "is-buffer@npm:2.0.5" - checksum: 764c9ad8b523a9f5a32af29bdf772b08eb48c04d2ad0a7240916ac2688c983bf5f8504bf25b35e66240edeb9d9085461f9b5dae1f3d2861c6b06a65fe983de42 - languageName: node - linkType: hard - "is-callable@npm:^1.1.3, is-callable@npm:^1.1.4, is-callable@npm:^1.2.7": version: 1.2.7 resolution: "is-callable@npm:1.2.7" @@ -12842,7 +14404,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"is-ci@npm:2.0.0, is-ci@npm:^2.0.0": +"is-ci@npm:2.0.0": version: 2.0.0 resolution: "is-ci@npm:2.0.0" dependencies: @@ -12853,6 +14415,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"is-ci@npm:^3.0.1": + version: 3.0.1 + resolution: "is-ci@npm:3.0.1" + dependencies: + ci-info: ^3.2.0 + bin: + is-ci: bin.js + checksum: 192c66dc7826d58f803ecae624860dccf1899fc1f3ac5505284c0a5cf5f889046ffeb958fa651e5725d5705c5bcb14f055b79150ea5fcad7456a9569de60260e + languageName: node + linkType: hard + "is-core-module@npm:^2.13.0, is-core-module@npm:^2.5.0, is-core-module@npm:^2.8.1": version: 2.13.0 resolution: "is-core-module@npm:2.13.0" @@ -12871,10 +14444,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"is-decimal@npm:^1.0.0": - version: 1.0.4 - resolution: "is-decimal@npm:1.0.4" - checksum: ed483a387517856dc395c68403a10201fddcc1b63dc56513fbe2fe86ab38766120090ecdbfed89223d84ca8b1cd28b0641b93cb6597b6e8f4c097a7c24e3fb96 +"is-decimal@npm:^2.0.0": + version: 2.0.1 + resolution: "is-decimal@npm:2.0.1" + checksum: 97132de7acdce77caa7b797632970a2ecd649a88e715db0e4dbc00ab0708b5e7574ba5903962c860cd4894a14fd12b100c0c4ac8aed445cf6f55c6cf747a4158 languageName: node linkType: hard @@ -12947,10 +14520,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"is-hexadecimal@npm:^1.0.0": - version: 1.0.4 - resolution: "is-hexadecimal@npm:1.0.4" - checksum: a452e047587b6069332d83130f54d30da4faf2f2ebaa2ce6d073c27b5703d030d58ed9e0b729c8e4e5b52c6f1dab26781bb77b7bc6c7805f14f320e328ff8cd5 +"is-hexadecimal@npm:^2.0.0": + version: 2.0.1 + resolution: "is-hexadecimal@npm:2.0.1" + checksum: 66a2ea85994c622858f063f23eda506db29d92b52580709eb6f4c19550552d4dcf3fb81952e52f7cf972097237959e00adc7bb8c9400cd12886e15bf06145321 languageName: node linkType: hard @@ -13022,10 +14595,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"is-npm@npm:^5.0.0": - version: 5.0.0 - resolution: "is-npm@npm:5.0.0" - checksum: 9baff02b0c69a3d3c79b162cb2f9e67fb40ef6d172c16601b2e2471c21e9a4fa1fc9885a308d7bc6f3a3cd2a324c27fa0bf284c133c3349bb22571ab70d041cc +"is-npm@npm:^6.0.0": + version: 6.0.0 + resolution: "is-npm@npm:6.0.0" + checksum: fafe1ddc772345f5460514891bb8014376904ccdbddd59eee7525c9adcc08d426933f28b087bef3e17524da7ebf35c03ef484ff3b6ba9d5fecd8c6e6a7d4bf11 languageName: node linkType: hard @@ -13080,13 +14653,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"is-plain-obj@npm:^2.0.0": - version: 2.1.0 - resolution: "is-plain-obj@npm:2.1.0" - checksum: cec9100678b0a9fe0248a81743041ed990c2d4c99f893d935545cfbc42876cbe86d207f3b895700c690ad2fa520e568c44afc1605044b535a7820c1d40e38daa - languageName: node - linkType: hard - "is-plain-obj@npm:^3.0.0": version: 3.0.0 resolution: "is-plain-obj@npm:3.0.0" @@ -13094,6 +14660,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"is-plain-obj@npm:^4.0.0": + version: 4.1.0 + resolution: "is-plain-obj@npm:4.1.0" + checksum: 6dc45da70d04a81f35c9310971e78a6a3c7a63547ef782e3a07ee3674695081b6ca4e977fbb8efc48dae3375e0b34558d2bcd722aec9bddfa2d7db5b041be8ce + languageName: node + linkType: hard + "is-plain-object@npm:^2.0.4": version: 2.0.4 resolution: "is-plain-object@npm:2.0.4" @@ -13124,6 +14697,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"is-reference@npm:^3.0.0": + version: 3.0.2 + resolution: "is-reference@npm:3.0.2" + dependencies: + "@types/estree": "*" + checksum: ac3bf5626fe9d0afbd7454760d73c47f16b9f471401b9749721ad3b66f0a39644390382acf88ca9d029c95782c1e2ec65662855e3ba91acf52d82231247a7fd3 + languageName: node + linkType: hard + "is-regex@npm:^1.1.4": version: 1.1.4 resolution: "is-regex@npm:1.1.4" @@ -13253,20 +14835,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"is-whitespace-character@npm:^1.0.0": - version: 1.0.4 - resolution: "is-whitespace-character@npm:1.0.4" - checksum: adab8ad9847ccfcb6f1b7000b8f622881b5ba2a09ce8be2794a6d2b10c3af325b469fc562c9fb889f468eed27be06e227ac609d0aa1e3a59b4dbcc88e2b0418e - languageName: node - linkType: hard - -"is-word-character@npm:^1.0.0": - version: 1.0.4 - resolution: "is-word-character@npm:1.0.4" - checksum: 1821d6c6abe5bc0b3abe3fdc565d66d7c8a74ea4e93bc77b4a47d26e2e2a306d6ab7d92b353b0d2b182869e3ecaa8f4a346c62d0e31d38ebc0ceaf7cae182c3f - languageName: node - linkType: hard - "is-wsl@npm:^2.2.0": version: 2.2.0 resolution: "is-wsl@npm:2.2.0" @@ -13276,10 +14844,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"is-yarn-global@npm:^0.3.0": - version: 0.3.0 - resolution: "is-yarn-global@npm:0.3.0" - checksum: bca013d65fee2862024c9fbb3ba13720ffca2fe750095174c1c80922fdda16402b5c233f5ac9e265bc12ecb5446e7b7f519a32d9541788f01d4d44e24d2bf481 +"is-yarn-global@npm:^0.4.0": + version: 0.4.1 + resolution: "is-yarn-global@npm:0.4.1" + checksum: 79ec4e6f581c53d4fefdf5f6c237f9a3ad8db29c85cdc4659e76ae345659317552052a97b7e56952aa5d94a23c798ebec8ccad72fb14d3b26dc647ddceddd716 languageName: node linkType: hard @@ -13899,12 +15467,12 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"jiti@npm:^1.18.2": - version: 1.20.0 - resolution: "jiti@npm:1.20.0" +"jiti@npm:^1.20.0": + version: 1.21.0 + resolution: "jiti@npm:1.21.0" bin: jiti: bin/jiti.js - checksum: 7924062b5675142e3e272a27735be84b7bfc0a0eb73217fc2dcafa034f37c4f7b4b9ffc07dd98bcff0f739a8811ce1544db205ae7e97b1c86f0df92c65ce3c72 + checksum: a7bd5d63921c170eaec91eecd686388181c7828e1fa0657ab374b9372bfc1f383cf4b039e6b272383d5cb25607509880af814a39abdff967322459cca41f2961 languageName: node linkType: hard @@ -13915,16 +15483,16 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"joi@npm:^17.6.0": - version: 17.10.2 - resolution: "joi@npm:17.10.2" +"joi@npm:^17.9.2": + version: 17.12.3 + resolution: "joi@npm:17.12.3" dependencies: - "@hapi/hoek": ^9.0.0 - "@hapi/topo": ^5.0.0 - "@sideway/address": ^4.1.3 + "@hapi/hoek": ^9.3.0 + "@hapi/topo": ^5.1.0 + "@sideway/address": ^4.1.5 "@sideway/formula": ^3.0.1 "@sideway/pinpoint": ^2.0.0 - checksum: 2fac59e83b35465d04ffcd33a937c39795264bdd3d392bebee8034710f84631a400cd320a3bb0bb736e70ce930abb1ea551bc3ffbeca023b53417d864eb216a4 + checksum: 6fc74ad7561261a3500fecfcb2cab30928526286a4ee737cad4d41e8556263326561e4146f6c2c9429d0eef27a7d1ba93b919795ade1e766d6dbb797fb4f1b77 languageName: node linkType: hard @@ -14023,13 +15591,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"json-buffer@npm:3.0.0": - version: 3.0.0 - resolution: "json-buffer@npm:3.0.0" - checksum: 0cecacb8025370686a916069a2ff81f7d55167421b6aa7270ee74e244012650dd6bce22b0852202ea7ff8624fce50ff0ec1bdf95914ccb4553426e290d5a63fa - languageName: node - linkType: hard - "json-buffer@npm:3.0.1": version: 3.0.1 resolution: "json-buffer@npm:3.0.1" @@ -14251,15 +15812,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"keyv@npm:^3.0.0": - version: 3.1.0 - resolution: "keyv@npm:3.1.0" - dependencies: - json-buffer: 3.0.0 - checksum: bb7e8f3acffdbafbc2dd5b63f377fe6ec4c0e2c44fc82720449ef8ab54f4a7ce3802671ed94c0f475ae0a8549703353a2124561fcf3317010c141b32ca1ce903 - languageName: node - linkType: hard - "keyv@npm:^4.0.0, keyv@npm:^4.5.3": version: 4.5.3 resolution: "keyv@npm:4.5.3" @@ -14297,12 +15849,12 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"latest-version@npm:^5.1.0": - version: 5.1.0 - resolution: "latest-version@npm:5.1.0" +"latest-version@npm:^7.0.0": + version: 7.0.0 + resolution: "latest-version@npm:7.0.0" dependencies: - package-json: ^6.3.0 - checksum: fbc72b071eb66c40f652441fd783a9cca62f08bf42433651937f078cd9ef94bf728ec7743992777826e4e89305aef24f234b515e6030503a2cbee7fc9bdc2c0f + package-json: ^8.1.0 + checksum: 1f0deba00d5a34394cce4463c938811f51bbb539b131674f4bb2062c63f2cc3b80bccd56ecade3bd5932d04a34cf0a5a8a2ccc4ec9e5e6b285a9a7b3e27d0d66 languageName: node linkType: hard @@ -14663,13 +16215,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"lodash.curry@npm:^4.0.1": - version: 4.1.1 - resolution: "lodash.curry@npm:4.1.1" - checksum: 9192b70fe7df4d1ff780c0260bee271afa9168c93fe4fa24bc861900240531b59781b5fdaadf4644fea8f4fbcd96f0700539ab294b579ffc1022c6c15dcc462a - languageName: node - linkType: hard - "lodash.debounce@npm:^4.0.8": version: 4.0.8 resolution: "lodash.debounce@npm:4.0.8" @@ -14677,34 +16222,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"lodash.escape@npm:^4.0.1": - version: 4.0.1 - resolution: "lodash.escape@npm:4.0.1" - checksum: fcb54f457497256964d619d5cccbd80a961916fca60df3fe0fa3e7f052715c2944c0ed5aefb4f9e047d127d44aa2d55555f3350cb42c6549e9e293fb30b41e7f - languageName: node - linkType: hard - -"lodash.flatten@npm:^4.4.0": - version: 4.4.0 - resolution: "lodash.flatten@npm:4.4.0" - checksum: 0ac34a393d4b795d4b7421153d27c13ae67e08786c9cbb60ff5b732210d46f833598eee3fb3844bb10070e8488efe390ea53bb567377e0cb47e9e630bf0811cb - languageName: node - linkType: hard - -"lodash.flow@npm:^3.3.0": - version: 3.5.0 - resolution: "lodash.flow@npm:3.5.0" - checksum: a9a62ad344e3c5a1f42bc121da20f64dd855aaafecee24b1db640f29b88bd165d81c37ff7e380a7191de6f70b26f5918abcebbee8396624f78f3618a0b18634c - languageName: node - linkType: hard - -"lodash.invokemap@npm:^4.6.0": - version: 4.6.0 - resolution: "lodash.invokemap@npm:4.6.0" - checksum: 646ceebbefbcb6da301f8c2868254680fd0bcdc6ada470495d9ae49c9c32938829c1b38a38c95d0258409a9655f85db404b16e648381c7450b7ed3d9c52d8808 - languageName: node - linkType: hard - "lodash.ismatch@npm:^4.4.0": version: 4.4.0 resolution: "lodash.ismatch@npm:4.4.0" @@ -14726,13 +16243,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"lodash.pullall@npm:^4.2.0": - version: 4.2.0 - resolution: "lodash.pullall@npm:4.2.0" - checksum: 7a5fbaedf186ec197ce1e0b9ba1d88a89773ebaf6a8291c7d273838cac59cb3b339cf36ef00e94172862ee84d2304c38face161846f08f5581d0553dcbdcd090 - languageName: node - linkType: hard - "lodash.sortby@npm:^4.7.0": version: 4.7.0 resolution: "lodash.sortby@npm:4.7.0" @@ -14747,20 +16257,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"lodash.uniq@npm:4.5.0, lodash.uniq@npm:^4.5.0": +"lodash.uniq@npm:^4.5.0": version: 4.5.0 resolution: "lodash.uniq@npm:4.5.0" checksum: a4779b57a8d0f3c441af13d9afe7ecff22dd1b8ce1129849f71d9bbc8e8ee4e46dfb4b7c28f7ad3d67481edd6e51126e4e2a6ee276e25906d10f7140187c392d languageName: node linkType: hard -"lodash.uniqby@npm:^4.7.0": - version: 4.7.0 - resolution: "lodash.uniqby@npm:4.7.0" - checksum: 659264545a95726d1493123345aad8cbf56e17810fa9a0b029852c6d42bc80517696af09d99b23bef1845d10d95e01b8b4a1da578f22aeba7a30d3e0022a4938 - languageName: node - linkType: hard - "lodash@npm:4.17.21, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.7.0, lodash@npm:~4.17.21": version: 4.17.21 resolution: "lodash@npm:4.17.21" @@ -14833,6 +16336,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"longest-streak@npm:^3.0.0": + version: 3.1.0 + resolution: "longest-streak@npm:3.1.0" + checksum: d7f952ed004cbdb5c8bcfc4f7f5c3d65449e6c5a9e9be4505a656e3df5a57ee125f284286b4bf8ecea0c21a7b3bf2b8f9001ad506c319b9815ad6a63a47d0fd0 + languageName: node + linkType: hard + "loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.2.0, loose-envify@npm:^1.3.1, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" @@ -14853,13 +16363,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"lowercase-keys@npm:^1.0.0, lowercase-keys@npm:^1.0.1": - version: 1.0.1 - resolution: "lowercase-keys@npm:1.0.1" - checksum: 4d045026595936e09953e3867722e309415ff2c80d7701d067546d75ef698dac218a4f53c6d1d0e7368b47e45fd7529df47e6cb56fbb90523ba599f898b3d147 - languageName: node - linkType: hard - "lowercase-keys@npm:^2.0.0": version: 2.0.0 resolution: "lowercase-keys@npm:2.0.0" @@ -14867,6 +16370,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"lowercase-keys@npm:^3.0.0": + version: 3.0.0 + resolution: "lowercase-keys@npm:3.0.0" + checksum: 67a3f81409af969bc0c4ca0e76cd7d16adb1e25aa1c197229587eaf8671275c8c067cd421795dbca4c81be0098e4c426a086a05e30de8a9c587b7a13c0c7ccc5 + languageName: node + linkType: hard + "lru-cache@npm:^4.0.0": version: 4.1.5 resolution: "lru-cache@npm:4.1.5" @@ -14925,7 +16435,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"make-dir@npm:3.1.0, make-dir@npm:^3.0.0, make-dir@npm:^3.0.2, make-dir@npm:^3.1.0": +"make-dir@npm:3.1.0": version: 3.1.0 resolution: "make-dir@npm:3.1.0" dependencies: @@ -15037,10 +16547,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"markdown-escapes@npm:^1.0.0": - version: 1.0.4 - resolution: "markdown-escapes@npm:1.0.4" - checksum: 6833a93d72d3f70a500658872312c6fa8015c20cc835a85ae6901fa232683fbc6ed7118ebe920fea7c80039a560f339c026597d96eee0e9de602a36921804997 +"markdown-extensions@npm:^2.0.0": + version: 2.0.0 + resolution: "markdown-extensions@npm:2.0.0" + checksum: ec4ffcb0768f112e778e7ac74cb8ef22a966c168c3e6c29829f007f015b0a0b5c79c73ee8599a0c72e440e7f5cfdbf19e80e2d77b9a313b8f66e180a330cf1b2 languageName: node linkType: hard @@ -15059,202 +16569,919 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"marked@npm:^4.0.14, marked@npm:^4.2.12": - version: 4.3.0 - resolution: "marked@npm:4.3.0" - bin: - marked: bin/marked.js - checksum: 0db6817893952c3ec710eb9ceafb8468bf5ae38cb0f92b7b083baa13d70b19774674be04db5b817681fa7c5c6a088f61300815e4dd75a59696f4716ad69f6260 +"markdown-table@npm:^3.0.0": + version: 3.0.3 + resolution: "markdown-table@npm:3.0.3" + checksum: 8fcd3d9018311120fbb97115987f8b1665a603f3134c93fbecc5d1463380c8036f789e2a62c19432058829e594fff8db9ff81c88f83690b2f8ed6c074f8d9e10 + languageName: node + linkType: hard + +"marked@npm:^4.0.14, marked@npm:^4.2.12": + version: 4.3.0 + resolution: "marked@npm:4.3.0" + bin: + marked: bin/marked.js + checksum: 0db6817893952c3ec710eb9ceafb8468bf5ae38cb0f92b7b083baa13d70b19774674be04db5b817681fa7c5c6a088f61300815e4dd75a59696f4716ad69f6260 + languageName: node + linkType: hard + +"mdast-util-directive@npm:^3.0.0": + version: 3.0.0 + resolution: "mdast-util-directive@npm:3.0.0" + dependencies: + "@types/mdast": ^4.0.0 + "@types/unist": ^3.0.0 + devlop: ^1.0.0 + mdast-util-from-markdown: ^2.0.0 + mdast-util-to-markdown: ^2.0.0 + parse-entities: ^4.0.0 + stringify-entities: ^4.0.0 + unist-util-visit-parents: ^6.0.0 + checksum: 593afdc4f39f99bb198f3774bf4648cb546cb99a055e40c82262a7faab10926d2529a725d0d3945300ed0a1f07c6c84215a3f76b899a89b3f410ec7375bbab17 + languageName: node + linkType: hard + +"mdast-util-find-and-replace@npm:^3.0.0, mdast-util-find-and-replace@npm:^3.0.1": + version: 3.0.1 + resolution: "mdast-util-find-and-replace@npm:3.0.1" + dependencies: + "@types/mdast": ^4.0.0 + escape-string-regexp: ^5.0.0 + unist-util-is: ^6.0.0 + unist-util-visit-parents: ^6.0.0 + checksum: 05d5c4ff02e31db2f8a685a13bcb6c3f44e040bd9dfa54c19a232af8de5268334c8755d79cb456ed4cced1300c4fb83e88444c7ae8ee9ff16869a580f29d08cd + languageName: node + linkType: hard + +"mdast-util-from-markdown@npm:^2.0.0": + version: 2.0.0 + resolution: "mdast-util-from-markdown@npm:2.0.0" + dependencies: + "@types/mdast": ^4.0.0 + "@types/unist": ^3.0.0 + decode-named-character-reference: ^1.0.0 + devlop: ^1.0.0 + mdast-util-to-string: ^4.0.0 + micromark: ^4.0.0 + micromark-util-decode-numeric-character-reference: ^2.0.0 + micromark-util-decode-string: ^2.0.0 + micromark-util-normalize-identifier: ^2.0.0 + micromark-util-symbol: ^2.0.0 + micromark-util-types: ^2.0.0 + unist-util-stringify-position: ^4.0.0 + checksum: 4e8d8a46b4b588486c41b80c39da333a91593bc8d60cd7421c6cd3c22003b8e5a62478292fb7bc97b9255b6301a2250cca32340ef43c309156e215453c5b92be + languageName: node + linkType: hard + +"mdast-util-frontmatter@npm:^2.0.0": + version: 2.0.1 + resolution: "mdast-util-frontmatter@npm:2.0.1" + dependencies: + "@types/mdast": ^4.0.0 + devlop: ^1.0.0 + escape-string-regexp: ^5.0.0 + mdast-util-from-markdown: ^2.0.0 + mdast-util-to-markdown: ^2.0.0 + micromark-extension-frontmatter: ^2.0.0 + checksum: 86a7c8d9eb183be2621d6d9134b9d33df2a3647e3255f68a9796e2425e25643ffae00a501e36c57d9c10973087b94aa5a2ffd865d33cdd274cc9b88cd2d90a2e + languageName: node + linkType: hard + +"mdast-util-gfm-autolink-literal@npm:^2.0.0": + version: 2.0.0 + resolution: "mdast-util-gfm-autolink-literal@npm:2.0.0" + dependencies: + "@types/mdast": ^4.0.0 + ccount: ^2.0.0 + devlop: ^1.0.0 + mdast-util-find-and-replace: ^3.0.0 + micromark-util-character: ^2.0.0 + checksum: 10322662e5302964bed7c9829c5fd3b0c9899d4f03e63fb8620ab141cf4f3de9e61fcb4b44d46aacc8a23f82bcd5d900980a211825dfe026b1dab5fdbc3e8742 + languageName: node + linkType: hard + +"mdast-util-gfm-footnote@npm:^2.0.0": + version: 2.0.0 + resolution: "mdast-util-gfm-footnote@npm:2.0.0" + dependencies: + "@types/mdast": ^4.0.0 + devlop: ^1.1.0 + mdast-util-from-markdown: ^2.0.0 + mdast-util-to-markdown: ^2.0.0 + micromark-util-normalize-identifier: ^2.0.0 + checksum: 45d26b40e7a093712e023105791129d76e164e2168d5268e113298a22de30c018162683fb7893cdc04ab246dac0087eed708b2a136d1d18ed2b32b3e0cae4a79 + languageName: node + linkType: hard + +"mdast-util-gfm-strikethrough@npm:^2.0.0": + version: 2.0.0 + resolution: "mdast-util-gfm-strikethrough@npm:2.0.0" + dependencies: + "@types/mdast": ^4.0.0 + mdast-util-from-markdown: ^2.0.0 + mdast-util-to-markdown: ^2.0.0 + checksum: fe9b1d0eba9b791ff9001c008744eafe3dd7a81b085f2bf521595ce4a8e8b1b44764ad9361761ad4533af3e5d913d8ad053abec38172031d9ee32a8ebd1c7dbd + languageName: node + linkType: hard + +"mdast-util-gfm-table@npm:^2.0.0": + version: 2.0.0 + resolution: "mdast-util-gfm-table@npm:2.0.0" + dependencies: + "@types/mdast": ^4.0.0 + devlop: ^1.0.0 + markdown-table: ^3.0.0 + mdast-util-from-markdown: ^2.0.0 + mdast-util-to-markdown: ^2.0.0 + checksum: 063a627fd0993548fd63ca0c24c437baf91ba7d51d0a38820bd459bc20bf3d13d7365ef8d28dca99176dd5eb26058f7dde51190479c186dfe6af2e11202957c9 + languageName: node + linkType: hard + +"mdast-util-gfm-task-list-item@npm:^2.0.0": + version: 2.0.0 + resolution: "mdast-util-gfm-task-list-item@npm:2.0.0" + dependencies: + "@types/mdast": ^4.0.0 + devlop: ^1.0.0 + mdast-util-from-markdown: ^2.0.0 + mdast-util-to-markdown: ^2.0.0 + checksum: 37db90c59b15330fc54d790404abf5ef9f2f83e8961c53666fe7de4aab8dd5e6b3c296b6be19797456711a89a27840291d8871ff0438e9b4e15c89d170efe072 + languageName: node + linkType: hard + +"mdast-util-gfm@npm:^3.0.0": + version: 3.0.0 + resolution: "mdast-util-gfm@npm:3.0.0" + dependencies: + mdast-util-from-markdown: ^2.0.0 + mdast-util-gfm-autolink-literal: ^2.0.0 + mdast-util-gfm-footnote: ^2.0.0 + mdast-util-gfm-strikethrough: ^2.0.0 + mdast-util-gfm-table: ^2.0.0 + mdast-util-gfm-task-list-item: ^2.0.0 + mdast-util-to-markdown: ^2.0.0 + checksum: 62039d2f682ae3821ea1c999454863d31faf94d67eb9b746589c7e136076d7fb35fabc67e02f025c7c26fd7919331a0ee1aabfae24f565d9a6a9ebab3371c626 + languageName: node + linkType: hard + +"mdast-util-mdx-expression@npm:^2.0.0": + version: 2.0.0 + resolution: "mdast-util-mdx-expression@npm:2.0.0" + dependencies: + "@types/estree-jsx": ^1.0.0 + "@types/hast": ^3.0.0 + "@types/mdast": ^4.0.0 + devlop: ^1.0.0 + mdast-util-from-markdown: ^2.0.0 + mdast-util-to-markdown: ^2.0.0 + checksum: 4e1183000e183e07a7264e192889b4fd57372806103031c71b9318967f85fd50a5dd0f92ef14f42c331e77410808f5de3341d7bc8ad4ee91b7fa8f0a30043a8a + languageName: node + linkType: hard + +"mdast-util-mdx-jsx@npm:^3.0.0": + version: 3.1.2 + resolution: "mdast-util-mdx-jsx@npm:3.1.2" + dependencies: + "@types/estree-jsx": ^1.0.0 + "@types/hast": ^3.0.0 + "@types/mdast": ^4.0.0 + "@types/unist": ^3.0.0 + ccount: ^2.0.0 + devlop: ^1.1.0 + mdast-util-from-markdown: ^2.0.0 + mdast-util-to-markdown: ^2.0.0 + parse-entities: ^4.0.0 + stringify-entities: ^4.0.0 + unist-util-remove-position: ^5.0.0 + unist-util-stringify-position: ^4.0.0 + vfile-message: ^4.0.0 + checksum: 33cb8a657702d5bb8d3f658d158f448c45147664cdb2475501a1c467e3a167d75842546296a06f758f07cce4d2a6ba1add405dbdb6caa145a6980c9782e411e2 + languageName: node + linkType: hard + +"mdast-util-mdx@npm:^3.0.0": + version: 3.0.0 + resolution: "mdast-util-mdx@npm:3.0.0" + dependencies: + mdast-util-from-markdown: ^2.0.0 + mdast-util-mdx-expression: ^2.0.0 + mdast-util-mdx-jsx: ^3.0.0 + mdast-util-mdxjs-esm: ^2.0.0 + mdast-util-to-markdown: ^2.0.0 + checksum: e2b007d826fcd49fd57ed03e190753c8b0f7d9eff6c7cb26ba609cde15cd3a472c0cd5e4a1ee3e39a40f14be22fdb57de243e093cea0c064d6f3366cff3e3af2 + languageName: node + linkType: hard + +"mdast-util-mdxjs-esm@npm:^2.0.0": + version: 2.0.1 + resolution: "mdast-util-mdxjs-esm@npm:2.0.1" + dependencies: + "@types/estree-jsx": ^1.0.0 + "@types/hast": ^3.0.0 + "@types/mdast": ^4.0.0 + devlop: ^1.0.0 + mdast-util-from-markdown: ^2.0.0 + mdast-util-to-markdown: ^2.0.0 + checksum: 1f9dad04d31d59005332e9157ea9510dc1d03092aadbc607a10475c7eec1c158b475aa0601a3a4f74e13097ca735deb8c2d9d37928ddef25d3029fd7c9e14dc3 + languageName: node + linkType: hard + +"mdast-util-phrasing@npm:^4.0.0": + version: 4.1.0 + resolution: "mdast-util-phrasing@npm:4.1.0" + dependencies: + "@types/mdast": ^4.0.0 + unist-util-is: ^6.0.0 + checksum: 3a97533e8ad104a422f8bebb34b3dde4f17167b8ed3a721cf9263c7416bd3447d2364e6d012a594aada40cac9e949db28a060bb71a982231693609034ed5324e + languageName: node + linkType: hard + +"mdast-util-to-hast@npm:^13.0.0": + version: 13.1.0 + resolution: "mdast-util-to-hast@npm:13.1.0" + dependencies: + "@types/hast": ^3.0.0 + "@types/mdast": ^4.0.0 + "@ungap/structured-clone": ^1.0.0 + devlop: ^1.0.0 + micromark-util-sanitize-uri: ^2.0.0 + trim-lines: ^3.0.0 + unist-util-position: ^5.0.0 + unist-util-visit: ^5.0.0 + vfile: ^6.0.0 + checksum: 640bc897286af8fe760cd477fb04bbf544a5a897cdc2220ce36fe2f892f067b483334610387aeb969511bd78a2d841a54851079cd676ac513d6a5ff75852514e + languageName: node + linkType: hard + +"mdast-util-to-markdown@npm:^2.0.0": + version: 2.1.0 + resolution: "mdast-util-to-markdown@npm:2.1.0" + dependencies: + "@types/mdast": ^4.0.0 + "@types/unist": ^3.0.0 + longest-streak: ^3.0.0 + mdast-util-phrasing: ^4.0.0 + mdast-util-to-string: ^4.0.0 + micromark-util-decode-string: ^2.0.0 + unist-util-visit: ^5.0.0 + zwitch: ^2.0.0 + checksum: 3a2cf3957e23b34e2e092e6e76ae72ee0b8745955bd811baba6814cf3a3d916c3fd52264b4b58f3bb3d512a428f84a1e998b6fc7e28434e388a9ae8fb6a9c173 + languageName: node + linkType: hard + +"mdast-util-to-string@npm:^4.0.0": + version: 4.0.0 + resolution: "mdast-util-to-string@npm:4.0.0" + dependencies: + "@types/mdast": ^4.0.0 + checksum: 35489fb5710d58cbc2d6c8b6547df161a3f81e0f28f320dfb3548a9393555daf07c310c0c497708e67ed4dfea4a06e5655799e7d631ca91420c288b4525d6c29 + languageName: node + linkType: hard + +"mdn-data@npm:2.0.14": + version: 2.0.14 + resolution: "mdn-data@npm:2.0.14" + checksum: 9d0128ed425a89f4cba8f787dca27ad9408b5cb1b220af2d938e2a0629d17d879a34d2cb19318bdb26c3f14c77dd5dfbae67211f5caaf07b61b1f2c5c8c7dc16 + languageName: node + linkType: hard + +"mdn-data@npm:2.0.4": + version: 2.0.4 + resolution: "mdn-data@npm:2.0.4" + checksum: add3c95e6d03d301b8a8bcfee3de33f4d07e4c5eee5b79f18d6d737de717e22472deadf67c1a8563983c0b603e10d7df40aa8e5fddf18884dfe118ccec7ae329 + languageName: node + linkType: hard + +"mdurl@npm:^1.0.1": + version: 1.0.1 + resolution: "mdurl@npm:1.0.1" + checksum: 71731ecba943926bfbf9f9b51e28b5945f9411c4eda80894221b47cc105afa43ba2da820732b436f0798fd3edbbffcd1fc1415843c41a87fea08a41cc1e3d02b + languageName: node + linkType: hard + +"measured-core@npm:^1.51.1": + version: 1.51.1 + resolution: "measured-core@npm:1.51.1" + dependencies: + binary-search: ^1.3.3 + optional-js: ^2.0.0 + checksum: b8c7cd5495d0802a2859abc76049781087c1c74817fe8c449a5b5aeeb91a54822b5e52e0b543e775c7031807fc63cbaa76165b3153151a76b0d2145e638ed617 + languageName: node + linkType: hard + +"measured-reporting@npm:^1.51.1": + version: 1.51.1 + resolution: "measured-reporting@npm:1.51.1" + dependencies: + console-log-level: ^1.4.1 + mapcap: ^1.0.0 + measured-core: ^1.51.1 + optional-js: ^2.0.0 + checksum: ae0ab419ea1d436d7b101de925677a184bd07070081c7b0913dc9da518d6ce603c94a0198c9b71b40c9cdc342c23f0f2c3aad82e6a385b9b36e8738ba6be1cd0 + languageName: node + linkType: hard + +"media-typer@npm:0.3.0": + version: 0.3.0 + resolution: "media-typer@npm:0.3.0" + checksum: af1b38516c28ec95d6b0826f6c8f276c58aec391f76be42aa07646b4e39d317723e869700933ca6995b056db4b09a78c92d5440dc23657e6764be5d28874bba1 + languageName: node + linkType: hard + +"memfs@npm:^3.1.2, memfs@npm:^3.4.3": + version: 3.5.3 + resolution: "memfs@npm:3.5.3" + dependencies: + fs-monkey: ^1.0.4 + checksum: 18dfdeacad7c8047b976a6ccd58bc98ba76e122ad3ca0e50a21837fe2075fc0d9aafc58ab9cf2576c2b6889da1dd2503083f2364191b695273f40969db2ecc44 + languageName: node + linkType: hard + +"memoizee@npm:^0.4.15": + version: 0.4.15 + resolution: "memoizee@npm:0.4.15" + dependencies: + d: ^1.0.1 + es5-ext: ^0.10.53 + es6-weak-map: ^2.0.3 + event-emitter: ^0.3.5 + is-promise: ^2.2.2 + lru-queue: ^0.1.0 + next-tick: ^1.1.0 + timers-ext: ^0.1.7 + checksum: 4065d94416dbadac56edf5947bf342beca0e9f051f33ad60d7c4baf3f6ca0f3c6fdb770c5caed5a89c0ceaf9121428582f396445d591785281383d60aa883418 + languageName: node + linkType: hard + +"memory-pager@npm:^1.0.2": + version: 1.5.0 + resolution: "memory-pager@npm:1.5.0" + checksum: d1a2e684583ef55c61cd3a49101da645b11ad57014dfc565e0b43baa9004b743f7e4ab81493d8fff2ab24e9950987cc3209c94bcc4fc8d7e30a475489a1f15e9 + languageName: node + linkType: hard + +"meow@npm:^11.0.0": + version: 11.0.0 + resolution: "meow@npm:11.0.0" + dependencies: + "@types/minimist": ^1.2.2 + camelcase-keys: ^8.0.2 + decamelize: ^6.0.0 + decamelize-keys: ^1.1.0 + hard-rejection: ^2.1.0 + minimist-options: 4.1.0 + normalize-package-data: ^4.0.1 + read-pkg-up: ^9.1.0 + redent: ^4.0.0 + trim-newlines: ^4.0.2 + type-fest: ^3.1.0 + yargs-parser: ^21.1.1 + checksum: 2e815b8d2acc6cda0ea10e0a6dcd6fbdcc2fb8b24412c3c70acd77220642ca0dc727c6fccd79d64b7ca811d099e8a9ad62ea261a8f39d4b61fcdcaf551c5c788 + languageName: node + linkType: hard + +"meow@npm:^8.0.0": + version: 8.1.2 + resolution: "meow@npm:8.1.2" + dependencies: + "@types/minimist": ^1.2.0 + camelcase-keys: ^6.2.2 + decamelize-keys: ^1.1.0 + hard-rejection: ^2.1.0 + minimist-options: 4.1.0 + normalize-package-data: ^3.0.0 + read-pkg-up: ^7.0.1 + redent: ^3.0.0 + trim-newlines: ^3.0.0 + type-fest: ^0.18.0 + yargs-parser: ^20.2.3 + checksum: bc23bf1b4423ef6a821dff9734406bce4b91ea257e7f10a8b7f896f45b59649f07adc0926e2917eacd8cf1df9e4cd89c77623cf63dfd0f8bf54de07a32ee5a85 + languageName: node + linkType: hard + +"merge-descriptors@npm:1.0.1": + version: 1.0.1 + resolution: "merge-descriptors@npm:1.0.1" + checksum: 5abc259d2ae25bb06d19ce2b94a21632583c74e2a9109ee1ba7fd147aa7362b380d971e0251069f8b3eb7d48c21ac839e21fa177b335e82c76ec172e30c31a26 + languageName: node + linkType: hard + +"merge-stream@npm:^2.0.0": + version: 2.0.0 + resolution: "merge-stream@npm:2.0.0" + checksum: 6fa4dcc8d86629705cea944a4b88ef4cb0e07656ebf223fa287443256414283dd25d91c1cd84c77987f2aec5927af1a9db6085757cb43d90eb170ebf4b47f4f4 + languageName: node + linkType: hard + +"merge2@npm:^1.3.0, merge2@npm:^1.4.1": + version: 1.4.1 + resolution: "merge2@npm:1.4.1" + checksum: 7268db63ed5169466540b6fb947aec313200bcf6d40c5ab722c22e242f651994619bcd85601602972d3c85bd2cc45a358a4c61937e9f11a061919a1da569b0c2 + languageName: node + linkType: hard + +"methods@npm:~1.1.2": + version: 1.1.2 + resolution: "methods@npm:1.1.2" + checksum: 0917ff4041fa8e2f2fda5425a955fe16ca411591fbd123c0d722fcf02b73971ed6f764d85f0a6f547ce49ee0221ce2c19a5fa692157931cecb422984f1dcd13a + languageName: node + linkType: hard + +"micromark-core-commonmark@npm:^2.0.0": + version: 2.0.0 + resolution: "micromark-core-commonmark@npm:2.0.0" + dependencies: + decode-named-character-reference: ^1.0.0 + devlop: ^1.0.0 + micromark-factory-destination: ^2.0.0 + micromark-factory-label: ^2.0.0 + micromark-factory-space: ^2.0.0 + micromark-factory-title: ^2.0.0 + micromark-factory-whitespace: ^2.0.0 + micromark-util-character: ^2.0.0 + micromark-util-chunked: ^2.0.0 + micromark-util-classify-character: ^2.0.0 + micromark-util-html-tag-name: ^2.0.0 + micromark-util-normalize-identifier: ^2.0.0 + micromark-util-resolve-all: ^2.0.0 + micromark-util-subtokenize: ^2.0.0 + micromark-util-symbol: ^2.0.0 + micromark-util-types: ^2.0.0 + checksum: 9c12fb580cf4ce71f60872043bd2794efe129f44d7b2b73afa155bbc0a66b7bc35655ba8cef438a6bd068441837ed3b6dc6ad7e5a18f815462c1750793e03a42 + languageName: node + linkType: hard + +"micromark-extension-directive@npm:^3.0.0": + version: 3.0.0 + resolution: "micromark-extension-directive@npm:3.0.0" + dependencies: + devlop: ^1.0.0 + micromark-factory-space: ^2.0.0 + micromark-factory-whitespace: ^2.0.0 + micromark-util-character: ^2.0.0 + micromark-util-symbol: ^2.0.0 + micromark-util-types: ^2.0.0 + parse-entities: ^4.0.0 + checksum: 8350106bdf039a544cba64cf7932261a710e07d73d43d6c645dd2b16577f30ebd04abf762e8ca74266f5de19938e1eeff6c237d79f8244dea23aef7f90df2c31 + languageName: node + linkType: hard + +"micromark-extension-frontmatter@npm:^2.0.0": + version: 2.0.0 + resolution: "micromark-extension-frontmatter@npm:2.0.0" + dependencies: + fault: ^2.0.0 + micromark-util-character: ^2.0.0 + micromark-util-symbol: ^2.0.0 + micromark-util-types: ^2.0.0 + checksum: f68032df38c00ae47de15b63bcd72515bfcce39de4a9262a3a1ac9c5990f253f8e41bdc65fd17ec4bb3d144c32529ce0829571331e4901a9a413f1a53785d1e8 + languageName: node + linkType: hard + +"micromark-extension-gfm-autolink-literal@npm:^2.0.0": + version: 2.0.0 + resolution: "micromark-extension-gfm-autolink-literal@npm:2.0.0" + dependencies: + micromark-util-character: ^2.0.0 + micromark-util-sanitize-uri: ^2.0.0 + micromark-util-symbol: ^2.0.0 + micromark-util-types: ^2.0.0 + checksum: fa16d59528239262d6d04d539a052baf1f81275954ec8bfadea40d81bfc25667d5c8e68b225a5358626df5e30a3933173a67fdad2fed011d37810a10b770b0b2 + languageName: node + linkType: hard + +"micromark-extension-gfm-footnote@npm:^2.0.0": + version: 2.0.0 + resolution: "micromark-extension-gfm-footnote@npm:2.0.0" + dependencies: + devlop: ^1.0.0 + micromark-core-commonmark: ^2.0.0 + micromark-factory-space: ^2.0.0 + micromark-util-character: ^2.0.0 + micromark-util-normalize-identifier: ^2.0.0 + micromark-util-sanitize-uri: ^2.0.0 + micromark-util-symbol: ^2.0.0 + micromark-util-types: ^2.0.0 + checksum: a426fddecfac6144fc622b845cd2dc09d46faa75be5b76ff022cb76a03301b1d4929a5e5e41e071491787936be65e03d0b03c7aebc0e0136b3cdbfadadd6632c + languageName: node + linkType: hard + +"micromark-extension-gfm-strikethrough@npm:^2.0.0": + version: 2.0.0 + resolution: "micromark-extension-gfm-strikethrough@npm:2.0.0" + dependencies: + devlop: ^1.0.0 + micromark-util-chunked: ^2.0.0 + micromark-util-classify-character: ^2.0.0 + micromark-util-resolve-all: ^2.0.0 + micromark-util-symbol: ^2.0.0 + micromark-util-types: ^2.0.0 + checksum: 4e35fbbf364bfce08066b70acd94b9d393a8fd09a5afbe0bae70d0c8a174640b1ba86ab6b78ee38f411a813e2a718b07959216cf0063d823ba1c569a7694e5ad + languageName: node + linkType: hard + +"micromark-extension-gfm-table@npm:^2.0.0": + version: 2.0.0 + resolution: "micromark-extension-gfm-table@npm:2.0.0" + dependencies: + devlop: ^1.0.0 + micromark-factory-space: ^2.0.0 + micromark-util-character: ^2.0.0 + micromark-util-symbol: ^2.0.0 + micromark-util-types: ^2.0.0 + checksum: 71484dcf8db7b189da0528f472cc81e4d6d1a64ae43bbe7fcb7e2e1dba758a0a4f785f9f1afb9459fe5b4a02bbe023d78c95c05204414a14083052eb8219e5eb + languageName: node + linkType: hard + +"micromark-extension-gfm-tagfilter@npm:^2.0.0": + version: 2.0.0 + resolution: "micromark-extension-gfm-tagfilter@npm:2.0.0" + dependencies: + micromark-util-types: ^2.0.0 + checksum: cf21552f4a63592bfd6c96ae5d64a5f22bda4e77814e3f0501bfe80e7a49378ad140f827007f36044666f176b3a0d5fea7c2e8e7973ce4b4579b77789f01ae95 + languageName: node + linkType: hard + +"micromark-extension-gfm-task-list-item@npm:^2.0.0": + version: 2.0.1 + resolution: "micromark-extension-gfm-task-list-item@npm:2.0.1" + dependencies: + devlop: ^1.0.0 + micromark-factory-space: ^2.0.0 + micromark-util-character: ^2.0.0 + micromark-util-symbol: ^2.0.0 + micromark-util-types: ^2.0.0 + checksum: 80e569ab1a1d1f89d86af91482e9629e24b7e3f019c9d7989190f36a9367c6de723b2af48e908c1b73479f35b2215d3d38c1fdbf02ab01eb2fc90a59d1cf4465 + languageName: node + linkType: hard + +"micromark-extension-gfm@npm:^3.0.0": + version: 3.0.0 + resolution: "micromark-extension-gfm@npm:3.0.0" + dependencies: + micromark-extension-gfm-autolink-literal: ^2.0.0 + micromark-extension-gfm-footnote: ^2.0.0 + micromark-extension-gfm-strikethrough: ^2.0.0 + micromark-extension-gfm-table: ^2.0.0 + micromark-extension-gfm-tagfilter: ^2.0.0 + micromark-extension-gfm-task-list-item: ^2.0.0 + micromark-util-combine-extensions: ^2.0.0 + micromark-util-types: ^2.0.0 + checksum: 2060fa62666a09532d6b3a272d413bc1b25bbb262f921d7402795ac021e1362c8913727e33d7528d5b4ccaf26922ec51208c43f795a702964817bc986de886c9 + languageName: node + linkType: hard + +"micromark-extension-mdx-expression@npm:^3.0.0": + version: 3.0.0 + resolution: "micromark-extension-mdx-expression@npm:3.0.0" + dependencies: + "@types/estree": ^1.0.0 + devlop: ^1.0.0 + micromark-factory-mdx-expression: ^2.0.0 + micromark-factory-space: ^2.0.0 + micromark-util-character: ^2.0.0 + micromark-util-events-to-acorn: ^2.0.0 + micromark-util-symbol: ^2.0.0 + micromark-util-types: ^2.0.0 + checksum: abd6ba0acdebc03bc0836c51a1ec4ca28e0be86f10420dd8cfbcd6c10dd37cd3f31e7c8b9792e9276e7526748883f4a30d0803d72b6285dae47d4e5348c23a10 + languageName: node + linkType: hard + +"micromark-extension-mdx-jsx@npm:^3.0.0": + version: 3.0.0 + resolution: "micromark-extension-mdx-jsx@npm:3.0.0" + dependencies: + "@types/acorn": ^4.0.0 + "@types/estree": ^1.0.0 + devlop: ^1.0.0 + estree-util-is-identifier-name: ^3.0.0 + micromark-factory-mdx-expression: ^2.0.0 + micromark-factory-space: ^2.0.0 + micromark-util-character: ^2.0.0 + micromark-util-symbol: ^2.0.0 + micromark-util-types: ^2.0.0 + vfile-message: ^4.0.0 + checksum: 5e2f45d381d1ce43afadc5376427b42ef8cd2a574ca3658473254eabe84db99ef1abc03055b3d86728fac7f1edfb1076e6f2f322ed8bfb1f2f14cafc2c8f0d0e + languageName: node + linkType: hard + +"micromark-extension-mdx-md@npm:^2.0.0": + version: 2.0.0 + resolution: "micromark-extension-mdx-md@npm:2.0.0" + dependencies: + micromark-util-types: ^2.0.0 + checksum: 7daf03372fd7faddf3f0ac87bdb0debb0bb770f33b586f72251e1072b222ceee75400ab6194c0e130dbf1e077369a5b627be6e9130d7a2e9e6b849f0d18ff246 + languageName: node + linkType: hard + +"micromark-extension-mdxjs-esm@npm:^3.0.0": + version: 3.0.0 + resolution: "micromark-extension-mdxjs-esm@npm:3.0.0" + dependencies: + "@types/estree": ^1.0.0 + devlop: ^1.0.0 + micromark-core-commonmark: ^2.0.0 + micromark-util-character: ^2.0.0 + micromark-util-events-to-acorn: ^2.0.0 + micromark-util-symbol: ^2.0.0 + micromark-util-types: ^2.0.0 + unist-util-position-from-estree: ^2.0.0 + vfile-message: ^4.0.0 + checksum: fb33d850200afce567b95c90f2f7d42259bd33eea16154349e4fa77c3ec934f46c8e5c111acea16321dce3d9f85aaa4c49afe8b810e31b34effc11617aeee8f6 + languageName: node + linkType: hard + +"micromark-extension-mdxjs@npm:^3.0.0": + version: 3.0.0 + resolution: "micromark-extension-mdxjs@npm:3.0.0" + dependencies: + acorn: ^8.0.0 + acorn-jsx: ^5.0.0 + micromark-extension-mdx-expression: ^3.0.0 + micromark-extension-mdx-jsx: ^3.0.0 + micromark-extension-mdx-md: ^2.0.0 + micromark-extension-mdxjs-esm: ^3.0.0 + micromark-util-combine-extensions: ^2.0.0 + micromark-util-types: ^2.0.0 + checksum: 7da6f0fb0e1e0270a2f5ad257e7422cc16e68efa7b8214c63c9d55bc264cb872e9ca4ac9a71b9dfd13daf52e010f730bac316086f4340e4fcc6569ec699915bf + languageName: node + linkType: hard + +"micromark-factory-destination@npm:^2.0.0": + version: 2.0.0 + resolution: "micromark-factory-destination@npm:2.0.0" + dependencies: + micromark-util-character: ^2.0.0 + micromark-util-symbol: ^2.0.0 + micromark-util-types: ^2.0.0 + checksum: d36e65ed1c072ff4148b016783148ba7c68a078991154625723e24bda3945160268fb91079fb28618e1613c2b6e70390a8ddc544c45410288aa27b413593071a + languageName: node + linkType: hard + +"micromark-factory-label@npm:^2.0.0": + version: 2.0.0 + resolution: "micromark-factory-label@npm:2.0.0" + dependencies: + devlop: ^1.0.0 + micromark-util-character: ^2.0.0 + micromark-util-symbol: ^2.0.0 + micromark-util-types: ^2.0.0 + checksum: c021dbd0ed367610d35f2bae21209bc804d1a6d1286ffce458fd6a717f4d7fe581a7cba7d5c2d7a63757c44eb927c80d6a571d6ea7969fae1b48ab6461d109c4 + languageName: node + linkType: hard + +"micromark-factory-mdx-expression@npm:^2.0.0": + version: 2.0.1 + resolution: "micromark-factory-mdx-expression@npm:2.0.1" + dependencies: + "@types/estree": ^1.0.0 + devlop: ^1.0.0 + micromark-util-character: ^2.0.0 + micromark-util-events-to-acorn: ^2.0.0 + micromark-util-symbol: ^2.0.0 + micromark-util-types: ^2.0.0 + unist-util-position-from-estree: ^2.0.0 + vfile-message: ^4.0.0 + checksum: 2ba0ae939d0174a5e5331b1a4c203b96862ccf06e8903d6bdcc2d51f75515e52d407cd394afcd182f9ff0e877dc2a14e3fa430ced0131e156650d45104de8311 + languageName: node + linkType: hard + +"micromark-factory-space@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-factory-space@npm:1.1.0" + dependencies: + micromark-util-character: ^1.0.0 + micromark-util-types: ^1.0.0 + checksum: b58435076b998a7e244259a4694eb83c78915581206b6e7fc07b34c6abd36a1726ade63df8972fbf6c8fa38eecb9074f4e17be8d53f942e3b3d23d1a0ecaa941 + languageName: node + linkType: hard + +"micromark-factory-space@npm:^2.0.0": + version: 2.0.0 + resolution: "micromark-factory-space@npm:2.0.0" + dependencies: + micromark-util-character: ^2.0.0 + micromark-util-types: ^2.0.0 + checksum: 4ffdcdc2f759887bbb356500cb460b3915ecddcb5d85c3618d7df68ad05d13ed02b1153ee1845677b7d8126df8f388288b84fcf0d943bd9c92bcc71cd7222e37 languageName: node linkType: hard -"mdast-squeeze-paragraphs@npm:^4.0.0": - version: 4.0.0 - resolution: "mdast-squeeze-paragraphs@npm:4.0.0" +"micromark-factory-title@npm:^2.0.0": + version: 2.0.0 + resolution: "micromark-factory-title@npm:2.0.0" dependencies: - unist-util-remove: ^2.0.0 - checksum: dfe8ec8e8a62171f020e82b088cc35cb9da787736dc133a3b45ce8811782a93e69bf06d147072e281079f09fac67be8a36153ffffd9bfbf89ed284e4c4f56f75 + micromark-factory-space: ^2.0.0 + micromark-util-character: ^2.0.0 + micromark-util-symbol: ^2.0.0 + micromark-util-types: ^2.0.0 + checksum: 39e1ac23af3554e6e652e56065579bc7faf21ade7b8704b29c175871b4152b7109b790bb3cae0f7e088381139c6bac9553b8400772c3d322e4fa635f813a3578 languageName: node linkType: hard -"mdast-util-definitions@npm:^4.0.0": - version: 4.0.0 - resolution: "mdast-util-definitions@npm:4.0.0" +"micromark-factory-whitespace@npm:^2.0.0": + version: 2.0.0 + resolution: "micromark-factory-whitespace@npm:2.0.0" dependencies: - unist-util-visit: ^2.0.0 - checksum: 2325f20b82b3fb8cb5fda77038ee0bbdd44f82cfca7c48a854724b58bc1fe5919630a3ce7c45e210726df59d46c881d020b2da7a493bfd1ee36eb2bbfef5d78e + micromark-factory-space: ^2.0.0 + micromark-util-character: ^2.0.0 + micromark-util-symbol: ^2.0.0 + micromark-util-types: ^2.0.0 + checksum: 9587c2546d1a58b4d5472b42adf05463f6212d0449455285662d63cd8eaed89c6b159ac82713fcee5f9dd88628c24307d9533cccd8971a2f3f4d48702f8f850a languageName: node linkType: hard -"mdast-util-to-hast@npm:10.0.1": - version: 10.0.1 - resolution: "mdast-util-to-hast@npm:10.0.1" +"micromark-util-character@npm:^1.0.0, micromark-util-character@npm:^1.1.0": + version: 1.2.0 + resolution: "micromark-util-character@npm:1.2.0" dependencies: - "@types/mdast": ^3.0.0 - "@types/unist": ^2.0.0 - mdast-util-definitions: ^4.0.0 - mdurl: ^1.0.0 - unist-builder: ^2.0.0 - unist-util-generated: ^1.0.0 - unist-util-position: ^3.0.0 - unist-util-visit: ^2.0.0 - checksum: e5f385757df7e9b37db4d6f326bf7b4fc1b40f9ad01fc335686578f44abe0ba46d3e60af4d5e5b763556d02e65069ef9a09c49db049b52659203a43e7fa9084d + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + checksum: 089e79162a19b4a28731736246579ab7e9482ac93cd681c2bfca9983dcff659212ef158a66a5957e9d4b1dba957d1b87b565d85418a5b009f0294f1f07f2aaac languageName: node linkType: hard -"mdast-util-to-string@npm:^2.0.0": - version: 2.0.0 - resolution: "mdast-util-to-string@npm:2.0.0" - checksum: 0b2113ada10e002fbccb014170506dabe2f2ddacaacbe4bc1045c33f986652c5a162732a2c057c5335cdb58419e2ad23e368e5be226855d4d4e280b81c4e9ec2 +"micromark-util-character@npm:^2.0.0": + version: 2.1.0 + resolution: "micromark-util-character@npm:2.1.0" + dependencies: + micromark-util-symbol: ^2.0.0 + micromark-util-types: ^2.0.0 + checksum: 36ee910f84077cf16626fa618cfe46ac25956b3242e3166b8e8e98c5a8c524af7e5bf3d70822264b1fd2d297a36104a7eb7e3462c19c28353eaca7b0d8717594 languageName: node linkType: hard -"mdn-data@npm:2.0.14": - version: 2.0.14 - resolution: "mdn-data@npm:2.0.14" - checksum: 9d0128ed425a89f4cba8f787dca27ad9408b5cb1b220af2d938e2a0629d17d879a34d2cb19318bdb26c3f14c77dd5dfbae67211f5caaf07b61b1f2c5c8c7dc16 +"micromark-util-chunked@npm:^2.0.0": + version: 2.0.0 + resolution: "micromark-util-chunked@npm:2.0.0" + dependencies: + micromark-util-symbol: ^2.0.0 + checksum: 324f95cccdae061332a8241936eaba6ef0782a1e355bac5c607ad2564fd3744929be7dc81651315a2921535747a33243e6a5606bcb64b7a56d49b6d74ea1a3d4 languageName: node linkType: hard -"mdn-data@npm:2.0.4": - version: 2.0.4 - resolution: "mdn-data@npm:2.0.4" - checksum: add3c95e6d03d301b8a8bcfee3de33f4d07e4c5eee5b79f18d6d737de717e22472deadf67c1a8563983c0b603e10d7df40aa8e5fddf18884dfe118ccec7ae329 +"micromark-util-classify-character@npm:^2.0.0": + version: 2.0.0 + resolution: "micromark-util-classify-character@npm:2.0.0" + dependencies: + micromark-util-character: ^2.0.0 + micromark-util-symbol: ^2.0.0 + micromark-util-types: ^2.0.0 + checksum: 086e52904deffebb793fb1c08c94aabb8901f76958142dfc3a6282890ebaa983b285e69bd602b9d507f1b758ed38e75a994d2ad9fbbefa7de2584f67a16af405 languageName: node linkType: hard -"mdurl@npm:^1.0.0, mdurl@npm:^1.0.1": - version: 1.0.1 - resolution: "mdurl@npm:1.0.1" - checksum: 71731ecba943926bfbf9f9b51e28b5945f9411c4eda80894221b47cc105afa43ba2da820732b436f0798fd3edbbffcd1fc1415843c41a87fea08a41cc1e3d02b +"micromark-util-combine-extensions@npm:^2.0.0": + version: 2.0.0 + resolution: "micromark-util-combine-extensions@npm:2.0.0" + dependencies: + micromark-util-chunked: ^2.0.0 + micromark-util-types: ^2.0.0 + checksum: 107c47700343f365b4ed81551e18bc3458b573c500e56ac052b2490bd548adc475216e41d2271633a8867fac66fc22ba3e0a2d74a31ed79b9870ca947eb4e3ba languageName: node linkType: hard -"measured-core@npm:^1.51.1": - version: 1.51.1 - resolution: "measured-core@npm:1.51.1" +"micromark-util-decode-numeric-character-reference@npm:^2.0.0": + version: 2.0.1 + resolution: "micromark-util-decode-numeric-character-reference@npm:2.0.1" dependencies: - binary-search: ^1.3.3 - optional-js: ^2.0.0 - checksum: b8c7cd5495d0802a2859abc76049781087c1c74817fe8c449a5b5aeeb91a54822b5e52e0b543e775c7031807fc63cbaa76165b3153151a76b0d2145e638ed617 + micromark-util-symbol: ^2.0.0 + checksum: 9512507722efd2033a9f08715eeef787fbfe27e23edf55db21423d46d82ab46f76c89b4f960be3f5e50a2d388d89658afc0647989cf256d051e9ea01277a1adb languageName: node linkType: hard -"measured-reporting@npm:^1.51.1": - version: 1.51.1 - resolution: "measured-reporting@npm:1.51.1" +"micromark-util-decode-string@npm:^2.0.0": + version: 2.0.0 + resolution: "micromark-util-decode-string@npm:2.0.0" dependencies: - console-log-level: ^1.4.1 - mapcap: ^1.0.0 - measured-core: ^1.51.1 - optional-js: ^2.0.0 - checksum: ae0ab419ea1d436d7b101de925677a184bd07070081c7b0913dc9da518d6ce603c94a0198c9b71b40c9cdc342c23f0f2c3aad82e6a385b9b36e8738ba6be1cd0 + decode-named-character-reference: ^1.0.0 + micromark-util-character: ^2.0.0 + micromark-util-decode-numeric-character-reference: ^2.0.0 + micromark-util-symbol: ^2.0.0 + checksum: a75daf32a4a6b549e9f19b4d833ebfeb09a32a9a1f9ce50f35dec6b6a3e4f9f121f49024ba7f9c91c55ebe792f7c7a332fc9604795181b6a612637df0df5b959 languageName: node linkType: hard -"media-typer@npm:0.3.0": - version: 0.3.0 - resolution: "media-typer@npm:0.3.0" - checksum: af1b38516c28ec95d6b0826f6c8f276c58aec391f76be42aa07646b4e39d317723e869700933ca6995b056db4b09a78c92d5440dc23657e6764be5d28874bba1 +"micromark-util-encode@npm:^2.0.0": + version: 2.0.0 + resolution: "micromark-util-encode@npm:2.0.0" + checksum: 853a3f33fce72aaf4ffa60b7f2b6fcfca40b270b3466e1b96561b02185d2bd8c01dd7948bc31a24ac014f4cc854e545ca9a8e9cf7ea46262f9d24c9e88551c66 languageName: node linkType: hard -"memfs@npm:^3.1.2, memfs@npm:^3.4.3": - version: 3.5.3 - resolution: "memfs@npm:3.5.3" +"micromark-util-events-to-acorn@npm:^2.0.0": + version: 2.0.2 + resolution: "micromark-util-events-to-acorn@npm:2.0.2" dependencies: - fs-monkey: ^1.0.4 - checksum: 18dfdeacad7c8047b976a6ccd58bc98ba76e122ad3ca0e50a21837fe2075fc0d9aafc58ab9cf2576c2b6889da1dd2503083f2364191b695273f40969db2ecc44 + "@types/acorn": ^4.0.0 + "@types/estree": ^1.0.0 + "@types/unist": ^3.0.0 + devlop: ^1.0.0 + estree-util-visit: ^2.0.0 + micromark-util-symbol: ^2.0.0 + micromark-util-types: ^2.0.0 + vfile-message: ^4.0.0 + checksum: bcb3eeac52a4ae5c3ca3d8cff514de3a7d1f272d9a94cce26a08c578bef64df4d61820874c01207e92fcace9eae5c9a7ecdddef0c6e10014b255a07b7880bf94 languageName: node linkType: hard -"memoizee@npm:^0.4.15": - version: 0.4.15 - resolution: "memoizee@npm:0.4.15" +"micromark-util-html-tag-name@npm:^2.0.0": + version: 2.0.0 + resolution: "micromark-util-html-tag-name@npm:2.0.0" + checksum: d786d4486f93eb0ac5b628779809ca97c5dc60f3c9fc03eb565809831db181cf8cb7f05f9ac76852f3eb35461af0f89fa407b46f3a03f4f97a96754d8dc540d8 + languageName: node + linkType: hard + +"micromark-util-normalize-identifier@npm:^2.0.0": + version: 2.0.0 + resolution: "micromark-util-normalize-identifier@npm:2.0.0" dependencies: - d: ^1.0.1 - es5-ext: ^0.10.53 - es6-weak-map: ^2.0.3 - event-emitter: ^0.3.5 - is-promise: ^2.2.2 - lru-queue: ^0.1.0 - next-tick: ^1.1.0 - timers-ext: ^0.1.7 - checksum: 4065d94416dbadac56edf5947bf342beca0e9f051f33ad60d7c4baf3f6ca0f3c6fdb770c5caed5a89c0ceaf9121428582f396445d591785281383d60aa883418 + micromark-util-symbol: ^2.0.0 + checksum: b36da2d3fd102053dadd953ce5c558328df12a63a8ac0e5aad13d4dda8e43b6a5d4a661baafe0a1cd8a260bead4b4a8e6e0e74193dd651e8484225bd4f4e68aa languageName: node linkType: hard -"memory-pager@npm:^1.0.2": - version: 1.5.0 - resolution: "memory-pager@npm:1.5.0" - checksum: d1a2e684583ef55c61cd3a49101da645b11ad57014dfc565e0b43baa9004b743f7e4ab81493d8fff2ab24e9950987cc3209c94bcc4fc8d7e30a475489a1f15e9 +"micromark-util-resolve-all@npm:^2.0.0": + version: 2.0.0 + resolution: "micromark-util-resolve-all@npm:2.0.0" + dependencies: + micromark-util-types: ^2.0.0 + checksum: 31fe703b85572cb3f598ebe32750e59516925c7ff1f66cfe6afaebe0771a395a9eaa770787f2523d3c46082ea80e6c14f83643303740b3d650af7c96ebd30ccc languageName: node linkType: hard -"meow@npm:^11.0.0": - version: 11.0.0 - resolution: "meow@npm:11.0.0" +"micromark-util-sanitize-uri@npm:^2.0.0": + version: 2.0.0 + resolution: "micromark-util-sanitize-uri@npm:2.0.0" dependencies: - "@types/minimist": ^1.2.2 - camelcase-keys: ^8.0.2 - decamelize: ^6.0.0 - decamelize-keys: ^1.1.0 - hard-rejection: ^2.1.0 - minimist-options: 4.1.0 - normalize-package-data: ^4.0.1 - read-pkg-up: ^9.1.0 - redent: ^4.0.0 - trim-newlines: ^4.0.2 - type-fest: ^3.1.0 - yargs-parser: ^21.1.1 - checksum: 2e815b8d2acc6cda0ea10e0a6dcd6fbdcc2fb8b24412c3c70acd77220642ca0dc727c6fccd79d64b7ca811d099e8a9ad62ea261a8f39d4b61fcdcaf551c5c788 + micromark-util-character: ^2.0.0 + micromark-util-encode: ^2.0.0 + micromark-util-symbol: ^2.0.0 + checksum: ea4c28bbffcf2430e9aff2d18554296789a8b0a1f54ac24020d1dde76624a7f93e8f2a83e88cd5a846b6d2c4287b71b1142d1b89fa7f1b0363a9b33711a141fe languageName: node linkType: hard -"meow@npm:^8.0.0": - version: 8.1.2 - resolution: "meow@npm:8.1.2" +"micromark-util-subtokenize@npm:^2.0.0": + version: 2.0.1 + resolution: "micromark-util-subtokenize@npm:2.0.1" dependencies: - "@types/minimist": ^1.2.0 - camelcase-keys: ^6.2.2 - decamelize-keys: ^1.1.0 - hard-rejection: ^2.1.0 - minimist-options: 4.1.0 - normalize-package-data: ^3.0.0 - read-pkg-up: ^7.0.1 - redent: ^3.0.0 - trim-newlines: ^3.0.0 - type-fest: ^0.18.0 - yargs-parser: ^20.2.3 - checksum: bc23bf1b4423ef6a821dff9734406bce4b91ea257e7f10a8b7f896f45b59649f07adc0926e2917eacd8cf1df9e4cd89c77623cf63dfd0f8bf54de07a32ee5a85 + devlop: ^1.0.0 + micromark-util-chunked: ^2.0.0 + micromark-util-symbol: ^2.0.0 + micromark-util-types: ^2.0.0 + checksum: 5d338883ad8889c63f9b262b9cae0c02a42088201981d820ae7af7aa6d38fab6585b89fd4cf2206a46a7c4002e41ee6c70e1a3e0ceb3ad8b7adcffaf166b1511 languageName: node linkType: hard -"merge-descriptors@npm:1.0.1": - version: 1.0.1 - resolution: "merge-descriptors@npm:1.0.1" - checksum: 5abc259d2ae25bb06d19ce2b94a21632583c74e2a9109ee1ba7fd147aa7362b380d971e0251069f8b3eb7d48c21ac839e21fa177b335e82c76ec172e30c31a26 +"micromark-util-symbol@npm:^1.0.0, micromark-util-symbol@npm:^1.0.1": + version: 1.1.0 + resolution: "micromark-util-symbol@npm:1.1.0" + checksum: 02414a753b79f67ff3276b517eeac87913aea6c028f3e668a19ea0fc09d98aea9f93d6222a76ca783d20299af9e4b8e7c797fe516b766185dcc6e93290f11f88 languageName: node linkType: hard -"merge-stream@npm:^2.0.0": +"micromark-util-symbol@npm:^2.0.0": version: 2.0.0 - resolution: "merge-stream@npm:2.0.0" - checksum: 6fa4dcc8d86629705cea944a4b88ef4cb0e07656ebf223fa287443256414283dd25d91c1cd84c77987f2aec5927af1a9db6085757cb43d90eb170ebf4b47f4f4 + resolution: "micromark-util-symbol@npm:2.0.0" + checksum: fa4a05bff575d9fbf0ad96a1013003e3bb6087ed6b34b609a141b6c0d2137b57df594aca409a95f4c5fda199f227b56a7d8b1f82cea0768df161d8a3a3660764 languageName: node linkType: hard -"merge2@npm:^1.3.0, merge2@npm:^1.4.1": - version: 1.4.1 - resolution: "merge2@npm:1.4.1" - checksum: 7268db63ed5169466540b6fb947aec313200bcf6d40c5ab722c22e242f651994619bcd85601602972d3c85bd2cc45a358a4c61937e9f11a061919a1da569b0c2 +"micromark-util-types@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-util-types@npm:1.1.0" + checksum: b0ef2b4b9589f15aec2666690477a6a185536927ceb7aa55a0f46475852e012d75a1ab945187e5c7841969a842892164b15d58ff8316b8e0d6cc920cabd5ede7 languageName: node linkType: hard -"methods@npm:~1.1.2": - version: 1.1.2 - resolution: "methods@npm:1.1.2" - checksum: 0917ff4041fa8e2f2fda5425a955fe16ca411591fbd123c0d722fcf02b73971ed6f764d85f0a6f547ce49ee0221ce2c19a5fa692157931cecb422984f1dcd13a +"micromark-util-types@npm:^2.0.0": + version: 2.0.0 + resolution: "micromark-util-types@npm:2.0.0" + checksum: 819fef3ab5770c37893d2a60381fb2694396c8d22803b6e103c830c3a1bc1490363c2b0470bb2acaaddad776dfbc2fc1fcfde39cb63c4f54d95121611672e3d0 + languageName: node + linkType: hard + +"micromark@npm:^4.0.0": + version: 4.0.0 + resolution: "micromark@npm:4.0.0" + dependencies: + "@types/debug": ^4.0.0 + debug: ^4.0.0 + decode-named-character-reference: ^1.0.0 + devlop: ^1.0.0 + micromark-core-commonmark: ^2.0.0 + micromark-factory-space: ^2.0.0 + micromark-util-character: ^2.0.0 + micromark-util-chunked: ^2.0.0 + micromark-util-combine-extensions: ^2.0.0 + micromark-util-decode-numeric-character-reference: ^2.0.0 + micromark-util-encode: ^2.0.0 + micromark-util-normalize-identifier: ^2.0.0 + micromark-util-resolve-all: ^2.0.0 + micromark-util-sanitize-uri: ^2.0.0 + micromark-util-subtokenize: ^2.0.0 + micromark-util-symbol: ^2.0.0 + micromark-util-types: ^2.0.0 + checksum: b84ab5ab1a0b28c063c52e9c2c9d7d44b954507235c10c9492d66e0b38f7de24bf298f914a1fbdf109f2a57a88cf0412de217c84cfac5fd60e3e42a74dbac085 languageName: node linkType: hard @@ -15323,7 +17550,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"mimic-response@npm:^1.0.0, mimic-response@npm:^1.0.1": +"mimic-response@npm:^1.0.0": version: 1.0.1 resolution: "mimic-response@npm:1.0.1" checksum: 034c78753b0e622bc03c983663b1cdf66d03861050e0c8606563d149bc2b02d63f62ce4d32be4ab50d0553ae0ffe647fc34d1f5281184c6e1e8cf4d85e8d9823 @@ -15337,6 +17564,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"mimic-response@npm:^4.0.0": + version: 4.0.0 + resolution: "mimic-response@npm:4.0.0" + checksum: 33b804cc961efe206efdb1fca6a22540decdcfce6c14eb5c0c50e5ae9022267ab22ce8f5568b1f7247ba67500fe20d523d81e0e9f009b321ccd9d472e78d1850 + languageName: node + linkType: hard + "min-indent@npm:^1.0.0, min-indent@npm:^1.0.1": version: 1.0.1 resolution: "min-indent@npm:1.0.1" @@ -15344,14 +17578,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"mini-css-extract-plugin@npm:^2.6.1": - version: 2.7.6 - resolution: "mini-css-extract-plugin@npm:2.7.6" +"mini-css-extract-plugin@npm:^2.7.6": + version: 2.9.0 + resolution: "mini-css-extract-plugin@npm:2.9.0" dependencies: schema-utils: ^4.0.0 + tapable: ^2.2.1 peerDependencies: webpack: ^5.0.0 - checksum: be6f7cefc6275168eb0a6b8fe977083a18c743c9612c9f00e6c1a62c3393ca7960e93fba1a7ebb09b75f36a0204ad087d772c1ef574bc29c90c0e8175a3c0b83 + checksum: ae192c67ba85ac8bffeab66774635bf90181f00d5dd6cf95412426192599ddf5506fb4b1550acbd7a5476476e39db53c770dd40f8378f7baf5de96e3fec4e6e9 languageName: node linkType: hard @@ -15873,12 +18108,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"node-emoji@npm:^1.10.0": - version: 1.11.0 - resolution: "node-emoji@npm:1.11.0" +"node-emoji@npm:^2.1.0": + version: 2.1.3 + resolution: "node-emoji@npm:2.1.3" dependencies: - lodash: ^4.17.21 - checksum: e8c856c04a1645062112a72e59a98b203505ed5111ff84a3a5f40611afa229b578c7d50f1e6a7f17aa62baeea4a640d2e2f61f63afc05423aa267af10977fb2b + "@sindresorhus/is": ^4.6.0 + char-regex: ^1.0.2 + emojilib: ^2.4.0 + skin-tone: ^2.0.0 + checksum: 9ae5a1fb12fd5ce6885f251f345986115de4bb82e7d06fdc943845fb19260d89d0aaaccbaf85cae39fe7aaa1fc391640558865ba690c9bb8a7236c3ac10bbab0 languageName: node linkType: hard @@ -15896,7 +18134,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.12, node-fetch@npm:^2.6.7, node-fetch@npm:^2.7.0": +"node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7, node-fetch@npm:^2.7.0": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: @@ -16100,13 +18338,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"normalize-url@npm:^4.1.0": - version: 4.5.1 - resolution: "normalize-url@npm:4.5.1" - checksum: 9a9dee01df02ad23e171171893e56e22d752f7cff86fb96aafeae074819b572ea655b60f8302e2d85dbb834dc885c972cc1c573892fea24df46b2765065dd05a - languageName: node - linkType: hard - "normalize-url@npm:^6.0.1": version: 6.1.0 resolution: "normalize-url@npm:6.1.0" @@ -16114,6 +18345,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"normalize-url@npm:^8.0.0": + version: 8.0.1 + resolution: "normalize-url@npm:8.0.1" + checksum: 43ea9ef0d6d135dd1556ab67aa4b74820f0d9d15aa504b59fa35647c729f1147dfce48d3ad504998fd1010f089cfb82c86c6d9126eb5c5bd2e9bd25f3a97749b + languageName: node + linkType: hard + "npm-bundled@npm:^1.1.1, npm-bundled@npm:^1.1.2": version: 1.1.2 resolution: "npm-bundled@npm:1.1.2" @@ -16494,7 +18732,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.0.1, object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" checksum: fcc6e4ea8c7fe48abfbb552578b1c53e0d194086e2e6bbbf59e0a536381a292f39943c6e9628af05b5528aa5e3318bb30d6b2e53cadaf5b8fe9e12c4b69af23f @@ -16821,13 +19059,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"p-cancelable@npm:^1.0.0": - version: 1.1.0 - resolution: "p-cancelable@npm:1.1.0" - checksum: 2db3814fef6d9025787f30afaee4496a8857a28be3c5706432cbad76c688a6db1874308f48e364a42f5317f5e41e8e7b4f2ff5c8ff2256dbb6264bc361704ece - languageName: node - linkType: hard - "p-cancelable@npm:^2.0.0": version: 2.1.1 resolution: "p-cancelable@npm:2.1.1" @@ -16835,6 +19066,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"p-cancelable@npm:^3.0.0": + version: 3.0.0 + resolution: "p-cancelable@npm:3.0.0" + checksum: 2b5ae34218f9c2cf7a7c18e5d9a726ef9b165ef07e6c959f6738371509e747334b5f78f3bcdeb03d8a12dcb978faf641fd87eb21486ed7d36fb823b8ddef3219 + languageName: node + linkType: hard + "p-finally@npm:^1.0.0": version: 1.0.0 resolution: "p-finally@npm:1.0.0" @@ -17019,15 +19257,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"package-json@npm:^6.3.0": - version: 6.5.0 - resolution: "package-json@npm:6.5.0" +"package-json@npm:^8.1.0": + version: 8.1.1 + resolution: "package-json@npm:8.1.1" dependencies: - got: ^9.6.0 - registry-auth-token: ^4.0.0 - registry-url: ^5.0.0 - semver: ^6.2.0 - checksum: cc9f890d3667d7610e6184decf543278b87f657d1ace0deb4a9c9155feca738ef88f660c82200763d3348010f4e42e9c7adc91e96ab0f86a770955995b5351e2 + got: ^12.1.0 + registry-auth-token: ^5.0.1 + registry-url: ^6.0.0 + semver: ^7.3.7 + checksum: 28bec6f42bf9fba66b7c8fea07576fc23d08ec7923433f7835d6cd8654e72169d74f9738b3785107d18a476ae76712e0daeb1dddcd6930e69f9e4b47eba7c0ca languageName: node linkType: hard @@ -17193,17 +19431,19 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"parse-entities@npm:^2.0.0": - version: 2.0.0 - resolution: "parse-entities@npm:2.0.0" +"parse-entities@npm:^4.0.0": + version: 4.0.1 + resolution: "parse-entities@npm:4.0.1" dependencies: - character-entities: ^1.0.0 - character-entities-legacy: ^1.0.0 - character-reference-invalid: ^1.0.0 - is-alphanumerical: ^1.0.0 - is-decimal: ^1.0.0 - is-hexadecimal: ^1.0.0 - checksum: 7addfd3e7d747521afac33c8121a5f23043c6973809756920d37e806639b4898385d386fcf4b3c8e2ecf1bc28aac5ae97df0b112d5042034efbe80f44081ebce + "@types/unist": ^2.0.0 + character-entities: ^2.0.0 + character-entities-legacy: ^3.0.0 + character-reference-invalid: ^2.0.0 + decode-named-character-reference: ^1.0.0 + is-alphanumerical: ^2.0.0 + is-decimal: ^2.0.0 + is-hexadecimal: ^2.0.0 + checksum: 32a6ff5b9acb9d2c4d71537308521fd265e685b9215691df73feedd9edfe041bb6da9f89bd0c35c4a2bc7d58e3e76e399bb6078c2fd7d2a343ff1dd46edbf1bd languageName: node linkType: hard @@ -17264,7 +19504,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"parse5@npm:6.0.1, parse5@npm:^6.0.0": +"parse5@npm:6.0.1": version: 6.0.1 resolution: "parse5@npm:6.0.1" checksum: 7d569a176c5460897f7c8f3377eff640d54132b9be51ae8a8fa4979af940830b2b0c296ce75e5bd8f4041520aadde13170dbdec44889975f906098ea0002f4bd @@ -17475,6 +19715,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"periscopic@npm:^3.0.0": + version: 3.1.0 + resolution: "periscopic@npm:3.1.0" + dependencies: + "@types/estree": ^1.0.0 + estree-walker: ^3.0.0 + is-reference: ^3.0.0 + checksum: 2153244352e58a0d76e7e8d9263e66fe74509495f809af388da20045fb30aa3e93f2f94468dc0b9166ecf206fcfc0d73d2c7641c6fbedc07b1de858b710142cb + languageName: node + linkType: hard + "picocolors@npm:^1.0.0": version: 1.0.0 resolution: "picocolors@npm:1.0.0" @@ -17566,7 +19817,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"pkg-dir@npm:4.2.0, pkg-dir@npm:^4.1.0, pkg-dir@npm:^4.2.0": +"pkg-dir@npm:4.2.0, pkg-dir@npm:^4.2.0": version: 4.2.0 resolution: "pkg-dir@npm:4.2.0" dependencies: @@ -17575,6 +19826,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"pkg-dir@npm:^7.0.0": + version: 7.0.0 + resolution: "pkg-dir@npm:7.0.0" + dependencies: + find-up: ^6.3.0 + checksum: 94298b20a446bfbbd66604474de8a0cdd3b8d251225170970f15d9646f633e056c80520dd5b4c1d1050c9fed8f6a9e5054b141c93806439452efe72e57562c03 + languageName: node + linkType: hard + "pkg-prebuilds@npm:^0.2.1": version: 0.2.1 resolution: "pkg-prebuilds@npm:0.2.1" @@ -17703,17 +19963,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"postcss-loader@npm:^7.0.0": - version: 7.3.3 - resolution: "postcss-loader@npm:7.3.3" +"postcss-loader@npm:^7.3.3": + version: 7.3.4 + resolution: "postcss-loader@npm:7.3.4" dependencies: - cosmiconfig: ^8.2.0 - jiti: ^1.18.2 - semver: ^7.3.8 + cosmiconfig: ^8.3.5 + jiti: ^1.20.0 + semver: ^7.5.4 peerDependencies: postcss: ^7.0.0 || ^8.0.1 webpack: ^5.0.0 - checksum: c724044d6ae56334535c26bb4efc9c151431d44d60bc8300157c760747281a242757d8dab32db72738434531175b38a408cb0b270bb96207c07584dcfcd899ff + checksum: f109eb266580eb296441a1ae057f93629b9b79ad962bdd3fc134417180431606a5419b6f5848c31e6d92c818e71fe96e4335a85cc5332c2f7b14e2869951e5b3 languageName: node linkType: hard @@ -17803,36 +20063,36 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"postcss-modules-extract-imports@npm:^3.0.0": - version: 3.0.0 - resolution: "postcss-modules-extract-imports@npm:3.0.0" +"postcss-modules-extract-imports@npm:^3.1.0": + version: 3.1.0 + resolution: "postcss-modules-extract-imports@npm:3.1.0" peerDependencies: postcss: ^8.1.0 - checksum: 4b65f2f1382d89c4bc3c0a1bdc5942f52f3cb19c110c57bd591ffab3a5fee03fcf831604168205b0c1b631a3dce2255c70b61aaae3ef39d69cd7eb450c2552d2 + checksum: b9192e0f4fb3d19431558be6f8af7ca45fc92baaad9b2778d1732a5880cd25c3df2074ce5484ae491e224f0d21345ffc2d419bd51c25b019af76d7a7af88c17f languageName: node linkType: hard -"postcss-modules-local-by-default@npm:^4.0.3": - version: 4.0.3 - resolution: "postcss-modules-local-by-default@npm:4.0.3" +"postcss-modules-local-by-default@npm:^4.0.5": + version: 4.0.5 + resolution: "postcss-modules-local-by-default@npm:4.0.5" dependencies: icss-utils: ^5.0.0 postcss-selector-parser: ^6.0.2 postcss-value-parser: ^4.1.0 peerDependencies: postcss: ^8.1.0 - checksum: 2f8083687f3d6067885f8863dd32dbbb4f779cfcc7e52c17abede9311d84faf6d3ed8760e7c54c6380281732ae1f78e5e56a28baf3c271b33f450a11c9e30485 + checksum: ca9b01f4a0a3dfb33e016299e2dfb7e85c3123292f7aec2efc0c6771b9955648598bfb4c1561f7ee9732fb27fb073681233661b32eef98baab43743f96735452 languageName: node linkType: hard -"postcss-modules-scope@npm:^3.0.0": - version: 3.0.0 - resolution: "postcss-modules-scope@npm:3.0.0" +"postcss-modules-scope@npm:^3.2.0": + version: 3.2.0 + resolution: "postcss-modules-scope@npm:3.2.0" dependencies: postcss-selector-parser: ^6.0.4 peerDependencies: postcss: ^8.1.0 - checksum: 330b9398dbd44c992c92b0dc612c0626135e2cc840fee41841eb61247a6cfed95af2bd6f67ead9dd9d0bb41f5b0367129d93c6e434fa3e9c58ade391d9a5a138 + checksum: 2ffe7e98c1fa993192a39c8dd8ade93fc4f59fbd1336ce34fcedaee0ee3bafb29e2e23fb49189256895b30e4f21af661c6a6a16ef7b17ae2c859301e4a4459ae languageName: node linkType: hard @@ -18002,7 +20262,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"postcss-sort-media-queries@npm:^4.2.1": +"postcss-sort-media-queries@npm:^4.4.1": version: 4.4.1 resolution: "postcss-sort-media-queries@npm:4.4.1" dependencies: @@ -18052,7 +20312,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"postcss@npm:^8.3.11, postcss@npm:^8.4.14, postcss@npm:^8.4.17, postcss@npm:^8.4.21": +"postcss@npm:^8.4.17, postcss@npm:^8.4.21": version: 8.4.30 resolution: "postcss@npm:8.4.30" dependencies: @@ -18063,6 +20323,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"postcss@npm:^8.4.26, postcss@npm:^8.4.33": + version: 8.4.38 + resolution: "postcss@npm:8.4.38" + dependencies: + nanoid: ^3.3.7 + picocolors: ^1.0.0 + source-map-js: ^1.2.0 + checksum: 649f9e60a763ca4b5a7bbec446a069edf07f057f6d780a5a0070576b841538d1ecf7dd888f2fbfd1f76200e26c969e405aeeae66332e6927dbdc8bdcb90b9451 + languageName: node + linkType: hard + "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -18070,13 +20341,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"prepend-http@npm:^2.0.0": - version: 2.0.0 - resolution: "prepend-http@npm:2.0.0" - checksum: 7694a9525405447662c1ffd352fcb41b6410c705b739b6f4e3a3e21cf5fdede8377890088e8934436b8b17ba55365a615f153960f30877bf0d0392f9e93503ea - languageName: node - linkType: hard - "prettier-linter-helpers@npm:^1.0.0": version: 1.0.0 resolution: "prettier-linter-helpers@npm:1.0.0" @@ -18134,16 +20398,19 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"prism-react-renderer@npm:^1.3.5": - version: 1.3.5 - resolution: "prism-react-renderer@npm:1.3.5" +"prism-react-renderer@npm:^2.1.0, prism-react-renderer@npm:^2.3.0": + version: 2.3.1 + resolution: "prism-react-renderer@npm:2.3.1" + dependencies: + "@types/prismjs": ^1.26.0 + clsx: ^2.0.0 peerDependencies: - react: ">=0.14.9" - checksum: c18806dcbc4c0b4fd6fd15bd06b4f7c0a6da98d93af235c3e970854994eb9b59e23315abb6cfc29e69da26d36709a47e25da85ab27fed81b6812f0a52caf6dfa + react: ">=16.0.0" + checksum: b12a7d502c1e764d94f7d3c84aee9cd6fccc676bb7e21dee94d37eb2e7e62e097a343999e1979887cb83a57cbdea48d2046aa74d07bce05caa25f4c296df30b6 languageName: node linkType: hard -"prismjs@npm:^1.28.0": +"prismjs@npm:^1.29.0": version: 1.29.0 resolution: "prismjs@npm:1.29.0" checksum: 007a8869d4456ff8049dc59404e32d5666a07d99c3b0e30a18bd3b7676dfa07d1daae9d0f407f20983865fd8da56de91d09cb08e6aa61f5bc420a27c0beeaf93 @@ -18232,15 +20499,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"promise@npm:^7.1.1": - version: 7.3.1 - resolution: "promise@npm:7.3.1" - dependencies: - asap: ~2.0.3 - checksum: 475bb069130179fbd27ed2ab45f26d8862376a137a57314cf53310bdd85cc986a826fd585829be97ebc0aaf10e9d8e68be1bfe5a4a0364144b1f9eedfa940cf1 - languageName: node - linkType: hard - "prompts@npm:^2.0.1, prompts@npm:^2.4.2": version: 2.4.2 resolution: "prompts@npm:2.4.2" @@ -18271,12 +20529,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"property-information@npm:^5.0.0, property-information@npm:^5.3.0": - version: 5.6.0 - resolution: "property-information@npm:5.6.0" - dependencies: - xtend: ^4.0.0 - checksum: fcf87c6542e59a8bbe31ca0b3255a4a63ac1059b01b04469680288998bcfa97f341ca989566adbb63975f4d85339030b82320c324a511532d390910d1c583893 +"property-information@npm:^6.0.0": + version: 6.5.0 + resolution: "property-information@npm:6.5.0" + checksum: 6e55664e2f64083b715011e5bafaa1e694faf36986c235b0907e95d09259cc37c38382e3cc94a4c3f56366e05336443db12c8a0f0968a8c0a1b1416eebfc8f53 languageName: node linkType: hard @@ -18376,12 +20632,12 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"pupa@npm:^2.1.1": - version: 2.1.1 - resolution: "pupa@npm:2.1.1" +"pupa@npm:^3.1.0": + version: 3.1.0 + resolution: "pupa@npm:3.1.0" dependencies: - escape-goat: ^2.0.0 - checksum: 49529e50372ffdb0cccf0efa0f3b3cb0a2c77805d0d9cc2725bd2a0f6bb414631e61c93a38561b26be1259550b7bb6c2cb92315aa09c8bf93f3bdcb49f2b2fb7 + escape-goat: ^4.0.0 + checksum: 0e4f4ab6bbdce600fa6d23b1833f1af57b2641246ff4cbe10f9d66e4e5479b0de2864a88d5bd629eef59524eda3c6680726acd7f3f873d9ed46b7f095d0bb5f6 languageName: node linkType: hard @@ -18405,13 +20661,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"pure-color@npm:^1.2.0": - version: 1.3.0 - resolution: "pure-color@npm:1.3.0" - checksum: 646d8bed6e6eab89affdd5e2c11f607a85b631a7fb03c061dfa658eb4dc4806881a15feed2ac5fd8c0bad8c00c632c640d5b1cb8b9a972e6e947393a1329371b - languageName: node - linkType: hard - "pure-rand@npm:^6.0.0": version: 6.0.3 resolution: "pure-rand@npm:6.0.3" @@ -18536,7 +20785,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"rc@npm:1.2.8, rc@npm:^1.2.8": +"rc@npm:1.2.8": version: 1.2.8 resolution: "rc@npm:1.2.8" dependencies: @@ -18550,18 +20799,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"react-base16-styling@npm:^0.6.0": - version: 0.6.0 - resolution: "react-base16-styling@npm:0.6.0" - dependencies: - base16: ^1.0.0 - lodash.curry: ^4.0.1 - lodash.flow: ^3.3.0 - pure-color: ^1.2.0 - checksum: 00a12dddafc8a9025cca933b0dcb65fca41c81fa176d1fc3a6a9d0242127042e2c0a604f4c724a3254dd2c6aeb5ef55095522ff22f5462e419641c1341a658e4 - languageName: node - linkType: hard - "react-dev-utils@npm:^12.0.1": version: 12.0.1 resolution: "react-dev-utils@npm:12.0.1" @@ -18607,6 +20844,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"react-dom@npm:^18.2.0": + version: 18.2.0 + resolution: "react-dom@npm:18.2.0" + dependencies: + loose-envify: ^1.1.0 + scheduler: ^0.23.0 + peerDependencies: + react: ^18.2.0 + checksum: 7d323310bea3a91be2965f9468d552f201b1c27891e45ddc2d6b8f717680c95a75ae0bc1e3f5cf41472446a2589a75aed4483aee8169287909fcd59ad149e8cc + languageName: node + linkType: hard + "react-error-overlay@npm:^6.0.11": version: 6.0.11 resolution: "react-error-overlay@npm:6.0.11" @@ -18651,25 +20900,12 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"react-json-view@npm:^1.21.3": - version: 1.21.3 - resolution: "react-json-view@npm:1.21.3" - dependencies: - flux: ^4.0.1 - react-base16-styling: ^0.6.0 - react-lifecycles-compat: ^3.0.4 - react-textarea-autosize: ^8.3.2 +"react-json-view-lite@npm:^1.2.0": + version: 1.3.0 + resolution: "react-json-view-lite@npm:1.3.0" peerDependencies: - react: ^17.0.0 || ^16.3.0 || ^15.5.4 - react-dom: ^17.0.0 || ^16.3.0 || ^15.5.4 - checksum: 5718bcd9210ad5b06eb9469cf8b9b44be9498845a7702e621343618e8251f26357e6e1c865532cf170db6165df1cb30202787e057309d8848c220bc600ec0d1a - languageName: node - linkType: hard - -"react-lifecycles-compat@npm:^3.0.4": - version: 3.0.4 - resolution: "react-lifecycles-compat@npm:3.0.4" - checksum: a904b0fc0a8eeb15a148c9feb7bc17cec7ef96e71188280061fc340043fd6d8ee3ff233381f0e8f95c1cf926210b2c4a31f38182c8f35ac55057e453d6df204f + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + checksum: 612d129c9a0b3429e540226b43d9c6113638a1814fd39a12dbdc0e1e6a13cf0d018193d8c21dbf8f3d1ca747667b0a0e5d37ab3532c63571e113cf571b589b6a languageName: node linkType: hard @@ -18697,7 +20933,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"react-router-dom@npm:^5.3.3": +"react-router-dom@npm:^5.3.4": version: 5.3.4 resolution: "react-router-dom@npm:5.3.4" dependencies: @@ -18714,7 +20950,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"react-router@npm:5.3.4, react-router@npm:^5.3.3": +"react-router@npm:5.3.4, react-router@npm:^5.3.4": version: 5.3.4 resolution: "react-router@npm:5.3.4" dependencies: @@ -18733,20 +20969,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"react-textarea-autosize@npm:^8.3.2": - version: 8.5.3 - resolution: "react-textarea-autosize@npm:8.5.3" - dependencies: - "@babel/runtime": ^7.20.13 - use-composed-ref: ^1.3.0 - use-latest: ^1.2.1 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: b317c3763f37a89621bbafd0e6e2d068e7876790a5ae77f497adfd6ba9334ceea138c8a0b7d907bae0f79c765cb24e8b2ca2b8033b4144c0bce28571a3658921 - languageName: node - linkType: hard - -"react@npm:^17.0.1, react@npm:^17.0.2": +"react@npm:^17.0.1": version: 17.0.2 resolution: "react@npm:17.0.2" dependencies: @@ -18756,6 +20979,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"react@npm:^18.2.0": + version: 18.2.0 + resolution: "react@npm:18.2.0" + dependencies: + loose-envify: ^1.1.0 + checksum: 88e38092da8839b830cda6feef2e8505dec8ace60579e46aa5490fc3dc9bba0bd50336507dc166f43e3afc1c42939c09fe33b25fae889d6f402721dcd78fca1b + languageName: node + linkType: hard + "read-cmd-shim@npm:3.0.0": version: 3.0.0 resolution: "read-cmd-shim@npm:3.0.0" @@ -19148,21 +21380,21 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"registry-auth-token@npm:^4.0.0": - version: 4.2.2 - resolution: "registry-auth-token@npm:4.2.2" +"registry-auth-token@npm:^5.0.1": + version: 5.0.2 + resolution: "registry-auth-token@npm:5.0.2" dependencies: - rc: 1.2.8 - checksum: c5030198546ecfdcbcb0722cbc3e260c4f5f174d8d07bdfedd4620e79bfdf17a2db735aa230d600bd388fce6edd26c0a9ed2eb7e9b4641ec15213a28a806688b + "@pnpm/npm-conf": ^2.1.0 + checksum: 0d7683b71ee418993e7872b389024b13645c4295eb7bb850d10728eaf46065db24ea4d47dc6cbb71a60d1aa4bef077b0d8b7363c9ac9d355fdba47bebdfb01dd languageName: node linkType: hard -"registry-url@npm:^5.0.0": - version: 5.1.0 - resolution: "registry-url@npm:5.1.0" +"registry-url@npm:^6.0.0": + version: 6.0.1 + resolution: "registry-url@npm:6.0.1" dependencies: - rc: ^1.2.8 - checksum: bcea86c84a0dbb66467b53187fadebfea79017cddfb4a45cf27530d7275e49082fe9f44301976eb0164c438e395684bcf3dae4819b36ff9d1640d8cc60c73df9 + rc: 1.2.8 + checksum: 33712aa1b489aab7aba2191c1cdadfdd71f5bf166d4792d81744a6be332c160bd7d9273af8269d8a01284b9562f14a5b31b7abcf7ad9306c44887ecff51c89ab languageName: node linkType: hard @@ -19177,6 +21409,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"rehype-raw@npm:^7.0.0": + version: 7.0.0 + resolution: "rehype-raw@npm:7.0.0" + dependencies: + "@types/hast": ^3.0.0 + hast-util-raw: ^9.0.0 + vfile: ^6.0.0 + checksum: f9e28dcbf4c6c7d91a97c10a840310f18ef3268aa45abb3e0428b6b191ff3c4fa8f753b910d768588a2dac5c7da7e557b4ddc3f1b6cd252e8d20cb62d60c65ed + languageName: node + linkType: hard + "relateurl@npm:^0.2.7": version: 0.2.7 resolution: "relateurl@npm:0.2.7" @@ -19191,70 +21434,100 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"remark-emoji@npm:^2.2.0": - version: 2.2.0 - resolution: "remark-emoji@npm:2.2.0" +"remark-directive@npm:^3.0.0": + version: 3.0.0 + resolution: "remark-directive@npm:3.0.0" dependencies: - emoticon: ^3.2.0 - node-emoji: ^1.10.0 - unist-util-visit: ^2.0.3 - checksum: 638d4be72eb4110a447f389d4b8c454921f188c0acabf1b6579f3ddaa301ee91010173d6eebd975ea622ae3de7ed4531c0315a4ffd4f9653d80c599ef9ec21a8 + "@types/mdast": ^4.0.0 + mdast-util-directive: ^3.0.0 + micromark-extension-directive: ^3.0.0 + unified: ^11.0.0 + checksum: 744d12bbe924bd0492a2481cbaf9250aa6622c0d2cc090bb7bc39975e355c8a46ae13cc4793204ada39f0af64c953f6b730a55420a50375e0f74a5dd5d201089 languageName: node linkType: hard -"remark-footnotes@npm:2.0.0": - version: 2.0.0 - resolution: "remark-footnotes@npm:2.0.0" - checksum: f2f87ffd6fe25892373c7164d6584a7cb03ab0ea4f186af493a73df519e24b72998a556e7f16cb996f18426cdb80556b95ff252769e252cf3ccba0fd2ca20621 +"remark-emoji@npm:^4.0.0": + version: 4.0.1 + resolution: "remark-emoji@npm:4.0.1" + dependencies: + "@types/mdast": ^4.0.2 + emoticon: ^4.0.1 + mdast-util-find-and-replace: ^3.0.1 + node-emoji: ^2.1.0 + unified: ^11.0.4 + checksum: 2c02d8c0b694535a9f0c4fe39180cb89a8fbd07eb873c94842c34dfde566b8a6703df9d28fe175a8c28584f96252121de722862baa756f2d875f2f1a4352c1f4 languageName: node linkType: hard -"remark-mdx@npm:1.6.22": - version: 1.6.22 - resolution: "remark-mdx@npm:1.6.22" +"remark-frontmatter@npm:^5.0.0": + version: 5.0.0 + resolution: "remark-frontmatter@npm:5.0.0" dependencies: - "@babel/core": 7.12.9 - "@babel/helper-plugin-utils": 7.10.4 - "@babel/plugin-proposal-object-rest-spread": 7.12.1 - "@babel/plugin-syntax-jsx": 7.12.1 - "@mdx-js/util": 1.6.22 - is-alphabetical: 1.0.4 - remark-parse: 8.0.3 - unified: 9.2.0 - checksum: 45e62f8a821c37261f94448d54f295de1c5c393f762ff96cd4d4b730715037fafeb6c89ef94adf6a10a09edfa72104afe1431b93b5ae5e40ce2a7677e133c3d9 + "@types/mdast": ^4.0.0 + mdast-util-frontmatter: ^2.0.0 + micromark-extension-frontmatter: ^2.0.0 + unified: ^11.0.0 + checksum: b36e11d528d1d0172489c74ce7961bb6073f7272e71ea1349f765fc79c4246a758aef949174d371a088c48e458af776fcfbb3b043c49cd1120ca8239aeafe16a languageName: node linkType: hard -"remark-parse@npm:8.0.3": - version: 8.0.3 - resolution: "remark-parse@npm:8.0.3" - dependencies: - ccount: ^1.0.0 - collapse-white-space: ^1.0.2 - is-alphabetical: ^1.0.0 - is-decimal: ^1.0.0 - is-whitespace-character: ^1.0.0 - is-word-character: ^1.0.0 - markdown-escapes: ^1.0.0 - parse-entities: ^2.0.0 - repeat-string: ^1.5.4 - state-toggle: ^1.0.0 - trim: 0.0.1 - trim-trailing-lines: ^1.0.0 - unherit: ^1.0.4 - unist-util-remove-position: ^2.0.0 - vfile-location: ^3.0.0 - xtend: ^4.0.1 - checksum: 2dfea250e7606ddfc9e223b9f41e0b115c5c701be4bd35181beaadd46ee59816bc00aadc6085a420f8df00b991ada73b590ea7fd34ace14557de4a0a41805be5 - languageName: node - linkType: hard - -"remark-squeeze-paragraphs@npm:4.0.0": +"remark-gfm@npm:^4.0.0": version: 4.0.0 - resolution: "remark-squeeze-paragraphs@npm:4.0.0" + resolution: "remark-gfm@npm:4.0.0" + dependencies: + "@types/mdast": ^4.0.0 + mdast-util-gfm: ^3.0.0 + micromark-extension-gfm: ^3.0.0 + remark-parse: ^11.0.0 + remark-stringify: ^11.0.0 + unified: ^11.0.0 + checksum: 84bea84e388061fbbb697b4b666089f5c328aa04d19dc544c229b607446bc10902e46b67b9594415a1017bbbd7c811c1f0c30d36682c6d1a6718b66a1558261b + languageName: node + linkType: hard + +"remark-mdx@npm:^3.0.0": + version: 3.0.1 + resolution: "remark-mdx@npm:3.0.1" + dependencies: + mdast-util-mdx: ^3.0.0 + micromark-extension-mdxjs: ^3.0.0 + checksum: e7fcffbe1ccb0c7dfcb01c6d9dbc48df9c668c8321745455db7346f4860c43dbcb98e36e3398a5117d773426ab5ef656a95c78a21208c59e92571f021b8e678e + languageName: node + linkType: hard + +"remark-parse@npm:^11.0.0": + version: 11.0.0 + resolution: "remark-parse@npm:11.0.0" + dependencies: + "@types/mdast": ^4.0.0 + mdast-util-from-markdown: ^2.0.0 + micromark-util-types: ^2.0.0 + unified: ^11.0.0 + checksum: d83d245290fa84bb04fb3e78111f09c74f7417e7c012a64dd8dc04fccc3699036d828fbd8eeec8944f774b6c30cc1d925c98f8c46495ebcee7c595496342ab7f + languageName: node + linkType: hard + +"remark-rehype@npm:^11.0.0": + version: 11.1.0 + resolution: "remark-rehype@npm:11.1.0" + dependencies: + "@types/hast": ^3.0.0 + "@types/mdast": ^4.0.0 + mdast-util-to-hast: ^13.0.0 + unified: ^11.0.0 + vfile: ^6.0.0 + checksum: f0c731f0ab92a122e7f9c9bcbd10d6a31fdb99f0ea3595d232ddd9f9d11a308c4ec0aff4d56e1d0d256042dfad7df23b9941e50b5038da29786959a5926814e1 + languageName: node + linkType: hard + +"remark-stringify@npm:^11.0.0": + version: 11.0.0 + resolution: "remark-stringify@npm:11.0.0" dependencies: - mdast-squeeze-paragraphs: ^4.0.0 - checksum: 2071eb74d0ecfefb152c4932690a9fd950c3f9f798a676f1378a16db051da68fb20bf288688cc153ba5019dded35408ff45a31dfe9686eaa7a9f1df9edbb6c81 + "@types/mdast": ^4.0.0 + mdast-util-to-markdown: ^2.0.0 + unified: ^11.0.0 + checksum: 59e07460eb629d6c3b3c0f438b0b236e7e6858fd5ab770303078f5a556ec00354d9c7fb9ef6d5f745a4617ac7da1ab618b170fbb4dac120e183fecd9cc86bce6 languageName: node linkType: hard @@ -19271,13 +21544,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"repeat-string@npm:^1.5.4": - version: 1.6.1 - resolution: "repeat-string@npm:1.6.1" - checksum: 1b809fc6db97decdc68f5b12c4d1a671c8e3f65ec4a40c238bc5200e44e85bcc52a54f78268ab9c29fcf5fe4f1343e805420056d1f30fa9a9ee4c2d93e3cc6c0 - languageName: node - linkType: hard - "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -19317,7 +21583,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"resolve-alpn@npm:^1.0.0": +"resolve-alpn@npm:^1.0.0, resolve-alpn@npm:^1.2.0": version: 1.2.1 resolution: "resolve-alpn@npm:1.2.1" checksum: f558071fcb2c60b04054c99aebd572a2af97ef64128d59bef7ab73bd50d896a222a056de40ffc545b633d99b304c259ea9d0c06830d5c867c34f0bfa60b8eae0 @@ -19396,15 +21662,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"responselike@npm:^1.0.2": - version: 1.0.2 - resolution: "responselike@npm:1.0.2" - dependencies: - lowercase-keys: ^1.0.0 - checksum: 2e9e70f1dcca3da621a80ce71f2f9a9cad12c047145c6ece20df22f0743f051cf7c73505e109814915f23f9e34fb0d358e22827723ee3d56b623533cab8eafcd - languageName: node - linkType: hard - "responselike@npm:^2.0.0": version: 2.0.1 resolution: "responselike@npm:2.0.1" @@ -19414,6 +21671,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"responselike@npm:^3.0.0": + version: 3.0.0 + resolution: "responselike@npm:3.0.0" + dependencies: + lowercase-keys: ^3.0.0 + checksum: e0cc9be30df4f415d6d83cdede3c5c887cd4a73e7cc1708bcaab1d50a28d15acb68460ac5b02bcc55a42f3d493729c8856427dcf6e57e6e128ad05cba4cfb95e + languageName: node + linkType: hard + "restore-cursor@npm:^3.1.0": version: 3.1.0 resolution: "restore-cursor@npm:3.1.0" @@ -19514,17 +21780,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"rtlcss@npm:^3.5.0": - version: 3.5.0 - resolution: "rtlcss@npm:3.5.0" +"rtlcss@npm:^4.1.0": + version: 4.1.1 + resolution: "rtlcss@npm:4.1.1" dependencies: - find-up: ^5.0.0 + escalade: ^3.1.1 picocolors: ^1.0.0 - postcss: ^8.3.11 + postcss: ^8.4.21 strip-json-comments: ^3.1.1 bin: rtlcss: bin/rtlcss.js - checksum: a3763cad2cb58ce1b950de155097c3c294e7aefc8bf328b58d0cc8d5efb88bf800865edc158a78ace6d1f7f99fea6fd66fb4a354d859b172dadd3dab3e0027b3 + checksum: dcf37d76265b5c84d610488afa68a2506d008f95feac968b35ccae9aa49e7019ae0336a80363303f8f8bbf60df3ecdeb60413548b049114a24748319b68aefde languageName: node linkType: hard @@ -19553,7 +21819,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"rxjs@npm:7.8.1, rxjs@npm:^7.5.4, rxjs@npm:^7.5.5": +"rxjs@npm:7.8.1, rxjs@npm:^7.5.5": version: 7.8.1 resolution: "rxjs@npm:7.8.1" dependencies: @@ -19655,6 +21921,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"scheduler@npm:^0.23.0": + version: 0.23.0 + resolution: "scheduler@npm:0.23.0" + dependencies: + loose-envify: ^1.1.0 + checksum: d79192eeaa12abef860c195ea45d37cbf2bbf5f66e3c4dcd16f54a7da53b17788a70d109ee3d3dde1a0fd50e6a8fc171f4300356c5aee4fc0171de526bf35f8a + languageName: node + linkType: hard + "schema-utils@npm:2.7.0": version: 2.7.0 resolution: "schema-utils@npm:2.7.0" @@ -19666,17 +21941,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"schema-utils@npm:^2.6.5": - version: 2.7.1 - resolution: "schema-utils@npm:2.7.1" - dependencies: - "@types/json-schema": ^7.0.5 - ajv: ^6.12.4 - ajv-keywords: ^3.5.2 - checksum: 32c62fc9e28edd101e1bd83453a4216eb9bd875cc4d3775e4452b541908fa8f61a7bbac8ffde57484f01d7096279d3ba0337078e85a918ecbeb72872fb09fb2b - languageName: node - linkType: hard - "schema-utils@npm:^3.0.0, schema-utils@npm:^3.1.1, schema-utils@npm:^3.2.0": version: 3.3.0 resolution: "schema-utils@npm:3.3.0" @@ -19726,12 +21990,12 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"semver-diff@npm:^3.1.1": - version: 3.1.1 - resolution: "semver-diff@npm:3.1.1" +"semver-diff@npm:^4.0.0": + version: 4.0.0 + resolution: "semver-diff@npm:4.0.0" dependencies: - semver: ^6.3.0 - checksum: 8bbe5a5d7add2d5e51b72314a9215cd294d71f41cdc2bf6bd59ee76411f3610b576172896f1d191d0d7294cb9f2f847438d2ee158adacc0c224dca79052812fe + semver: ^7.3.5 + checksum: 4a958d6f76c7e7858268e1e2cf936712542441c9e003e561b574167279eee0a9bd55cc7eae1bfb31d3e7ad06a9fc370e7dd412fcfefec8c0daf1ce5aea623559 languageName: node linkType: hard @@ -19766,7 +22030,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"semver@npm:^6.0.0, semver@npm:^6.1.0, semver@npm:^6.2.0, semver@npm:^6.3.0, semver@npm:^6.3.1": +"semver@npm:^6.0.0, semver@npm:^6.1.0, semver@npm:^6.3.0, semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" bin: @@ -19836,7 +22100,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"serve-handler@npm:^6.1.3": +"serve-handler@npm:^6.1.5": version: 6.1.5 resolution: "serve-handler@npm:6.1.5" dependencies: @@ -19897,13 +22161,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"setimmediate@npm:^1.0.5": - version: 1.0.5 - resolution: "setimmediate@npm:1.0.5" - checksum: c9a6f2c5b51a2dabdc0247db9c46460152ffc62ee139f3157440bd48e7c59425093f42719ac1d7931f054f153e2d26cf37dfeb8da17a794a58198a2705e527fd - languageName: node - linkType: hard - "setprototypeof@npm:1.1.0": version: 1.1.0 resolution: "setprototypeof@npm:1.1.0" @@ -20099,6 +22356,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"skin-tone@npm:^2.0.0": + version: 2.0.0 + resolution: "skin-tone@npm:2.0.0" + dependencies: + unicode-emoji-modifier-base: ^1.0.0 + checksum: 19de157586b8019cacc55eb25d9d640f00fc02415761f3e41a4527142970fd4e7f6af0333bc90e879858766c20a976107bb386ffd4c812289c01d51f2c8d182c + languageName: node + linkType: hard + "slash@npm:3.0.0, slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0" @@ -20189,15 +22455,17 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "sofie-documentation@workspace:documentation" dependencies: - "@docusaurus/core": 2.4.3 - "@docusaurus/preset-classic": 2.4.3 - "@mdx-js/react": ^1.6.22 + "@docusaurus/core": 3.2.1 + "@docusaurus/module-type-aliases": 3.2.1 + "@docusaurus/preset-classic": 3.2.1 + "@docusaurus/types": 3.2.1 + "@mdx-js/react": ^3.0.0 "@svgr/webpack": ^5.5.0 clsx: ^1.2.1 file-loader: ^6.2.0 - prism-react-renderer: ^1.3.5 - react: ^17.0.2 - react-dom: ^17.0.2 + prism-react-renderer: ^2.1.0 + react: ^18.2.0 + react-dom: ^18.2.0 url-loader: ^4.1.1 languageName: unknown linkType: soft @@ -20235,6 +22503,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"source-map-js@npm:^1.2.0": + version: 1.2.0 + resolution: "source-map-js@npm:1.2.0" + checksum: 791a43306d9223792e84293b00458bf102a8946e7188f3db0e4e22d8d530b5f80a4ce468eb5ec0bf585443ad55ebbd630bf379c98db0b1f317fd902500217f97 + languageName: node + linkType: hard + "source-map-support@npm:0.5.13": version: 0.5.13 resolution: "source-map-support@npm:0.5.13" @@ -20269,6 +22544,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"source-map@npm:^0.7.0": + version: 0.7.4 + resolution: "source-map@npm:0.7.4" + checksum: 01cc5a74b1f0e1d626a58d36ad6898ea820567e87f18dfc9d24a9843a351aaa2ec09b87422589906d6ff1deed29693e176194dc88bcae7c9a852dc74b311dbf5 + languageName: node + linkType: hard + "source-map@npm:^0.8.0-beta.0": version: 0.8.0-beta.0 resolution: "source-map@npm:0.8.0-beta.0" @@ -20278,10 +22560,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"space-separated-tokens@npm:^1.0.0": - version: 1.1.5 - resolution: "space-separated-tokens@npm:1.1.5" - checksum: 8ef68f1cfa8ccad316b7f8d0df0919d0f1f6d32101e8faeee34ea3a923ce8509c1ad562f57388585ee4951e92d27afa211ed0a077d3d5995b5ba9180331be708 +"space-separated-tokens@npm:^2.0.0": + version: 2.0.2 + resolution: "space-separated-tokens@npm:2.0.2" + checksum: 202e97d7ca1ba0758a0aa4fe226ff98142073bcceeff2da3aad037968878552c3bbce3b3231970025375bbba5aee00c5b8206eda408da837ab2dc9c0f26be990 languageName: node linkType: hard @@ -20430,6 +22712,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"srcset@npm:^4.0.0": + version: 4.0.0 + resolution: "srcset@npm:4.0.0" + checksum: aceb898c9281101ef43bfbf96bf04dfae828e1bf942a45df6fad74ae9f8f0a425f4bca1480e0d22879beb40dd2bc6947e0e1e5f4d307a714666196164bc5769d + languageName: node + linkType: hard + "ssri@npm:9.0.1, ssri@npm:^9.0.0": version: 9.0.1 resolution: "ssri@npm:9.0.1" @@ -20478,13 +22767,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"state-toggle@npm:^1.0.0": - version: 1.0.3 - resolution: "state-toggle@npm:1.0.3" - checksum: 17398af928413e8d8b866cf0c81fd1b1348bb7d65d8983126ff6ff2317a80d6ee023484fba0c54d8169f5aa544f125434a650ae3a71eddc935cae307d4692b4f - languageName: node - linkType: hard - "statuses@npm:2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" @@ -20539,7 +22821,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.0.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.2, string-width@npm:^4.2.3": +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -20619,6 +22901,16 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"stringify-entities@npm:^4.0.0": + version: 4.0.4 + resolution: "stringify-entities@npm:4.0.4" + dependencies: + character-entities-html4: ^2.0.0 + character-entities-legacy: ^3.0.0 + checksum: ac1344ef211eacf6cf0a0a8feaf96f9c36083835b406560d2c6ff5a87406a41b13f2f0b4c570a3b391f465121c4fd6822b863ffb197e8c0601a64097862cc5b5 + languageName: node + linkType: hard + "stringify-object@npm:^3.3.0": version: 3.3.0 resolution: "stringify-object@npm:3.3.0" @@ -20747,12 +23039,21 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"style-to-object@npm:0.3.0, style-to-object@npm:^0.3.0": - version: 0.3.0 - resolution: "style-to-object@npm:0.3.0" +"style-to-object@npm:^0.4.0": + version: 0.4.4 + resolution: "style-to-object@npm:0.4.4" dependencies: inline-style-parser: 0.1.1 - checksum: 4d7084015207f2a606dfc10c29cb5ba569f2fe8005551df7396110dd694d6ff650f2debafa95bd5d147dfb4ca50f57868e2a7f91bf5d11ef734fe7ccbd7abf59 + checksum: 41656c06f93ac0a7ac260ebc2f9d09a8bd74b8ec1836f358cc58e169235835a3a356977891d2ebbd76f0e08a53616929069199f9cce543214d3dc98346e19c9a + languageName: node + linkType: hard + +"style-to-object@npm:^1.0.0": + version: 1.0.6 + resolution: "style-to-object@npm:1.0.6" + dependencies: + inline-style-parser: 0.2.3 + checksum: 5b58295dcc2c21f1da1b9308de1e81b4a987b876a177e677453a76b2e3151a0e21afc630e99c1ea6c82dd8dbec0d01a8b1a51a829422aca055162b03e52572a9 languageName: node linkType: hard @@ -20881,7 +23182,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"tapable@npm:^2.0.0, tapable@npm:^2.1.1, tapable@npm:^2.2.0": +"tapable@npm:^2.0.0, tapable@npm:^2.1.1, tapable@npm:^2.2.0, tapable@npm:^2.2.1": version: 2.2.1 resolution: "tapable@npm:2.2.1" checksum: 3b7a1b4d86fa940aad46d9e73d1e8739335efd4c48322cb37d073eb6f80f5281889bf0320c6d8ffcfa1a0dd5bfdbd0f9d037e252ef972aca595330538aac4d51 @@ -20996,15 +23297,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"terser-webpack-plugin@npm:^5.3.3, terser-webpack-plugin@npm:^5.3.7": - version: 5.3.9 - resolution: "terser-webpack-plugin@npm:5.3.9" +"terser-webpack-plugin@npm:^5.3.10, terser-webpack-plugin@npm:^5.3.9": + version: 5.3.10 + resolution: "terser-webpack-plugin@npm:5.3.10" dependencies: - "@jridgewell/trace-mapping": ^0.3.17 + "@jridgewell/trace-mapping": ^0.3.20 jest-worker: ^27.4.5 schema-utils: ^3.1.1 serialize-javascript: ^6.0.1 - terser: ^5.16.8 + terser: ^5.26.0 peerDependencies: webpack: ^5.1.0 peerDependenciesMeta: @@ -21014,11 +23315,11 @@ asn1@evs-broadcast/node-asn1: optional: true uglify-js: optional: true - checksum: 41705713d6f9cb83287936b21e27c658891c78c4392159f5148b5623f0e8c48559869779619b058382a4c9758e7820ea034695e57dc7c474b4962b79f553bc5f + checksum: bd6e7596cf815f3353e2a53e79cbdec959a1b0276f5e5d4e63e9d7c3c5bb5306df567729da287d1c7b39d79093e56863c569c42c6c24cc34c76aa313bd2cbcea languageName: node linkType: hard -"terser@npm:^5.10.0, terser@npm:^5.16.8": +"terser@npm:^5.10.0": version: 5.19.4 resolution: "terser@npm:5.19.4" dependencies: @@ -21032,6 +23333,20 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"terser@npm:^5.15.1, terser@npm:^5.26.0": + version: 5.30.3 + resolution: "terser@npm:5.30.3" + dependencies: + "@jridgewell/source-map": ^0.3.3 + acorn: ^8.8.2 + commander: ^2.20.0 + source-map-support: ~0.5.20 + bin: + terser: bin/terser + checksum: 8c680ed32a948f806fade0969c52aab94b6de174e4a78610f5d3abf9993b161eb19b88b2ceadff09b153858727c02deb6709635e4bfbd519f67d54e0394e2983 + languageName: node + linkType: hard + "test-exclude@npm:^6.0.0": version: 6.0.0 resolution: "test-exclude@npm:6.0.0" @@ -21256,13 +23571,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"to-readable-stream@npm:^1.0.0": - version: 1.0.0 - resolution: "to-readable-stream@npm:1.0.0" - checksum: 2bd7778490b6214a2c40276065dd88949f4cf7037ce3964c76838b8cb212893aeb9cceaaf4352a4c486e3336214c350270f3263e1ce7a0c38863a715a4d9aeb5 - languageName: node - linkType: hard - "to-regex-range@npm:^5.0.1": version: 5.0.1 resolution: "to-regex-range@npm:5.0.1" @@ -21392,6 +23700,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"trim-lines@npm:^3.0.0": + version: 3.0.1 + resolution: "trim-lines@npm:3.0.1" + checksum: e241da104682a0e0d807222cc1496b92e716af4db7a002f4aeff33ae6a0024fef93165d49eab11aa07c71e1347c42d46563f91dfaa4d3fb945aa535cdead53ed + languageName: node + linkType: hard + "trim-newlines@npm:^3.0.0": version: 3.0.1 resolution: "trim-newlines@npm:3.0.1" @@ -21415,20 +23730,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"trim-trailing-lines@npm:^1.0.0": - version: 1.1.4 - resolution: "trim-trailing-lines@npm:1.1.4" - checksum: 5d39d21c0d4b258667012fcd784f73129e148ea1c213b1851d8904f80499fc91df6710c94c7dd49a486a32da2b9cb86020dda79f285a9a2586cfa622f80490c2 - languageName: node - linkType: hard - -"trim@npm:0.0.1": - version: 0.0.1 - resolution: "trim@npm:0.0.1" - checksum: 2b4646dff99a222e8e1526edd4e3a43bbd925af0b8e837c340455d250157e7deefaa4da49bb891ab841e5c27b1afc5e9e32d4b57afb875d2dfcabf4e319b8f7f - languageName: node - linkType: hard - "triple-beam@npm:^1.3.0": version: 1.4.1 resolution: "triple-beam@npm:1.4.1" @@ -21436,10 +23737,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"trough@npm:^1.0.0": - version: 1.0.5 - resolution: "trough@npm:1.0.5" - checksum: d6c8564903ed00e5258bab92134b020724dbbe83148dc72e4bf6306c03ed8843efa1bcc773fa62410dd89161ecb067432dd5916501793508a9506cacbc408e25 +"trough@npm:^2.0.0": + version: 2.2.0 + resolution: "trough@npm:2.2.0" + checksum: 6097df63169aca1f9b08c263b1b501a9b878387f46e161dde93f6d0bba7febba93c95f876a293c5ea370f6cb03bcb687b2488c8955c3cfb66c2c0161ea8c00f6 languageName: node linkType: hard @@ -21811,13 +24112,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"ua-parser-js@npm:^1.0.35": - version: 1.0.36 - resolution: "ua-parser-js@npm:1.0.36" - checksum: 5b2c8a5e3443dfbba7624421805de946457c26ae167cb2275781a2729d1518f7067c9d5c74c3b0acac4b9ff3278cae4eace08ca6eecb63848bc3b2f6a63cc975 - languageName: node - linkType: hard - "uc.micro@npm:^1.0.1, uc.micro@npm:^1.0.5": version: 1.0.6 resolution: "uc.micro@npm:1.0.6" @@ -21893,16 +24187,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"unherit@npm:^1.0.4": - version: 1.1.3 - resolution: "unherit@npm:1.1.3" - dependencies: - inherits: ^2.0.0 - xtend: ^4.0.0 - checksum: fd7922f84fc0bfb7c4df6d1f5a50b5b94a0218e3cda98a54dbbd209226ddd4072d742d3df44d0e295ab08d5ccfd304a1e193dfe31a86d2a91b7cb9fdac093194 - languageName: node - linkType: hard - "unicode-byte-truncate@npm:^1.0.0": version: 1.0.0 resolution: "unicode-byte-truncate@npm:1.0.0" @@ -21920,6 +24204,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"unicode-emoji-modifier-base@npm:^1.0.0": + version: 1.0.0 + resolution: "unicode-emoji-modifier-base@npm:1.0.0" + checksum: 6e1521d35fa69493207eb8b41f8edb95985d8b3faf07c01d820a1830b5e8403e20002563e2f84683e8e962a49beccae789f0879356bf92a4ec7a4dd8e2d16fdb + languageName: node + linkType: hard + "unicode-match-property-ecmascript@npm:^2.0.0": version: 2.0.0 resolution: "unicode-match-property-ecmascript@npm:2.0.0" @@ -21951,31 +24242,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"unified@npm:9.2.0": - version: 9.2.0 - resolution: "unified@npm:9.2.0" - dependencies: - bail: ^1.0.0 - extend: ^3.0.0 - is-buffer: ^2.0.0 - is-plain-obj: ^2.0.0 - trough: ^1.0.0 - vfile: ^4.0.0 - checksum: 0cac4ae119893fbd49d309b4db48595e4d4e9f0a2dc1dde4d0074059f9a46012a2905f37c1346715e583f30c970bc8078db8462675411d39ff5036ae18b4fb8a - languageName: node - linkType: hard - -"unified@npm:^9.2.2": - version: 9.2.2 - resolution: "unified@npm:9.2.2" +"unified@npm:^11.0.0, unified@npm:^11.0.3, unified@npm:^11.0.4": + version: 11.0.4 + resolution: "unified@npm:11.0.4" dependencies: - bail: ^1.0.0 + "@types/unist": ^3.0.0 + bail: ^2.0.0 + devlop: ^1.0.0 extend: ^3.0.0 - is-buffer: ^2.0.0 - is-plain-obj: ^2.0.0 - trough: ^1.0.0 - vfile: ^4.0.0 - checksum: 7c24461be7de4145939739ce50d18227c5fbdf9b3bc5a29dabb1ce26dd3e8bd4a1c385865f6f825f3b49230953ee8b591f23beab3bb3643e3e9dc37aa8a089d5 + is-plain-obj: ^4.0.0 + trough: ^2.0.0 + vfile: ^6.0.0 + checksum: cfb023913480ac2bd5e787ffb8c27782c43e6be4a55f8f1c288233fce46a7ebe7718ccc5adb80bf8d56b7ef85f5fc32239c7bfccda006f9f2382e0cc2e2a77e4 languageName: node linkType: hard @@ -22033,79 +24311,70 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"unist-builder@npm:2.0.3, unist-builder@npm:^2.0.0": - version: 2.0.3 - resolution: "unist-builder@npm:2.0.3" - checksum: e946fdf77dbfc320feaece137ce4959ae2da6614abd1623bd39512dc741a9d5f313eb2ba79f8887d941365dccddec7fef4e953827475e392bf49b45336f597f6 - languageName: node - linkType: hard - -"unist-util-generated@npm:^1.0.0": - version: 1.1.6 - resolution: "unist-util-generated@npm:1.1.6" - checksum: 86239ff88a08800d52198f2f0e15911f05bab2dad17cef95550f7c2728f15ebb0344694fcc3101d05762d88adaf86cb85aa7a3300fedabd0b6d7d00b41cdcb7f - languageName: node - linkType: hard - -"unist-util-is@npm:^4.0.0": - version: 4.1.0 - resolution: "unist-util-is@npm:4.1.0" - checksum: 726484cd2adc9be75a939aeedd48720f88294899c2e4a3143da413ae593f2b28037570730d5cf5fd910ff41f3bc1501e3d636b6814c478d71126581ef695f7ea +"unist-util-is@npm:^6.0.0": + version: 6.0.0 + resolution: "unist-util-is@npm:6.0.0" + dependencies: + "@types/unist": ^3.0.0 + checksum: f630a925126594af9993b091cf807b86811371e465b5049a6283e08537d3e6ba0f7e248e1e7dab52cfe33f9002606acef093441137181b327f6fe504884b20e2 languageName: node linkType: hard -"unist-util-position@npm:^3.0.0": - version: 3.1.0 - resolution: "unist-util-position@npm:3.1.0" - checksum: 10b3952e32a1ffabbecad41c3946237f7059f5bb6436796da05531a285f50b97e4f37cfc2f7164676d041063f40fe1ad92fbb8ca38d3ae8747328ebe738d738f +"unist-util-position-from-estree@npm:^2.0.0": + version: 2.0.0 + resolution: "unist-util-position-from-estree@npm:2.0.0" + dependencies: + "@types/unist": ^3.0.0 + checksum: d3b3048a5727c2367f64ef6dcc5b20c4717215ef8b1372ff9a7c426297c5d1e5776409938acd01531213e2cd2543218d16e73f9f862f318e9496e2c73bb18354 languageName: node linkType: hard -"unist-util-remove-position@npm:^2.0.0": - version: 2.0.1 - resolution: "unist-util-remove-position@npm:2.0.1" +"unist-util-position@npm:^5.0.0": + version: 5.0.0 + resolution: "unist-util-position@npm:5.0.0" dependencies: - unist-util-visit: ^2.0.0 - checksum: 4149294969f1a78a367b5d03eb0a138aa8320a39e1b15686647a2bec5945af3df27f2936a1e9752ecbb4a82dc23bd86f7e5a0ee048e5eeaedc2deb9237872795 + "@types/unist": ^3.0.0 + checksum: f89b27989b19f07878de9579cd8db2aa0194c8360db69e2c99bd2124a480d79c08f04b73a64daf01a8fb3af7cba65ff4b45a0b978ca243226084ad5f5d441dde languageName: node linkType: hard -"unist-util-remove@npm:^2.0.0": - version: 2.1.0 - resolution: "unist-util-remove@npm:2.1.0" +"unist-util-remove-position@npm:^5.0.0": + version: 5.0.0 + resolution: "unist-util-remove-position@npm:5.0.0" dependencies: - unist-util-is: ^4.0.0 - checksum: 99e54f3ea0523f8cf957579a6e84e5b58427bffab929cc7f6aa5119581f929db683dd4691ea5483df0c272f486dda9dbd04f4ab74dca6cae1f3ebe8e4261a4d9 + "@types/unist": ^3.0.0 + unist-util-visit: ^5.0.0 + checksum: 8aabdb9d0e3e744141bc123d8f87b90835d521209ad3c6c4619d403b324537152f0b8f20dda839b40c3aa0abfbf1828b3635a7a8bb159c3ed469e743023510ee languageName: node linkType: hard -"unist-util-stringify-position@npm:^2.0.0": - version: 2.0.3 - resolution: "unist-util-stringify-position@npm:2.0.3" +"unist-util-stringify-position@npm:^4.0.0": + version: 4.0.0 + resolution: "unist-util-stringify-position@npm:4.0.0" dependencies: - "@types/unist": ^2.0.2 - checksum: f755cadc959f9074fe999578a1a242761296705a7fe87f333a37c00044de74ab4b184b3812989a57d4cd12211f0b14ad397b327c3a594c7af84361b1c25a7f09 + "@types/unist": ^3.0.0 + checksum: e2e7aee4b92ddb64d314b4ac89eef7a46e4c829cbd3ee4aee516d100772b490eb6b4974f653ba0717a0071ca6ea0770bf22b0a2ea62c65fcba1d071285e96324 languageName: node linkType: hard -"unist-util-visit-parents@npm:^3.0.0": - version: 3.1.1 - resolution: "unist-util-visit-parents@npm:3.1.1" +"unist-util-visit-parents@npm:^6.0.0": + version: 6.0.1 + resolution: "unist-util-visit-parents@npm:6.0.1" dependencies: - "@types/unist": ^2.0.0 - unist-util-is: ^4.0.0 - checksum: 1170e397dff88fab01e76d5154981666eb0291019d2462cff7a2961a3e76d3533b42eaa16b5b7e2d41ad42a5ea7d112301458283d255993e660511387bf67bc3 + "@types/unist": ^3.0.0 + unist-util-is: ^6.0.0 + checksum: 08927647c579f63b91aafcbec9966dc4a7d0af1e5e26fc69f4e3e6a01215084835a2321b06f3cbe7bf7914a852830fc1439f0fc3d7153d8804ac3ef851ddfa20 languageName: node linkType: hard -"unist-util-visit@npm:2.0.3, unist-util-visit@npm:^2.0.0, unist-util-visit@npm:^2.0.3": - version: 2.0.3 - resolution: "unist-util-visit@npm:2.0.3" +"unist-util-visit@npm:^5.0.0": + version: 5.0.0 + resolution: "unist-util-visit@npm:5.0.0" dependencies: - "@types/unist": ^2.0.0 - unist-util-is: ^4.0.0 - unist-util-visit-parents: ^3.0.0 - checksum: 1fe19d500e212128f96d8c3cfa3312846e586b797748a1fd195fe6479f06bc90a6f6904deb08eefc00dd58e83a1c8a32fb8677252d2273ad7a5e624525b69b8f + "@types/unist": ^3.0.0 + unist-util-is: ^6.0.0 + unist-util-visit-parents: ^6.0.0 + checksum: 9ec42e618e7e5d0202f3c191cd30791b51641285732767ee2e6bcd035931032e3c1b29093f4d7fd0c79175bbc1f26f24f26ee49770d32be76f8730a652a857e6 languageName: node linkType: hard @@ -22172,25 +24441,25 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"update-notifier@npm:^5.1.0": - version: 5.1.0 - resolution: "update-notifier@npm:5.1.0" - dependencies: - boxen: ^5.0.0 - chalk: ^4.1.0 - configstore: ^5.0.1 - has-yarn: ^2.1.0 - import-lazy: ^2.1.0 - is-ci: ^2.0.0 +"update-notifier@npm:^6.0.2": + version: 6.0.2 + resolution: "update-notifier@npm:6.0.2" + dependencies: + boxen: ^7.0.0 + chalk: ^5.0.1 + configstore: ^6.0.0 + has-yarn: ^3.0.0 + import-lazy: ^4.0.0 + is-ci: ^3.0.1 is-installed-globally: ^0.4.0 - is-npm: ^5.0.0 - is-yarn-global: ^0.3.0 - latest-version: ^5.1.0 - pupa: ^2.1.1 - semver: ^7.3.4 - semver-diff: ^3.1.1 - xdg-basedir: ^4.0.0 - checksum: 461e5e5b002419296d3868ee2abe0f9ab3e1846d9db642936d0c46f838872ec56069eddfe662c45ce1af0a8d6d5026353728de2e0a95ab2e3546a22ea077caf1 + is-npm: ^6.0.0 + is-yarn-global: ^0.4.0 + latest-version: ^7.0.0 + pupa: ^3.1.0 + semver: ^7.3.7 + semver-diff: ^4.0.0 + xdg-basedir: ^5.1.0 + checksum: 4bae7b3eca7b2068b6b87dde88c9dad24831fa913a5b83ecb39a7e4702c93e8b05fd9bcac5f1a005178f6e5dc859e0b3817ddda833d2a7ab92c6485e078b3cc8 languageName: node linkType: hard @@ -22227,15 +24496,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"url-parse-lax@npm:^3.0.0": - version: 3.0.0 - resolution: "url-parse-lax@npm:3.0.0" - dependencies: - prepend-http: ^2.0.0 - checksum: 1040e357750451173132228036aff1fd04abbd43eac1fb3e4fca7495a078bcb8d33cb765fe71ad7e473d9c94d98fd67adca63bd2716c815a2da066198dd37217 - languageName: node - linkType: hard - "url-parse@npm:^1.5.3, url-parse@npm:~1.5.10": version: 1.5.10 resolution: "url-parse@npm:1.5.10" @@ -22246,41 +24506,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"use-composed-ref@npm:^1.3.0": - version: 1.3.0 - resolution: "use-composed-ref@npm:1.3.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: f771cbadfdc91e03b7ab9eb32d0fc0cc647755711801bf507e891ad38c4bbc5f02b2509acadf9c965ec9c5f2f642fd33bdfdfb17b0873c4ad0a9b1f5e5e724bf - languageName: node - linkType: hard - -"use-isomorphic-layout-effect@npm:^1.1.1": - version: 1.1.2 - resolution: "use-isomorphic-layout-effect@npm:1.1.2" - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - "@types/react": - optional: true - checksum: a6532f7fc9ae222c3725ff0308aaf1f1ddbd3c00d685ef9eee6714fd0684de5cb9741b432fbf51e61a784e2955424864f7ea9f99734a02f237b17ad3e18ea5cb - languageName: node - linkType: hard - -"use-latest@npm:^1.2.1": - version: 1.2.1 - resolution: "use-latest@npm:1.2.1" - dependencies: - use-isomorphic-layout-effect: ^1.1.1 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - "@types/react": - optional: true - checksum: ed3f2ddddf6f21825e2ede4c2e0f0db8dcce5129802b69d1f0575fc1b42380436e8c76a6cd885d4e9aa8e292e60fb8b959c955f33c6a9123b83814a1a1875367 - languageName: node - linkType: hard - "use-resize-observer@npm:^8.0.0": version: 8.0.0 resolution: "use-resize-observer@npm:8.0.0" @@ -22293,15 +24518,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"use-sync-external-store@npm:^1.2.0": - version: 1.2.0 - resolution: "use-sync-external-store@npm:1.2.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 5c639e0f8da3521d605f59ce5be9e094ca772bd44a4ce7322b055a6f58eeed8dda3c94cabd90c7a41fb6fa852210092008afe48f7038792fd47501f33299116a - languageName: node - linkType: hard - "utf-8-validate@npm:^5.0.10": version: 5.0.10 resolution: "utf-8-validate@npm:5.0.10" @@ -22451,32 +24667,34 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"vfile-location@npm:^3.0.0, vfile-location@npm:^3.2.0": - version: 3.2.0 - resolution: "vfile-location@npm:3.2.0" - checksum: 9bb3df6d0be31b5dd2d8da0170c27b7045c64493a8ba7b6ff7af8596c524fc8896924b8dd85ae12d201eead2709217a0fbc44927b7264f4bbf0aa8027a78be9c +"vfile-location@npm:^5.0.0": + version: 5.0.2 + resolution: "vfile-location@npm:5.0.2" + dependencies: + "@types/unist": ^3.0.0 + vfile: ^6.0.0 + checksum: b61c048cedad3555b4f007f390412c6503f58a6a130b58badf4ee340c87e0d7421e9c86bbc1494c57dedfccadb60f5176cc60ba3098209d99fb3a3d8804e4c38 languageName: node linkType: hard -"vfile-message@npm:^2.0.0": - version: 2.0.4 - resolution: "vfile-message@npm:2.0.4" +"vfile-message@npm:^4.0.0": + version: 4.0.2 + resolution: "vfile-message@npm:4.0.2" dependencies: - "@types/unist": ^2.0.0 - unist-util-stringify-position: ^2.0.0 - checksum: 1bade499790f46ca5aba04bdce07a1e37c2636a8872e05cf32c26becc912826710b7eb063d30c5754fdfaeedc8a7658e78df10b3bc535c844890ec8a184f5643 + "@types/unist": ^3.0.0 + unist-util-stringify-position: ^4.0.0 + checksum: 964e7e119f4c0e0270fc269119c41c96da20afa01acb7c9809a88365c8e0c64aa692fafbd952669382b978002ecd7ad31ef4446d85e8a22cdb62f6df20186c2d languageName: node linkType: hard -"vfile@npm:^4.0.0": - version: 4.2.1 - resolution: "vfile@npm:4.2.1" +"vfile@npm:^6.0.0, vfile@npm:^6.0.1": + version: 6.0.1 + resolution: "vfile@npm:6.0.1" dependencies: - "@types/unist": ^2.0.0 - is-buffer: ^2.0.0 - unist-util-stringify-position: ^2.0.0 - vfile-message: ^2.0.0 - checksum: ee5726e10d170472cde778fc22e0f7499caa096eb85babea5d0ce0941455b721037ee1c9e6ae506ca2803250acd313d0f464328ead0b55cfe7cb6315f1b462d6 + "@types/unist": ^3.0.0 + unist-util-stringify-position: ^4.0.0 + vfile-message: ^4.0.0 + checksum: 05ccee73aeb00402bc8a5d0708af299e9f4a33f5132805449099295085e3ca3b0d018328bad9ff44cf2e6f4cd364f1d558d3fb9b394243a25b2739207edcb0ed languageName: node linkType: hard @@ -22512,21 +24730,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"wait-on@npm:^6.0.1": - version: 6.0.1 - resolution: "wait-on@npm:6.0.1" - dependencies: - axios: ^0.25.0 - joi: ^17.6.0 - lodash: ^4.17.21 - minimist: ^1.2.5 - rxjs: ^7.5.4 - bin: - wait-on: bin/wait-on - checksum: e4d62aa4145d99fe34747ccf7506d4b4d6e60dd677c0eb18a51e316d38116ace2d194e4b22a9eb7b767b0282f39878ddcc4ae9440dcb0c005c9150668747cf5b - languageName: node - linkType: hard - "walk-up-path@npm:^1.0.0": version: 1.0.0 resolution: "walk-up-path@npm:1.0.0" @@ -22552,13 +24755,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"watchpack@npm:^2.4.0": - version: 2.4.0 - resolution: "watchpack@npm:2.4.0" +"watchpack@npm:^2.4.1": + version: 2.4.1 + resolution: "watchpack@npm:2.4.1" dependencies: glob-to-regexp: ^0.4.1 graceful-fs: ^4.1.2 - checksum: 23d4bc58634dbe13b86093e01c6a68d8096028b664ab7139d58f0c37d962d549a940e98f2f201cecdabd6f9c340338dc73ef8bf094a2249ef582f35183d1a131 + checksum: 5b0179348655dcdf19cac7cb4ff923fdc024d630650c0bf6bec8899cf47c60e19d4f810a88dba692ed0e7f684cf0fcffea86efdbf6c35d81f031e328043b7fab languageName: node linkType: hard @@ -22595,10 +24798,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"web-namespaces@npm:^1.0.0": - version: 1.1.4 - resolution: "web-namespaces@npm:1.1.4" - checksum: 5149842ccbfbc56fe4f8758957b3f8c8616a281874a5bb84aa1b305e4436a9bad853d21c629a7b8f174902449e1489c7a6c724fccf60965077c5636bd8aed42b +"web-namespaces@npm:^2.0.0": + version: 2.0.1 + resolution: "web-namespaces@npm:2.0.1" + checksum: b6d9f02f1a43d0ef0848a812d89c83801d5bbad57d8bb61f02eb6d7eb794c3736f6cc2e1191664bb26136594c8218ac609f4069722c6f56d9fc2d808fa9271c6 languageName: node linkType: hard @@ -22646,36 +24849,31 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"webpack-bundle-analyzer@npm:^4.5.0": - version: 4.9.1 - resolution: "webpack-bundle-analyzer@npm:4.9.1" +"webpack-bundle-analyzer@npm:^4.9.0": + version: 4.10.2 + resolution: "webpack-bundle-analyzer@npm:4.10.2" dependencies: "@discoveryjs/json-ext": 0.5.7 acorn: ^8.0.4 acorn-walk: ^8.0.0 commander: ^7.2.0 + debounce: ^1.2.1 escape-string-regexp: ^4.0.0 gzip-size: ^6.0.0 - is-plain-object: ^5.0.0 - lodash.debounce: ^4.0.8 - lodash.escape: ^4.0.1 - lodash.flatten: ^4.4.0 - lodash.invokemap: ^4.6.0 - lodash.pullall: ^4.2.0 - lodash.uniqby: ^4.7.0 + html-escaper: ^2.0.2 opener: ^1.5.2 picocolors: ^1.0.0 sirv: ^2.0.3 ws: ^7.3.1 bin: webpack-bundle-analyzer: lib/bin/analyzer.js - checksum: 7e891c28d5a903242893e55ecc714fa01d7ad6bedade143235c07091b235915349812fa048968462781d59187507962f38b6c61ed7d25fb836ba0ac0ee919a39 + checksum: 4f0275e7d87bb6203a618ca5d2d4953943979d986fa2b91be1bf1ad0bcd22bec13398803273d11699f9fbcf106896311208a72d63fe5f8a47b687a226e598dc1 languageName: node linkType: hard -"webpack-dev-middleware@npm:^5.3.1": - version: 5.3.3 - resolution: "webpack-dev-middleware@npm:5.3.3" +"webpack-dev-middleware@npm:^5.3.4": + version: 5.3.4 + resolution: "webpack-dev-middleware@npm:5.3.4" dependencies: colorette: ^2.0.10 memfs: ^3.4.3 @@ -22684,13 +24882,13 @@ asn1@evs-broadcast/node-asn1: schema-utils: ^4.0.0 peerDependencies: webpack: ^4.0.0 || ^5.0.0 - checksum: dd332cc6da61222c43d25e5a2155e23147b777ff32fdf1f1a0a8777020c072fbcef7756360ce2a13939c3f534c06b4992a4d659318c4a7fe2c0530b52a8a6621 + checksum: 90cf3e27d0714c1a745454a1794f491b7076434939340605b9ee8718ba2b85385b120939754e9fdbd6569811e749dee53eec319e0d600e70e0b0baffd8e3fb13 languageName: node linkType: hard -"webpack-dev-server@npm:^4.9.3": - version: 4.15.1 - resolution: "webpack-dev-server@npm:4.15.1" +"webpack-dev-server@npm:^4.15.1": + version: 4.15.2 + resolution: "webpack-dev-server@npm:4.15.2" dependencies: "@types/bonjour": ^3.5.9 "@types/connect-history-api-fallback": ^1.3.5 @@ -22720,7 +24918,7 @@ asn1@evs-broadcast/node-asn1: serve-index: ^1.9.1 sockjs: ^0.3.24 spdy: ^4.0.2 - webpack-dev-middleware: ^5.3.1 + webpack-dev-middleware: ^5.3.4 ws: ^8.13.0 peerDependencies: webpack: ^4.37.0 || ^5.0.0 @@ -22731,61 +24929,62 @@ asn1@evs-broadcast/node-asn1: optional: true bin: webpack-dev-server: bin/webpack-dev-server.js - checksum: cd0063b068d2b938fd76c412d555374186ac2fa84bbae098265212ed50a5c15d6f03aa12a5a310c544a242943eb58c0bfde4c296d5c36765c182f53799e1bc71 + checksum: 123507129cb4d55fdc5fabdd177574f31133605748372bb11353307b7a583ef25c6fd27b6addf56bf070ba44c88d5da861771c2ec55f52405082ec9efd01f039 languageName: node linkType: hard -"webpack-merge@npm:^5.8.0": - version: 5.9.0 - resolution: "webpack-merge@npm:5.9.0" +"webpack-merge@npm:^5.9.0": + version: 5.10.0 + resolution: "webpack-merge@npm:5.10.0" dependencies: clone-deep: ^4.0.1 + flat: ^5.0.2 wildcard: ^2.0.0 - checksum: 64fe2c23aacc5f19684452a0e84ec02c46b990423aee6fcc5c18d7d471155bd14e9a6adb02bd3656eb3e0ac2532c8e97d69412ad14c97eeafe32fa6d10050872 + checksum: 1fe8bf5309add7298e1ac72fb3f2090e1dfa80c48c7e79fa48aa60b5961332c7d0d61efa8851acb805e6b91a4584537a347bc106e05e9aec87fa4f7088c62f2f languageName: node linkType: hard -"webpack-sources@npm:^3.2.2, webpack-sources@npm:^3.2.3": +"webpack-sources@npm:^3.2.3": version: 3.2.3 resolution: "webpack-sources@npm:3.2.3" checksum: 989e401b9fe3536529e2a99dac8c1bdc50e3a0a2c8669cbafad31271eadd994bc9405f88a3039cd2e29db5e6d9d0926ceb7a1a4e7409ece021fe79c37d9c4607 languageName: node linkType: hard -"webpack@npm:^5.73.0": - version: 5.88.2 - resolution: "webpack@npm:5.88.2" +"webpack@npm:^5.88.1": + version: 5.91.0 + resolution: "webpack@npm:5.91.0" dependencies: "@types/eslint-scope": ^3.7.3 - "@types/estree": ^1.0.0 - "@webassemblyjs/ast": ^1.11.5 - "@webassemblyjs/wasm-edit": ^1.11.5 - "@webassemblyjs/wasm-parser": ^1.11.5 + "@types/estree": ^1.0.5 + "@webassemblyjs/ast": ^1.12.1 + "@webassemblyjs/wasm-edit": ^1.12.1 + "@webassemblyjs/wasm-parser": ^1.12.1 acorn: ^8.7.1 acorn-import-assertions: ^1.9.0 - browserslist: ^4.14.5 + browserslist: ^4.21.10 chrome-trace-event: ^1.0.2 - enhanced-resolve: ^5.15.0 + enhanced-resolve: ^5.16.0 es-module-lexer: ^1.2.1 eslint-scope: 5.1.1 events: ^3.2.0 glob-to-regexp: ^0.4.1 - graceful-fs: ^4.2.9 + graceful-fs: ^4.2.11 json-parse-even-better-errors: ^2.3.1 loader-runner: ^4.2.0 mime-types: ^2.1.27 neo-async: ^2.6.2 schema-utils: ^3.2.0 tapable: ^2.1.1 - terser-webpack-plugin: ^5.3.7 - watchpack: ^2.4.0 + terser-webpack-plugin: ^5.3.10 + watchpack: ^2.4.1 webpack-sources: ^3.2.3 peerDependenciesMeta: webpack-cli: optional: true bin: webpack: bin/webpack.js - checksum: 79476a782da31a21f6dd38fbbd06b68da93baf6a62f0d08ca99222367f3b8668f5a1f2086b7bb78e23172e31fa6df6fa7ab09b25e827866c4fc4dc2b30443ce2 + checksum: f1073715dbb1ed5c070affef293d800a867708bcbc5aba4d8baee87660e0cf53c55966a6f36fab078d1d6c9567cdcd0a9086bdfb607cab87ea68c6449791b9a3 languageName: node linkType: hard @@ -22959,15 +25158,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"widest-line@npm:^3.1.0": - version: 3.1.0 - resolution: "widest-line@npm:3.1.0" - dependencies: - string-width: ^4.0.0 - checksum: 03db6c9d0af9329c37d74378ff1d91972b12553c7d72a6f4e8525fe61563fa7adb0b9d6e8d546b7e059688712ea874edd5ded475999abdeedf708de9849310e0 - languageName: node - linkType: hard - "widest-line@npm:^4.0.1": version: 4.0.1 resolution: "widest-line@npm:4.0.1" @@ -23089,7 +25279,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"write-file-atomic@npm:^3.0.0": +"write-file-atomic@npm:^3.0.3": version: 3.0.3 resolution: "write-file-atomic@npm:3.0.3" dependencies: @@ -23206,10 +25396,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"xdg-basedir@npm:^4.0.0": - version: 4.0.0 - resolution: "xdg-basedir@npm:4.0.0" - checksum: 0073d5b59a37224ed3a5ac0dd2ec1d36f09c49f0afd769008a6e9cd3cd666bd6317bd1c7ce2eab47e1de285a286bad11a9b038196413cd753b79770361855f3c +"xdg-basedir@npm:^5.0.1, xdg-basedir@npm:^5.1.0": + version: 5.1.0 + resolution: "xdg-basedir@npm:5.1.0" + checksum: b60e8a2c663ccb1dac77c2d913f3b96de48dafbfa083657171d3d50e10820b8a04bb4edfe9f00808c8c20e5f5355e1927bea9029f03136e29265cb98291e1fea languageName: node linkType: hard @@ -23262,7 +25452,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"xtend@npm:^4.0.0, xtend@npm:^4.0.1, xtend@npm:~4.0.1": +"xtend@npm:~4.0.1": version: 4.0.2 resolution: "xtend@npm:4.0.2" checksum: ac5dfa738b21f6e7f0dd6e65e1b3155036d68104e67e5d5d1bde74892e327d7e5636a076f625599dc394330a731861e87343ff184b0047fef1360a7ec0a5a36a @@ -23411,9 +25601,9 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"zwitch@npm:^1.0.0": - version: 1.0.5 - resolution: "zwitch@npm:1.0.5" - checksum: 28a1bebacab3bc60150b6b0a2ba1db2ad033f068e81f05e4892ec0ea13ae63f5d140a1d692062ac0657840c8da076f35b94433b5f1c329d7803b247de80f064a +"zwitch@npm:^2.0.0": + version: 2.0.4 + resolution: "zwitch@npm:2.0.4" + checksum: f22ec5fc2d5f02c423c93d35cdfa83573a3a3bd98c66b927c368ea4d0e7252a500df2a90a6b45522be536a96a73404393c958e945fdba95e6832c200791702b6 languageName: node linkType: hard From 08e513048d9b4660dd89ae37c1ae29a20ab48880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20St=C3=A6rk=C3=A6r?= Date: Mon, 22 Apr 2024 12:28:50 +0200 Subject: [PATCH 278/479] fix: broken link Also lints (cherry picked from commit ca492e599c452314669777e4b03a379b5de6728e) --- .../user-guide/features/prompter.md | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/packages/documentation/versioned_docs/version-1.47.0/user-guide/features/prompter.md b/packages/documentation/versioned_docs/version-1.47.0/user-guide/features/prompter.md index cef63bb00e..82ae1a2854 100644 --- a/packages/documentation/versioned_docs/version-1.47.0/user-guide/features/prompter.md +++ b/packages/documentation/versioned_docs/version-1.47.0/user-guide/features/prompter.md @@ -4,7 +4,7 @@ sidebar_position: 3 # Prompter -See [Sofie views](sofie-views.md#prompter-view) for how to access the prompter page. +See [Sofie views](sofie-views.mdx#prompter-view) for how to access the prompter page. ![Prompter screen before the first Part is taken](/img/docs/main/features/prompter-view.png) @@ -28,7 +28,7 @@ The prompter UI can be configured using query parameters: | `showmarker` | 0 / 1 | If the marker is not set to "hide", control if the marker is hidden or not | `1` | | `showscroll` | 0 / 1 | Whether the scroll bar should be shown | `1` | | `followtake` | 0 / 1 | Whether the prompter should automatically scroll to current segment when the operator TAKE:s it | `1` | -| `showoverunder` | 0 / 1 | The timer in the top-right of the prompter, showing the overtime/undertime of the current show. | `1` | +| `showoverunder` | 0 / 1 | The timer in the top-right of the prompter, showing the overtime/undertime of the current show. | `1` | | `debug` | 0 / 1 | Whether to display a debug box showing controller input values and the calculated speed the prompter is currently scrolling at. Used to tweak speedMaps and ranges. | `0` | Example: [http://127.0.0.1/prompter/studio0/?mode=mouse&followtake=0&fontsize=20](http://127.0.0.1/prompter/studio0/?mode=mouse&followtake=0&fontsize=20) @@ -73,7 +73,7 @@ Keyboard control is intended to be used when having a "keyboard"-device, such as This mode is intended to be used when having a Contour ShuttleXpress or X-keys device, configured to work as a keyboard device. These devices have jog/shuttle wheels, and their software/firmware allow them to map scroll movement to keystrokes from any key-combination. Since we only listen for key combinations, it effectively means that any device outputing keystrokes will work in this mode. -From Release 30, the speedMap has a prefix: **shuttle\_** \(i.e. shuttle\_speedMap\) +From Release 30, the speedMap has a prefix: **shuttle\_** \(i.e. shuttle_speedMap\) | Key combination | Function | | :--------------------------------------------------------- | :------------------------------------- | @@ -89,10 +89,10 @@ From Release 30, the speedMap has a prefix: **shuttle\_** \(i.e. shuttle\_speedM Configuration files that can be used in their respective driver software: -* [Contour ShuttleXpress](https://github.com/nrkno/sofie-core/blob/release26/resources/prompter_layout_shuttlexpress.pref) -* [X-keys](https://github.com/nrkno/sofie-core/blob/release26/resources/prompter_layout_xkeys.mw3) +- [Contour ShuttleXpress](https://github.com/nrkno/sofie-core/blob/release26/resources/prompter_layout_shuttlexpress.pref) +- [X-keys](https://github.com/nrkno/sofie-core/blob/release26/resources/prompter_layout_xkeys.mw3) -#### +#### #### Control using midi input \(_?mode=pedal_\) @@ -100,7 +100,7 @@ This mode listens to MIDI CC-notes on channel 8, expecting a linear range like i If you want to use traditional analogue pedals with 5 volt TRS connection, a converter such as the _Beat Bars EX2M_ will work well. -From Release 30, the parameters for the pedal have a prefix: **pedal\_** \(i.e. pedal\_speedMap, pedal\_reverseSpeedMap etc\) +From Release 30, the parameters for the pedal have a prefix: **pedal\_** \(i.e. pedal_speedMap, pedal_reverseSpeedMap etc\) | Query parameter | Type | Description | Default | | :---------------- | :--------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------- | @@ -111,11 +111,11 @@ From Release 30, the parameters for the pedal have a prefix: **pedal\_** \(i.e. | `rangeNeutralMax` | number | The minimum input to run forward, the start of the forward-range \(min speed\). This is also the end of any "deadband" you want filter out before starting moving forwards. | `80` | | `rangeFwdMax` | number | The maximum input, the end of the forward-range \(max speed\) | `127` | -* `rangeNeutralMin` has to be greater than `rangeRevMin` -* `rangeNeutralMax` has to be greater than `rangeNeutralMin` -* `rangeFwdMax` has to be greater than `rangeNeutralMax` +- `rangeNeutralMin` has to be greater than `rangeRevMin` +- `rangeNeutralMax` has to be greater than `rangeNeutralMin` +- `rangeFwdMax` has to be greater than `rangeNeutralMax` -![Yamaha FC7 mapped for both a forward \(80-127\) and backwards \(0-35\) range.](/img/docs/main/features/yamaha-fc7.jpg) +![Yamaha FC7 mapped for both a forward (80-127) and backwards (0-35) range.](/img/docs/main/features/yamaha-fc7.jpg) The default values allow for both going forwards and backwards. This matches the _Yamaha FC7_ expression pedal. The default values create a forward-range from 80-127, a neutral zone from 35-80 and a reverse-range from 0-35. @@ -131,7 +131,7 @@ Any movement within forward range will map to the _speedMap_ with interpolation | _"I have to go too far back to reverse"_ | Increse `rangeNeutralMin` | | _"As I find a good speed, it varies a bit in speed up/down even if I hold my foot still"_ | Use `?debug=1` to see what speed is calculated in the position the presenter wants to rest the foot in. Add more of that number in a sequence in the `speedMap` to flatten out the speed curve, i.e. `[1, 2, 3, 4, 4, 4, 4, 5, ...]` | -**Note:** The default values are set up to work with the _Yamaha FC7_ expression pedal, and will probably not be good for pedals with one continuous linear range from fully released to fully depressed. A suggested configuration for such pedals \(i.e. the _Mission Engineering EP-1_\) will be like: +**Note:** The default values are set up to work with the _Yamaha FC7_ expression pedal, and will probably not be good for pedals with one continuous linear range from fully released to fully depressed. A suggested configuration for such pedals \(i.e. the _Mission Engineering EP-1_\) will be like: | Query parameter | Suggestion | | :---------------- | :-------------------------------------- | @@ -148,7 +148,7 @@ This mode uses the browsers Gamapad API and polls connected Joycons for their st The Joycons can operate in 3 modes, the L-stick, the R-stick or both L+R sticks together. Reconnections and jumping between modes works, with one known limitation: **Transition from L+R to a single stick blocks all input, and requires a reconnect of the sticks you want to use.** This seems to be a bug in either the Joycons themselves or in the Gamepad API in general. -From Release 30, the parameters for the JoyCon have a prefix: **joycon\_** \(i.e. joycon\_speedMap, joycon\_reverseSpeedMap etc\) +From Release 30, the parameters for the JoyCon have a prefix: **joycon\_** \(i.e. joycon_speedMap, joycon_reverseSpeedMap etc\) | Query parameter | Type | Description | Default | | :---------------- | :--------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------- | @@ -159,9 +159,9 @@ From Release 30, the parameters for the JoyCon have a prefix: **joycon\_** \(i.e | `rangeNeutralMax` | number | The minimum input to run forward, the start of the forward-range \(min speed\). This is also the end of any "deadband" you want filter out before starting moving forwards. | `0.25` | | `rangeFwdMax` | number | The maximum input, the end of the forward-range \(max speed\) | `1` | -* `rangeNeutralMin` has to be greater than `rangeRevMin` -* `rangeNeutralMax` has to be greater than `rangeNeutralMin` -* `rangeFwdMax` has to be greater than `rangeNeutralMax` +- `rangeNeutralMin` has to be greater than `rangeRevMin` +- `rangeNeutralMax` has to be greater than `rangeNeutralMin` +- `rangeFwdMax` has to be greater than `rangeNeutralMax` ![Nintendo Swith Joycons](/img/docs/main/features/nintendo-switch-joycons.jpg) @@ -177,8 +177,6 @@ You can turn on `?debug=1` to see how your input maps to an output. | Left / Y | Go to the previous story | | Right / A | Go to the following story | - - **Calibration guide:** | **Symptom** | Adjustment | @@ -189,4 +187,3 @@ You can turn on `?debug=1` to see how your input maps to an output. | _"I can't reach max speed backwards"_ | Increase `rangeRevMin` | | _"I can't reach max speed forwards"_ | Decrease `rangeFwdMax` | | _"As I find a good speed, it varies a bit in speed up/down even if I hold my finger still"_ | Use `?debug=1` to see what speed is calculated in the position the presenter wants to rest their finger in. Add more of that number in a sequence in the `speedMap` to flatten out the speed curve, i.e. `[1, 2, 3, 4, 4, 4, 4, 5, ...]` | - From 7a6d881e6e61b94bac1e4211d2a1541afb5af837 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 22 Apr 2024 15:08:11 +0100 Subject: [PATCH 279/479] wip: skipLibCheck for server-core-integration because of got dependency conflicts in typings --- packages/server-core-integration/tsconfig.build.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/server-core-integration/tsconfig.build.json b/packages/server-core-integration/tsconfig.build.json index dfb3d12d5e..ce14e834d1 100755 --- a/packages/server-core-integration/tsconfig.build.json +++ b/packages/server-core-integration/tsconfig.build.json @@ -10,6 +10,7 @@ "@sofie-automation/server-core-integration": ["./src/index.ts"] }, "resolveJsonModule": true, - "types": ["node"] + "types": ["node"], + "skipLibCheck": true } } From 33ed635534164e6d2c62c4722850f4946626cfa4 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Tue, 30 Apr 2024 12:35:19 +0200 Subject: [PATCH 280/479] chore: refactor multistep chevron on floating inspector --- .../FloatingInspectors/NoraFloatingInspector.tsx | 16 ++++++++-------- .../SegmentContainer/PieceMultistepChevron.tsx | 13 +++++++++++-- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/meteor/client/ui/FloatingInspectors/NoraFloatingInspector.tsx b/meteor/client/ui/FloatingInspectors/NoraFloatingInspector.tsx index c5d234d244..bc882af37c 100644 --- a/meteor/client/ui/FloatingInspectors/NoraFloatingInspector.tsx +++ b/meteor/client/ui/FloatingInspectors/NoraFloatingInspector.tsx @@ -1,7 +1,8 @@ -import React, { useEffect, useImperativeHandle } from 'react' import { NoraContent } from '@sofie-automation/blueprints-integration' -import Escape from './../../lib/Escape' +import React, { useEffect, useImperativeHandle } from 'react' import _ from 'underscore' +import { getNoraContentSteps } from '../SegmentContainer/PieceMultistepChevron' +import Escape from './../../lib/Escape' interface IPropsHeader { noraContent: NoraContent | undefined @@ -194,8 +195,7 @@ export class NoraPreviewRenderer extends React.Component<{}, IStateHeader> { } render(): JSX.Element { - const stepContent = this.state.noraContent?.payload?.step - const isMultiStep = this.state.noraContent?.payload?.step?.enabled === true + const hasStepChevron = getNoraContentSteps(this.state.noraContent) const rendererUrl = this.state.noraContent?.previewRenderer const dimensions = this.state.noraContent?.previewRendererDimensions @@ -218,12 +218,12 @@ export class NoraPreviewRenderer extends React.Component<{}, IStateHeader> { > )}
- {isMultiStep && stepContent ? ( + {hasStepChevron ? (
- {stepContent.to === 'next' ? (stepContent.from || 0) + 1 : stepContent.to || 1} - {typeof stepContent.total === 'number' && stepContent.total > 0 ? ( + {hasStepChevron.currentStep} + {typeof hasStepChevron.allSteps === 'number' && hasStepChevron.allSteps > 0 ? ( - /{stepContent.total} + /{hasStepChevron.allSteps} ) : null}
diff --git a/meteor/client/ui/SegmentContainer/PieceMultistepChevron.tsx b/meteor/client/ui/SegmentContainer/PieceMultistepChevron.tsx index 29b1868fdc..56c114c254 100644 --- a/meteor/client/ui/SegmentContainer/PieceMultistepChevron.tsx +++ b/meteor/client/ui/SegmentContainer/PieceMultistepChevron.tsx @@ -27,8 +27,17 @@ export function getPieceSteps(piece: PieceExtended): { currentStep: number; allS const noraContent = piece.instance.piece.content as NoraContent | undefined const hasStepChevron = - (piece.sourceLayer?.type === SourceLayerType.GRAPHICS || piece.sourceLayer?.type === SourceLayerType.LOWER_THIRD) && - noraContent?.payload?.step?.enabled + piece.sourceLayer?.type === SourceLayerType.GRAPHICS || piece.sourceLayer?.type === SourceLayerType.LOWER_THIRD + + if (!noraContent || !hasStepChevron) return null + + return getNoraContentSteps(noraContent) +} + +export function getNoraContentSteps( + noraContent: NoraContent | undefined +): { currentStep: number; allSteps: number } | null { + const hasStepChevron = noraContent?.payload?.step?.enabled if (!hasStepChevron) return null From 7194a591ecc5cfb27b03a06aeb19f4a72451b946 Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Wed, 1 May 2024 07:52:45 +0000 Subject: [PATCH 281/479] chore: add openapi ci generator --- .github/workflows/node.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index 80686529dd..c21b080546 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -672,3 +672,22 @@ jobs: node scripts/checkForMultipleVersions.mjs env: CI: true + + build-stable-api: + name: Build OpenAPI typescript client + runs-on: ubuntu-latest + continue-on-error: true + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Generate code + uses: hatamiarash7/openapi-generator@v0.2.0 + working-directory: ./packages/openapi + with: + generator: typescript-fetch + openapi-file: ./api/actions.yaml + output-dir: client/ts + command-args: supportsES6=true From ef6569e055f0479a675cbbd7de7006f038573f86 Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Wed, 1 May 2024 07:54:12 +0000 Subject: [PATCH 282/479] chore: wip --- .github/workflows/node.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index c21b080546..dc21134ae9 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -684,8 +684,8 @@ jobs: persist-credentials: false - name: Generate code - uses: hatamiarash7/openapi-generator@v0.2.0 working-directory: ./packages/openapi + uses: hatamiarash7/openapi-generator@v0.2.0 with: generator: typescript-fetch openapi-file: ./api/actions.yaml From 3f1c8af3bb47ece7101eb3d516f42bdeb51bc77b Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Wed, 1 May 2024 07:56:30 +0000 Subject: [PATCH 283/479] chore: wip --- .github/workflows/node.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index dc21134ae9..827a9ed858 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -684,10 +684,9 @@ jobs: persist-credentials: false - name: Generate code - working-directory: ./packages/openapi uses: hatamiarash7/openapi-generator@v0.2.0 with: generator: typescript-fetch - openapi-file: ./api/actions.yaml - output-dir: client/ts + openapi-file: ./packages/openapi/api/actions.yaml + output-dir: ./packages/openapi/client/ts command-args: supportsES6=true From 43adc8d634de3b20a6205ff377367bba8a378b60 Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Wed, 1 May 2024 08:00:17 +0000 Subject: [PATCH 284/479] chore: wip --- .github/workflows/node.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index 827a9ed858..14b7ca0df1 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -684,9 +684,9 @@ jobs: persist-credentials: false - name: Generate code - uses: hatamiarash7/openapi-generator@v0.2.0 + uses: hatamiarash7/openapi-generator@v0.3.0 with: generator: typescript-fetch openapi-file: ./packages/openapi/api/actions.yaml output-dir: ./packages/openapi/client/ts - command-args: supportsES6=true + command-args: -p supportsES6=true From 618ce57398c56f5121b27410a15e4fd1804ffc63 Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Wed, 1 May 2024 09:49:24 +0000 Subject: [PATCH 285/479] chore: wip --- .github/workflows/node.yaml | 36 +++++++++++++++++++++++++-- .github/workflows/prerelease-libs.yml | 15 +++++++++++ packages/openapi/package.json | 1 - 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index 14b7ca0df1..0fba492aa7 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -618,6 +618,21 @@ jobs: yarn build env: CI: true + - name: Generate OpenAPI client library + if: ${{ steps.do-publish.outputs.tag }} + uses: hatamiarash7/openapi-generator@v0.3.0 + with: + generator: typescript-fetch + openapi-file: ./packages/openapi/api/actions.yaml + output-dir: ./packages/openapi/client/ts + command-args: -p supportsES6=true + - name: Build OpenAPI client library + if: ${{ steps.do-publish.outputs.tag }} + run: | + cd packages/openapi + yarn build:main + env: + CI: true - name: Modify dependencies to use npm packages run: node scripts/prepublish.js - name: Publish to NPM @@ -673,16 +688,33 @@ jobs: env: CI: true - build-stable-api: - name: Build OpenAPI typescript client + release-openapi-lib: + name: Build and release OpenAPI typescript client runs-on: ubuntu-latest continue-on-error: true timeout-minutes: 15 + + # only run for tags + if: contains(github.ref, 'refs/tags/') + steps: - uses: actions/checkout@v4 with: persist-credentials: false + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version-file: ".node-version" + - name: Prepare Environment # have to run this first to make sure the semver lib is available + run: | + yarn config set cacheFolder /home/runner/release-libs-cache + + cd packages + yarn install + env: + CI: true + - name: Generate code uses: hatamiarash7/openapi-generator@v0.3.0 with: diff --git a/.github/workflows/prerelease-libs.yml b/.github/workflows/prerelease-libs.yml index cee7b42fd6..6d34115b6a 100644 --- a/.github/workflows/prerelease-libs.yml +++ b/.github/workflows/prerelease-libs.yml @@ -131,6 +131,21 @@ jobs: yarn build env: CI: true + - name: Generate OpenAPI client library + if: ${{ steps.do-publish.outputs.tag }} + uses: hatamiarash7/openapi-generator@v0.3.0 + with: + generator: typescript-fetch + openapi-file: ./packages/openapi/api/actions.yaml + output-dir: ./packages/openapi/client/ts + command-args: -p supportsES6=true + - name: Build OpenAPI client library + if: ${{ steps.do-publish.outputs.tag }} + run: | + cd packages/openapi + yarn build:main + env: + CI: true - name: Modify dependencies to use npm packages run: node scripts/prepublish.js - name: Publish to NPM diff --git a/packages/openapi/package.json b/packages/openapi/package.json index 36b467ed95..b9c4217dbe 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -1,7 +1,6 @@ { "name": "@sofie-automation/openapi", "version": "1.50.1", - "private": true, "license": "MIT", "repository": { "type": "git", From d075d90947ff203851b1aa6bffb002044ecdb380 Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Wed, 1 May 2024 09:50:52 +0000 Subject: [PATCH 286/479] chore: wip --- .github/workflows/node.yaml | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index 0fba492aa7..767ebf0541 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -687,38 +687,3 @@ jobs: node scripts/checkForMultipleVersions.mjs env: CI: true - - release-openapi-lib: - name: Build and release OpenAPI typescript client - runs-on: ubuntu-latest - continue-on-error: true - timeout-minutes: 15 - - # only run for tags - if: contains(github.ref, 'refs/tags/') - - steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version-file: ".node-version" - - name: Prepare Environment # have to run this first to make sure the semver lib is available - run: | - yarn config set cacheFolder /home/runner/release-libs-cache - - cd packages - yarn install - env: - CI: true - - - name: Generate code - uses: hatamiarash7/openapi-generator@v0.3.0 - with: - generator: typescript-fetch - openapi-file: ./packages/openapi/api/actions.yaml - output-dir: ./packages/openapi/client/ts - command-args: -p supportsES6=true From 5b87e184b0dbf9ae4a05275bf95452b0a30ba19d Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Wed, 1 May 2024 10:01:31 +0000 Subject: [PATCH 287/479] chore: wip --- packages/openapi/LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 packages/openapi/LICENSE diff --git a/packages/openapi/LICENSE b/packages/openapi/LICENSE new file mode 100644 index 0000000000..78f0f2dbb8 --- /dev/null +++ b/packages/openapi/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. From 67b71a16da1370825974d69cb67acfcd8a8aae79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20St=C3=A6rk=C3=A6r?= Date: Mon, 6 May 2024 09:03:13 +0200 Subject: [PATCH 288/479] Create CODEOWNERS --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..72a337e9e8 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @SofieTeam From e5c906bd3ce4ee1396dabf311a55807589dd5d4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20St=C3=A6rk=C3=A6r?= Date: Mon, 6 May 2024 11:08:24 +0200 Subject: [PATCH 289/479] Update CODEOWNERS --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 72a337e9e8..cdc0028efd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @SofieTeam +* @nrkno/sofieteam From 0a87a9519ca1f344429e9b4d47a44c1a9acddff2 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Mon, 6 May 2024 14:50:07 +0200 Subject: [PATCH 290/479] feat: backport of release51 live-status-gateway onto release50 This is a copy of the live-status-gateway package from R51 branch, as well as the minimum modifications needed to build & be compatible with R50 types and publications --- packages/corelib/src/pubsub.ts | 137 +++++++++ .../live-status-gateway/api/asyncapi.yaml | 15 +- .../api/schemas/activePieces.yaml | 26 ++ .../api/schemas/activePlaylist.yaml | 117 ++++--- .../api/schemas/adLibs.yaml | 81 +++++ .../live-status-gateway/api/schemas/root.yaml | 2 +- .../api/schemas/segments.yaml | 4 + .../sample-client/index.html | 3 + .../sample-client/script.js | 13 + .../src/collections/adLibActionsHandler.ts | 37 +-- .../src/collections/adLibsHandler.ts | 39 ++- .../collections/globalAdLibActionsHandler.ts | 36 ++- .../src/collections/globalAdLibsHandler.ts | 36 ++- .../src/collections/partHandler.ts | 34 +-- .../src/collections/partInstancesHandler.ts | 78 ++--- .../src/collections/partsHandler.ts | 15 +- .../src/collections/pieceInstancesHandler.ts | 217 +++++++++++++ .../src/collections/playlistHandler.ts | 40 ++- .../src/collections/rundownHandler.ts | 38 +-- .../src/collections/rundownsHandler.ts | 14 +- .../src/collections/segmentHandler.ts | 29 +- .../src/collections/segmentsHandler.ts | 14 +- .../src/collections/showStyleBaseHandler.ts | 99 ++++-- .../src/collections/studioHandler.ts | 26 +- .../live-status-gateway/src/coreHandler.ts | 129 ++++++-- .../src/liveStatusServer.ts | 35 ++- .../src/topics/__tests__/activePieces.spec.ts | 70 +++++ .../topics/__tests__/activePlaylist.spec.ts | 116 ++----- .../src/topics/__tests__/adLibs.spec.ts | 107 +++++++ .../topics/__tests__/segmentsTopic.spec.ts | 115 ++++++- .../src/topics/__tests__/utils.ts | 10 + .../src/topics/activePiecesTopic.ts | 117 +++++++ .../src/topics/activePlaylistTopic.ts | 286 +++++------------- .../src/topics/adLibsTopic.ts | 251 +++++++++++++++ .../src/topics/helpers/pieceStatus.ts | 28 ++ .../live-status-gateway/src/topics/root.ts | 7 +- .../src/topics/segmentsTopic.ts | 8 +- .../src/topics/studioTopic.ts | 4 +- packages/live-status-gateway/src/wsHandler.ts | 77 ++++- .../mos-gateway/src/CoreMosDeviceHandler.ts | 2 +- packages/mos-gateway/src/coreHandler.ts | 2 +- packages/mos-gateway/src/mosHandler.ts | 2 +- packages/playout-gateway/src/coreHandler.ts | 4 +- packages/playout-gateway/src/tsrHandler.ts | 2 +- .../src/lib/CoreConnectionChild.ts | 2 +- .../src/lib/coreConnection.ts | 2 +- .../src/lib/ddpClient.ts | 38 ++- .../shared-lib/src/lib/throttleToNextTick.ts | 18 ++ 48 files changed, 1889 insertions(+), 693 deletions(-) create mode 100644 packages/corelib/src/pubsub.ts create mode 100644 packages/live-status-gateway/api/schemas/activePieces.yaml create mode 100644 packages/live-status-gateway/api/schemas/adLibs.yaml create mode 100644 packages/live-status-gateway/src/collections/pieceInstancesHandler.ts create mode 100644 packages/live-status-gateway/src/topics/__tests__/activePieces.spec.ts create mode 100644 packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts create mode 100644 packages/live-status-gateway/src/topics/activePiecesTopic.ts create mode 100644 packages/live-status-gateway/src/topics/adLibsTopic.ts create mode 100644 packages/live-status-gateway/src/topics/helpers/pieceStatus.ts create mode 100644 packages/shared-lib/src/lib/throttleToNextTick.ts diff --git a/packages/corelib/src/pubsub.ts b/packages/corelib/src/pubsub.ts new file mode 100644 index 0000000000..ca199f6a28 --- /dev/null +++ b/packages/corelib/src/pubsub.ts @@ -0,0 +1,137 @@ +import { DBPart } from './dataModel/Part' +import { CollectionName } from './dataModel/Collections' +import { AdLibAction } from './dataModel/AdlibAction' +import { AdLibPiece } from './dataModel/AdLibPiece' +import { RundownBaselineAdLibAction } from './dataModel/RundownBaselineAdLibAction' +import { RundownBaselineAdLibItem } from './dataModel/RundownBaselineAdLibPiece' +import { DBPartInstance } from './dataModel/PartInstance' +import { DBRundown } from './dataModel/Rundown' +import { DBRundownPlaylist } from './dataModel/RundownPlaylist' +import { DBSegment } from './dataModel/Segment' +import { DBShowStyleBase } from './dataModel/ShowStyleBase' +import { DBShowStyleVariant } from './dataModel/ShowStyleVariant' +import { DBStudio } from './dataModel/Studio' +import { IngestDataCacheObj } from './dataModel/IngestDataCache' +import { DBTimelineDatastoreEntry } from '@sofie-automation/shared-lib/dist/core/model/TimelineDatastore' +import { Blueprint } from './dataModel/Blueprint' +import { BucketAdLibAction } from './dataModel/BucketAdLibAction' +import { BucketAdLib } from './dataModel/BucketAdLibPiece' +import { ExpectedMediaItem } from './dataModel/ExpectedMediaItem' +import { ExpectedPackageWorkStatus } from './dataModel/ExpectedPackageWorkStatuses' +import { ExpectedPackageDBBase } from './dataModel/ExpectedPackages' +import { ExternalMessageQueueObj } from './dataModel/ExternalMessageQueue' +import { PackageContainerStatusDB } from './dataModel/PackageContainerStatus' +import { PeripheralDevice } from './dataModel/PeripheralDevice' +import { Piece } from './dataModel/Piece' +import { PieceInstance } from './dataModel/PieceInstance' +import { TimelineComplete } from './dataModel/Timeline' +import { PackageInfoDB } from './dataModel/PackageInfos' + +// This is a hack, this contains R50 compatible pubsub definitions. To be replaced with R51 ones. +export enum CorelibPubSub { + blueprints = 'blueprints', + coreSystem = 'coreSystem', + evaluations = 'evaluations', + expectedPlayoutItems = 'expectedPlayoutItems', + expectedMediaItems = 'expectedMediaItems', + externalMessageQueue = 'externalMessageQueue', + peripheralDeviceCommands = 'peripheralDeviceCommands', + peripheralDevices = 'peripheralDevices', + peripheralDevicesAndSubDevices = ' peripheralDevicesAndSubDevices', + rundownBaselineAdLibPieces = 'rundownBaselineAdLibPieces', + rundownBaselineAdLibActions = 'rundownBaselineAdLibActions', + ingestDataCache = 'ingestDataCache', + rundownPlaylists = 'rundownPlaylists', + rundowns = 'rundowns', + adLibActions = 'adLibActions', + adLibPieces = 'adLibPieces', + pieces = 'pieces', + pieceInstances = 'pieceInstances', + pieceInstancesSimple = 'pieceInstancesSimple', + parts = 'parts', + partInstances = 'partInstances', + partInstancesSimple = 'partInstancesSimple', + partInstancesForSegmentPlayout = 'partInstancesForSegmentPlayout', + segments = 'segments', + showStyleBases = 'showStyleBases', + showStyleVariants = 'showStyleVariants', + triggeredActions = 'triggeredActions', + snapshots = 'snapshots', + studios = 'studios', + timeline = 'timeline', + timelineDatastore = 'timelineDatastore', + userActionsLog = 'userActionsLog', + /** @deprecated */ + mediaWorkFlows = 'mediaWorkFlows', + /** @deprecated */ + mediaWorkFlowSteps = 'mediaWorkFlowSteps', + rundownLayouts = 'rundownLayouts', + loggedInUser = 'loggedInUser', + usersInOrganization = 'usersInOrganization', + organization = 'organization', + buckets = 'buckets', + bucketAdLibPieces = 'bucketAdLibPieces', + translationsBundles = 'translationsBundles', + bucketAdLibActions = 'bucketAdLibActions', + expectedPackages = 'expectedPackages', + expectedPackageWorkStatuses = 'expectedPackageWorkStatuses', + packageContainerStatuses = 'packageContainerStatuses', + packageInfos = 'packageInfos', + + // For a PeripheralDevice + rundownsForDevice = 'rundownsForDevice', + + // custom publications: + peripheralDeviceForDevice = 'peripheralDeviceForDevice', + mappingsForDevice = 'mappingsForDevice', + timelineForDevice = 'timelineForDevice', + timelineDatastoreForDevice = 'timelineDatastoreForDevice', + mappingsForStudio = 'mappingsForStudio', + timelineForStudio = 'timelineForStudio', + + uiShowStyleBase = 'uiShowStyleBase', + uiStudio = 'uiStudio', + uiTriggeredActions = 'uiTriggeredActions', + + mountedTriggersForDevice = 'mountedTriggersForDevice', + mountedTriggersForDevicePreview = 'mountedTriggersForDevicePreview', + deviceTriggersPreview = 'deviceTriggersPreview', + + uiSegmentPartNotes = 'uiSegmentPartNotes', + uiPieceContentStatuses = 'uiPieceContentStatuses', + uiBucketContentStatuses = 'uiBucketContentStatuses', + + packageManagerPlayoutContext = 'packageManagerPlayoutContext', + packageManagerPackageContainers = 'packageManagerPackageContainers', + packageManagerExpectedPackages = 'packageManagerExpectedPackages', +} + +export type CorelibPubSubCollections = { + [CollectionName.AdLibActions]: AdLibAction + [CollectionName.AdLibPieces]: AdLibPiece + [CollectionName.Blueprints]: Blueprint + [CollectionName.BucketAdLibActions]: BucketAdLibAction + [CollectionName.BucketAdLibPieces]: BucketAdLib + [CollectionName.ExpectedMediaItems]: ExpectedMediaItem + [CollectionName.ExpectedPackages]: ExpectedPackageDBBase + [CollectionName.ExpectedPackageWorkStatuses]: ExpectedPackageWorkStatus + [CollectionName.ExternalMessageQueue]: ExternalMessageQueueObj + [CollectionName.IngestDataCache]: IngestDataCacheObj + [CollectionName.PartInstances]: DBPartInstance + [CollectionName.PackageContainerStatuses]: PackageContainerStatusDB + [CollectionName.PackageInfos]: PackageInfoDB + [CollectionName.Parts]: DBPart + [CollectionName.PeripheralDevices]: PeripheralDevice + [CollectionName.PieceInstances]: PieceInstance + [CollectionName.Pieces]: Piece + [CollectionName.RundownBaselineAdLibActions]: RundownBaselineAdLibAction + [CollectionName.RundownBaselineAdLibPieces]: RundownBaselineAdLibItem + [CollectionName.RundownPlaylists]: DBRundownPlaylist + [CollectionName.Rundowns]: DBRundown + [CollectionName.Segments]: DBSegment + [CollectionName.ShowStyleBases]: DBShowStyleBase + [CollectionName.ShowStyleVariants]: DBShowStyleVariant + [CollectionName.Studios]: DBStudio + [CollectionName.Timelines]: TimelineComplete + [CollectionName.TimelineDatastore]: DBTimelineDatastoreEntry +} diff --git a/packages/live-status-gateway/api/asyncapi.yaml b/packages/live-status-gateway/api/asyncapi.yaml index 0651c17c7f..1f7d363471 100644 --- a/packages/live-status-gateway/api/asyncapi.yaml +++ b/packages/live-status-gateway/api/asyncapi.yaml @@ -2,7 +2,7 @@ asyncapi: 2.5.0 info: title: Sofie Live Status Service description: This service provides subscriptions for status updates from Sofie - version: 1.0.0 + version: 2.0.0 license: name: MIT License url: http://opensource.org/licenses/MIT @@ -34,7 +34,9 @@ channels: - $ref: '#/components/messages/subscriptionStatus' - $ref: '#/components/messages/studio' - $ref: '#/components/messages/activePlaylist' + - $ref: '#/components/messages/activePieces' - $ref: '#/components/messages/segments' + - $ref: '#/components/messages/adLibs' components: messages: ping: @@ -84,8 +86,19 @@ components: description: Active Playlist status payload: $ref: './schemas/activePlaylist.yaml#/$defs/activePlaylist' + activePieces: + name: activePieces + summary: Active Pieces status + description: Pieces from the active Playlist that are currently active (on air) + payload: + $ref: './schemas/activePieces.yaml#/$defs/activePieces' segments: name: segments description: Segments in active Playlist payload: $ref: './schemas/segments.yaml#/$defs/segments' + adLibs: + name: adLibs + description: AdLibs in active Playlist + payload: + $ref: './schemas/adLibs.yaml#/$defs/adLibs' diff --git a/packages/live-status-gateway/api/schemas/activePieces.yaml b/packages/live-status-gateway/api/schemas/activePieces.yaml new file mode 100644 index 0000000000..01d777f443 --- /dev/null +++ b/packages/live-status-gateway/api/schemas/activePieces.yaml @@ -0,0 +1,26 @@ +title: Active Pieces +description: Active Pieces schema for websocket subscriptions +$defs: + activePieces: + type: object + properties: + event: + type: string + const: activePieces + rundownPlaylistId: + description: Unique id of the rundown playlist, or null if no playlist is active + oneOf: + - type: string + - type: 'null' + activePieces: + description: Pieces that are currently active (on air) + type: array + items: + $ref: './activePlaylist.yaml#/$defs/piece' + required: [event, rundownPlaylistId, activePieces] + additionalProperties: false + examples: + - event: activePieces + rundownPlaylistId: 'OKAgZmZ0Buc99lE_2uPPSKVbMrQ_' + activePieces: + - $ref: './activePlaylist.yaml#/$defs/piece/examples/0' diff --git a/packages/live-status-gateway/api/schemas/activePlaylist.yaml b/packages/live-status-gateway/api/schemas/activePlaylist.yaml index 59f5615b95..5b373bfcc3 100644 --- a/packages/live-status-gateway/api/schemas/activePlaylist.yaml +++ b/packages/live-status-gateway/api/schemas/activePlaylist.yaml @@ -27,17 +27,9 @@ $defs: nextPart: description: The next Part - if empty, no part will follow live part $ref: '#/$defs/part' - adLibs: - description: The available AdLibs for this playlist - type: array - items: - $ref: '#/$defs/adLib' - globalAdLibs: - description: The available Global AdLibs for this playlist - type: array - items: - $ref: '#/$defs/adLib' - required: [event, id, name, rundownIds, currentPart, currentSegment, nextPart, adLibs, globalAdLibs] + publicData: + description: Optional arbitrary data + required: [event, id, name, rundownIds, currentPart, currentSegment, nextPart] additionalProperties: false examples: - event: activePlaylist @@ -50,10 +42,8 @@ $defs: $ref: '#/$defs/currentSegment/examples/0' nextPart: $ref: '#/$defs/part/examples/0' - adLibs: - $ref: '#/$defs/adLib/examples' - globals: - $ref: '#/$defs/adLib/examples' + publicData: + category: 'Evening News' partBase: type: object properties: @@ -70,13 +60,24 @@ $defs: description: If this part will progress to the next automatically type: boolean default: false - required: [id, name, segmentId] - additionalProperties: false + pieces: + description: All pieces in this part + type: array + items: + $ref: '#/$defs/piece' + publicData: + description: Optional arbitrary data + required: [id, name, segmentId, pieces] + # additionalProperties: false examples: - id: 'H5CBGYjThrMSmaYvRaa5FVKJIzk_' name: 'Intro' segmentId: 'n1mOVd5_K5tt4sfk6HYfTuwumGQ_' autoNext: false + pieces: + - $ref: '#/$defs/piece/examples/0' + publicData: + partType: 'intro' part: oneOf: - $ref: '#/$defs/partBase' @@ -86,6 +87,8 @@ $defs: name: 'Intro' segmentId: 'n1mOVd5_K5tt4sfk6HYfTuwumGQ_' autoNext: false + pieces: + - $ref: '#/$defs/piece/examples/0' currentPart: oneOf: - allOf: @@ -117,50 +120,10 @@ $defs: startTime: 1600000060000 expectedDurationMs: 15000 projectedEndTime: 1600000075000 - adLib: - type: object - properties: - id: - description: Unique id of the AdLib - type: string - name: - description: The user defined AdLib name - type: string - sourceLayer: - description: The source layer name for this AdLib - type: string - outputLayer: - description: The output layer name for this AdLib - type: string - actionType: - description: The available action type names that can be used to modify the execution of the AdLib - type: array - items: - type: object - properties: - name: - description: The string to be passed to the ExecuteAdlib function - type: string - label: - description: The label for the AdLib type - type: string - required: [name, label] - additionalProperties: false - tags: - description: Tags attached to this AdLib - type: array - items: - type: string - required: [id, name, sourceLayer, actionType] - additionalProperties: false - examples: - - id: 'C6K_yIMuGFUk8X_L9A9_jRT6aq4_' - name: Music video clip - sourceLayer: Video Clip - actionType: - - name: pvw - label: Preview - tags: ['music_video'] + pieces: + - $ref: '#/$defs/piece/examples/0' + publicData: + partType: 'intro' currentSegment: type: object properties: @@ -189,3 +152,35 @@ $defs: expectedDurationMs: 15000 budgetDurationMs: 20000 projectedEndTime: 1600000075000 + piece: + type: object + properties: + id: + description: Unique id of the Piece + type: string + name: + description: User-facing name of the Piece + type: string + sourceLayer: + description: The source layer name for this Piece + type: string + outputLayer: + description: The output layer name for this Piece + type: string + tags: + description: Tags attached to this Piece + type: array + items: + type: string + publicData: + description: Optional arbitrary data + required: [id, name, sourceLayer, outputLayer] + additionalProperties: false + examples: + - id: 'H5CBGYjThrMSmaYvRaa5FVKJIzk_' + name: 'Camera 1' + sourceLayer: 'Camera' + outputLayer: 'PGM' + tags: ['camera'] + publicData: + switcherSource: 1 diff --git a/packages/live-status-gateway/api/schemas/adLibs.yaml b/packages/live-status-gateway/api/schemas/adLibs.yaml new file mode 100644 index 0000000000..2d66ec8c0b --- /dev/null +++ b/packages/live-status-gateway/api/schemas/adLibs.yaml @@ -0,0 +1,81 @@ +title: AdLibs +description: AdLibs schema for websocket subscriptions +$defs: + adLibs: + type: object + properties: + event: + type: string + const: adLibs + rundownPlaylistId: + description: Unique id of the rundown playlist, or null if no playlist is active + oneOf: + - type: string + - type: 'null' + adLibs: + description: The available AdLibs for this playlist + type: array + items: + $ref: '#/$defs/adLib' + globalAdLibs: + description: The available Global AdLibs for this playlist + type: array + items: + $ref: '#/$defs/adLib' + required: [event, rundownPlaylistId, adLibs, globalAdLibs] + additionalProperties: false + examples: + - event: adLibs + rundownPlaylistId: 'OKAgZmZ0Buc99lE_2uPPSKVbMrQ_' + adLibs: + $ref: '#/$defs/adLib/examples' + globalAdLibs: + $ref: '#/$defs/adLib/examples' + adLib: + type: object + properties: + id: + description: Unique id of the AdLib + type: string + name: + description: The user defined AdLib name + type: string + sourceLayer: + description: The source layer name for this AdLib + type: string + outputLayer: + description: The output layer name for this AdLib + type: string + actionType: + description: The available action type names that can be used to modify the execution of the AdLib + type: array + items: + type: object + properties: + name: + description: The string to be passed to the ExecuteAdlib function + type: string + label: + description: The label for the AdLib type + type: string + required: [name, label] + additionalProperties: false + tags: + description: Tags attached to this AdLib + type: array + items: + type: string + publicData: + description: Optional arbitrary data + required: [id, name, sourceLayer, actionType] + additionalProperties: false + examples: + - id: 'C6K_yIMuGFUk8X_L9A9_jRT6aq4_' + name: Music video clip + sourceLayer: Video Clip + actionType: + - name: pvw + label: Preview + tags: ['music_video'] + publicData: + fileName: MV000123.mxf diff --git a/packages/live-status-gateway/api/schemas/root.yaml b/packages/live-status-gateway/api/schemas/root.yaml index e8a671cffc..488a5d3054 100644 --- a/packages/live-status-gateway/api/schemas/root.yaml +++ b/packages/live-status-gateway/api/schemas/root.yaml @@ -89,7 +89,7 @@ $defs: oneOf: - $ref: '#/$defs/subscriptionStatusError' - $ref: '#/$defs/subscriptionStatusSuccess' - additionalProperties: false + # additionalProperties: false examples: - $ref: '#/$defs/subscriptionStatusCommon/examples/0' subscriptionStatusError: diff --git a/packages/live-status-gateway/api/schemas/segments.yaml b/packages/live-status-gateway/api/schemas/segments.yaml index 0949e0b3c1..29180418ea 100644 --- a/packages/live-status-gateway/api/schemas/segments.yaml +++ b/packages/live-status-gateway/api/schemas/segments.yaml @@ -49,6 +49,8 @@ $defs: description: Budget duration of the segment (milliseconds) type: number required: [expectedDurationMs] + publicData: + description: Optional arbitrary data required: [id, rundownId, name, timing] additionalProperties: false examples: @@ -58,3 +60,5 @@ $defs: timing: expectedDurationMs: 15000 budgetDurationMs: 20000 + publicData: + containsLiveSource: true diff --git a/packages/live-status-gateway/sample-client/index.html b/packages/live-status-gateway/sample-client/index.html index 94eb84896f..d1fb8ca255 100644 --- a/packages/live-status-gateway/sample-client/index.html +++ b/packages/live-status-gateway/sample-client/index.html @@ -8,7 +8,10 @@
Time of day:
Segment remaining:
Part remaining:
+
Active Pieces:
+
Next Pieces:
+ Segments:
diff --git a/packages/live-status-gateway/sample-client/script.js b/packages/live-status-gateway/sample-client/script.js index 137291ef02..eba6dcd2e4 100644 --- a/packages/live-status-gateway/sample-client/script.js +++ b/packages/live-status-gateway/sample-client/script.js @@ -59,6 +59,8 @@ const TIME_OF_DAY_SPAN_ID = 'time-of-day' const SEGMENT_DURATION_SPAN_CLASS = 'segment-duration' const SEGMENT_REMAINIG_SPAN_ID = 'segment-remaining' const PART_REMAINIG_SPAN_ID = 'part-remaining' +const ACTIVE_PIECES_SPAN_ID = 'active-pieces' +const NEXT_PIECES_SPAN_ID = 'next-pieces' const SEGMENTS_DIV_ID = 'segments' const ENABLE_SYNCED_TICKS = true @@ -66,6 +68,16 @@ let activePlaylist = {} function handleActivePlaylist(data) { activePlaylist = data + const activePiecesEl = document.getElementById(ACTIVE_PIECES_SPAN_ID) + const nextPiecesEl = document.getElementById(NEXT_PIECES_SPAN_ID) + activePiecesEl.innerHTML = + '
  • ' + + activePlaylist.activePieces.map((p) => `${p.name} [${p.tags || []}]`).join('
  • ') + + '
    • ' + nextPiecesEl.innerHTML = + '
      • ' + + activePlaylist.nextPart.pieces.map((p) => `${p.name} [${p.tags || []}]`).join('
      • ') + + '
        • ' } setInterval(() => { @@ -85,6 +97,7 @@ setInterval(() => { } if (segmentEndTime) segmentRemainingEl.textContent = formatMillisecondsToTime(segmentEndTime - now) if (partEndTime) partRemainingEl.textContent = formatMillisecondsToTime(Math.ceil(partEndTime / 1000) * 1000 - now) + updateClock() }, 100) diff --git a/packages/live-status-gateway/src/collections/adLibActionsHandler.ts b/packages/live-status-gateway/src/collections/adLibActionsHandler.ts index 7148ba2b18..8fc813f925 100644 --- a/packages/live-status-gateway/src/collections/adLibActionsHandler.ts +++ b/packages/live-status-gateway/src/collections/adLibActionsHandler.ts @@ -1,31 +1,28 @@ import { Logger } from 'winston' import { CoreHandler } from '../coreHandler' import { CollectionBase, Collection, CollectionObserver } from '../wsHandler' -import { CoreConnection } from '@sofie-automation/server-core-integration' import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' -import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' -import _ = require('underscore') +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { AdLibActionId, RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { SelectedPartInstances } from './partInstancesHandler' export class AdLibActionsHandler - extends CollectionBase + extends CollectionBase implements Collection, CollectionObserver { public observerName: string - private _core: CoreConnection - private _curRundownId: string | undefined + private _curRundownId: RundownId | undefined private _curPartInstance: DBPartInstance | undefined constructor(logger: Logger, coreHandler: CoreHandler) { - super(AdLibActionsHandler.name, CollectionName.AdLibActions, 'adLibActions', logger, coreHandler) - this._core = coreHandler.coreConnection + super(AdLibActionsHandler.name, CollectionName.AdLibActions, CorelibPubSub.adLibActions, logger, coreHandler) this.observerName = this._name } - async changed(id: string, changeType: string): Promise { - this._logger.info(`${this._name} ${changeType} ${id}`) + async changed(id: AdLibActionId, changeType: string): Promise { + this.logDocumentChange(id, changeType) if (!this._collectionName) return const col = this._core.getCollection(this._collectionName) if (!col) throw new Error(`collection '${this._collectionName}' not found!`) @@ -34,35 +31,39 @@ export class AdLibActionsHandler } async update(source: string, data: SelectedPartInstances | undefined): Promise { - this._logger.info(`${this._name} received partInstances update from ${source}`) + this.logUpdateReceived('partInstances', source) const prevRundownId = this._curRundownId - const prevCurPartInstance = this._curPartInstance this._curPartInstance = data ? data.current ?? data.next : undefined - this._curRundownId = this._curPartInstance ? unprotectString(this._curPartInstance.rundownId) : undefined + this._curRundownId = this._curPartInstance ? this._curPartInstance.rundownId : undefined await new Promise(process.nextTick.bind(this)) if (!this._collectionName) return if (!this._publicationName) return - if (prevRundownId !== this._curRundownId || !_.isEqual(prevCurPartInstance, this._curPartInstance)) { + if (prevRundownId !== this._curRundownId) { if (this._subscriptionId) this._coreHandler.unsubscribe(this._subscriptionId) if (this._dbObserver) this._dbObserver.stop() if (this._curRundownId && this._curPartInstance) { this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, { rundownId: this._curRundownId, }) + // this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, [ + // this._curRundownId, + // ]) // R51 this._dbObserver = this._coreHandler.setupObserver(this._collectionName) - this._dbObserver.added = (id: string) => { + this._dbObserver.added = (id) => { void this.changed(id, 'added').catch(this._logger.error) } - this._dbObserver.changed = (id: string) => { + this._dbObserver.changed = (id) => { void this.changed(id, 'changed').catch(this._logger.error) } + this._dbObserver.removed = (id) => { + void this.changed(id, 'removed').catch(this._logger.error) + } const collection = this._core.getCollection(this._collectionName) if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) this._collectionData = collection.find({ rundownId: this._curRundownId, - partId: this._curPartInstance.part._id, }) await this.notify(this._collectionData) } @@ -71,7 +72,7 @@ export class AdLibActionsHandler // override notify to implement empty array handling async notify(data: AdLibAction[] | undefined): Promise { - this._logger.info(`${this._name} notifying update with ${data?.length} adLibActions`) + this.logNotifyingUpdate(data?.length) if (data !== undefined) { for (const observer of this._observers) { await observer.update(this._name, data) diff --git a/packages/live-status-gateway/src/collections/adLibsHandler.ts b/packages/live-status-gateway/src/collections/adLibsHandler.ts index 689a36d977..e0670cb7d7 100644 --- a/packages/live-status-gateway/src/collections/adLibsHandler.ts +++ b/packages/live-status-gateway/src/collections/adLibsHandler.ts @@ -1,31 +1,29 @@ import { Logger } from 'winston' import { CoreHandler } from '../coreHandler' import { CollectionBase, Collection, CollectionObserver } from '../wsHandler' -import { CoreConnection } from '@sofie-automation/server-core-integration' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' -import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' -import _ = require('underscore') +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { PieceId, RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { SelectedPartInstances } from './partInstancesHandler' export class AdLibsHandler - extends CollectionBase + extends CollectionBase implements Collection, CollectionObserver { public observerName: string - private _core: CoreConnection - private _currentRundownId: string | undefined + // private _core: CoreConnection + private _currentRundownId: RundownId | undefined private _currentPartInstance: DBPartInstance | undefined constructor(logger: Logger, coreHandler: CoreHandler) { - super(AdLibsHandler.name, CollectionName.AdLibPieces, 'adLibPieces', logger, coreHandler) - this._core = coreHandler.coreConnection + super(AdLibsHandler.name, CollectionName.AdLibPieces, CorelibPubSub.adLibPieces, logger, coreHandler) this.observerName = this._name } - async changed(id: string, changeType: string): Promise { - this._logger.info(`${this._name} ${changeType} ${id}`) + async changed(id: PieceId, changeType: string): Promise { + this.logDocumentChange(id, changeType) if (!this._collectionName) return const col = this._core.getCollection(this._collectionName) if (!col) throw new Error(`collection '${this._collectionName}' not found!`) @@ -34,32 +32,32 @@ export class AdLibsHandler } async update(source: string, data: SelectedPartInstances | undefined): Promise { - this._logger.info(`${this._name} received adLibs update from ${source}`) + this.logUpdateReceived('partInstances', source) const prevRundownId = this._currentRundownId - const prevCurPartInstance = this._currentPartInstance this._currentPartInstance = data ? data.current ?? data.next : undefined - this._currentRundownId = this._currentPartInstance - ? unprotectString(this._currentPartInstance.rundownId) - : undefined + this._currentRundownId = this._currentPartInstance?.rundownId await new Promise(process.nextTick.bind(this)) if (!this._collectionName) return if (!this._publicationName) return - if (prevRundownId !== this._currentRundownId || !_.isEqual(prevCurPartInstance, this._currentPartInstance)) { + if (prevRundownId !== this._currentRundownId) { if (this._subscriptionId) this._coreHandler.unsubscribe(this._subscriptionId) if (this._dbObserver) this._dbObserver.stop() if (this._currentRundownId && this._currentPartInstance) { this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, { rundownId: this._currentRundownId, }) + // this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, [ + // this._currentRundownId, + // ]) // R51 this._dbObserver = this._coreHandler.setupObserver(this._collectionName) - this._dbObserver.added = (id: string) => { + this._dbObserver.added = (id) => { void this.changed(id, 'added').catch(this._logger.error) } - this._dbObserver.changed = (id: string) => { + this._dbObserver.changed = (id) => { void this.changed(id, 'changed').catch(this._logger.error) } - this._dbObserver.removed = (id: string) => { + this._dbObserver.removed = (id) => { void this.changed(id, 'removed').catch(this._logger.error) } @@ -67,7 +65,6 @@ export class AdLibsHandler if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) this._collectionData = collection.find({ rundownId: this._currentRundownId, - partId: this._currentPartInstance.part._id, }) await this.notify(this._collectionData) } @@ -76,7 +73,7 @@ export class AdLibsHandler // override notify to implement empty array handling async notify(data: AdLibPiece[] | undefined): Promise { - this._logger.info(`${this._name} notifying update with ${data?.length} adLibs`) + this.logNotifyingUpdate(data?.length) if (data !== undefined) { for (const observer of this._observers) { await observer.update(this._name, data) diff --git a/packages/live-status-gateway/src/collections/globalAdLibActionsHandler.ts b/packages/live-status-gateway/src/collections/globalAdLibActionsHandler.ts index 68b951628e..bd1a8e9623 100644 --- a/packages/live-status-gateway/src/collections/globalAdLibActionsHandler.ts +++ b/packages/live-status-gateway/src/collections/globalAdLibActionsHandler.ts @@ -1,34 +1,36 @@ import { Logger } from 'winston' import { CoreHandler } from '../coreHandler' import { CollectionBase, Collection, CollectionObserver } from '../wsHandler' -import { CoreConnection } from '@sofie-automation/server-core-integration' import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' -import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { RundownBaselineAdLibActionId, RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { SelectedPartInstances } from './partInstancesHandler' export class GlobalAdLibActionsHandler - extends CollectionBase + extends CollectionBase< + RundownBaselineAdLibAction[], + CorelibPubSub.rundownBaselineAdLibActions, + CollectionName.RundownBaselineAdLibActions + > implements Collection, CollectionObserver { public observerName: string - private _core: CoreConnection - private _currentRundownId: string | undefined + private _currentRundownId: RundownId | undefined constructor(logger: Logger, coreHandler: CoreHandler) { super( GlobalAdLibActionsHandler.name, CollectionName.RundownBaselineAdLibActions, - 'rundownBaselineAdLibActions', + CorelibPubSub.rundownBaselineAdLibActions, logger, coreHandler ) - this._core = coreHandler.coreConnection this.observerName = this._name } - async changed(id: string, changeType: string): Promise { - this._logger.info(`${this._name} ${changeType} ${id}`) + async changed(id: RundownBaselineAdLibActionId, changeType: string): Promise { + this.logDocumentChange(id, changeType) if (!this._collectionName) return const col = this._core.getCollection(this._collectionName) if (!col) throw new Error(`collection '${this._collectionName}' not found!`) @@ -37,10 +39,10 @@ export class GlobalAdLibActionsHandler } async update(source: string, data: SelectedPartInstances | undefined): Promise { - this._logger.info(`${this._name} received partInstances update from ${source}`) + this.logUpdateReceived('partInstances', source) const prevRundownId = this._currentRundownId const partInstance = data ? data.current ?? data.next : undefined - this._currentRundownId = partInstance ? unprotectString(partInstance.rundownId) : undefined + this._currentRundownId = partInstance?.rundownId await new Promise(process.nextTick.bind(this)) if (!this._collectionName) return @@ -52,13 +54,19 @@ export class GlobalAdLibActionsHandler this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, { rundownId: this._currentRundownId, }) + // this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, [ + // this._currentRundownId, + // ]) // In R51 this._dbObserver = this._coreHandler.setupObserver(this._collectionName) - this._dbObserver.added = (id: string) => { + this._dbObserver.added = (id) => { void this.changed(id, 'added').catch(this._logger.error) } - this._dbObserver.changed = (id: string) => { + this._dbObserver.changed = (id) => { void this.changed(id, 'changed').catch(this._logger.error) } + this._dbObserver.removed = (id) => { + void this.changed(id, 'removed').catch(this._logger.error) + } const collection = this._core.getCollection(this._collectionName) if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) @@ -70,7 +78,7 @@ export class GlobalAdLibActionsHandler // override notify to implement empty array handling async notify(data: RundownBaselineAdLibAction[] | undefined): Promise { - this._logger.info(`${this._name} notifying update with ${data?.length} globalAdLibActions`) + this.logNotifyingUpdate(data?.length) if (data !== undefined) { for (const observer of this._observers) { await observer.update(this._name, data) diff --git a/packages/live-status-gateway/src/collections/globalAdLibsHandler.ts b/packages/live-status-gateway/src/collections/globalAdLibsHandler.ts index 83b8249572..aba02ef6cf 100644 --- a/packages/live-status-gateway/src/collections/globalAdLibsHandler.ts +++ b/packages/live-status-gateway/src/collections/globalAdLibsHandler.ts @@ -1,34 +1,36 @@ import { Logger } from 'winston' import { CoreHandler } from '../coreHandler' import { CollectionBase, Collection, CollectionObserver } from '../wsHandler' -import { CoreConnection } from '@sofie-automation/server-core-integration' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' -import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { PieceId, RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { SelectedPartInstances } from './partInstancesHandler' export class GlobalAdLibsHandler - extends CollectionBase + extends CollectionBase< + RundownBaselineAdLibItem[], + CorelibPubSub.rundownBaselineAdLibPieces, + CollectionName.RundownBaselineAdLibPieces + > implements Collection, CollectionObserver { public observerName: string - private _core: CoreConnection - private _currentRundownId: string | undefined + private _currentRundownId: RundownId | undefined constructor(logger: Logger, coreHandler: CoreHandler) { super( GlobalAdLibsHandler.name, CollectionName.RundownBaselineAdLibPieces, - 'rundownBaselineAdLibPieces', + CorelibPubSub.rundownBaselineAdLibPieces, logger, coreHandler ) - this._core = coreHandler.coreConnection this.observerName = this._name } - async changed(id: string, changeType: string): Promise { - this._logger.info(`${this._name} ${changeType} ${id}`) + async changed(id: PieceId, changeType: string): Promise { + this.logDocumentChange(id, changeType) if (!this._collectionName) return const collection = this._core.getCollection(this._collectionName) if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) @@ -37,10 +39,10 @@ export class GlobalAdLibsHandler } async update(source: string, data: SelectedPartInstances | undefined): Promise { - this._logger.info(`${this._name} received globalAdLibs update from ${source}`) + this.logUpdateReceived('globalAdLibs', source) const prevRundownId = this._currentRundownId const partInstance = data ? data.current ?? data.next : undefined - this._currentRundownId = partInstance ? unprotectString(partInstance.rundownId) : undefined + this._currentRundownId = partInstance?.rundownId await new Promise(process.nextTick.bind(this)) if (!this._collectionName) return @@ -52,13 +54,19 @@ export class GlobalAdLibsHandler this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, { rundownId: this._currentRundownId, }) + // this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, [ + // this._currentRundownId, + // ]) // In R51 this._dbObserver = this._coreHandler.setupObserver(this._collectionName) - this._dbObserver.added = (id: string) => { + this._dbObserver.added = (id) => { void this.changed(id, 'added').catch(this._logger.error) } - this._dbObserver.changed = (id: string) => { + this._dbObserver.changed = (id) => { void this.changed(id, 'changed').catch(this._logger.error) } + this._dbObserver.removed = (id) => { + void this.changed(id, 'removed').catch(this._logger.error) + } const collection = this._core.getCollection(this._collectionName) if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) @@ -70,7 +78,7 @@ export class GlobalAdLibsHandler // override notify to implement empty array handling async notify(data: RundownBaselineAdLibItem[] | undefined): Promise { - this._logger.info(`${this._name} notifying update with ${data?.length} globalAdLibs`) + this.logNotifyingUpdate(data?.length) if (data !== undefined) { for (const observer of this._observers) { await observer.update(this._name, data) diff --git a/packages/live-status-gateway/src/collections/partHandler.ts b/packages/live-status-gateway/src/collections/partHandler.ts index 5886f1991e..c1eed0cd3a 100644 --- a/packages/live-status-gateway/src/collections/partHandler.ts +++ b/packages/live-status-gateway/src/collections/partHandler.ts @@ -1,34 +1,32 @@ import { Logger } from 'winston' import { CoreHandler } from '../coreHandler' import { CollectionBase, Collection, CollectionObserver } from '../wsHandler' -import { CoreConnection } from '@sofie-automation/server-core-integration' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' -import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { PartInstancesHandler, SelectedPartInstances } from './partInstancesHandler' -import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' import { PlaylistHandler } from './playlistHandler' -import areElementsShallowEqual from '@sofie-automation/shared-lib/dist/lib/isShallowEqual' import { PartsHandler } from './partsHandler' +import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { PartId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import areElementsShallowEqual from '@sofie-automation/shared-lib/dist/lib/isShallowEqual' export class PartHandler - extends CollectionBase + extends CollectionBase implements Collection, CollectionObserver, CollectionObserver { public observerName: string - private _core: CoreConnection private _activePlaylist: DBRundownPlaylist | undefined private _currentPartInstance: DBPartInstance | undefined constructor(logger: Logger, coreHandler: CoreHandler, private _partsHandler: PartsHandler) { - super(PartHandler.name, CollectionName.Parts, 'parts', logger, coreHandler) - this._core = coreHandler.coreConnection + super(PartHandler.name, CollectionName.Parts, CorelibPubSub.parts, logger, coreHandler) this.observerName = this._name } - async changed(id: string, changeType: string): Promise { - this._logger.info(`${this._name} ${changeType} ${id}`) + async changed(id: PartId, changeType: string): Promise { + this.logDocumentChange(id, changeType) if (!this._collectionName) return const collection = this._core.getCollection(this._collectionName) if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) @@ -48,11 +46,11 @@ export class PartHandler const partInstances = data as SelectedPartInstances switch (source) { case PlaylistHandler.name: - this._logger.info(`${this._name} received playlist update ${rundownPlaylist?._id}`) + this.logUpdateReceived('playlist', source, `rundownPlaylistId ${rundownPlaylist?._id}`) this._activePlaylist = rundownPlaylist break case PartInstancesHandler.name: - this._logger.info(`${this._name} received partInstances update from ${source}`) + this.logUpdateReceived('partInstances', source) this._currentPartInstance = partInstances.current break default: @@ -67,20 +65,20 @@ export class PartHandler if (this._subscriptionId) this._coreHandler.unsubscribe(this._subscriptionId) if (this._dbObserver) this._dbObserver.stop() if (this._activePlaylist) { - const rundownIds = this._activePlaylist?.rundownIdsInOrder.map((r) => unprotectString(r)) + const rundownIds = this._activePlaylist.rundownIdsInOrder this._subscriptionId = await this._coreHandler.setupSubscription( this._publicationName, rundownIds, - undefined + null ) this._dbObserver = this._coreHandler.setupObserver(this._collectionName) - this._dbObserver.added = (id: string) => { + this._dbObserver.added = (id) => { void this.changed(id, 'added').catch(this._logger.error) } - this._dbObserver.changed = (id: string) => { + this._dbObserver.changed = (id) => { void this.changed(id, 'changed').catch(this._logger.error) } - this._dbObserver.removed = (id: string) => { + this._dbObserver.removed = (id) => { void this.changed(id, 'removed').catch(this._logger.error) } } @@ -91,7 +89,7 @@ export class PartHandler await this._partsHandler.setParts(allParts) } if (prevCurPartInstance !== this._currentPartInstance) { - this._logger.info( + this._logger.debug( `${this._name} found updated partInstances with current part ${this._activePlaylist?.currentPartInfo?.partInstanceId}` ) if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) diff --git a/packages/live-status-gateway/src/collections/partInstancesHandler.ts b/packages/live-status-gateway/src/collections/partInstancesHandler.ts index c361b2ea03..36b7176c7f 100644 --- a/packages/live-status-gateway/src/collections/partInstancesHandler.ts +++ b/packages/live-status-gateway/src/collections/partInstancesHandler.ts @@ -1,13 +1,14 @@ import { Logger } from 'winston' import { CoreHandler } from '../coreHandler' import { CollectionBase, Collection, CollectionObserver } from '../wsHandler' -import { CoreConnection } from '@sofie-automation/server-core-integration' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' -import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' import areElementsShallowEqual from '@sofie-automation/shared-lib/dist/lib/isShallowEqual' import _ = require('underscore') +import throttleToNextTick from '@sofie-automation/shared-lib/dist/lib/throttleToNextTick' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { PartInstanceId, RundownId, RundownPlaylistActivationId } from '@sofie-automation/corelib/dist/dataModel/Ids' export interface SelectedPartInstances { current: DBPartInstance | undefined @@ -17,18 +18,22 @@ export interface SelectedPartInstances { } export class PartInstancesHandler - extends CollectionBase + extends CollectionBase implements Collection, CollectionObserver { public observerName: string - private _core: CoreConnection private _currentPlaylist: DBRundownPlaylist | undefined - private _rundownIds: string[] = [] - private _activationId: string | undefined + private _rundownIds: RundownId[] = [] + private _activationId: RundownPlaylistActivationId | undefined + private _subscriptionPending = false + + private _throttledUpdateAndNotify = throttleToNextTick(() => { + this.updateCollectionData() + this.notify(this._collectionData).catch(this._logger.error) + }) constructor(logger: Logger, coreHandler: CoreHandler) { - super(PartInstancesHandler.name, CollectionName.PartInstances, 'partInstances', logger, coreHandler) - this._core = coreHandler.coreConnection + super(PartInstancesHandler.name, CollectionName.PartInstances, CorelibPubSub.partInstances, logger, coreHandler) this.observerName = this._name this._collectionData = { current: undefined, @@ -38,12 +43,11 @@ export class PartInstancesHandler } } - async changed(id: string, changeType: string): Promise { - this._logger.info(`${this._name} ${changeType} ${id}`) - if (!this._collectionName) return - this.updateCollectionData() + async changed(id: PartInstanceId, changeType: string): Promise { + this.logDocumentChange(id, changeType) + if (!this._collectionName || this._subscriptionPending) return - await this.notify(this._collectionData) + this._throttledUpdateAndNotify() } private updateCollectionData(): boolean { @@ -97,18 +101,16 @@ export class PartInstancesHandler const prevRundownIds = this._rundownIds.map((rid) => rid) const prevActivationId = this._activationId - this._logger.info( - `${this._name} received playlist update ${data?._id}, active ${ - data?.activationId ? true : false - } from ${source}` + this.logUpdateReceived( + 'playlist', + source, + `rundownPlaylistId ${data?._id}, active ${data?.activationId ? true : false}` ) this._currentPlaylist = data if (!this._collectionName) return - this._rundownIds = this._currentPlaylist - ? this._currentPlaylist.rundownIdsInOrder.map((r) => unprotectString(r)) - : [] - this._activationId = unprotectString(this._currentPlaylist?.activationId) + this._rundownIds = this._currentPlaylist ? this._currentPlaylist.rundownIdsInOrder : [] + this._activationId = this._currentPlaylist?.activationId if (this._currentPlaylist && this._rundownIds.length && this._activationId) { const sameSubscription = areElementsShallowEqual(this._rundownIds, prevRundownIds) && prevActivationId === this._activationId @@ -118,37 +120,43 @@ export class PartInstancesHandler if (!this._publicationName) return if (!this._currentPlaylist) return if (this._subscriptionId) this._coreHandler.unsubscribe(this._subscriptionId) + this._subscriptionPending = true this._subscriptionId = await this._coreHandler.setupSubscription( this._publicationName, this._rundownIds, this._activationId ) + this._subscriptionPending = false this._dbObserver = this._coreHandler.setupObserver(this._collectionName) - this._dbObserver.added = (id: string) => { + this._dbObserver.added = (id) => { void this.changed(id, 'added').catch(this._logger.error) } - this._dbObserver.changed = (id: string) => { + this._dbObserver.changed = (id) => { void this.changed(id, 'changed').catch(this._logger.error) } - this._dbObserver.removed = (id: string) => { + this._dbObserver.removed = (id) => { void this.changed(id, 'removed').catch(this._logger.error) } - const hasAnythingChanged = this.updateCollectionData() - if (hasAnythingChanged) { - await this.notify(this._collectionData) - } + await this.updateAndNotify() } else if (this._subscriptionId) { - const hasAnythingChanged = this.updateCollectionData() - if (hasAnythingChanged) { - await this.notify(this._collectionData) - } + await this.updateAndNotify() } else { - this.clearCollectionData() - await this.notify(this._collectionData) + await this.clearAndNotify() } } else { - this.clearCollectionData() + await this.clearAndNotify() + } + } + + private async clearAndNotify() { + this.clearCollectionData() + await this.notify(this._collectionData) + } + + private async updateAndNotify() { + const hasAnythingChanged = this.updateCollectionData() + if (hasAnythingChanged) { await this.notify(this._collectionData) } } diff --git a/packages/live-status-gateway/src/collections/partsHandler.ts b/packages/live-status-gateway/src/collections/partsHandler.ts index 7a30013282..aece1f9c6d 100644 --- a/packages/live-status-gateway/src/collections/partsHandler.ts +++ b/packages/live-status-gateway/src/collections/partsHandler.ts @@ -3,29 +3,32 @@ import { CoreHandler } from '../coreHandler' import { CollectionBase, Collection } from '../wsHandler' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import _ = require('underscore') +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' const THROTTLE_PERIOD_MS = 200 -export class PartsHandler extends CollectionBase implements Collection { +export class PartsHandler + extends CollectionBase + implements Collection +{ public observerName: string private throttledNotify: (data: DBPart[]) => Promise constructor(logger: Logger, coreHandler: CoreHandler) { - super(PartsHandler.name, undefined, undefined, logger, coreHandler) + super(PartsHandler.name, CollectionName.Parts, CorelibPubSub.parts, logger, coreHandler) this.observerName = this._name this.throttledNotify = _.throttle(this.notify.bind(this), THROTTLE_PERIOD_MS, { leading: true, trailing: true }) } async setParts(parts: DBPart[]): Promise { - this._logger.info(`'${this._name}' handler received parts update with ${parts.length} parts`) + this.logUpdateReceived('parts', parts.length) this._collectionData = parts await this.throttledNotify(this._collectionData) } async notify(data: DBPart[] | undefined): Promise { - this._logger.info( - `${this._name} notifying all observers of an update with ${this._collectionData?.length} parts` - ) + this.logNotifyingUpdate(this._collectionData?.length) if (data !== undefined) { for (const observer of this._observers) { await observer.update(this._name, data) diff --git a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts new file mode 100644 index 0000000000..22445e4c5c --- /dev/null +++ b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts @@ -0,0 +1,217 @@ +import { Logger } from 'winston' +import { CoreHandler } from '../coreHandler' +import { CollectionBase, Collection, CollectionObserver } from '../wsHandler' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' +import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' +import areElementsShallowEqual from '@sofie-automation/shared-lib/dist/lib/isShallowEqual' +import throttleToNextTick from '@sofie-automation/shared-lib/dist/lib/throttleToNextTick' +import _ = require('underscore') +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { PartInstanceId, PieceInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' + +export type PieceInstanceMin = Omit + +export interface SelectedPieceInstances { + // Pieces reported by the Playout Gateway as active + active: PieceInstanceMin[] + + // Pieces present in the current part instance + currentPartInstance: PieceInstanceMin[] + + // Pieces present in the current part instance + nextPartInstance: PieceInstanceMin[] +} + +export class PieceInstancesHandler + extends CollectionBase + implements Collection, CollectionObserver +{ + public observerName: string + private _currentPlaylist: DBRundownPlaylist | undefined + private _partInstanceIds: PartInstanceId[] = [] + private _activationId: string | undefined + private _subscriptionPending = false + + private _throttledUpdateAndNotify = throttleToNextTick(() => { + this.updateCollectionData() + this.notify(this._collectionData).catch(this._logger.error) + }) + + constructor(logger: Logger, coreHandler: CoreHandler) { + super( + PieceInstancesHandler.name, + CollectionName.PieceInstances, + CorelibPubSub.pieceInstances, + logger, + coreHandler + ) + this.observerName = this._name + this._collectionData = { + active: [], + currentPartInstance: [], + nextPartInstance: [], + } + } + + async changed(id: PieceInstanceId, changeType: string): Promise { + this.logDocumentChange(id, changeType) + if (!this._collectionName || this._subscriptionPending) return + this._throttledUpdateAndNotify() + } + + private updateCollectionData(): boolean { + if (!this._collectionName || !this._collectionData) return false + const collection = this._core.getCollection(this._collectionName) + if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) + const active = this._currentPlaylist?.currentPartInfo?.partInstanceId + ? collection.find((pieceInstance: PieceInstance) => this.isPieceInstanceActive(pieceInstance)) + : [] + const inCurrentPartInstance = this._currentPlaylist?.currentPartInfo?.partInstanceId + ? collection.find({ partInstanceId: this._currentPlaylist.currentPartInfo.partInstanceId }) + : [] + const inNextPartInstance = this._currentPlaylist?.nextPartInfo?.partInstanceId + ? collection.find({ partInstanceId: this._currentPlaylist.nextPartInfo.partInstanceId }) + : [] + + let hasAnythingChanged = false + if (!areElementsShallowEqual(this._collectionData.active, active)) { + this._collectionData.active = active + hasAnythingChanged = true + } + if ( + !areElementsShallowEqual(this._collectionData.currentPartInstance, inCurrentPartInstance) && + (this._collectionData.currentPartInstance.length !== inCurrentPartInstance.length || + this._collectionData.currentPartInstance.some((pieceInstance, index) => { + return !arePropertiesShallowEqual(inCurrentPartInstance[index], pieceInstance, [ + 'reportedStartedPlayback', + 'reportedStoppedPlayback', + ]) + })) + ) { + this._collectionData.currentPartInstance = inCurrentPartInstance + hasAnythingChanged = true + } + if (!areElementsShallowEqual(this._collectionData.nextPartInstance, inNextPartInstance)) { + this._collectionData.nextPartInstance = inNextPartInstance + hasAnythingChanged = true + } + return hasAnythingChanged + } + + private clearCollectionData() { + if (!this._collectionName || !this._collectionData) return + this._collectionData.active = [] + this._collectionData.currentPartInstance = [] + this._collectionData.nextPartInstance = [] + } + + async update(source: string, data: DBRundownPlaylist | undefined): Promise { + const prevPartInstanceIds = this._partInstanceIds + const prevActivationId = this._activationId + + this.logUpdateReceived('playlist', source, `rundownPlaylistId ${data?._id}, active ${!!data?.activationId}`) + this._currentPlaylist = data + if (!this._collectionName) return + + this._partInstanceIds = this._currentPlaylist + ? _.compact([ + this._currentPlaylist.previousPartInfo?.partInstanceId, + this._currentPlaylist.nextPartInfo?.partInstanceId, + this._currentPlaylist.currentPartInfo?.partInstanceId, + ]) + : [] + this._activationId = unprotectString(this._currentPlaylist?.activationId) + if (this._currentPlaylist && this._partInstanceIds.length && this._activationId) { + const sameSubscription = + areElementsShallowEqual(this._partInstanceIds, prevPartInstanceIds) && + prevActivationId === this._activationId + if (!sameSubscription) { + await new Promise(process.nextTick.bind(this)) + if (!this._collectionName) return + if (!this._publicationName) return + if (!this._currentPlaylist) return + if (this._subscriptionId) this._coreHandler.unsubscribe(this._subscriptionId) + this._subscriptionPending = true + this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, { + rundownId: { $in: this._currentPlaylist.rundownIdsInOrder }, + partInstanceId: { $in: this._partInstanceIds }, + }) + // this._subscriptionId = await this._coreHandler.setupSubscription( + // this._publicationName, + // this._currentPlaylist.rundownIdsInOrder, + // this._partInstanceIds, + // {} + // ) + this._subscriptionPending = false + this._dbObserver = this._coreHandler.setupObserver(this._collectionName) + this._dbObserver.added = (id) => { + void this.changed(id, 'added').catch(this._logger.error) + } + this._dbObserver.changed = (id) => { + void this.changed(id, 'changed').catch(this._logger.error) + } + this._dbObserver.removed = (id) => { + void this.changed(id, 'removed').catch(this._logger.error) + } + + await this.updateAndNotify() + } else if (this._subscriptionId) { + await this.updateAndNotify() + } else { + await this.clearAndNotify() + } + } else { + this.clearCollectionData() + await this.notify(this._collectionData) + } + } + + private async clearAndNotify() { + this.clearCollectionData() + await this.notify(this._collectionData) + } + + private async updateAndNotify() { + const hasAnythingChanged = this.updateCollectionData() + if (hasAnythingChanged) { + await this.notify(this._collectionData) + } + } + + private isPieceInstanceActive(pieceInstance: PieceInstance) { + return ( + pieceInstance.reportedStoppedPlayback == null && + (pieceInstance.partInstanceId === this._currentPlaylist?.previousPartInfo?.partInstanceId || // a piece from previous part instance may be active during transition + pieceInstance.partInstanceId === this._currentPlaylist?.currentPartInfo?.partInstanceId) && + (pieceInstance.reportedStartedPlayback != null || // has been reported to have started by the Playout Gateway + (pieceInstance.partInstanceId === this._currentPlaylist?.currentPartInfo?.partInstanceId && + pieceInstance.piece.enable.start === 0) || // this is to speed things up immediately after a part instance is taken when not yet reported by the Playout Gateway + pieceInstance.infinite?.fromPreviousPart) // infinites from previous part also are on air from the start of the current part + ) + } +} + +export function arePropertiesShallowEqual>( + a: T, + b: T, + omitProperties: Array +): boolean { + if (typeof a !== 'object' || a == null || typeof b !== 'object' || b == null) { + return false + } + + const keysA = Object.keys(a).filter((key) => !omitProperties.includes(key)) + const keysB = Object.keys(b).filter((key) => !omitProperties.includes(key)) + + if (keysA.length !== keysB.length) return false + + for (const key of keysA) { + if (!keysB.includes(key) || a[key] !== b[key]) { + return false + } + } + + return true +} diff --git a/packages/live-status-gateway/src/collections/playlistHandler.ts b/packages/live-status-gateway/src/collections/playlistHandler.ts index 7b70829ea8..8f4d8a8916 100644 --- a/packages/live-status-gateway/src/collections/playlistHandler.ts +++ b/packages/live-status-gateway/src/collections/playlistHandler.ts @@ -1,29 +1,31 @@ import { Logger } from 'winston' import { CoreHandler } from '../coreHandler' import { CollectionBase, Collection } from '../wsHandler' -import { CoreConnection } from '@sofie-automation/server-core-integration' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' -export class PlaylistsHandler extends CollectionBase implements Collection { +export class PlaylistsHandler + extends CollectionBase + implements Collection +{ public observerName: string constructor(logger: Logger, coreHandler: CoreHandler) { - super(PlaylistsHandler.name, undefined, undefined, logger, coreHandler) + super(PlaylistsHandler.name, CollectionName.RundownPlaylists, undefined, logger, coreHandler) this.observerName = this._name } async setPlaylists(playlists: DBRundownPlaylist[]): Promise { - this._logger.info(`'${this._name}' handler received playlists update with ${playlists.length} playlists`) + this.logUpdateReceived('playlists', playlists.length) this._collectionData = playlists await this.notify(this._collectionData) } // override notify to implement empty array handling async notify(data: DBRundownPlaylist[] | undefined): Promise { - this._logger.info( - `${this._name} notifying all observers of an update with ${this._collectionData?.length} playlists` - ) + this.logNotifyingUpdate(this._collectionData?.length) if (data !== undefined) { for (const observer of this._observers) { await observer.update(this._name, data) @@ -32,14 +34,21 @@ export class PlaylistsHandler extends CollectionBase implem } } -export class PlaylistHandler extends CollectionBase implements Collection { +export class PlaylistHandler + extends CollectionBase + implements Collection +{ public observerName: string - private _core: CoreConnection private _playlistsHandler: PlaylistsHandler constructor(logger: Logger, coreHandler: CoreHandler) { - super(PlaylistHandler.name, CollectionName.RundownPlaylists, 'rundownPlaylists', logger, coreHandler) - this._core = coreHandler.coreConnection + super( + PlaylistHandler.name, + CollectionName.RundownPlaylists, + CorelibPubSub.rundownPlaylists, + logger, + coreHandler + ) this.observerName = this._name this._playlistsHandler = new PlaylistsHandler(this._logger, this._coreHandler) } @@ -52,6 +61,7 @@ export class PlaylistHandler extends CollectionBase implement this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, { studioId: this._studioId, }) + // this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, null, [this._studioId]) // in R51 this._dbObserver = this._coreHandler.setupObserver(this._collectionName) if (this._collectionName) { const col = this._core.getCollection(this._collectionName) @@ -59,17 +69,17 @@ export class PlaylistHandler extends CollectionBase implement const playlists = col.find(undefined) this._collectionData = playlists.find((p) => p.activationId) await this._playlistsHandler.setPlaylists(playlists) - this._dbObserver.added = (id: string) => { + this._dbObserver.added = (id) => { void this.changed(id, 'added').catch(this._logger.error) } - this._dbObserver.changed = (id: string) => { + this._dbObserver.changed = (id) => { void this.changed(id, 'changed').catch(this._logger.error) } } } - async changed(id: string, changeType: string): Promise { - this._logger.info(`${this._name} ${changeType} ${id}`) + async changed(id: RundownPlaylistId, changeType: string): Promise { + this.logDocumentChange(id, changeType) if (!this._collectionName) return const collection = this._core.getCollection(this._collectionName) if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) diff --git a/packages/live-status-gateway/src/collections/rundownHandler.ts b/packages/live-status-gateway/src/collections/rundownHandler.ts index 6922de60e3..646aedde2d 100644 --- a/packages/live-status-gateway/src/collections/rundownHandler.ts +++ b/packages/live-status-gateway/src/collections/rundownHandler.ts @@ -1,34 +1,32 @@ import { Logger } from 'winston' import { CoreHandler } from '../coreHandler' import { CollectionBase, Collection, CollectionObserver } from '../wsHandler' -import { CoreConnection } from '@sofie-automation/server-core-integration' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' +import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { PartInstancesHandler, SelectedPartInstances } from './partInstancesHandler' import { RundownId, RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' import { PlaylistHandler } from './playlistHandler' import { RundownsHandler } from './rundownsHandler' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' export class RundownHandler - extends CollectionBase + extends CollectionBase implements Collection, CollectionObserver, CollectionObserver { public observerName: string - private _core: CoreConnection private _currentPlaylistId: RundownPlaylistId | undefined private _currentRundownId: RundownId | undefined constructor(logger: Logger, coreHandler: CoreHandler, private _rundownsHandler?: RundownsHandler) { - super(RundownHandler.name, CollectionName.Rundowns, 'rundowns', logger, coreHandler) - this._core = coreHandler.coreConnection + super(RundownHandler.name, CollectionName.Rundowns, CorelibPubSub.rundowns, logger, coreHandler) this.observerName = this._name } - async changed(id: string, changeType: string): Promise { - this._logger.info(`${this._name} ${changeType} ${id}`) - if (protectString(id) !== this._currentRundownId) + async changed(id: RundownId, changeType: string): Promise { + this.logDocumentChange(id, changeType) + if (id !== this._currentRundownId) throw new Error(`${this._name} received change with unexpected id ${id} !== ${this._currentRundownId}`) if (!this._collectionName) return const collection = this._core.getCollection(this._collectionName) @@ -45,12 +43,12 @@ export class RundownHandler const partInstances = data as SelectedPartInstances switch (source) { case PlaylistHandler.name: - this._logger.info(`${this._name} received playlist update ${rundownPlaylist?._id}`) + this.logUpdateReceived('playlist', source, unprotectString(rundownPlaylist?._id)) this._currentPlaylistId = rundownPlaylist?._id break case PartInstancesHandler.name: - this._logger.info(`${this._name} received partInstances update from ${source}`) - this._currentRundownId = partInstances.current?.rundownId + this.logUpdateReceived('partInstances', source) + this._currentRundownId = partInstances.current?.rundownId ?? partInstances.next?.rundownId break default: throw new Error(`${this._name} received unsupported update from ${source}}`) @@ -68,22 +66,26 @@ export class RundownHandler [this._currentPlaylistId], undefined ) + // this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, [ + // this._currentPlaylistId, + // ]) // R51 this._dbObserver = this._coreHandler.setupObserver(this._collectionName) - this._dbObserver.added = (id: string) => { + this._dbObserver.added = (id) => { void this.changed(id, 'added').catch(this._logger.error) } - this._dbObserver.changed = (id: string) => { + this._dbObserver.changed = (id) => { void this.changed(id, 'changed').catch(this._logger.error) } } } - if (prevCurRundownId !== this._currentRundownId) { - if (this._currentRundownId) { + if (prevCurRundownId !== this._currentPlaylistId) { + const currentPlaylistId = this._currentRundownId + if (currentPlaylistId) { const collection = this._core.getCollection(this._collectionName) if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) - const rundown = collection.findOne(this._currentRundownId) - if (!rundown) throw new Error(`rundown '${this._currentRundownId}' not found!`) + const rundown = collection.findOne(currentPlaylistId) + if (!rundown) throw new Error(`rundown '${currentPlaylistId}' not found!`) this._collectionData = rundown } else this._collectionData = undefined await this.notify(this._collectionData) diff --git a/packages/live-status-gateway/src/collections/rundownsHandler.ts b/packages/live-status-gateway/src/collections/rundownsHandler.ts index 3f048225b0..0ffb07422c 100644 --- a/packages/live-status-gateway/src/collections/rundownsHandler.ts +++ b/packages/live-status-gateway/src/collections/rundownsHandler.ts @@ -2,26 +2,28 @@ import { Logger } from 'winston' import { CoreHandler } from '../coreHandler' import { CollectionBase, Collection } from '../wsHandler' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' -export class RundownsHandler extends CollectionBase implements Collection { +export class RundownsHandler + extends CollectionBase + implements Collection +{ public observerName: string constructor(logger: Logger, coreHandler: CoreHandler) { - super(RundownsHandler.name, undefined, undefined, logger, coreHandler) + super(RundownsHandler.name, CollectionName.Rundowns, undefined, logger, coreHandler) this.observerName = this._name } async setRundowns(rundowns: DBRundown[]): Promise { - this._logger.info(`'${this._name}' handler received rundowns update with ${rundowns.length} rundowns`) + this.logUpdateReceived('rundowns', rundowns.length) this._collectionData = rundowns await this.notify(this._collectionData) } // override notify to implement empty array handling async notify(data: DBRundown[] | undefined): Promise { - this._logger.info( - `${this._name} notifying all observers of an update with ${this._collectionData?.length} rundowns` - ) + this.logNotifyingUpdate(this._collectionData?.length) if (data !== undefined) { for (const observer of this._observers) { await observer.update(this._name, data) diff --git a/packages/live-status-gateway/src/collections/segmentHandler.ts b/packages/live-status-gateway/src/collections/segmentHandler.ts index ede4eefddb..6345cef2b0 100644 --- a/packages/live-status-gateway/src/collections/segmentHandler.ts +++ b/packages/live-status-gateway/src/collections/segmentHandler.ts @@ -1,7 +1,6 @@ import { Logger } from 'winston' import { CoreHandler } from '../coreHandler' import { CollectionBase, Collection, CollectionObserver } from '../wsHandler' -import { CoreConnection } from '@sofie-automation/server-core-integration' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { PartInstancesHandler, SelectedPartInstances } from './partInstancesHandler' import { RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' @@ -10,24 +9,23 @@ import areElementsShallowEqual from '@sofie-automation/shared-lib/dist/lib/isSha import { SegmentsHandler } from './segmentsHandler' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PlaylistHandler } from './playlistHandler' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' export class SegmentHandler - extends CollectionBase + extends CollectionBase implements Collection, CollectionObserver, CollectionObserver { public observerName: string - private _core: CoreConnection private _currentSegmentId: SegmentId | undefined private _rundownIds: RundownId[] = [] constructor(logger: Logger, coreHandler: CoreHandler, private _segmentsHandler: SegmentsHandler) { - super(SegmentHandler.name, CollectionName.Segments, 'segments', logger, coreHandler) - this._core = coreHandler.coreConnection + super(SegmentHandler.name, CollectionName.Segments, CorelibPubSub.segments, logger, coreHandler) this.observerName = this._name } - async changed(id: string, changeType: string): Promise { - this._logger.info(`${this._name} ${changeType} ${id}`) + async changed(id: SegmentId, changeType: string): Promise { + this.logDocumentChange(id, changeType) if (!this._collectionName) return const collection = this._core.getCollection(this._collectionName) if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) @@ -45,13 +43,13 @@ export class SegmentHandler switch (source) { case PartInstancesHandler.name: { - this._logger.info(`${this._name} received update from ${source}`) + this.logUpdateReceived('partInstances', source) const partInstanceMap = data as SelectedPartInstances this._currentSegmentId = data ? partInstanceMap.current?.segmentId : undefined break } case PlaylistHandler.name: { - this._logger.info(`${this._name} received update from ${source}`) + this.logUpdateReceived('playlist', source) this._rundownIds = (data as DBRundownPlaylist | undefined)?.rundownIdsInOrder ?? [] break } @@ -71,14 +69,21 @@ export class SegmentHandler rundownId: { $in: this._rundownIds }, isHidden: { $ne: true }, }) + // this._subscriptionId = await this._coreHandler.setupSubscription( + // this._publicationName, + // this._rundownIds, + // { + // omitHidden: true, + // } + // ) // in R51 this._dbObserver = this._coreHandler.setupObserver(this._collectionName) - this._dbObserver.added = (id: string) => { + this._dbObserver.added = (id) => { void this.changed(id, 'added').catch(this._logger.error) } - this._dbObserver.changed = (id: string) => { + this._dbObserver.changed = (id) => { void this.changed(id, 'changed').catch(this._logger.error) } - this._dbObserver.removed = (id: string) => { + this._dbObserver.removed = (id) => { void this.changed(id, 'removed').catch(this._logger.error) } } diff --git a/packages/live-status-gateway/src/collections/segmentsHandler.ts b/packages/live-status-gateway/src/collections/segmentsHandler.ts index 6309cd42aa..401def1ead 100644 --- a/packages/live-status-gateway/src/collections/segmentsHandler.ts +++ b/packages/live-status-gateway/src/collections/segmentsHandler.ts @@ -3,30 +3,32 @@ import { CoreHandler } from '../coreHandler' import { CollectionBase, Collection } from '../wsHandler' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import * as _ from 'underscore' +import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' const THROTTLE_PERIOD_MS = 200 -export class SegmentsHandler extends CollectionBase implements Collection { +export class SegmentsHandler + extends CollectionBase + implements Collection +{ public observerName: string private throttledNotify: (data: DBSegment[]) => Promise constructor(logger: Logger, coreHandler: CoreHandler) { - super(SegmentsHandler.name, undefined, undefined, logger, coreHandler) + super(SegmentsHandler.name, CollectionName.Segments, undefined, logger, coreHandler) this.observerName = this._name this.throttledNotify = _.throttle(this.notify.bind(this), THROTTLE_PERIOD_MS, { leading: true, trailing: true }) } async setSegments(segments: DBSegment[]): Promise { - this._logger.info(`'${this._name}' handler received segments update with ${segments.length} segments`) + this.logUpdateReceived('segments', segments.length) this._collectionData = segments await this.throttledNotify(this._collectionData) } // override notify to implement empty array handling async notify(data: DBSegment[] | undefined): Promise { - this._logger.info( - `${this._name} notifying all observers of an update with ${this._collectionData?.length} segments` - ) + this.logNotifyingUpdate(this._collectionData?.length) if (data !== undefined) { for (const observer of this._observers) { await observer.update(this._name, data) diff --git a/packages/live-status-gateway/src/collections/showStyleBaseHandler.ts b/packages/live-status-gateway/src/collections/showStyleBaseHandler.ts index 3d6a0410c6..8a01c78273 100644 --- a/packages/live-status-gateway/src/collections/showStyleBaseHandler.ts +++ b/packages/live-status-gateway/src/collections/showStyleBaseHandler.ts @@ -1,41 +1,50 @@ import { Logger } from 'winston' import { CoreHandler } from '../coreHandler' import { CollectionBase, Collection, CollectionObserver } from '../wsHandler' -import { CoreConnection } from '@sofie-automation/server-core-integration' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' +import { DBShowStyleBase, OutputLayers, SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { IOutputLayer, ISourceLayer } from '@sofie-automation/blueprints-integration' + +export interface ShowStyleBaseExt extends DBShowStyleBase { + sourceLayerNamesById: ReadonlyMap + outputLayerNamesById: ReadonlyMap +} export class ShowStyleBaseHandler - extends CollectionBase - implements Collection, CollectionObserver + extends CollectionBase + implements Collection, CollectionObserver { public observerName: string - private _core: CoreConnection private _showStyleBaseId: ShowStyleBaseId | undefined + private _sourceLayersMap: Map = new Map() + private _outputLayersMap: Map = new Map() constructor(logger: Logger, coreHandler: CoreHandler) { - super(ShowStyleBaseHandler.name, CollectionName.ShowStyleBases, 'showStyleBases', logger, coreHandler) - this._core = coreHandler.coreConnection + super( + ShowStyleBaseHandler.name, + CollectionName.ShowStyleBases, + CorelibPubSub.showStyleBases, + logger, + coreHandler + ) this.observerName = this._name } - async changed(id: string, changeType: string): Promise { - this._logger.info(`${this._name} ${changeType} ${id}`) + async changed(id: ShowStyleBaseId, changeType: string): Promise { + this.logDocumentChange(id, changeType) if (!this._collectionName) return - const collection = this._core.getCollection(this._collectionName) - if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) if (this._showStyleBaseId) { - this._collectionData = collection.findOne(this._showStyleBaseId) + this.updateCollectionData() await this.notify(this._collectionData) } } async update(source: string, data: DBRundown | undefined): Promise { - this._logger.info( - `${this._name} received rundown update ${data?._id}, showStyleBaseId ${data?.showStyleBaseId} from ${source}` - ) + this.logUpdateReceived('rundown', source, `rundownId ${data?._id}, showStyleBaseId ${data?.showStyleBaseId}`) const prevShowStyleBaseId = this._showStyleBaseId this._showStyleBaseId = data?.showStyleBaseId @@ -49,19 +58,69 @@ export class ShowStyleBaseHandler this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, { _id: this._showStyleBaseId, }) + // this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, [ + // this._showStyleBaseId, + // ]) // In R51 this._dbObserver = this._coreHandler.setupObserver(this._collectionName) - this._dbObserver.added = (id: string) => { + this._dbObserver.added = (id) => { void this.changed(id, 'added').catch(this._logger.error) } - this._dbObserver.changed = (id: string) => { + this._dbObserver.changed = (id) => { void this.changed(id, 'changed').catch(this._logger.error) } - const collection = this._core.getCollection(this._collectionName) - if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) - this._collectionData = collection.findOne(this._showStyleBaseId) + this.updateCollectionData() await this.notify(this._collectionData) } } } + + updateCollectionData(): void { + const collection = this._core.getCollection(this._collectionName) + if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) + if (!this._showStyleBaseId) return + const showStyleBase = collection.findOne(this._showStyleBaseId) + if (!showStyleBase) { + this._collectionData = undefined + return + } + const sourceLayers: SourceLayers = showStyleBase + ? applyAndValidateOverrides(showStyleBase.sourceLayersWithOverrides).obj + : {} + const outputLayers: OutputLayers = showStyleBase + ? applyAndValidateOverrides(showStyleBase.outputLayersWithOverrides).obj + : {} + this._logger.info( + `${this._name} received showStyleBase update with sourceLayers [${Object.values( + sourceLayers + ).map( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (s) => s!.name + )}]` + ) + this._logger.info( + `${this._name} received showStyleBase update with outputLayers [${Object.values( + outputLayers + ).map( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (s) => s!.name + )}]` + ) + this._sourceLayersMap.clear() + this._outputLayersMap.clear() + for (const [layerId, sourceLayer] of Object.entries(sourceLayers)) { + if (sourceLayer === undefined || sourceLayer === null) continue + this._sourceLayersMap.set(layerId, sourceLayer.name) + } + for (const [layerId, outputLayer] of Object.entries(outputLayers)) { + if (outputLayer === undefined || outputLayer === null) continue + this._outputLayersMap.set(layerId, outputLayer.name) + } + const showStyleBaseExt = { + ...showStyleBase, + sourceLayerNamesById: this._sourceLayersMap, + outputLayerNamesById: this._outputLayersMap, + } + this._collectionData = showStyleBaseExt + } } diff --git a/packages/live-status-gateway/src/collections/studioHandler.ts b/packages/live-status-gateway/src/collections/studioHandler.ts index ad1a36fa1e..5ecd371813 100644 --- a/packages/live-status-gateway/src/collections/studioHandler.ts +++ b/packages/live-status-gateway/src/collections/studioHandler.ts @@ -1,18 +1,19 @@ import { Logger } from 'winston' -import { CoreConnection } from '@sofie-automation/server-core-integration' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { CoreHandler } from '../coreHandler' import { CollectionBase, Collection } from '../wsHandler' -import { protectString, unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { StudioId } from '@sofie-automation/server-core-integration' -export class StudioHandler extends CollectionBase implements Collection { +export class StudioHandler + extends CollectionBase + implements Collection +{ public observerName: string - private _core: CoreConnection constructor(logger: Logger, coreHandler: CoreHandler) { - super(StudioHandler.name, CollectionName.Studios, 'studios', logger, coreHandler) - this._core = coreHandler.coreConnection + super(StudioHandler.name, CollectionName.Studios, CorelibPubSub.studios, logger, coreHandler) this.observerName = this._name } @@ -22,6 +23,7 @@ export class StudioHandler extends CollectionBase implements Collectio if (!this._publicationName) return if (!this._studioId) return this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, { _id: this._studioId }) + // this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, [this._studioId]) // in R51 this._dbObserver = this._coreHandler.setupObserver(this._collectionName) if (this._collectionName) { @@ -30,21 +32,21 @@ export class StudioHandler extends CollectionBase implements Collectio const studio = col.findOne(this._studioId) if (!studio) throw new Error(`studio '${this._studioId}' not found!`) this._collectionData = studio - this._dbObserver.added = (id: string) => { + this._dbObserver.added = (id) => { void this.changed(id, 'added').catch(this._logger.error) } - this._dbObserver.changed = (id: string) => { + this._dbObserver.changed = (id) => { void this.changed(id, 'changed').catch(this._logger.error) } } } - async changed(id: string, changeType: string): Promise { - this._logger.info(`${this._name} ${changeType} ${id}`) - if (!(id === unprotectString(this._studioId) && this._collectionName)) return + async changed(id: StudioId, changeType: string): Promise { + this.logDocumentChange(id, changeType) + if (!(id === this._studioId && this._collectionName)) return const collection = this._core.getCollection(this._collectionName) if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) - const studio = collection.findOne(protectString(id)) + const studio = collection.findOne(id) if (!studio) throw new Error(`studio '${this._studioId}' not found on changed!`) this._collectionData = studio await this.notify(this._collectionData) diff --git a/packages/live-status-gateway/src/coreHandler.ts b/packages/live-status-gateway/src/coreHandler.ts index cd66b276e7..3c5130f7b5 100644 --- a/packages/live-status-gateway/src/coreHandler.ts +++ b/packages/live-status-gateway/src/coreHandler.ts @@ -3,6 +3,7 @@ import { CoreOptions, DDPConnectorOptions, Observer, + PeripheralDeviceForDevice, stringifyError, } from '@sofie-automation/server-core-integration' import { DeviceConfig } from './connector' @@ -13,12 +14,16 @@ import { PeripheralDeviceCategory, PeripheralDeviceType, } from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI' -import { protectString, unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' -import { PeripheralDeviceId, StudioId } from '@sofie-automation/shared-lib/dist/core/model/Ids' +import { protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' +import { + PeripheralDeviceCommandId, + PeripheralDeviceId, + StudioId, +} from '@sofie-automation/shared-lib/dist/core/model/Ids' import { StatusCode } from '@sofie-automation/shared-lib/dist/lib/status' import { PeripheralDeviceCommand } from '@sofie-automation/shared-lib/dist/core/model/PeripheralDeviceCommand' -import { PeripheralDeviceForDevice } from '@sofie-automation/shared-lib/dist/core/model/peripheralDevice' import { LiveStatusGatewayConfig } from './generated/options' +import { CorelibPubSubCollections } from '@sofie-automation/corelib/dist/pubsub' export interface CoreConfig { host: string @@ -41,7 +46,7 @@ export class CoreHandler { private _deviceOptions: DeviceConfig private _onConnected?: () => any - private _executedFunctions: { [id: string]: boolean } = {} + private _executedFunctions = new Set() private _coreConfig?: CoreConfig private _process?: Process @@ -97,19 +102,20 @@ export class CoreHandler { await this.updateCoreStatus() } - async setupSubscription(collection: string, ...params: any[]): Promise { - this.logger.info(`Core: Set up subscription for '${collection}'`) + async setupSubscription(collection: Key, ...params: any[]): Promise { + // tmp R50 hack + this.logger.debug(`Core: Set up subscription for '${collection}'`) const subscriptionId = await this.core.autoSubscribe(collection, ...params) - this.logger.info(`Core: Subscription for '${collection}' set up with id ${subscriptionId}`) + this.logger.debug(`Core: Subscription for '${collection}' set up with id ${subscriptionId}`) return subscriptionId } - unsubscribe(subscriptionId: string): void { - this.logger.info(`Core: Unsubscribing id '${subscriptionId}'`) + unsubscribe(subscriptionId: SubscriptionId): void { + this.logger.debug(`Core: Unsubscribing id '${subscriptionId}'`) this.core.unsubscribe(subscriptionId) } - setupObserver(collection: string): Observer { + setupObserver(collection: K): Observer { return this.core.observe(collection) } @@ -118,8 +124,8 @@ export class CoreHandler { this.logger.info('DeviceId: ' + this.core.deviceId) await Promise.all([ - this.core.autoSubscribe('peripheralDeviceForDevice', this.core.deviceId), - this.core.autoSubscribe('peripheralDeviceCommands', this.core.deviceId), + this.core.autoSubscribe(PeripheralDevicePubSub.peripheralDeviceForDevice, this.core.deviceId), + this.core.autoSubscribe(PeripheralDevicePubSub.peripheralDeviceCommands, this.core.deviceId), ]) this.logger.info('Core: Subscriptions are set up!') if (this._observers.length) { @@ -130,9 +136,9 @@ export class CoreHandler { this._observers = [] } // setup observers - const observer = this.core.observe('peripheralDeviceForDevice') - observer.added = (id: string) => this.onDeviceChanged(protectString(id)) - observer.changed = (id: string) => this.onDeviceChanged(protectString(id)) + const observer = this.core.observe(PeripheralDevicePubSubCollectionsNames.peripheralDeviceForDevice) + observer.added = (id) => this.onDeviceChanged(id) + observer.changed = (id) => this.onDeviceChanged(id) this.setupObserverForPeripheralDeviceCommands(this) } async destroy(): Promise { @@ -174,7 +180,9 @@ export class CoreHandler { onDeviceChanged(id: PeripheralDeviceId): void { if (id !== this.core.deviceId) return - const col = this.core.getCollection('peripheralDeviceForDevice') + const col = this.core.getCollection( + PeripheralDevicePubSubCollectionsNames.peripheralDeviceForDevice + ) if (!col) throw new Error('collection "peripheralDeviceForDevice" not found!') const device = col.findOne(id) @@ -214,14 +222,14 @@ export class CoreHandler { executeFunction(cmd: PeripheralDeviceCommand, fcnObject: CoreHandler): void { if (cmd) { - if (this._executedFunctions[unprotectString(cmd._id)]) return // prevent it from running multiple times + if (this._executedFunctions.has(cmd._id)) return // prevent it from running multiple times // Ignore specific commands, to reduce noise: if (cmd.functionName !== 'getDebugStates') { this.logger.debug(`Executing function "${cmd.functionName}", args: ${JSON.stringify(cmd.args)}`) } - this._executedFunctions[unprotectString(cmd._id)] = true + this._executedFunctions.add(cmd._id) const cb = (errStr: string | null, res?: any) => { if (errStr) { this.logger.error(`executeFunction error: ${errStr}`) @@ -247,16 +255,18 @@ export class CoreHandler { } } } - retireExecuteFunction(cmdId: string): void { - delete this._executedFunctions[cmdId] + retireExecuteFunction(cmdId: PeripheralDeviceCommandId): void { + this._executedFunctions.delete(cmdId) } setupObserverForPeripheralDeviceCommands(functionObject: CoreHandler): void { - const observer = functionObject.core.observe('peripheralDeviceCommands') + const observer = functionObject.core.observe(PeripheralDevicePubSubCollectionsNames.peripheralDeviceCommands) functionObject._observers.push(observer) - const addedChangedCommand = (id: string) => { - const cmds = functionObject.core.getCollection('peripheralDeviceCommands') + const addedChangedCommand = (id: PeripheralDeviceCommandId) => { + const cmds = functionObject.core.getCollection( + PeripheralDevicePubSubCollectionsNames.peripheralDeviceCommands + ) if (!cmds) throw Error('"peripheralDeviceCommands" collection not found!') - const cmd = cmds.findOne(protectString(id)) + const cmd = cmds.findOne(id) if (!cmd) throw Error('PeripheralCommand "' + id + '" not found!') // console.log('addedChangedCommand', id) if (cmd.deviceId === functionObject.core.deviceId) { @@ -265,16 +275,18 @@ export class CoreHandler { // console.log('not mine', cmd.deviceId, this.core.deviceId) } } - observer.added = (id: string) => { + observer.added = (id) => { addedChangedCommand(id) } - observer.changed = (id: string) => { + observer.changed = (id) => { addedChangedCommand(id) } - observer.removed = (id: string) => { + observer.removed = (id) => { this.retireExecuteFunction(id) } - const cmds = functionObject.core.getCollection('peripheralDeviceCommands') + const cmds = functionObject.core.getCollection( + PeripheralDevicePubSubCollectionsNames.peripheralDeviceCommands + ) if (!cmds) throw Error('"peripheralDeviceCommands" collection not found!') cmds.find({}).forEach((cmd) => { if (cmd.deviceId === functionObject.core.deviceId) { @@ -332,3 +344,64 @@ export class CoreHandler { return versions } } + +// tmp, remove in R51 ------------------------------------------------------------------ +type SubscriptionId = string +enum PeripheralDevicePubSubCollectionsNames { + // Real Mongodb collections: + peripheralDeviceCommands = 'peripheralDeviceCommands', + rundowns = 'rundowns', + expectedPlayoutItems = 'expectedPlayoutItems', + timelineDatastore = 'timelineDatastore', + + // Custom collections: + peripheralDeviceForDevice = 'peripheralDeviceForDevice', + studioMappings = 'studioMappings', + studioTimeline = 'studioTimeline', + + mountedTriggersPreviews = 'mountedTriggersPreviews', + mountedTriggers = 'mountedTriggers', + + packageManagerPlayoutContext = 'packageManagerPlayoutContext', + packageManagerPackageContainers = 'packageManagerPackageContainers', + packageManagerExpectedPackages = 'packageManagerExpectedPackages', +} +export enum PeripheralDevicePubSub { + // Common: + + /** Commands for the PeripheralDevice to execute */ + peripheralDeviceCommands = 'peripheralDeviceCommands', + /** Properties/settings of the PeripheralDevice */ + peripheralDeviceForDevice = 'peripheralDeviceForDevice', + + // Playout gateway: + + /** Playout gateway: Rundowns in the Studio of the PeripheralDevice */ + rundownsForDevice = 'rundownsForDevice', + + /** Playout gateway: Simplified timeline mappings in the Studio of the PeripheralDevice */ + mappingsForDevice = 'mappingsForDevice', + /** Playout gateway: Simplified timeline in the Studio of the PeripheralDevice */ + timelineForDevice = 'timelineForDevice', + /** Playout gateway: Timeline datastore entries in the Studio of the PeripheralDevice */ + timelineDatastoreForDevice = 'timelineDatastoreForDevice', + /** Playout gateway: ExpectedPlayoutItems in the Studio of the PeripheralDevice */ + expectedPlayoutItemsForDevice = 'expectedPlayoutItemsForDevice', + + // Input gateway: + + /** Input gateway: Calculated triggered actions */ + mountedTriggersForDevice = 'mountedTriggersForDevice', + /** Input gateway: Calculated trigger previews */ + mountedTriggersForDevicePreview = 'mountedTriggersForDevicePreview', + + // Package manager: + + /** Package manager: Info about the active playlist in the Studio of the PeripheralDevice */ + packageManagerPlayoutContext = 'packageManagerPlayoutContext', + /** Package manager: The package containers in the Studio of the PeripheralDevice */ + packageManagerPackageContainers = 'packageManagerPackageContainers', + /** Package manager: The expected packages in the Studio of the PeripheralDevice */ + packageManagerExpectedPackages = 'packageManagerExpectedPackages', +} +// ------------------------------------------------------------------------------------ diff --git a/packages/live-status-gateway/src/liveStatusServer.ts b/packages/live-status-gateway/src/liveStatusServer.ts index 6949271a7c..addc8c3934 100644 --- a/packages/live-status-gateway/src/liveStatusServer.ts +++ b/packages/live-status-gateway/src/liveStatusServer.ts @@ -11,7 +11,7 @@ import { SegmentHandler } from './collections/segmentHandler' import { PartInstancesHandler } from './collections/partInstancesHandler' import { AdLibActionsHandler } from './collections/adLibActionsHandler' import { GlobalAdLibActionsHandler } from './collections/globalAdLibActionsHandler' -import { RootChannel } from './topics/root' +import { RootChannel, StatusChannels } from './topics/root' import { StudioTopic } from './topics/studioTopic' import { ActivePlaylistTopic } from './topics/activePlaylistTopic' import { AdLibsHandler } from './collections/adLibsHandler' @@ -20,6 +20,9 @@ import { SegmentsTopic } from './topics/segmentsTopic' import { SegmentsHandler } from './collections/segmentsHandler' import { PartHandler } from './collections/partHandler' import { PartsHandler } from './collections/partsHandler' +import { PieceInstancesHandler } from './collections/pieceInstancesHandler' +import { AdLibsTopic } from './topics/adLibsTopic' +import { ActivePiecesTopic } from './topics/activePiecesTopic' export class LiveStatusServer { _logger: Logger @@ -37,12 +40,16 @@ export class LiveStatusServer { const rootChannel = new RootChannel(this._logger) const studioTopic = new StudioTopic(this._logger) + const activePiecesTopic = new ActivePiecesTopic(this._logger) const activePlaylistTopic = new ActivePlaylistTopic(this._logger) const segmentsTopic = new SegmentsTopic(this._logger) + const adLibsTopic = new AdLibsTopic(this._logger) - rootChannel.addTopic('studio', studioTopic) - rootChannel.addTopic('activePlaylist', activePlaylistTopic) - rootChannel.addTopic('segments', segmentsTopic) + rootChannel.addTopic(StatusChannels.studio, studioTopic) + rootChannel.addTopic(StatusChannels.activePlaylist, activePlaylistTopic) + rootChannel.addTopic(StatusChannels.activePieces, activePiecesTopic) + rootChannel.addTopic(StatusChannels.segments, segmentsTopic) + rootChannel.addTopic(StatusChannels.adLibs, adLibsTopic) const studioHandler = new StudioHandler(this._logger, this._coreHandler) await studioHandler.init() @@ -64,6 +71,8 @@ export class LiveStatusServer { await partHandler.init() const partInstancesHandler = new PartInstancesHandler(this._logger, this._coreHandler) await partInstancesHandler.init() + const pieceInstancesHandler = new PieceInstancesHandler(this._logger, this._coreHandler) + await pieceInstancesHandler.init() const adLibActionsHandler = new AdLibActionsHandler(this._logger, this._coreHandler) await adLibActionsHandler.init() const adLibsHandler = new AdLibsHandler(this._logger, this._coreHandler) @@ -78,6 +87,7 @@ export class LiveStatusServer { await playlistHandler.subscribe(segmentHandler) await playlistHandler.subscribe(partHandler) await playlistHandler.subscribe(partInstancesHandler) + await playlistHandler.subscribe(pieceInstancesHandler) await rundownHandler.subscribe(showStyleBaseHandler) await partInstancesHandler.subscribe(rundownHandler) await partInstancesHandler.subscribe(segmentHandler) @@ -90,19 +100,28 @@ export class LiveStatusServer { // add observers for websocket topic updates await studioHandler.subscribe(studioTopic) await playlistHandler.playlistsHandler.subscribe(studioTopic) + await playlistHandler.subscribe(activePlaylistTopic) await showStyleBaseHandler.subscribe(activePlaylistTopic) await partInstancesHandler.subscribe(activePlaylistTopic) - await adLibActionsHandler.subscribe(activePlaylistTopic) - await adLibsHandler.subscribe(activePlaylistTopic) - await globalAdLibActionsHandler.subscribe(activePlaylistTopic) - await globalAdLibsHandler.subscribe(activePlaylistTopic) await partsHandler.subscribe(activePlaylistTopic) + await pieceInstancesHandler.subscribe(activePlaylistTopic) + + await playlistHandler.subscribe(activePiecesTopic) + await showStyleBaseHandler.subscribe(activePiecesTopic) + await pieceInstancesHandler.subscribe(activePiecesTopic) await playlistHandler.subscribe(segmentsTopic) await segmentsHandler.subscribe(segmentsTopic) await partsHandler.subscribe(segmentsTopic) + await showStyleBaseHandler.subscribe(adLibsTopic) + await playlistHandler.subscribe(adLibsTopic) + await adLibActionsHandler.subscribe(adLibsTopic) + await adLibsHandler.subscribe(adLibsTopic) + await globalAdLibActionsHandler.subscribe(adLibsTopic) + await globalAdLibsHandler.subscribe(adLibsTopic) + const wss = new WebSocketServer({ port: 8080 }) wss.on('connection', (ws, request) => { this._logger.info(`WebSocket connection requested for path '${request.url}'`) diff --git a/packages/live-status-gateway/src/topics/__tests__/activePieces.spec.ts b/packages/live-status-gateway/src/topics/__tests__/activePieces.spec.ts new file mode 100644 index 0000000000..7e7f469e13 --- /dev/null +++ b/packages/live-status-gateway/src/topics/__tests__/activePieces.spec.ts @@ -0,0 +1,70 @@ +import { makeMockLogger, makeMockSubscriber, makeTestPlaylist, makeTestShowStyleBase } from './utils' +import { PlaylistHandler } from '../../collections/playlistHandler' +import { ShowStyleBaseExt, ShowStyleBaseHandler } from '../../collections/showStyleBaseHandler' +import { protectString } from '@sofie-automation/server-core-integration/dist' +import { PartialDeep } from 'type-fest' +import { literal } from '@sofie-automation/corelib/dist/lib' +import { PieceInstancesHandler, SelectedPieceInstances } from '../../collections/pieceInstancesHandler' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { ActivePiecesStatus, ActivePiecesTopic } from '../activePiecesTopic' + +describe('ActivePiecesTopic', () => { + it('provides active pieces', async () => { + const topic = new ActivePiecesTopic(makeMockLogger()) + const mockSubscriber = makeMockSubscriber() + + const currentPartInstanceId = 'CURRENT_PART_INSTANCE_ID' + + const playlist = makeTestPlaylist() + playlist.activationId = protectString('somethingRandom') + playlist.currentPartInfo = { + consumesQueuedSegmentId: false, + manuallySelected: false, + partInstanceId: protectString(currentPartInstanceId), + rundownId: playlist.rundownIdsInOrder[0], + } + await topic.update(PlaylistHandler.name, playlist) + + const testShowStyleBase = makeTestShowStyleBase() + await topic.update(ShowStyleBaseHandler.name, testShowStyleBase as ShowStyleBaseExt) + + const testPieceInstances: PartialDeep = { + currentPartInstance: [], + nextPartInstance: [], + active: [ + literal>({ + _id: protectString('PIECE_1'), + piece: { + name: 'Piece 1', + outputLayerId: 'pgm', + sourceLayerId: 'layer0', + tags: ['my_tag'], + publicData: { c: 'd' }, + }, + }), + ] as PieceInstance[], + } + await topic.update(PieceInstancesHandler.name, testPieceInstances as SelectedPieceInstances) + + topic.addSubscriber(mockSubscriber) + + const expectedStatus: PartialDeep = { + event: 'activePieces', + + activePieces: [ + { + id: 'PIECE_1', + name: 'Piece 1', + sourceLayer: 'Layer 0', + outputLayer: 'PGM', + tags: ['my_tag'], + publicData: { c: 'd' }, + }, + ], + } + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockSubscriber.send).toHaveBeenCalledTimes(1) + expect(JSON.parse(mockSubscriber.send.mock.calls[0][0] as string)).toMatchObject(expectedStatus) + }) +}) diff --git a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts index 0ecbd3c81d..91f28d6d08 100644 --- a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts @@ -1,35 +1,16 @@ import { ActivePlaylistStatus, ActivePlaylistTopic } from '../activePlaylistTopic' -import { makeMockLogger, makeMockSubscriber, makeTestPlaylist } from './utils' +import { makeMockLogger, makeMockSubscriber, makeTestPlaylist, makeTestShowStyleBase } from './utils' import { PlaylistHandler } from '../../collections/playlistHandler' -import { ShowStyleBaseHandler } from '../../collections/showStyleBaseHandler' -import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' -import { SourceLayerType } from '@sofie-automation/blueprints-integration/dist' +import { ShowStyleBaseExt, ShowStyleBaseHandler } from '../../collections/showStyleBaseHandler' import { PartInstancesHandler, SelectedPartInstances } from '../../collections/partInstancesHandler' -import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' import { protectString, unprotectString, unprotectStringArray } from '@sofie-automation/server-core-integration/dist' -import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' -import { AdLibActionsHandler } from '../../collections/adLibActionsHandler' -import { GlobalAdLibActionsHandler } from '../../collections/globalAdLibActionsHandler' import { PartialDeep } from 'type-fest' import { literal } from '@sofie-automation/corelib/dist/lib' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { PartsHandler } from '../../collections/partsHandler' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' -function makeTestShowStyleBase(): Pick { - return { - sourceLayersWithOverrides: { - defaults: { layer0: { _id: 'layer0', name: 'Layer 0', _rank: 0, type: SourceLayerType.VT } }, - overrides: [], - }, - outputLayersWithOverrides: { - defaults: { pgm: { _id: 'pgm', name: 'PGM', _rank: 0, isPGM: true } }, - overrides: [], - }, - } -} - -function makeTestPartInstanceMap(): SelectedPartInstances { +function makeEmptyTestPartInstances(): SelectedPartInstances { return { current: undefined, firstInSegmentPlayout: undefined, @@ -38,47 +19,6 @@ function makeTestPartInstanceMap(): SelectedPartInstances { } } -function makeTestAdLibActions(): AdLibAction[] { - return [ - { - _id: protectString('ACTION_0'), - actionId: 'ACTION_0', - partId: protectString('part0'), - rundownId: protectString('RUNDOWN_0'), - display: { - content: {}, - label: { key: 'An Action' }, - sourceLayerId: 'layer0', - outputLayerId: 'pgm', - tags: ['adlib_tag'], - }, - externalId: 'NCS_ACTION_0', - userData: {}, - userDataManifest: {}, - }, - ] -} - -function makeTestGlobalAdLibActions(): RundownBaselineAdLibAction[] { - return [ - { - _id: protectString('GLOBAL_ACTION_0'), - actionId: 'GLOBAL_ACTION_0', - rundownId: protectString('RUNDOWN_0'), - display: { - content: {}, - label: { key: 'A Global Action' }, - sourceLayerId: 'layer0', - outputLayerId: 'pgm', - tags: ['global_adlib_tag'], - }, - externalId: 'NCS_GLOBAL_ACTION_0', - userData: {}, - userDataManifest: {}, - }, - ] -} - describe('ActivePlaylistTopic', () => { it('notifies subscribers', async () => { const topic = new ActivePlaylistTopic(makeMockLogger()) @@ -89,54 +29,29 @@ describe('ActivePlaylistTopic', () => { await topic.update(PlaylistHandler.name, playlist) const testShowStyleBase = makeTestShowStyleBase() - await topic.update(ShowStyleBaseHandler.name, testShowStyleBase as DBShowStyleBase) + await topic.update(ShowStyleBaseHandler.name, testShowStyleBase as ShowStyleBaseExt) - const testPartInstancesMap = makeTestPartInstanceMap() + const testPartInstancesMap = makeEmptyTestPartInstances() await topic.update(PartInstancesHandler.name, testPartInstancesMap) - const testAdLibActions = makeTestAdLibActions() - await topic.update(AdLibActionsHandler.name, testAdLibActions) - - const testGlobalAdLibActions = makeTestGlobalAdLibActions() - await topic.update(GlobalAdLibActionsHandler.name, testGlobalAdLibActions) - - // TODO: AdLibPieces and Global AdLibPieces - topic.addSubscriber(mockSubscriber) const expectedStatus: ActivePlaylistStatus = { event: 'activePlaylist', name: playlist.name, id: unprotectString(playlist._id), - adLibs: [ - { - actionType: [], - id: 'ACTION_0', - name: 'An Action', - outputLayer: 'PGM', - sourceLayer: 'Layer 0', - tags: ['adlib_tag'], - }, - ], currentPart: null, nextPart: null, currentSegment: null, - globalAdLibs: [ - { - actionType: [], - id: 'GLOBAL_ACTION_0', - name: 'A Global Action', - outputLayer: 'PGM', - sourceLayer: 'Layer 0', - tags: ['global_adlib_tag'], - }, - ], rundownIds: unprotectStringArray(playlist.rundownIdsInOrder), + publicData: undefined, } // eslint-disable-next-line @typescript-eslint/unbound-method expect(mockSubscriber.send).toHaveBeenCalledTimes(1) - expect(JSON.parse(mockSubscriber.send.mock.calls[0][0] as string)).toMatchObject(expectedStatus) + expect(JSON.parse(mockSubscriber.send.mock.calls[0][0] as string)).toMatchObject( + JSON.parse(JSON.stringify(expectedStatus)) + ) }) it('provides segment and part', async () => { @@ -156,13 +71,14 @@ describe('ActivePlaylistTopic', () => { await topic.update(PlaylistHandler.name, playlist) const testShowStyleBase = makeTestShowStyleBase() - await topic.update(ShowStyleBaseHandler.name, testShowStyleBase as DBShowStyleBase) + await topic.update(ShowStyleBaseHandler.name, testShowStyleBase as ShowStyleBaseExt) const part1: Partial = { _id: protectString('PART_1'), title: 'Test Part', segmentId: protectString('SEGMENT_1'), expectedDurationWithPreroll: 10000, expectedDuration: 10000, + publicData: { b: 'c' }, } const testPartInstances: PartialDeep = { current: { @@ -189,12 +105,14 @@ describe('ActivePlaylistTopic', () => { event: 'activePlaylist', name: playlist.name, id: unprotectString(playlist._id), - adLibs: [], currentPart: { id: 'PART_1', name: 'Test Part', segmentId: 'SEGMENT_1', timing: { startTime: 1600000060000, expectedDurationMs: 10000, projectedEndTime: 1600000070000 }, + pieces: [], + autoNext: undefined, + publicData: { b: 'c' }, }, nextPart: null, currentSegment: { @@ -204,12 +122,14 @@ describe('ActivePlaylistTopic', () => { projectedEndTime: 1600000070000, }, }, - globalAdLibs: [], rundownIds: unprotectStringArray(playlist.rundownIdsInOrder), + publicData: { a: 'b' }, } // eslint-disable-next-line @typescript-eslint/unbound-method expect(mockSubscriber.send).toHaveBeenCalledTimes(1) - expect(JSON.parse(mockSubscriber.send.mock.calls[0][0] as string)).toMatchObject(expectedStatus) + expect(JSON.parse(mockSubscriber.send.mock.calls[0][0] as string)).toMatchObject( + JSON.parse(JSON.stringify(expectedStatus)) + ) }) }) diff --git a/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts b/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts new file mode 100644 index 0000000000..0ba5af4013 --- /dev/null +++ b/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts @@ -0,0 +1,107 @@ +import { protectString, unprotectString } from '@sofie-automation/server-core-integration' +import { makeMockLogger, makeMockSubscriber, makeTestPlaylist, makeTestShowStyleBase } from './utils' +import { AdLibsStatus, AdLibsTopic } from '../adLibsTopic' +import { PlaylistHandler } from '../../collections/playlistHandler' +import { ShowStyleBaseExt, ShowStyleBaseHandler } from '../../collections/showStyleBaseHandler' +import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' +import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' +import { AdLibActionsHandler } from '../../collections/adLibActionsHandler' +import { GlobalAdLibActionsHandler } from '../../collections/globalAdLibActionsHandler' + +function makeTestAdLibActions(): AdLibAction[] { + return [ + { + _id: protectString('ACTION_0'), + actionId: 'ACTION_0', + partId: protectString('part0'), + rundownId: protectString('RUNDOWN_0'), + display: { + content: {}, + label: { key: 'An Action' }, + sourceLayerId: 'layer0', + outputLayerId: 'pgm', + tags: ['adlib_tag'], + }, + externalId: 'NCS_ACTION_0', + userData: {}, + userDataManifest: {}, + publicData: { a: 'b' }, + }, + ] +} + +function makeTestGlobalAdLibActions(): RundownBaselineAdLibAction[] { + return [ + { + _id: protectString('GLOBAL_ACTION_0'), + actionId: 'GLOBAL_ACTION_0', + rundownId: protectString('RUNDOWN_0'), + display: { + content: {}, + label: { key: 'A Global Action' }, + sourceLayerId: 'layer0', + outputLayerId: 'pgm', + tags: ['global_adlib_tag'], + }, + externalId: 'NCS_GLOBAL_ACTION_0', + userData: {}, + userDataManifest: {}, + publicData: { c: 'd' }, + }, + ] +} + +describe('ActivePlaylistTopic', () => { + it('notifies subscribers', async () => { + const topic = new AdLibsTopic(makeMockLogger()) + const mockSubscriber = makeMockSubscriber() + + const playlist = makeTestPlaylist() + playlist.activationId = protectString('somethingRandom') + await topic.update(PlaylistHandler.name, playlist) + + const testShowStyleBase = makeTestShowStyleBase() + await topic.update(ShowStyleBaseHandler.name, testShowStyleBase as ShowStyleBaseExt) + + const testAdLibActions = makeTestAdLibActions() + await topic.update(AdLibActionsHandler.name, testAdLibActions) + + const testGlobalAdLibActions = makeTestGlobalAdLibActions() + await topic.update(GlobalAdLibActionsHandler.name, testGlobalAdLibActions) + + // TODO: AdLibPieces and Global AdLibPieces + + topic.addSubscriber(mockSubscriber) + + const expectedStatus: AdLibsStatus = { + event: 'adLibs', + rundownPlaylistId: unprotectString(playlist._id), + adLibs: [ + { + actionType: [], + id: 'ACTION_0', + name: 'An Action', + outputLayer: 'PGM', + sourceLayer: 'Layer 0', + tags: ['adlib_tag'], + publicData: { a: 'b' }, + }, + ], + globalAdLibs: [ + { + actionType: [], + id: 'GLOBAL_ACTION_0', + name: 'A Global Action', + outputLayer: 'PGM', + sourceLayer: 'Layer 0', + tags: ['global_adlib_tag'], + publicData: { c: 'd' }, + }, + ], + } + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockSubscriber.send).toHaveBeenCalledTimes(1) + expect(JSON.parse(mockSubscriber.send.mock.calls[0][0] as string)).toMatchObject(expectedStatus) + }) +}) diff --git a/packages/live-status-gateway/src/topics/__tests__/segmentsTopic.spec.ts b/packages/live-status-gateway/src/topics/__tests__/segmentsTopic.spec.ts index fc5851702f..05d7a1649f 100644 --- a/packages/live-status-gateway/src/topics/__tests__/segmentsTopic.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/segmentsTopic.spec.ts @@ -154,10 +154,34 @@ describe('SegmentsTopic', () => { event: 'segments', rundownPlaylistId: unprotectString(testPlaylist._id), segments: [ - { id: '1_1', rundownId: RUNDOWN_1_ID, name: 'Segment 1_1', timing: { expectedDurationMs: 0 } }, - { id: '1_2', rundownId: RUNDOWN_1_ID, name: 'Segment 1_2', timing: { expectedDurationMs: 0 } }, - { id: '2_1', rundownId: RUNDOWN_2_ID, name: 'Segment 2_1', timing: { expectedDurationMs: 0 } }, - { id: '2_2', rundownId: RUNDOWN_2_ID, name: 'Segment 2_2', timing: { expectedDurationMs: 0 } }, + { + id: '1_1', + rundownId: RUNDOWN_1_ID, + name: 'Segment 1_1', + timing: { expectedDurationMs: 0 }, + publicData: undefined, + }, + { + id: '1_2', + rundownId: RUNDOWN_1_ID, + name: 'Segment 1_2', + timing: { expectedDurationMs: 0 }, + publicData: undefined, + }, + { + id: '2_1', + rundownId: RUNDOWN_2_ID, + name: 'Segment 2_1', + timing: { expectedDurationMs: 0 }, + publicData: undefined, + }, + { + id: '2_2', + rundownId: RUNDOWN_2_ID, + name: 'Segment 2_2', + timing: { expectedDurationMs: 0 }, + publicData: undefined, + }, ], } expect(mockSubscriber.send.mock.calls).toEqual([[JSON.stringify(expectedStatus)]]) @@ -188,10 +212,34 @@ describe('SegmentsTopic', () => { event: 'segments', rundownPlaylistId: unprotectString(testPlaylist._id), segments: [ - { id: '2_1', rundownId: RUNDOWN_2_ID, name: 'Segment 2_1', timing: { expectedDurationMs: 0 } }, - { id: '2_2', rundownId: RUNDOWN_2_ID, name: 'Segment 2_2', timing: { expectedDurationMs: 0 } }, - { id: '1_1', rundownId: RUNDOWN_1_ID, name: 'Segment 1_1', timing: { expectedDurationMs: 0 } }, - { id: '1_2', rundownId: RUNDOWN_1_ID, name: 'Segment 1_2', timing: { expectedDurationMs: 0 } }, + { + id: '2_1', + rundownId: RUNDOWN_2_ID, + name: 'Segment 2_1', + timing: { expectedDurationMs: 0 }, + publicData: undefined, + }, + { + id: '2_2', + rundownId: RUNDOWN_2_ID, + name: 'Segment 2_2', + timing: { expectedDurationMs: 0 }, + publicData: undefined, + }, + { + id: '1_1', + rundownId: RUNDOWN_1_ID, + name: 'Segment 1_1', + timing: { expectedDurationMs: 0 }, + publicData: undefined, + }, + { + id: '1_2', + rundownId: RUNDOWN_1_ID, + name: 'Segment 1_2', + timing: { expectedDurationMs: 0 }, + publicData: undefined, + }, ], } expect(mockSubscriber.send.mock.calls).toEqual([[JSON.stringify(expectedStatus)]]) @@ -251,26 +299,37 @@ describe('SegmentsTopic', () => { rundownId: RUNDOWN_1_ID, name: 'Segment 1_1', timing: { expectedDurationMs: 0, budgetDurationMs: 5000 }, + publicData: undefined, }, { id: '1_2', rundownId: RUNDOWN_1_ID, name: 'Segment 1_2', timing: { expectedDurationMs: 0, budgetDurationMs: 15000 }, + publicData: undefined, + }, + { + id: '2_1', + rundownId: RUNDOWN_2_ID, + name: 'Segment 2_1', + timing: { expectedDurationMs: 0 }, + publicData: undefined, }, - { id: '2_1', rundownId: RUNDOWN_2_ID, name: 'Segment 2_1', timing: { expectedDurationMs: 0 } }, { id: '2_2', rundownId: RUNDOWN_2_ID, name: 'Segment 2_2', timing: { expectedDurationMs: 0, budgetDurationMs: 51000 }, + publicData: undefined, }, ], } // eslint-disable-next-line @typescript-eslint/unbound-method expect(mockSubscriber.send).toHaveBeenCalledTimes(1) - expect(JSON.parse(mockSubscriber.send.mock.calls[0][0] as string)).toEqual(expectedStatus) + expect(JSON.parse(mockSubscriber.send.mock.calls[0][0] as string)).toEqual( + JSON.parse(JSON.stringify(expectedStatus)) + ) }) it('exposes expectedDuration', async () => { @@ -322,10 +381,34 @@ describe('SegmentsTopic', () => { event: 'segments', rundownPlaylistId: unprotectString(testPlaylist._id), segments: [ - { id: '1_1', rundownId: RUNDOWN_1_ID, name: 'Segment 1_1', timing: { expectedDurationMs: 5000 } }, - { id: '1_2', rundownId: RUNDOWN_1_ID, name: 'Segment 1_2', timing: { expectedDurationMs: 15000 } }, - { id: '2_1', rundownId: RUNDOWN_2_ID, name: 'Segment 2_1', timing: { expectedDurationMs: 0 } }, - { id: '2_2', rundownId: RUNDOWN_2_ID, name: 'Segment 2_2', timing: { expectedDurationMs: 51000 } }, + { + id: '1_1', + rundownId: RUNDOWN_1_ID, + name: 'Segment 1_1', + timing: { expectedDurationMs: 5000 }, + publicData: undefined, + }, + { + id: '1_2', + rundownId: RUNDOWN_1_ID, + name: 'Segment 1_2', + timing: { expectedDurationMs: 15000 }, + publicData: undefined, + }, + { + id: '2_1', + rundownId: RUNDOWN_2_ID, + name: 'Segment 2_1', + timing: { expectedDurationMs: 0 }, + publicData: undefined, + }, + { + id: '2_2', + rundownId: RUNDOWN_2_ID, + name: 'Segment 2_2', + timing: { expectedDurationMs: 51000 }, + publicData: undefined, + }, ], } expect(mockSubscriber.send.mock.calls).toEqual([[JSON.stringify(expectedStatus)]]) @@ -358,6 +441,8 @@ describe('SegmentsTopic', () => { { id: '1_2', rundownId: RUNDOWN_1_ID, name: 'Segment 1_2', identifier: 'SomeIdentifier' }, ], } as SegmentsStatus - expect(JSON.parse(mockSubscriber.send.mock.calls[0][0] as string)).toMatchObject(expectedStatus) + expect(JSON.parse(mockSubscriber.send.mock.calls[0][0] as string)).toMatchObject( + JSON.parse(JSON.stringify(expectedStatus)) + ) }) }) diff --git a/packages/live-status-gateway/src/topics/__tests__/utils.ts b/packages/live-status-gateway/src/topics/__tests__/utils.ts index c091820a31..730b5fac52 100644 --- a/packages/live-status-gateway/src/topics/__tests__/utils.ts +++ b/packages/live-status-gateway/src/topics/__tests__/utils.ts @@ -1,7 +1,9 @@ import { PlaylistTimingType } from '@sofie-automation/blueprints-integration/dist/documents/playlistTiming' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' +// eslint-disable-next-line node/no-extraneous-import import { mock, MockProxy } from 'jest-mock-extended' +import { ShowStyleBaseExt } from '../../collections/showStyleBaseHandler' import { Logger } from 'winston' import { WebSocket } from 'ws' @@ -30,5 +32,13 @@ export function makeTestPlaylist(id?: string): DBRundownPlaylist { rundownIdsInOrder: [protectString(RUNDOWN_1_ID), protectString(RUNDOWN_2_ID)], studioId: protectString('STUDIO_1'), timing: { type: PlaylistTimingType.None }, + publicData: { a: 'b' }, + } +} + +export function makeTestShowStyleBase(): Pick { + return { + sourceLayerNamesById: new Map([['layer0', 'Layer 0']]), + outputLayerNamesById: new Map([['pgm', 'PGM']]), } } diff --git a/packages/live-status-gateway/src/topics/activePiecesTopic.ts b/packages/live-status-gateway/src/topics/activePiecesTopic.ts new file mode 100644 index 0000000000..e90d8e71af --- /dev/null +++ b/packages/live-status-gateway/src/topics/activePiecesTopic.ts @@ -0,0 +1,117 @@ +import { Logger } from 'winston' +import { WebSocket } from 'ws' +import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { literal } from '@sofie-automation/shared-lib/dist/lib/lib' +import { WebSocketTopicBase, WebSocketTopic, CollectionObserver } from '../wsHandler' +import { PlaylistHandler } from '../collections/playlistHandler' +import { ShowStyleBaseExt, ShowStyleBaseHandler } from '../collections/showStyleBaseHandler' +import _ = require('underscore') +import { SelectedPieceInstances, PieceInstancesHandler, PieceInstanceMin } from '../collections/pieceInstancesHandler' +import { PieceStatus, toPieceStatus } from './helpers/pieceStatus' +import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' + +const THROTTLE_PERIOD_MS = 100 + +export interface ActivePiecesStatus { + event: 'activePieces' + rundownPlaylistId: string | null + activePieces: PieceStatus[] +} + +export class ActivePiecesTopic + extends WebSocketTopicBase + implements + WebSocketTopic, + CollectionObserver, + CollectionObserver, + CollectionObserver +{ + public observerName = ActivePiecesTopic.name + private _activePlaylistId: RundownPlaylistId | undefined + private _activePieceInstances: PieceInstanceMin[] | undefined + private _showStyleBaseExt: ShowStyleBaseExt | undefined + private throttledSendStatusToAll: () => void + + constructor(logger: Logger) { + super(ActivePiecesTopic.name, logger) + this.throttledSendStatusToAll = _.throttle(this.sendStatusToAll.bind(this), THROTTLE_PERIOD_MS, { + leading: false, + trailing: true, + }) + } + + addSubscriber(ws: WebSocket): void { + super.addSubscriber(ws) + this.sendStatus([ws]) + } + + sendStatus(subscribers: Iterable): void { + const message = this._activePlaylistId + ? literal({ + event: 'activePieces', + rundownPlaylistId: unprotectString(this._activePlaylistId), + activePieces: + this._activePieceInstances?.map((piece) => toPieceStatus(piece, this._showStyleBaseExt)) ?? [], + }) + : literal({ + event: 'activePieces', + rundownPlaylistId: null, + activePieces: [], + }) + + for (const subscriber of subscribers) { + this.sendMessage(subscriber, message) + } + } + + async update( + source: string, + data: DBRundownPlaylist | ShowStyleBaseExt | SelectedPieceInstances | undefined + ): Promise { + let hasAnythingChanged = false + switch (source) { + case PlaylistHandler.name: { + const rundownPlaylist = data ? (data as DBRundownPlaylist) : undefined + this._logger.info( + `${this._name} received playlist update ${rundownPlaylist?._id}, activationId ${rundownPlaylist?.activationId}` + ) + const previousActivePlaylistId = this._activePlaylistId + this._activePlaylistId = unprotectString(rundownPlaylist?.activationId) + ? rundownPlaylist?._id + : undefined + + if (previousActivePlaylistId !== this._activePlaylistId) { + hasAnythingChanged = true + } + break + } + case ShowStyleBaseHandler.name: { + const showStyleBaseExt = data ? (data as ShowStyleBaseExt) : undefined + this._logger.info(`${this._name} received showStyleBase update from ${source}`) + this._showStyleBaseExt = showStyleBaseExt + hasAnythingChanged = true + break + } + case PieceInstancesHandler.name: { + const pieceInstances = data as SelectedPieceInstances + this._logger.info(`${this._name} received pieceInstances update from ${source}`) + if (pieceInstances.active !== this._activePieceInstances) { + hasAnythingChanged = true + } + this._activePieceInstances = pieceInstances.active + break + } + default: + throw new Error(`${this._name} received unsupported update from ${source}}`) + } + + if (hasAnythingChanged) { + this.throttledSendStatusToAll() + } + } + + private sendStatusToAll() { + this.sendStatus(this._subscribers) + } +} diff --git a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts index 28ecce1256..2bb60c118c 100644 --- a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts +++ b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts @@ -2,41 +2,29 @@ import { Logger } from 'winston' import { WebSocket } from 'ws' import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { DBShowStyleBase, OutputLayers, SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' -import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' -import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' -import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' -import { - IBlueprintActionManifestDisplayContent, - IOutputLayer, - ISourceLayer, -} from '@sofie-automation/blueprints-integration' import { literal } from '@sofie-automation/shared-lib/dist/lib/lib' import { WebSocketTopicBase, WebSocketTopic, CollectionObserver } from '../wsHandler' import { SelectedPartInstances, PartInstancesHandler } from '../collections/partInstancesHandler' -import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' -import { interpollateTranslation } from '@sofie-automation/corelib/dist/TranslatableMessage' -import { AdLibsHandler } from '../collections/adLibsHandler' -import { GlobalAdLibsHandler } from '../collections/globalAdLibsHandler' import { PlaylistHandler } from '../collections/playlistHandler' -import { ShowStyleBaseHandler } from '../collections/showStyleBaseHandler' -import { AdLibActionsHandler } from '../collections/adLibActionsHandler' -import { GlobalAdLibActionsHandler } from '../collections/globalAdLibActionsHandler' +import { ShowStyleBaseExt, ShowStyleBaseHandler } from '../collections/showStyleBaseHandler' import { CurrentSegmentTiming, calculateCurrentSegmentTiming } from './helpers/segmentTiming' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { PartsHandler } from '../collections/partsHandler' import _ = require('underscore') import { PartTiming, calculateCurrentPartTiming } from './helpers/partTiming' +import { SelectedPieceInstances, PieceInstancesHandler, PieceInstanceMin } from '../collections/pieceInstancesHandler' +import { PieceStatus, toPieceStatus } from './helpers/pieceStatus' -const THROTTLE_PERIOD_MS = 20 +const THROTTLE_PERIOD_MS = 100 interface PartStatus { id: string segmentId: string name: string - autoNext?: boolean + autoNext: boolean | undefined + pieces: PieceStatus[] + publicData: unknown } interface CurrentPartStatus extends PartStatus { @@ -48,20 +36,6 @@ interface CurrentSegmentStatus { timing: CurrentSegmentTiming } -interface AdLibActionType { - name: string - label: string -} - -interface AdLibStatus { - id: string - name: string - sourceLayer: string - outputLayer: string - actionType: AdLibActionType[] - tags?: string[] -} - export interface ActivePlaylistStatus { event: string id: string | null @@ -70,8 +44,7 @@ export interface ActivePlaylistStatus { currentPart: CurrentPartStatus | null currentSegment: CurrentSegmentStatus | null nextPart: PartStatus | null - adLibs: AdLibStatus[] - globalAdLibs: AdLibStatus[] + publicData: unknown } export class ActivePlaylistTopic @@ -79,24 +52,21 @@ export class ActivePlaylistTopic implements WebSocketTopic, CollectionObserver, + CollectionObserver, CollectionObserver, - CollectionObserver, - CollectionObserver, - CollectionObserver + CollectionObserver, + CollectionObserver { public observerName = ActivePlaylistTopic.name - private _sourceLayersMap: Map = new Map() - private _outputLayersMap: Map = new Map() private _activePlaylist: DBRundownPlaylist | undefined private _currentPartInstance: DBPartInstance | undefined private _nextPartInstance: DBPartInstance | undefined private _firstInstanceInSegmentPlayout: DBPartInstance | undefined private _partInstancesInCurrentSegment: DBPartInstance[] = [] - private _adLibActions: AdLibAction[] | undefined - private _abLibs: AdLibPiece[] | undefined - private _globalAdLibActions: RundownBaselineAdLibAction[] | undefined - private _globalAdLibs: RundownBaselineAdLibItem[] | undefined private _partsBySegmentId: Record = {} + private _pieceInstancesInCurrentPartInstance: PieceInstanceMin[] | undefined + private _pieceInstancesInNextPartInstance: PieceInstanceMin[] | undefined + private _showStyleBaseExt: ShowStyleBaseExt | undefined private throttledSendStatusToAll: () => void constructor(logger: Logger) { @@ -113,110 +83,13 @@ export class ActivePlaylistTopic } sendStatus(subscribers: Iterable): void { - if ( - this._currentPartInstance?._id !== this._activePlaylist?.currentPartInfo?.partInstanceId || - this._nextPartInstance?._id !== this._activePlaylist?.nextPartInfo?.partInstanceId - ) { + if (this.isDataInconsistent()) { // data is inconsistent, let's wait return } const currentPart = this._currentPartInstance ? this._currentPartInstance.part : null const nextPart = this._nextPartInstance ? this._nextPartInstance.part : null - const adLibs: AdLibStatus[] = [] - const globalAdLibs: AdLibStatus[] = [] - - if (this._adLibActions) { - adLibs.push( - ...this._adLibActions.map((action) => { - const sourceLayerName = this._sourceLayersMap.get( - (action.display as IBlueprintActionManifestDisplayContent).sourceLayerId - ) - const outputLayerName = this._outputLayersMap.get( - (action.display as IBlueprintActionManifestDisplayContent).outputLayerId - ) - const triggerModes = action.triggerModes - ? action.triggerModes.map((t) => - literal({ - name: t.data, - label: interpollateTranslation(t.display.label.key, t.display.label.args), - }) - ) - : [] - return literal({ - id: unprotectString(action._id), - name: interpollateTranslation(action.display.label.key, action.display.label.args), - sourceLayer: sourceLayerName ?? 'invalid', - outputLayer: outputLayerName ?? 'invalid', - actionType: triggerModes, - tags: action.display.tags, - }) - }) - ) - } - - if (this._abLibs) { - adLibs.push( - ...this._abLibs.map((adLib) => { - const sourceLayerName = this._sourceLayersMap.get(adLib.sourceLayerId) - const outputLayerName = this._outputLayersMap.get(adLib.outputLayerId) - return literal({ - id: unprotectString(adLib._id), - name: adLib.name, - sourceLayer: sourceLayerName ?? 'invalid', - outputLayer: outputLayerName ?? 'invalid', - actionType: [], - tags: adLib.tags, - }) - }) - ) - } - - if (this._globalAdLibActions) { - globalAdLibs.push( - ...this._globalAdLibActions.map((action) => { - const sourceLayerName = this._sourceLayersMap.get( - (action.display as IBlueprintActionManifestDisplayContent).sourceLayerId - ) - const outputLayerName = this._outputLayersMap.get( - (action.display as IBlueprintActionManifestDisplayContent).outputLayerId - ) - const triggerModes = action.triggerModes - ? action.triggerModes.map((t) => - literal({ - name: t.data, - label: interpollateTranslation(t.display.label.key, t.display.label.args), - }) - ) - : [] - return literal({ - id: unprotectString(action._id), - name: interpollateTranslation(action.display.label.key, action.display.label.args), - sourceLayer: sourceLayerName ?? 'invalid', - outputLayer: outputLayerName ?? 'invalid', - actionType: triggerModes, - tags: action.display.tags, - }) - }) - ) - } - - if (this._globalAdLibs) { - globalAdLibs.push( - ...this._globalAdLibs.map((adLib) => { - const sourceLayerName = this._sourceLayersMap.get(adLib.sourceLayerId) - const outputLayerName = this._outputLayersMap.get(adLib.outputLayerId) - return literal({ - id: unprotectString(adLib._id), - name: adLib.name, - sourceLayer: sourceLayerName ?? 'invalid', - outputLayer: outputLayerName ?? 'invalid', - actionType: [], - tags: adLib.tags, - }) - }) - ) - } const message = this._activePlaylist ? literal({ @@ -235,6 +108,11 @@ export class ActivePlaylistTopic this._currentPartInstance, this._partInstancesInCurrentSegment ), + pieces: + this._pieceInstancesInCurrentPartInstance?.map((piece) => + toPieceStatus(piece, this._showStyleBaseExt) + ) ?? [], + publicData: null, // currentPart.publicData, }) : null, currentSegment: @@ -255,10 +133,14 @@ export class ActivePlaylistTopic name: nextPart.title, autoNext: nextPart.autoNext, segmentId: unprotectString(nextPart.segmentId), + pieces: + this._pieceInstancesInNextPartInstance?.map((piece) => + toPieceStatus(piece, this._showStyleBaseExt) + ) ?? [], + publicData: null, // nextPart.publicData, tmp, introduced in R51 }) : null, - adLibs, - globalAdLibs, + publicData: null, // this._activePlaylist.publicData, tmp, introduced in R51 }) : literal({ event: 'activePlaylist', @@ -268,8 +150,7 @@ export class ActivePlaylistTopic currentPart: null, currentSegment: null, nextPart: null, - adLibs: [], - globalAdLibs: [], + publicData: undefined, }) for (const subscriber of subscribers) { @@ -277,108 +158,87 @@ export class ActivePlaylistTopic } } + private isDataInconsistent() { + return ( + this._currentPartInstance?._id !== this._activePlaylist?.currentPartInfo?.partInstanceId || + this._nextPartInstance?._id !== this._activePlaylist?.nextPartInfo?.partInstanceId || + (this._pieceInstancesInCurrentPartInstance?.[0] && + this._pieceInstancesInCurrentPartInstance?.[0].partInstanceId !== this._currentPartInstance?._id) || + (this._pieceInstancesInNextPartInstance?.[0] && + this._pieceInstancesInNextPartInstance?.[0].partInstanceId !== this._nextPartInstance?._id) + ) + } + async update( source: string, data: | DBRundownPlaylist - | DBShowStyleBase + | ShowStyleBaseExt | SelectedPartInstances - | AdLibAction[] - | RundownBaselineAdLibAction[] - | AdLibPiece[] - | RundownBaselineAdLibItem[] | DBPart[] + | SelectedPieceInstances | undefined ): Promise { + let hasAnythingChanged = false switch (source) { case PlaylistHandler.name: { const rundownPlaylist = data ? (data as DBRundownPlaylist) : undefined - this._logger.info( - `${this._name} received playlist update ${rundownPlaylist?._id}, activationId ${rundownPlaylist?.activationId}` + this.logUpdateReceived( + 'playlist', + source, + `rundownPlaylistId ${rundownPlaylist?._id}, activationId ${rundownPlaylist?.activationId}` ) this._activePlaylist = unprotectString(rundownPlaylist?.activationId) ? rundownPlaylist : undefined + hasAnythingChanged = true break } case ShowStyleBaseHandler.name: { - const sourceLayers: SourceLayers = data - ? applyAndValidateOverrides((data as DBShowStyleBase).sourceLayersWithOverrides).obj - : {} - const outputLayers: OutputLayers = data - ? applyAndValidateOverrides((data as DBShowStyleBase).outputLayersWithOverrides).obj - : {} - this._logger.info( - `${this._name} received showStyleBase update with sourceLayers [${Object.values< - ISourceLayer | undefined - >(sourceLayers).map( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (s) => s!.name - )}]` - ) - this._logger.info( - `${this._name} received showStyleBase update with outputLayers [${Object.values< - IOutputLayer | undefined - >(outputLayers).map( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (s) => s!.name - )}]` - ) - this._sourceLayersMap.clear() - this._outputLayersMap.clear() - for (const [layerId, sourceLayer] of Object.entries(sourceLayers)) { - if (sourceLayer === undefined || sourceLayer === null) continue - this._sourceLayersMap.set(layerId, sourceLayer.name) - } - for (const [layerId, outputLayer] of Object.entries(outputLayers)) { - if (outputLayer === undefined || outputLayer === null) continue - this._outputLayersMap.set(layerId, outputLayer.name) - } + const showStyleBaseExt = data ? (data as ShowStyleBaseExt) : undefined + this.logUpdateReceived('showStyleBase', source) + this._showStyleBaseExt = showStyleBaseExt + hasAnythingChanged = true break } case PartInstancesHandler.name: { const partInstances = data as SelectedPartInstances - this._logger.info( - `${this._name} received partInstances update from ${source} with ${partInstances.inCurrentSegment.length} instances in segment` + this.logUpdateReceived( + 'partInstances', + source, + `${partInstances.inCurrentSegment.length} instances in segment` ) this._currentPartInstance = partInstances.current this._nextPartInstance = partInstances.next this._firstInstanceInSegmentPlayout = partInstances.firstInSegmentPlayout this._partInstancesInCurrentSegment = partInstances.inCurrentSegment - break - } - case AdLibActionsHandler.name: { - const adLibActions = data ? (data as AdLibAction[]) : [] - this._logger.info(`${this._name} received adLibActions update from ${source}`) - this._adLibActions = adLibActions - break - } - case GlobalAdLibActionsHandler.name: { - const globalAdLibActions = data ? (data as RundownBaselineAdLibAction[]) : [] - this._logger.info(`${this._name} received globalAdLibActions update from ${source}`) - this._globalAdLibActions = globalAdLibActions - break - } - case AdLibsHandler.name: { - const adLibs = data ? (data as AdLibPiece[]) : [] - this._logger.info(`${this._name} received adLibs update from ${source}`) - this._abLibs = adLibs - break - } - case GlobalAdLibsHandler.name: { - const globalAdLibs = data ? (data as RundownBaselineAdLibItem[]) : [] - this._logger.info(`${this._name} received globalAdLibs update from ${source}`) - this._globalAdLibs = globalAdLibs + hasAnythingChanged = true break } case PartsHandler.name: { this._partsBySegmentId = _.groupBy(data as DBPart[], 'segmentId') - this._logger.info(`${this._name} received parts update from ${source}`) + this.logUpdateReceived('parts', source) + hasAnythingChanged = true // TODO: can this be smarter? + break + } + case PieceInstancesHandler.name: { + const pieceInstances = data as SelectedPieceInstances + this.logUpdateReceived('pieceInstances', source) + if ( + pieceInstances.currentPartInstance !== this._pieceInstancesInCurrentPartInstance || + pieceInstances.nextPartInstance !== this._pieceInstancesInNextPartInstance + ) { + hasAnythingChanged = true + } + this._pieceInstancesInCurrentPartInstance = pieceInstances.currentPartInstance + this._pieceInstancesInNextPartInstance = pieceInstances.nextPartInstance break } default: throw new Error(`${this._name} received unsupported update from ${source}}`) } - this.throttledSendStatusToAll() + if (hasAnythingChanged) { + this.throttledSendStatusToAll() + } } private sendStatusToAll() { diff --git a/packages/live-status-gateway/src/topics/adLibsTopic.ts b/packages/live-status-gateway/src/topics/adLibsTopic.ts new file mode 100644 index 0000000000..b6cebbcc30 --- /dev/null +++ b/packages/live-status-gateway/src/topics/adLibsTopic.ts @@ -0,0 +1,251 @@ +import { Logger } from 'winston' +import { WebSocket } from 'ws' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { WebSocketTopicBase, WebSocketTopic, CollectionObserver } from '../wsHandler' +import { PlaylistHandler } from '../collections/playlistHandler' +import { literal } from '@sofie-automation/corelib/dist/lib' +import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' +import _ = require('underscore') +import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' +import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' +import { AdLibActionsHandler } from '../collections/adLibActionsHandler' +import { GlobalAdLibActionsHandler } from '../collections/globalAdLibActionsHandler' +import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' +import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' +import { IBlueprintActionManifestDisplayContent } from '@sofie-automation/blueprints-integration' +import { ShowStyleBaseExt, ShowStyleBaseHandler } from '../collections/showStyleBaseHandler' +import { interpollateTranslation } from '@sofie-automation/corelib/dist/TranslatableMessage' +import { AdLibsHandler } from '../collections/adLibsHandler' +import { GlobalAdLibsHandler } from '../collections/globalAdLibsHandler' + +const THROTTLE_PERIOD_MS = 100 + +export interface AdLibsStatus { + event: 'adLibs' + rundownPlaylistId: string | null + adLibs: AdLibStatus[] + globalAdLibs: AdLibStatus[] +} + +interface AdLibActionType { + name: string + label: string +} + +interface AdLibStatus { + id: string + name: string + sourceLayer: string + outputLayer: string + actionType: AdLibActionType[] + tags?: string[] + publicData: unknown +} + +export class AdLibsTopic + extends WebSocketTopicBase + implements + WebSocketTopic, + CollectionObserver, + CollectionObserver, + CollectionObserver, + CollectionObserver +{ + public observerName = AdLibsTopic.name + private _activePlaylist: DBRundownPlaylist | undefined + private _sourceLayersMap: ReadonlyMap = new Map() + private _outputLayersMap: ReadonlyMap = new Map() + private _adLibActions: AdLibAction[] | undefined + private _abLibs: AdLibPiece[] | undefined + private _globalAdLibActions: RundownBaselineAdLibAction[] | undefined + private _globalAdLibs: RundownBaselineAdLibItem[] | undefined + private throttledSendStatusToAll: () => void + + constructor(logger: Logger) { + super(AdLibsTopic.name, logger) + this.throttledSendStatusToAll = _.throttle(this.sendStatusToAll.bind(this), THROTTLE_PERIOD_MS, { + leading: true, + trailing: true, + }) + } + + addSubscriber(ws: WebSocket): void { + super.addSubscriber(ws) + this.sendStatus([ws]) + } + + sendStatus(subscribers: Iterable): void { + const adLibs: AdLibStatus[] = [] + const globalAdLibs: AdLibStatus[] = [] + + if (this._adLibActions) { + adLibs.push( + ...this._adLibActions.map((action) => { + const sourceLayerName = this._sourceLayersMap.get( + (action.display as IBlueprintActionManifestDisplayContent).sourceLayerId + ) + const outputLayerName = this._outputLayersMap.get( + (action.display as IBlueprintActionManifestDisplayContent).outputLayerId + ) + const triggerModes = action.triggerModes + ? action.triggerModes.map((t) => + literal({ + name: t.data, + label: interpollateTranslation(t.display.label.key, t.display.label.args), + }) + ) + : [] + return literal({ + id: unprotectString(action._id), + name: interpollateTranslation(action.display.label.key, action.display.label.args), + sourceLayer: sourceLayerName ?? 'invalid', + outputLayer: outputLayerName ?? 'invalid', + actionType: triggerModes, + tags: action.display.tags, + publicData: null, // action.publicData, tmp, introduced in R51 + }) + }) + ) + } + + if (this._abLibs) { + adLibs.push( + ...this._abLibs.map((adLib) => { + const sourceLayerName = this._sourceLayersMap.get(adLib.sourceLayerId) + const outputLayerName = this._outputLayersMap.get(adLib.outputLayerId) + return literal({ + id: unprotectString(adLib._id), + name: adLib.name, + sourceLayer: sourceLayerName ?? 'invalid', + outputLayer: outputLayerName ?? 'invalid', + actionType: [], + tags: adLib.tags, + publicData: null, // adLib.publicData, tmp, introduced in R51 + }) + }) + ) + } + + if (this._globalAdLibActions) { + globalAdLibs.push( + ...this._globalAdLibActions.map((action) => { + const sourceLayerName = this._sourceLayersMap.get( + (action.display as IBlueprintActionManifestDisplayContent).sourceLayerId + ) + const outputLayerName = this._outputLayersMap.get( + (action.display as IBlueprintActionManifestDisplayContent).outputLayerId + ) + const triggerModes = action.triggerModes + ? action.triggerModes.map((t) => + literal({ + name: t.data, + label: interpollateTranslation(t.display.label.key, t.display.label.args), + }) + ) + : [] + return literal({ + id: unprotectString(action._id), + name: interpollateTranslation(action.display.label.key, action.display.label.args), + sourceLayer: sourceLayerName ?? 'invalid', + outputLayer: outputLayerName ?? 'invalid', + actionType: triggerModes, + tags: action.display.tags, + publicData: null, // action.publicData, tmp, introduced in R51 + }) + }) + ) + } + + if (this._globalAdLibs) { + globalAdLibs.push( + ...this._globalAdLibs.map((adLib) => { + const sourceLayerName = this._sourceLayersMap.get(adLib.sourceLayerId) + const outputLayerName = this._outputLayersMap.get(adLib.outputLayerId) + return literal({ + id: unprotectString(adLib._id), + name: adLib.name, + sourceLayer: sourceLayerName ?? 'invalid', + outputLayer: outputLayerName ?? 'invalid', + actionType: [], + tags: adLib.tags, + publicData: null, // adLib.publicData, tmp, introduced in R51 + }) + }) + ) + } + + const adLibsStatus: AdLibsStatus = this._activePlaylist + ? { + event: 'adLibs', + rundownPlaylistId: unprotectString(this._activePlaylist._id), + adLibs, + globalAdLibs, + } + : { event: 'adLibs', rundownPlaylistId: null, adLibs: [], globalAdLibs: [] } + + for (const subscriber of subscribers) { + this.sendMessage(subscriber, adLibsStatus) + } + } + + async update( + source: string, + data: + | DBRundownPlaylist + | ShowStyleBaseExt + | AdLibAction[] + | RundownBaselineAdLibAction[] + | AdLibPiece[] + | RundownBaselineAdLibItem[] + | undefined + ): Promise { + switch (source) { + case PlaylistHandler.name: { + const previousPlaylistId = this._activePlaylist?._id + this._activePlaylist = data as DBRundownPlaylist | undefined + this.logUpdateReceived('playlist', source) + if (previousPlaylistId === this._activePlaylist?._id) return + break + } + case AdLibActionsHandler.name: { + const adLibActions = data ? (data as AdLibAction[]) : [] + this.logUpdateReceived('adLibActions', source) + this._adLibActions = adLibActions + break + } + case GlobalAdLibActionsHandler.name: { + const globalAdLibActions = data ? (data as RundownBaselineAdLibAction[]) : [] + this.logUpdateReceived('globalAdLibActions', source) + this._globalAdLibActions = globalAdLibActions + break + } + case AdLibsHandler.name: { + const adLibs = data ? (data as AdLibPiece[]) : [] + this.logUpdateReceived('adLibs', source) + this._abLibs = adLibs + break + } + case GlobalAdLibsHandler.name: { + const globalAdLibs = data ? (data as RundownBaselineAdLibItem[]) : [] + this.logUpdateReceived('globalAdLibs', source) + this._globalAdLibs = globalAdLibs + break + } + case ShowStyleBaseHandler.name: { + const showStyleBaseExt = data ? (data as ShowStyleBaseExt) : undefined + this.logUpdateReceived('showStyleBase', source) + this._sourceLayersMap = showStyleBaseExt?.sourceLayerNamesById ?? new Map() + this._outputLayersMap = showStyleBaseExt?.outputLayerNamesById ?? new Map() + break + } + default: + throw new Error(`${this._name} received unsupported update from ${source}}`) + } + + this.throttledSendStatusToAll() + } + + private sendStatusToAll() { + this.sendStatus(this._subscribers) + } +} diff --git a/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts b/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts new file mode 100644 index 0000000000..c3e7086adf --- /dev/null +++ b/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts @@ -0,0 +1,28 @@ +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { unprotectString } from '@sofie-automation/server-core-integration' +import { ShowStyleBaseExt } from '../../collections/showStyleBaseHandler' + +export interface PieceStatus { + id: string + name: string + sourceLayer: string + outputLayer: string + tags: string[] | undefined + publicData: unknown +} + +export function toPieceStatus( + pieceInstance: PieceInstance, + showStyleBaseExt: ShowStyleBaseExt | undefined +): PieceStatus { + const sourceLayerName = showStyleBaseExt?.sourceLayerNamesById.get(pieceInstance.piece.sourceLayerId) + const outputLayerName = showStyleBaseExt?.outputLayerNamesById.get(pieceInstance.piece.outputLayerId) + return { + id: unprotectString(pieceInstance._id), + name: pieceInstance.piece.name, + sourceLayer: sourceLayerName ?? 'invalid', + outputLayer: outputLayerName ?? 'invalid', + tags: pieceInstance.piece.tags, + publicData: null, // pieceInstance.piece.publicData, tmp, introduced in R51 + } +} diff --git a/packages/live-status-gateway/src/topics/root.ts b/packages/live-status-gateway/src/topics/root.ts index 42e6c70ad9..444ec7fe23 100644 --- a/packages/live-status-gateway/src/topics/root.ts +++ b/packages/live-status-gateway/src/topics/root.ts @@ -34,7 +34,9 @@ interface SubscriptionResponse { export enum StatusChannels { studio = 'studio', activePlaylist = 'activePlaylist', + activePieces = 'activePieces', segments = 'segments', + adLibs = 'adLibs', } interface RootMsg { @@ -51,10 +53,7 @@ export class RootChannel extends WebSocketTopicBase implements WebSocketTopic { constructor(logger: Logger) { super('Root', logger) - this._heartbeat = setInterval( - () => this._subscribers.forEach((ws) => this.sendMessage(ws, { event: 'heartbeat' })), - 2000 - ) + this._heartbeat = setInterval(() => this._subscribers.forEach((ws) => this.sendHeartbeat(ws)), 2000) } close(): void { diff --git a/packages/live-status-gateway/src/topics/segmentsTopic.ts b/packages/live-status-gateway/src/topics/segmentsTopic.ts index a037f15ec0..b55d2660d9 100644 --- a/packages/live-status-gateway/src/topics/segmentsTopic.ts +++ b/packages/live-status-gateway/src/topics/segmentsTopic.ts @@ -21,6 +21,7 @@ interface SegmentStatus { rundownId: string name: string timing: SegmentTiming + publicData: unknown } export interface SegmentsStatus { @@ -69,6 +70,7 @@ export class SegmentsTopic name: segment.name, timing: calculateSegmentTiming(this._partsBySegment[segmentId] ?? []), identifier: segment.identifier, + publicData: null, // segment.publicData, tmp, introduced in R51 } }), } @@ -86,17 +88,17 @@ export class SegmentsTopic switch (source) { case PlaylistHandler.name: { this._activePlaylist = data as DBRundownPlaylist | undefined - this._logger.info(`${this._name} received playlist update from ${source}`) + this.logUpdateReceived('playlist', source) break } case SegmentsHandler.name: { this._segments = data as DBSegment[] - this._logger.info(`${this._name} received segments update from ${source}`) + this.logUpdateReceived('segments', source) break } case PartsHandler.name: { this._partsBySegment = _.groupBy(data as DBPart[], 'segmentId') - this._logger.info(`${this._name} received parts update from ${source}`) + this.logUpdateReceived('parts', source) break } default: diff --git a/packages/live-status-gateway/src/topics/studioTopic.ts b/packages/live-status-gateway/src/topics/studioTopic.ts index 5b1ab82c9d..4998a1afd1 100644 --- a/packages/live-status-gateway/src/topics/studioTopic.ts +++ b/packages/live-status-gateway/src/topics/studioTopic.ts @@ -65,11 +65,11 @@ export class StudioTopic const studio = data ? (data as DBStudio) : undefined switch (source) { case StudioHandler.name: - this._logger.info(`${this._name} received studio update ${studio?._id}`) + this.logUpdateReceived('studio', source, `studioId ${studio?._id}`) this._studio = studio break case PlaylistsHandler.name: - this._logger.info(`${this._name} received playlists update from ${source}`) + this.logUpdateReceived('playlists', source) this._playlists = rundownPlaylists.map((p) => { let activationStatus: PlaylistActivationStatus = p.activationId === undefined ? 'deactivated' : 'activated' diff --git a/packages/live-status-gateway/src/wsHandler.ts b/packages/live-status-gateway/src/wsHandler.ts index f6f0aa529e..90a43f64e4 100644 --- a/packages/live-status-gateway/src/wsHandler.ts +++ b/packages/live-status-gateway/src/wsHandler.ts @@ -1,9 +1,9 @@ import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { Observer } from '@sofie-automation/server-core-integration' +import { CoreConnection, Observer, ProtectedString } from '@sofie-automation/server-core-integration' import { Logger } from 'winston' import { WebSocket } from 'ws' import { CoreHandler } from './coreHandler' -import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' +import { CorelibPubSub, CorelibPubSubCollections } from '@sofie-automation/corelib/dist/pubsub' export abstract class WebSocketTopicBase { protected _name: string @@ -36,9 +36,23 @@ export abstract class WebSocketTopicBase { sendMessage(ws: WebSocket, msg: object): void { const msgStr = JSON.stringify(msg) - this._logger.info(`Send ${this._name} message '${msgStr}'`) + this._logger.debug(`Send ${this._name} message '${msgStr}'`) ws.send(msgStr) } + + sendHeartbeat(ws: WebSocket): void { + const msgStr = JSON.stringify({ event: 'heartbeat' }) + this._logger.silly(`Send ${this._name} message '${msgStr}'`) + ws.send(msgStr) + } + + protected logUpdateReceived(collectionName: string, source: string, extraInfo?: string): void { + let message = `${this._name} received ${collectionName} update from ${source}` + if (extraInfo) { + message += `, ${extraInfo}` + } + this._logger.debug(message) + } } export interface WebSocketTopic { @@ -49,10 +63,18 @@ export interface WebSocketTopic { sendMessage(ws: WebSocket, msg: object): void } -export abstract class CollectionBase { +export type ObserverForCollection = T extends keyof CorelibPubSubCollections + ? Observer + : undefined + +export abstract class CollectionBase< + T, + TPubSub extends CorelibPubSub | undefined, + TCollection extends keyof CorelibPubSubCollections +> { protected _name: string - protected _collectionName: CollectionName | undefined - protected _publicationName: string | undefined + protected _collectionName: TCollection + protected _publicationName: TPubSub protected _logger: Logger protected _coreHandler: CoreHandler protected _studioId!: StudioId @@ -60,15 +82,14 @@ export abstract class CollectionBase { protected _observers: Set> = new Set() protected _collectionData: T | undefined protected _subscriptionId: string | undefined - protected _dbObserver: Observer | undefined - - constructor( - name: string, - collection: CollectionName | undefined, - publication: string | undefined, - logger: Logger, - coreHandler: CoreHandler - ) { + protected _dbObserver: ObserverForCollection | undefined + + protected get _core(): CoreConnection { + // In R51: CoreConnection { + return this._coreHandler.core + } + + constructor(name: string, collection: TCollection, publication: TPubSub, logger: Logger, coreHandler: CoreHandler) { this._name = name this._collectionName = collection this._publicationName = publication @@ -105,6 +126,32 @@ export abstract class CollectionBase { await observer.update(this._name, data) } } + + protected logDocumentChange(documentId: string | ProtectedString, changeType: string): void { + this._logger.silly(`${this._name} ${changeType} ${documentId}`) + } + + protected logUpdateReceived(collectionName: string, updateCount: number | undefined): void + protected logUpdateReceived(collectionName: string, source: string, extraInfo?: string): void + protected logUpdateReceived( + collectionName: string, + sourceOrUpdateCount: string | number | undefined, + extraInfo?: string + ): void { + if (typeof sourceOrUpdateCount === 'string') { + let message = `${this._name} received ${collectionName} update from ${sourceOrUpdateCount}` + if (extraInfo) { + message += `, ${extraInfo}` + } + this._logger.debug(message) + } else { + this._logger.debug(`'${this._name}' handler received ${sourceOrUpdateCount} ${collectionName}`) + } + } + + protected logNotifyingUpdate(updateCount: number | undefined): void { + this._logger.debug(`${this._name} notifying update with ${updateCount} ${this._collectionName}`) + } } export interface Collection { diff --git a/packages/mos-gateway/src/CoreMosDeviceHandler.ts b/packages/mos-gateway/src/CoreMosDeviceHandler.ts index 36b827c7d6..94130036f6 100644 --- a/packages/mos-gateway/src/CoreMosDeviceHandler.ts +++ b/packages/mos-gateway/src/CoreMosDeviceHandler.ts @@ -66,7 +66,7 @@ interface IStoryItemChange { export class CoreMosDeviceHandler { core!: CoreConnectionChild - public _observers: Array = [] + public _observers: Array> = [] public _mosDevice: IMOSDevice private _coreParentHandler: CoreHandler private _mosHandler: MosHandler diff --git a/packages/mos-gateway/src/coreHandler.ts b/packages/mos-gateway/src/coreHandler.ts index 550b276a5a..f4f54d4d5d 100644 --- a/packages/mos-gateway/src/coreHandler.ts +++ b/packages/mos-gateway/src/coreHandler.ts @@ -30,7 +30,7 @@ export interface CoreConfig { export class CoreHandler { core: CoreConnection | undefined logger: Winston.Logger - public _observers: Array = [] + public _observers: Array> = [] private _deviceOptions: DeviceConfig private _coreMosHandlers: Array = [] private _onConnected?: () => any diff --git a/packages/mos-gateway/src/mosHandler.ts b/packages/mos-gateway/src/mosHandler.ts index e2ad9bfa0f..342f67d9ec 100644 --- a/packages/mos-gateway/src/mosHandler.ts +++ b/packages/mos-gateway/src/mosHandler.ts @@ -60,7 +60,7 @@ export class MosHandler { private _disposed = false private _settings?: MosGatewayConfig private _coreHandler: CoreHandler | undefined - private _observers: Array = [] + private _observers: Array> = [] private _triggerupdateDevicesTimeout: any = null private mosTypes: MosTypes diff --git a/packages/playout-gateway/src/coreHandler.ts b/packages/playout-gateway/src/coreHandler.ts index 2fd2a29d5f..235a23b27f 100644 --- a/packages/playout-gateway/src/coreHandler.ts +++ b/packages/playout-gateway/src/coreHandler.ts @@ -43,7 +43,7 @@ export interface MemoryUsageReport { export class CoreHandler { core!: CoreConnection logger: Logger - public _observers: Array = [] + public _observers: Array> = [] public deviceSettings: PlayoutGatewayConfig = {} public multithreading = false @@ -449,7 +449,7 @@ export class CoreHandler { export class CoreTSRDeviceHandler { core!: CoreConnectionChild - public _observers: Array = [] + public _observers: Array> = [] public _devicePr: Promise> public _deviceId: string public _device!: BaseRemoteDeviceIntegration diff --git a/packages/playout-gateway/src/tsrHandler.ts b/packages/playout-gateway/src/tsrHandler.ts index 3043eb7684..f87f4d7e55 100644 --- a/packages/playout-gateway/src/tsrHandler.ts +++ b/packages/playout-gateway/src/tsrHandler.ts @@ -99,7 +99,7 @@ export class TSRHandler { private _coreHandler!: CoreHandler private _triggerupdateExpectedPlayoutItemsTimeout: any = null private _coreTsrHandlers: { [deviceId: string]: CoreTSRDeviceHandler } = {} - private _observers: Array = [] + private _observers: Array> = [] private _cachedStudioId: StudioId | null = null private _initialized = false diff --git a/packages/server-core-integration/src/lib/CoreConnectionChild.ts b/packages/server-core-integration/src/lib/CoreConnectionChild.ts index db49c92030..270ef10932 100644 --- a/packages/server-core-integration/src/lib/CoreConnectionChild.ts +++ b/packages/server-core-integration/src/lib/CoreConnectionChild.ts @@ -202,7 +202,7 @@ export class CoreConnectionChild extends EventEmitter this.ddp.ddpClient?.unsubscribe(subscriptionId) delete this._autoSubscriptions[subscriptionId] } - observe(collectionName: string): Observer { + observe(collectionName: string): Observer { if (!this._parent) throw new Error('Connection has been destroyed') return this._parent.observe(collectionName) diff --git a/packages/server-core-integration/src/lib/coreConnection.ts b/packages/server-core-integration/src/lib/coreConnection.ts index c1b10197df..993f74a9a6 100644 --- a/packages/server-core-integration/src/lib/coreConnection.ts +++ b/packages/server-core-integration/src/lib/coreConnection.ts @@ -357,7 +357,7 @@ export class CoreConnection extends EventEmitter { this.ddp.ddpClient?.unsubscribe(subscriptionId) delete this._autoSubscriptions[subscriptionId] } - observe(collectionName: string): Observer { + observe(collectionName: string): Observer { if (!this.ddp.ddpClient) { throw new Error('observe: DDP client not initialised') } diff --git a/packages/server-core-integration/src/lib/ddpClient.ts b/packages/server-core-integration/src/lib/ddpClient.ts index d5f8328322..73782ea5ae 100644 --- a/packages/server-core-integration/src/lib/ddpClient.ts +++ b/packages/server-core-integration/src/lib/ddpClient.ts @@ -12,6 +12,7 @@ import * as WebSocket from 'faye-websocket' import * as EJSON from 'ejson' import { EventEmitter } from 'eventemitter3' import got from 'got' +import { ProtectedString } from '@sofie-automation/shared-lib/dist/lib/protectedString' export interface TLSOpts { // Described in https://nodejs.org/api/tls.html#tls_tls_connect_options_callback @@ -48,27 +49,22 @@ export interface DDPConnectorOptions { /** * Observer watching for changes to a collection. */ -export interface Observer { +export interface Observer | string }> { /** Name of the collection being observed */ readonly name: string /** Identifier of this observer */ - readonly id: string + readonly id: Doc['_id'] /** * Callback when a document is added to a collection. * @callback * @param id Identifier of the document added * @param fields The added document */ - added: (id: string, fields?: { [attr: string]: unknown }) => void + added: (id: Doc['_id'], fields?: Partial) => void /** Callback when a document is changed in a collection. */ - changed: ( - id: string, - oldFields: { [attr: string]: unknown }, - clearedFields: Array, - newFields: { [attr: string]: unknown } - ) => void + changed: (id: Doc['_id'], oldFields: Partial, clearedFields: Array, newFields: Partial) => void /** Callback when a document is removed from a collection. */ - removed: (id: string, oldValue: { [attr: string]: unknown }) => void + removed: (id: Doc['_id'], oldValue: Partial) => void /** Request to stop observing the collection */ stop: () => void } @@ -398,7 +394,7 @@ export class DDPClient extends EventEmitter { private callbacks: { [id: string]: (error?: DDPError, result?: unknown) => void } = {} private updatedCallbacks: { [name: string]: () => void } = {} private pendingMethods: { [id: string]: boolean } = {} - private observers: { [name: string]: { [_id: string]: Observer } } = {} + private observers: { [name: string]: { [_id: string]: Observer } } = {} private reconnectTimeout: NodeJS.Timeout | null = null constructor(opts?: DDPConnectorOptions) { @@ -531,7 +527,7 @@ export class DDPClient extends EventEmitter { this.collections[name][id] = addedDocument if (this.observers[name]) { - Object.values(this.observers[name]).forEach((ob) => ob.added(id, data.fields)) + Object.values>(this.observers[name]).forEach((ob) => ob.added(id, data.fields)) } } } @@ -550,7 +546,7 @@ export class DDPClient extends EventEmitter { delete this.collections[name][id] if (this.observers[name]) { - Object.values(this.observers[name]).forEach((ob) => ob.removed(id, oldValue)) + Object.values>(this.observers[name]).forEach((ob) => ob.removed(id, oldValue)) } } } @@ -591,7 +587,7 @@ export class DDPClient extends EventEmitter { this.collections[name][id] = updatedDocument if (this.observers[name]) { - Object.values(this.observers[name]).forEach((ob) => + Object.values>(this.observers[name]).forEach((ob) => ob.changed(id, oldFields, clearedFields, newFields) ) } @@ -646,14 +642,14 @@ export class DDPClient extends EventEmitter { return (this.nextId += 1).toString() } - private addObserver(observer: Observer): void { + private addObserver(observer: Observer): void { if (!this.observers[observer.name]) { this.observers[observer.name] = {} } this.observers[observer.name][observer.id] = observer } - private removeObserver(observer: Observer): void { + private removeObserver(observer: Observer): void { if (!this.observers[observer.name]) { return } @@ -878,11 +874,11 @@ export class DDPClient extends EventEmitter { */ observe( collectionName: string, - added?: Observer['added'], - changed?: Observer['changed'], - removed?: Observer['removed'] - ): Observer { - const observer: Observer = { + added?: Observer['added'], + changed?: Observer['changed'], + removed?: Observer['removed'] + ): Observer { + const observer: Observer = { id: this.getNextId(), name: collectionName, added: diff --git a/packages/shared-lib/src/lib/throttleToNextTick.ts b/packages/shared-lib/src/lib/throttleToNextTick.ts new file mode 100644 index 0000000000..7cb0eec9f9 --- /dev/null +++ b/packages/shared-lib/src/lib/throttleToNextTick.ts @@ -0,0 +1,18 @@ +/** + * Wraps a function so that consecutive calls are throttled until the next tick + * @param callback the callback to wrap + * @returns wrapped callback + */ +export default function throttleToNextTick(fn: () => void): () => void { + let scheduled = false + + return (): void => { + if (!scheduled) { + scheduled = true + process.nextTick(() => { + fn() + scheduled = false + }) + } + } +} From 9314fd13b1000843d864712401421af74c163ca1 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Tue, 7 May 2024 07:47:50 +0200 Subject: [PATCH 291/479] chore: sample-client --- .../sample-client/index.html | 46 +++++++++++++------ .../sample-client/script.js | 35 +++++++++++--- 2 files changed, 60 insertions(+), 21 deletions(-) diff --git a/packages/live-status-gateway/sample-client/index.html b/packages/live-status-gateway/sample-client/index.html index d1fb8ca255..1c30839f81 100644 --- a/packages/live-status-gateway/sample-client/index.html +++ b/packages/live-status-gateway/sample-client/index.html @@ -1,19 +1,35 @@ - - Live Status Gateway client - - - - Segments: -
          + + Live Status Gateway client + + + +
          + Segments: +
          +
          +
          + Adlibs: +
          +
          +
          + Global Adlibs: +
          +
          - - + + diff --git a/packages/live-status-gateway/sample-client/script.js b/packages/live-status-gateway/sample-client/script.js index eba6dcd2e4..7f7a2eea50 100644 --- a/packages/live-status-gateway/sample-client/script.js +++ b/packages/live-status-gateway/sample-client/script.js @@ -1,6 +1,7 @@ const ws = new WebSocket(`ws://localhost:8080`) ws.addEventListener('message', (message) => { const data = JSON.parse(message.data) + console.log('Received message', data) switch (data.event) { case 'pong': handlePong(data) @@ -20,6 +21,12 @@ ws.addEventListener('message', (message) => { case 'segments': handleSegments(data) break + case 'activePieces': + handleActivePieces(data) + break + case 'adLibs': + handleAdLibs(data) + break } }) @@ -29,6 +36,8 @@ ws.addEventListener('open', () => { ws.send(JSON.stringify({ event: 'subscribe', subscription: { name: 'activePlaylist' }, reqid: 1 })) ws.send(JSON.stringify({ event: 'subscribe', subscription: { name: 'segments' }, reqid: 2 })) + ws.send(JSON.stringify({ event: 'subscribe', subscription: { name: 'activePieces' }, reqid: 3 })) + ws.send(JSON.stringify({ event: 'subscribe', subscription: { name: 'adLibs' }, reqid: 4 })) }) ws.addEventListener('close', () => { @@ -62,23 +71,37 @@ const PART_REMAINIG_SPAN_ID = 'part-remaining' const ACTIVE_PIECES_SPAN_ID = 'active-pieces' const NEXT_PIECES_SPAN_ID = 'next-pieces' const SEGMENTS_DIV_ID = 'segments' +const ADLIBS_DIV_ID = 'adlibs' +const GLOBAL_ADLIBS_DIV_ID = 'global-adlibs' const ENABLE_SYNCED_TICKS = true let activePlaylist = {} - function handleActivePlaylist(data) { activePlaylist = data - const activePiecesEl = document.getElementById(ACTIVE_PIECES_SPAN_ID) + const nextPiecesEl = document.getElementById(NEXT_PIECES_SPAN_ID) - activePiecesEl.innerHTML = - '
          • ' + - activePlaylist.activePieces.map((p) => `${p.name} [${p.tags || []}]`).join('
          • ') + - '
            • ' nextPiecesEl.innerHTML = '
              • ' + activePlaylist.nextPart.pieces.map((p) => `${p.name} [${p.tags || []}]`).join('
              • ') + '
                • ' } +let activePieces = {} +function handleActivePieces(data) { + activePieces = data + const activePiecesEl = document.getElementById(ACTIVE_PIECES_SPAN_ID) + activePiecesEl.innerHTML = + '
                  • ' + activePieces.activePieces.map((p) => `${p.name} [${p.tags || []}]`).join('
                  • ') + '
                    • ' +} +let adLibs = {} +function handleAdLibs(data) { + adLibs = data + const activePiecesEl = document.getElementById(ADLIBS_DIV_ID) + activePiecesEl.innerHTML = + '
                      • ' + adLibs.adLibs.map((p) => `${p.name} [${p.tags || []}]`).join('
                      • ') + '
                        • ' + const globalActivePiecesEl = document.getElementById(GLOBAL_ADLIBS_DIV_ID) + globalActivePiecesEl.innerHTML = + '
                          • ' + adLibs.globalAdLibs.map((p) => `${p.name}`).join('
                          • ') + '
                            • ' +} setInterval(() => { const segmentRemainingEl = document.getElementById(SEGMENT_REMAINIG_SPAN_ID) From 69780d4ffd9e6015bc838fbf37177f2b8b9d9dde Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Tue, 7 May 2024 09:02:47 +0200 Subject: [PATCH 292/479] chore: backport unit tests --- .../src/topics/__tests__/activePieces.spec.ts | 4 +-- .../topics/__tests__/activePlaylist.spec.ts | 6 ++-- .../src/topics/__tests__/adLibs.spec.ts | 8 ++--- .../topics/__tests__/segmentsTopic.spec.ts | 32 +++++++++---------- .../src/topics/__tests__/utils.ts | 2 +- 5 files changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/live-status-gateway/src/topics/__tests__/activePieces.spec.ts b/packages/live-status-gateway/src/topics/__tests__/activePieces.spec.ts index 7e7f469e13..8a01c4e432 100644 --- a/packages/live-status-gateway/src/topics/__tests__/activePieces.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/activePieces.spec.ts @@ -39,7 +39,7 @@ describe('ActivePiecesTopic', () => { outputLayerId: 'pgm', sourceLayerId: 'layer0', tags: ['my_tag'], - publicData: { c: 'd' }, + // publicData: { c: 'd' }, }, }), ] as PieceInstance[], @@ -58,7 +58,7 @@ describe('ActivePiecesTopic', () => { sourceLayer: 'Layer 0', outputLayer: 'PGM', tags: ['my_tag'], - publicData: { c: 'd' }, + publicData: null, // { c: 'd' }, }, ], } diff --git a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts index 91f28d6d08..39a1364135 100644 --- a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts @@ -78,7 +78,7 @@ describe('ActivePlaylistTopic', () => { segmentId: protectString('SEGMENT_1'), expectedDurationWithPreroll: 10000, expectedDuration: 10000, - publicData: { b: 'c' }, + // publicData: { b: 'c' }, } const testPartInstances: PartialDeep = { current: { @@ -112,7 +112,7 @@ describe('ActivePlaylistTopic', () => { timing: { startTime: 1600000060000, expectedDurationMs: 10000, projectedEndTime: 1600000070000 }, pieces: [], autoNext: undefined, - publicData: { b: 'c' }, + publicData: null, // { b: 'c' }, }, nextPart: null, currentSegment: { @@ -123,7 +123,7 @@ describe('ActivePlaylistTopic', () => { }, }, rundownIds: unprotectStringArray(playlist.rundownIdsInOrder), - publicData: { a: 'b' }, + publicData: null, // { a: 'b' }, } // eslint-disable-next-line @typescript-eslint/unbound-method diff --git a/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts b/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts index 0ba5af4013..57e3a30bb9 100644 --- a/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts @@ -25,7 +25,7 @@ function makeTestAdLibActions(): AdLibAction[] { externalId: 'NCS_ACTION_0', userData: {}, userDataManifest: {}, - publicData: { a: 'b' }, + // publicData: { a: 'b' }, }, ] } @@ -46,7 +46,7 @@ function makeTestGlobalAdLibActions(): RundownBaselineAdLibAction[] { externalId: 'NCS_GLOBAL_ACTION_0', userData: {}, userDataManifest: {}, - publicData: { c: 'd' }, + // publicData: { c: 'd' }, }, ] } @@ -84,7 +84,7 @@ describe('ActivePlaylistTopic', () => { outputLayer: 'PGM', sourceLayer: 'Layer 0', tags: ['adlib_tag'], - publicData: { a: 'b' }, + publicData: null, // { a: 'b' }, }, ], globalAdLibs: [ @@ -95,7 +95,7 @@ describe('ActivePlaylistTopic', () => { outputLayer: 'PGM', sourceLayer: 'Layer 0', tags: ['global_adlib_tag'], - publicData: { c: 'd' }, + publicData: null, // { c: 'd' }, }, ], } diff --git a/packages/live-status-gateway/src/topics/__tests__/segmentsTopic.spec.ts b/packages/live-status-gateway/src/topics/__tests__/segmentsTopic.spec.ts index 05d7a1649f..db52a980a9 100644 --- a/packages/live-status-gateway/src/topics/__tests__/segmentsTopic.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/segmentsTopic.spec.ts @@ -159,28 +159,28 @@ describe('SegmentsTopic', () => { rundownId: RUNDOWN_1_ID, name: 'Segment 1_1', timing: { expectedDurationMs: 0 }, - publicData: undefined, + publicData: null, // undefined, }, { id: '1_2', rundownId: RUNDOWN_1_ID, name: 'Segment 1_2', timing: { expectedDurationMs: 0 }, - publicData: undefined, + publicData: null, // undefined, }, { id: '2_1', rundownId: RUNDOWN_2_ID, name: 'Segment 2_1', timing: { expectedDurationMs: 0 }, - publicData: undefined, + publicData: null, // undefined, }, { id: '2_2', rundownId: RUNDOWN_2_ID, name: 'Segment 2_2', timing: { expectedDurationMs: 0 }, - publicData: undefined, + publicData: null, // undefined, }, ], } @@ -217,28 +217,28 @@ describe('SegmentsTopic', () => { rundownId: RUNDOWN_2_ID, name: 'Segment 2_1', timing: { expectedDurationMs: 0 }, - publicData: undefined, + publicData: null, // undefined, }, { id: '2_2', rundownId: RUNDOWN_2_ID, name: 'Segment 2_2', timing: { expectedDurationMs: 0 }, - publicData: undefined, + publicData: null, // undefined, }, { id: '1_1', rundownId: RUNDOWN_1_ID, name: 'Segment 1_1', timing: { expectedDurationMs: 0 }, - publicData: undefined, + publicData: null, // undefined, }, { id: '1_2', rundownId: RUNDOWN_1_ID, name: 'Segment 1_2', timing: { expectedDurationMs: 0 }, - publicData: undefined, + publicData: null, // undefined, }, ], } @@ -299,28 +299,28 @@ describe('SegmentsTopic', () => { rundownId: RUNDOWN_1_ID, name: 'Segment 1_1', timing: { expectedDurationMs: 0, budgetDurationMs: 5000 }, - publicData: undefined, + publicData: null, // undefined, }, { id: '1_2', rundownId: RUNDOWN_1_ID, name: 'Segment 1_2', timing: { expectedDurationMs: 0, budgetDurationMs: 15000 }, - publicData: undefined, + publicData: null, // undefined, }, { id: '2_1', rundownId: RUNDOWN_2_ID, name: 'Segment 2_1', timing: { expectedDurationMs: 0 }, - publicData: undefined, + publicData: null, // undefined, }, { id: '2_2', rundownId: RUNDOWN_2_ID, name: 'Segment 2_2', timing: { expectedDurationMs: 0, budgetDurationMs: 51000 }, - publicData: undefined, + publicData: null, // undefined, }, ], } @@ -386,28 +386,28 @@ describe('SegmentsTopic', () => { rundownId: RUNDOWN_1_ID, name: 'Segment 1_1', timing: { expectedDurationMs: 5000 }, - publicData: undefined, + publicData: null, // undefined, }, { id: '1_2', rundownId: RUNDOWN_1_ID, name: 'Segment 1_2', timing: { expectedDurationMs: 15000 }, - publicData: undefined, + publicData: null, // undefined, }, { id: '2_1', rundownId: RUNDOWN_2_ID, name: 'Segment 2_1', timing: { expectedDurationMs: 0 }, - publicData: undefined, + publicData: null, // undefined, }, { id: '2_2', rundownId: RUNDOWN_2_ID, name: 'Segment 2_2', timing: { expectedDurationMs: 51000 }, - publicData: undefined, + publicData: null, // undefined, }, ], } diff --git a/packages/live-status-gateway/src/topics/__tests__/utils.ts b/packages/live-status-gateway/src/topics/__tests__/utils.ts index 730b5fac52..442712ff2a 100644 --- a/packages/live-status-gateway/src/topics/__tests__/utils.ts +++ b/packages/live-status-gateway/src/topics/__tests__/utils.ts @@ -32,7 +32,7 @@ export function makeTestPlaylist(id?: string): DBRundownPlaylist { rundownIdsInOrder: [protectString(RUNDOWN_1_ID), protectString(RUNDOWN_2_ID)], studioId: protectString('STUDIO_1'), timing: { type: PlaylistTimingType.None }, - publicData: { a: 'b' }, + // publicData: { a: 'b' }, } } From aa790960e86f850baec1842c80e136fad2ed9e67 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 7 May 2024 15:16:23 +0100 Subject: [PATCH 293/479] fix: expose metaData as publicData as a temporary solution for R50 --- .../src/topics/__tests__/activePieces.spec.ts | 4 ++-- .../src/topics/__tests__/activePlaylist.spec.ts | 4 ++-- .../src/topics/__tests__/adLibs.spec.ts | 12 ++++++------ .../src/topics/activePlaylistTopic.ts | 4 ++-- .../live-status-gateway/src/topics/adLibsTopic.ts | 8 ++++---- .../src/topics/helpers/pieceStatus.ts | 2 +- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/live-status-gateway/src/topics/__tests__/activePieces.spec.ts b/packages/live-status-gateway/src/topics/__tests__/activePieces.spec.ts index 8a01c4e432..ae7bc189e0 100644 --- a/packages/live-status-gateway/src/topics/__tests__/activePieces.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/activePieces.spec.ts @@ -39,7 +39,7 @@ describe('ActivePiecesTopic', () => { outputLayerId: 'pgm', sourceLayerId: 'layer0', tags: ['my_tag'], - // publicData: { c: 'd' }, + metaData: { c: 'd' }, // tmp, introduced in R51 }, }), ] as PieceInstance[], @@ -58,7 +58,7 @@ describe('ActivePiecesTopic', () => { sourceLayer: 'Layer 0', outputLayer: 'PGM', tags: ['my_tag'], - publicData: null, // { c: 'd' }, + publicData: { c: 'd' }, }, ], } diff --git a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts index 39a1364135..173bfbb690 100644 --- a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts @@ -78,7 +78,7 @@ describe('ActivePlaylistTopic', () => { segmentId: protectString('SEGMENT_1'), expectedDurationWithPreroll: 10000, expectedDuration: 10000, - // publicData: { b: 'c' }, + metaData: { b: 'c' }, // tmp, publicData was introduced in R51 } const testPartInstances: PartialDeep = { current: { @@ -112,7 +112,7 @@ describe('ActivePlaylistTopic', () => { timing: { startTime: 1600000060000, expectedDurationMs: 10000, projectedEndTime: 1600000070000 }, pieces: [], autoNext: undefined, - publicData: null, // { b: 'c' }, + publicData: { b: 'c' }, }, nextPart: null, currentSegment: { diff --git a/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts b/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts index 57e3a30bb9..424816a581 100644 --- a/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts @@ -23,9 +23,9 @@ function makeTestAdLibActions(): AdLibAction[] { tags: ['adlib_tag'], }, externalId: 'NCS_ACTION_0', - userData: {}, + // userData: {}, userDataManifest: {}, - // publicData: { a: 'b' }, + userData: { a: 'b' }, // tmp, publicData was introduced in R51 }, ] } @@ -44,9 +44,9 @@ function makeTestGlobalAdLibActions(): RundownBaselineAdLibAction[] { tags: ['global_adlib_tag'], }, externalId: 'NCS_GLOBAL_ACTION_0', - userData: {}, + // userData: {}, userDataManifest: {}, - // publicData: { c: 'd' }, + userData: { c: 'd' }, // tmp, publicData was introduced in R51 }, ] } @@ -84,7 +84,7 @@ describe('ActivePlaylistTopic', () => { outputLayer: 'PGM', sourceLayer: 'Layer 0', tags: ['adlib_tag'], - publicData: null, // { a: 'b' }, + publicData: { a: 'b' }, }, ], globalAdLibs: [ @@ -95,7 +95,7 @@ describe('ActivePlaylistTopic', () => { outputLayer: 'PGM', sourceLayer: 'Layer 0', tags: ['global_adlib_tag'], - publicData: null, // { c: 'd' }, + publicData: { c: 'd' }, }, ], } diff --git a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts index 2bb60c118c..5d92aa0d79 100644 --- a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts +++ b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts @@ -112,7 +112,7 @@ export class ActivePlaylistTopic this._pieceInstancesInCurrentPartInstance?.map((piece) => toPieceStatus(piece, this._showStyleBaseExt) ) ?? [], - publicData: null, // currentPart.publicData, + publicData: currentPart.metaData, // tmp, publicData was introduced in R51 }) : null, currentSegment: @@ -137,7 +137,7 @@ export class ActivePlaylistTopic this._pieceInstancesInNextPartInstance?.map((piece) => toPieceStatus(piece, this._showStyleBaseExt) ) ?? [], - publicData: null, // nextPart.publicData, tmp, introduced in R51 + publicData: nextPart.metaData, // tmp, publicData was introduced in R51 }) : null, publicData: null, // this._activePlaylist.publicData, tmp, introduced in R51 diff --git a/packages/live-status-gateway/src/topics/adLibsTopic.ts b/packages/live-status-gateway/src/topics/adLibsTopic.ts index b6cebbcc30..d2c93871bc 100644 --- a/packages/live-status-gateway/src/topics/adLibsTopic.ts +++ b/packages/live-status-gateway/src/topics/adLibsTopic.ts @@ -102,7 +102,7 @@ export class AdLibsTopic outputLayer: outputLayerName ?? 'invalid', actionType: triggerModes, tags: action.display.tags, - publicData: null, // action.publicData, tmp, introduced in R51 + publicData: action.userData, // tmp, publicData was introduced in R51 }) }) ) @@ -120,7 +120,7 @@ export class AdLibsTopic outputLayer: outputLayerName ?? 'invalid', actionType: [], tags: adLib.tags, - publicData: null, // adLib.publicData, tmp, introduced in R51 + publicData: adLib.metaData, // tmp, publicData was introduced in R51 }) }) ) @@ -150,7 +150,7 @@ export class AdLibsTopic outputLayer: outputLayerName ?? 'invalid', actionType: triggerModes, tags: action.display.tags, - publicData: null, // action.publicData, tmp, introduced in R51 + publicData: action.userData, // tmp, publicData was introduced in R51 }) }) ) @@ -168,7 +168,7 @@ export class AdLibsTopic outputLayer: outputLayerName ?? 'invalid', actionType: [], tags: adLib.tags, - publicData: null, // adLib.publicData, tmp, introduced in R51 + publicData: adLib.metaData, // tmp, publicData was introduced in R51 }) }) ) diff --git a/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts b/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts index c3e7086adf..bacab0c100 100644 --- a/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts +++ b/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts @@ -23,6 +23,6 @@ export function toPieceStatus( sourceLayer: sourceLayerName ?? 'invalid', outputLayer: outputLayerName ?? 'invalid', tags: pieceInstance.piece.tags, - publicData: null, // pieceInstance.piece.publicData, tmp, introduced in R51 + publicData: pieceInstance.piece.metaData, // tmp, publicData was introduced in R51 } } From 596ab8ddbde3d590b62558c80ef675f24d1ad28c Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Wed, 8 May 2024 11:37:22 +0200 Subject: [PATCH 294/479] fix: expose metaData propery to liveStatusGateway --- meteor/lib/api/pubsub.ts | 16 ++++-- meteor/server/publications/rundown.ts | 56 ++++++++++++++++--- .../src/collections/adLibActionsHandler.ts | 10 +++- .../src/collections/adLibsHandler.ts | 10 +++- .../collections/globalAdLibActionsHandler.ts | 10 +++- .../src/collections/globalAdLibsHandler.ts | 10 +++- .../src/collections/partHandler.ts | 6 ++ .../src/collections/partInstancesHandler.ts | 8 ++- .../src/collections/pieceInstancesHandler.ts | 12 ++-- .../src/collections/segmentHandler.ts | 12 ++-- .../src/topics/activePlaylistTopic.ts | 2 +- .../src/topics/segmentsTopic.ts | 2 +- 12 files changed, 119 insertions(+), 35 deletions(-) diff --git a/meteor/lib/api/pubsub.ts b/meteor/lib/api/pubsub.ts index cebe2394f6..128ad22598 100644 --- a/meteor/lib/api/pubsub.ts +++ b/meteor/lib/api/pubsub.ts @@ -163,6 +163,7 @@ export interface PubSubTypes { [PubSub.peripheralDevicesAndSubDevices]: (selector: MongoQuery) => PeripheralDevice [PubSub.rundownBaselineAdLibPieces]: ( selector: MongoQuery, + includeMetadata?: boolean, token?: string ) => RundownBaselineAdLibItem [PubSub.rundownBaselineAdLibActions]: ( @@ -179,19 +180,24 @@ export interface PubSubTypes { token?: string ) => DBRundown [PubSub.adLibActions]: (selector: MongoQuery, token?: string) => AdLibAction - [PubSub.adLibPieces]: (selector: MongoQuery, token?: string) => AdLibPiece + [PubSub.adLibPieces]: (selector: MongoQuery, includeMetadata?: boolean, token?: string) => AdLibPiece [PubSub.pieces]: (selector: MongoQuery, token?: string) => Piece - [PubSub.pieceInstances]: (selector: MongoQuery, token?: string) => PieceInstance + [PubSub.pieceInstances]: ( + selector: MongoQuery, + includeMetadata?: boolean, + token?: string + ) => PieceInstance [PubSub.pieceInstancesSimple]: (selector: MongoQuery, token?: string) => PieceInstance - [PubSub.parts]: (rundownIds: RundownId[], token?: string) => DBPart + [PubSub.parts]: (rundownIds: RundownId[], includeMetadata?: boolean, token?: string) => DBPart [PubSub.partInstances]: ( rundownIds: RundownId[], playlistActivationId: RundownPlaylistActivationId | undefined, + includeMetadata?: boolean, token?: string ) => PartInstance [PubSub.partInstancesSimple]: (selector: MongoQuery, token?: string) => PartInstance [PubSub.partInstancesForSegmentPlayout]: (selector: MongoQuery, token?: string) => PartInstance - [PubSub.segments]: (selector: MongoQuery, token?: string) => DBSegment + [PubSub.segments]: (selector: MongoQuery, includeMetadata?: boolean, token?: string) => DBSegment [PubSub.showStyleBases]: (selector: MongoQuery, token?: string) => DBShowStyleBase [PubSub.showStyleVariants]: (selector: MongoQuery, token?: string) => DBShowStyleVariant [PubSub.triggeredActions]: (selector: MongoQuery, token?: string) => DBTriggeredActions @@ -224,7 +230,7 @@ export interface PubSubTypes { [PubSub.packageInfos]: (deviceId: PeripheralDeviceId, token?: string) => PackageInfoDB // For a PeripheralDevice - [PubSub.rundownsForDevice]: (deviceId: PeripheralDeviceId, token: string) => DBRundown + [PubSub.rundownsForDevice]: (deviceId: PeripheralDeviceId, includeMetadata?: boolean, token?: string) => DBRundown // custom publications: [PubSub.peripheralDeviceForDevice]: (deviceId: PeripheralDeviceId, token?: string) => PeripheralDeviceForDevice diff --git a/meteor/server/publications/rundown.ts b/meteor/server/publications/rundown.ts index 0a792ef222..f15d00949e 100644 --- a/meteor/server/publications/rundown.ts +++ b/meteor/server/publications/rundown.ts @@ -39,7 +39,12 @@ import { literal } from '@sofie-automation/corelib/dist/lib' import { MongoFieldSpecifierZeroes } from '@sofie-automation/corelib/dist/mongo' import { resolveCredentials } from '../security/lib/credentials' -meteorPublish(PubSub.rundownsForDevice, async function (deviceId, token) { +meteorPublish(PubSub.rundownsForDevice, async function (deviceId, includeMetadata, token) { + // Backwards compatibility, token is the last argument + if (typeof includeMetadata === 'string' && token === undefined) { + token = includeMetadata + includeMetadata = false + } check(deviceId, String) check(token, String) @@ -62,6 +67,7 @@ meteorPublish(PubSub.rundownsForDevice, async function (deviceId, token) { metaData: 0, }, } + if (includeMetadata) delete modifier.fields?.metaData if (NoSecurityReadAccess.any() || (await StudioReadAccess.studioContent(selector.studioId, resolvedCred))) { return Rundowns.findWithCursor(selector, modifier) @@ -104,13 +110,19 @@ meteorPublish(PubSub.rundowns, async function (playlistIds, showStyleBaseIds, to } return null }) -meteorPublish(PubSub.segments, async function (selector, token) { +meteorPublish(PubSub.segments, async function (selector, includeMetadata, token) { + // Backwards compatibility, token is the last argument + if (typeof includeMetadata === 'string' && token === undefined) { + token = includeMetadata + includeMetadata = false + } if (!selector) throw new Meteor.Error(400, 'selector argument missing') const modifier: FindOptions = { fields: { metaData: 0, }, } + if (includeMetadata) delete modifier.fields?.metaData if ( NoSecurityReadAccess.any() || (selector.rundownId && @@ -122,7 +134,12 @@ meteorPublish(PubSub.segments, async function (selector, token) { return null }) -meteorPublish(PubSub.parts, async function (rundownIds, token) { +meteorPublish(PubSub.parts, async function (rundownIds, includeMetadata, token) { + // Backwards compatibility, token is the last argument + if (typeof includeMetadata === 'string' && token === undefined) { + token = includeMetadata + includeMetadata = false + } check(rundownIds, Array) if (rundownIds.length === 0) return null @@ -132,6 +149,7 @@ meteorPublish(PubSub.parts, async function (rundownIds, token) { metaData: 0, }, } + if (includeMetadata) delete modifier.fields?.metaData const selector: MongoQuery = { rundownId: { $in: rundownIds }, @@ -148,7 +166,12 @@ meteorPublish(PubSub.parts, async function (rundownIds, token) { } return null }) -meteorPublish(PubSub.partInstances, async function (rundownIds, playlistActivationId, token?: string) { +meteorPublish(PubSub.partInstances, async function (rundownIds, playlistActivationId, includeMetadata, token?: string) { + // Backwards compatibility, token is the last argument + if (typeof includeMetadata === 'string' && token === undefined) { + token = includeMetadata + includeMetadata = false + } check(rundownIds, Array) check(playlistActivationId, Match.Maybe(String)) @@ -160,6 +183,7 @@ meteorPublish(PubSub.partInstances, async function (rundownIds, playlistActivati 'part.metaData': 0, }, } + if (includeMetadata) delete modifier.fields?.['part.metaData'] const selector: MongoQuery = { rundownId: { $in: rundownIds }, @@ -237,7 +261,13 @@ meteorPublish(PubSub.pieces, async function (selector: MongoQuery, token? return null }) -meteorPublish(PubSub.adLibPieces, async function (selector, token) { +meteorPublish(PubSub.adLibPieces, async function (selector, includeMetadata, token) { + // Backwards compatibility, token is the last argument + if (typeof includeMetadata === 'string' && token === undefined) { + token = includeMetadata + includeMetadata = false + } + if (!selector) throw new Meteor.Error(400, 'selector argument missing') const modifier: FindOptions = { fields: { @@ -245,6 +275,7 @@ meteorPublish(PubSub.adLibPieces, async function (selector, token) { timelineObjectsString: 0, }, } + if (includeMetadata) delete modifier.fields?.metaData if ( NoSecurityReadAccess.any() || (await RundownReadAccess.rundownContent(selector.rundownId, { userId: this.userId, token })) @@ -253,7 +284,12 @@ meteorPublish(PubSub.adLibPieces, async function (selector, token) { } return null }) -meteorPublish(PubSub.pieceInstances, async function (selector, token) { +meteorPublish(PubSub.pieceInstances, async function (selector, includeMetadata, token) { + // Backwards compatibility, token is the last argument + if (typeof includeMetadata === 'string' && token === undefined) { + token = includeMetadata + includeMetadata = false + } if (!selector) throw new Meteor.Error(400, 'selector argument missing') const modifier: FindOptions = { fields: { @@ -262,6 +298,7 @@ meteorPublish(PubSub.pieceInstances, async function (selector, token) { 'piece.timelineObjectsString': 0, }, } + if (includeMetadata) delete modifier.fields?.['piece.metaData'] // Enforce only not-reset selector.reset = { $ne: true } @@ -348,7 +385,12 @@ meteorPublish(PubSub.ingestDataCache, async function (selector, token) { }) meteorPublish( PubSub.rundownBaselineAdLibPieces, - async function (selector: MongoQuery, token?: string) { + async function (selector: MongoQuery, includeMetadata, token?: string) { + // Backwards compatibility, token is the last argument + if (typeof includeMetadata === 'string' && token === undefined) { + token = includeMetadata + includeMetadata = false + } if (!selector) throw new Meteor.Error(400, 'selector argument missing') const modifier: FindOptions = { fields: { diff --git a/packages/live-status-gateway/src/collections/adLibActionsHandler.ts b/packages/live-status-gateway/src/collections/adLibActionsHandler.ts index 8fc813f925..3aa3bb9316 100644 --- a/packages/live-status-gateway/src/collections/adLibActionsHandler.ts +++ b/packages/live-status-gateway/src/collections/adLibActionsHandler.ts @@ -43,9 +43,13 @@ export class AdLibActionsHandler if (this._subscriptionId) this._coreHandler.unsubscribe(this._subscriptionId) if (this._dbObserver) this._dbObserver.stop() if (this._curRundownId && this._curPartInstance) { - this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, { - rundownId: this._curRundownId, - }) + this._subscriptionId = await this._coreHandler.setupSubscription( + this._publicationName, + { + rundownId: this._curRundownId, + }, + true + ) // this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, [ // this._curRundownId, // ]) // R51 diff --git a/packages/live-status-gateway/src/collections/adLibsHandler.ts b/packages/live-status-gateway/src/collections/adLibsHandler.ts index e0670cb7d7..4028b34fe6 100644 --- a/packages/live-status-gateway/src/collections/adLibsHandler.ts +++ b/packages/live-status-gateway/src/collections/adLibsHandler.ts @@ -44,9 +44,13 @@ export class AdLibsHandler if (this._subscriptionId) this._coreHandler.unsubscribe(this._subscriptionId) if (this._dbObserver) this._dbObserver.stop() if (this._currentRundownId && this._currentPartInstance) { - this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, { - rundownId: this._currentRundownId, - }) + this._subscriptionId = await this._coreHandler.setupSubscription( + this._publicationName, + { + rundownId: this._currentRundownId, + }, + true + ) // this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, [ // this._currentRundownId, // ]) // R51 diff --git a/packages/live-status-gateway/src/collections/globalAdLibActionsHandler.ts b/packages/live-status-gateway/src/collections/globalAdLibActionsHandler.ts index bd1a8e9623..41448d8de0 100644 --- a/packages/live-status-gateway/src/collections/globalAdLibActionsHandler.ts +++ b/packages/live-status-gateway/src/collections/globalAdLibActionsHandler.ts @@ -51,9 +51,13 @@ export class GlobalAdLibActionsHandler if (this._subscriptionId) this._coreHandler.unsubscribe(this._subscriptionId) if (this._dbObserver) this._dbObserver.stop() if (this._currentRundownId) { - this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, { - rundownId: this._currentRundownId, - }) + this._subscriptionId = await this._coreHandler.setupSubscription( + this._publicationName, + { + rundownId: this._currentRundownId, + }, + true + ) // this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, [ // this._currentRundownId, // ]) // In R51 diff --git a/packages/live-status-gateway/src/collections/globalAdLibsHandler.ts b/packages/live-status-gateway/src/collections/globalAdLibsHandler.ts index aba02ef6cf..0da85a7cc6 100644 --- a/packages/live-status-gateway/src/collections/globalAdLibsHandler.ts +++ b/packages/live-status-gateway/src/collections/globalAdLibsHandler.ts @@ -51,9 +51,13 @@ export class GlobalAdLibsHandler if (this._subscriptionId) this._coreHandler.unsubscribe(this._subscriptionId) if (this._dbObserver) this._dbObserver.stop() if (this._currentRundownId) { - this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, { - rundownId: this._currentRundownId, - }) + this._subscriptionId = await this._coreHandler.setupSubscription( + this._publicationName, + { + rundownId: this._currentRundownId, + }, + true + ) // this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, [ // this._currentRundownId, // ]) // In R51 diff --git a/packages/live-status-gateway/src/collections/partHandler.ts b/packages/live-status-gateway/src/collections/partHandler.ts index c1eed0cd3a..46eb431ef2 100644 --- a/packages/live-status-gateway/src/collections/partHandler.ts +++ b/packages/live-status-gateway/src/collections/partHandler.ts @@ -69,8 +69,14 @@ export class PartHandler this._subscriptionId = await this._coreHandler.setupSubscription( this._publicationName, rundownIds, + true, null ) + // this._subscriptionId = await this._coreHandler.setupSubscription( + // this._publicationName, + // rundownIds, + // null, + // ) this._dbObserver = this._coreHandler.setupObserver(this._collectionName) this._dbObserver.added = (id) => { void this.changed(id, 'added').catch(this._logger.error) diff --git a/packages/live-status-gateway/src/collections/partInstancesHandler.ts b/packages/live-status-gateway/src/collections/partInstancesHandler.ts index 36b7176c7f..79ae9464f6 100644 --- a/packages/live-status-gateway/src/collections/partInstancesHandler.ts +++ b/packages/live-status-gateway/src/collections/partInstancesHandler.ts @@ -124,8 +124,14 @@ export class PartInstancesHandler this._subscriptionId = await this._coreHandler.setupSubscription( this._publicationName, this._rundownIds, - this._activationId + this._activationId, + true ) + // this._subscriptionId = await this._coreHandler.setupSubscription( + // this._publicationName, + // this._rundownIds, + // this._activationId + // ) this._subscriptionPending = false this._dbObserver = this._coreHandler.setupObserver(this._collectionName) this._dbObserver.added = (id) => { diff --git a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts index 22445e4c5c..f68fe377b5 100644 --- a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts +++ b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts @@ -134,10 +134,14 @@ export class PieceInstancesHandler if (!this._currentPlaylist) return if (this._subscriptionId) this._coreHandler.unsubscribe(this._subscriptionId) this._subscriptionPending = true - this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, { - rundownId: { $in: this._currentPlaylist.rundownIdsInOrder }, - partInstanceId: { $in: this._partInstanceIds }, - }) + this._subscriptionId = await this._coreHandler.setupSubscription( + this._publicationName, + { + rundownId: { $in: this._currentPlaylist.rundownIdsInOrder }, + partInstanceId: { $in: this._partInstanceIds }, + }, + true + ) // this._subscriptionId = await this._coreHandler.setupSubscription( // this._publicationName, // this._currentPlaylist.rundownIdsInOrder, diff --git a/packages/live-status-gateway/src/collections/segmentHandler.ts b/packages/live-status-gateway/src/collections/segmentHandler.ts index 6345cef2b0..463fe58e64 100644 --- a/packages/live-status-gateway/src/collections/segmentHandler.ts +++ b/packages/live-status-gateway/src/collections/segmentHandler.ts @@ -65,10 +65,14 @@ export class SegmentHandler if (this._subscriptionId) this._coreHandler.unsubscribe(this._subscriptionId) if (this._dbObserver) this._dbObserver.stop() if (this._rundownIds.length) { - this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, { - rundownId: { $in: this._rundownIds }, - isHidden: { $ne: true }, - }) + this._subscriptionId = await this._coreHandler.setupSubscription( + this._publicationName, + { + rundownId: { $in: this._rundownIds }, + isHidden: { $ne: true }, + }, + true + ) // this._subscriptionId = await this._coreHandler.setupSubscription( // this._publicationName, // this._rundownIds, diff --git a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts index 5d92aa0d79..98c3e46f1b 100644 --- a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts +++ b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts @@ -140,7 +140,7 @@ export class ActivePlaylistTopic publicData: nextPart.metaData, // tmp, publicData was introduced in R51 }) : null, - publicData: null, // this._activePlaylist.publicData, tmp, introduced in R51 + publicData: this._activePlaylist.metaData, // null, // this._activePlaylist.publicData, tmp, introduced in R51 }) : literal({ event: 'activePlaylist', diff --git a/packages/live-status-gateway/src/topics/segmentsTopic.ts b/packages/live-status-gateway/src/topics/segmentsTopic.ts index b55d2660d9..5532528058 100644 --- a/packages/live-status-gateway/src/topics/segmentsTopic.ts +++ b/packages/live-status-gateway/src/topics/segmentsTopic.ts @@ -70,7 +70,7 @@ export class SegmentsTopic name: segment.name, timing: calculateSegmentTiming(this._partsBySegment[segmentId] ?? []), identifier: segment.identifier, - publicData: null, // segment.publicData, tmp, introduced in R51 + publicData: segment.metaData, // segment.publicData, tmp, introduced in R51 } }), } From 79ae7f4207459310a84778b8fe1474c05c12ce89 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 13 May 2024 09:43:34 +0100 Subject: [PATCH 295/479] fix: hide create playlist drop box when dragging a rundown not in a multi-rundown playlist SOFIE-2462 (#1181) --- meteor/client/ui/RundownList/DragAndDropTypes.ts | 1 + meteor/client/ui/RundownList/RundownDropZone.tsx | 2 +- meteor/client/ui/RundownList/RundownListItem.tsx | 3 ++- meteor/client/ui/RundownList/RundownPlaylistUi.tsx | 1 + 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/meteor/client/ui/RundownList/DragAndDropTypes.ts b/meteor/client/ui/RundownList/DragAndDropTypes.ts index e646e37013..ca2d88c837 100644 --- a/meteor/client/ui/RundownList/DragAndDropTypes.ts +++ b/meteor/client/ui/RundownList/DragAndDropTypes.ts @@ -10,6 +10,7 @@ enum RundownListDragDropTypes { interface IRundownDragObject { id: RundownId rundownLayouts: Array + isOnlyRundownInPlaylist: boolean } function isRundownDragObject(obj: unknown): obj is IRundownDragObject { diff --git a/meteor/client/ui/RundownList/RundownDropZone.tsx b/meteor/client/ui/RundownList/RundownDropZone.tsx index 40fcddabbc..d41968cf30 100644 --- a/meteor/client/ui/RundownList/RundownDropZone.tsx +++ b/meteor/client/ui/RundownList/RundownDropZone.tsx @@ -17,7 +17,7 @@ export function RundownDropZone(): JSX.Element { accept: RundownListDragDropTypes.RUNDOWN, collect: (monitor) => { return { - activated: !!monitor.getItemType(), + activated: !!monitor.getItemType() && !monitor.getItem()?.isOnlyRundownInPlaylist, } }, drop: (item) => { diff --git a/meteor/client/ui/RundownList/RundownListItem.tsx b/meteor/client/ui/RundownList/RundownListItem.tsx index 35e06fa72e..2b7ac93ec7 100644 --- a/meteor/client/ui/RundownList/RundownListItem.tsx +++ b/meteor/client/ui/RundownList/RundownListItem.tsx @@ -32,7 +32,7 @@ export function RundownListItem({ rundownLayouts: Array swapRundownOrder: (a: RundownId, b: RundownId) => void playlistId: RundownPlaylistId - isOnlyRundownInPlaylist?: boolean + isOnlyRundownInPlaylist: boolean action?: IRundownPlaylistUiAction }>): JSX.Element | null { const { t } = useTranslation() @@ -65,6 +65,7 @@ export function RundownListItem({ item: { id: rundown._id, rundownLayouts, + isOnlyRundownInPlaylist, }, }, [rundown._id, rundownLayouts] diff --git a/meteor/client/ui/RundownList/RundownPlaylistUi.tsx b/meteor/client/ui/RundownList/RundownPlaylistUi.tsx index 47d81e9e28..8c44ffead8 100644 --- a/meteor/client/ui/RundownList/RundownPlaylistUi.tsx +++ b/meteor/client/ui/RundownList/RundownPlaylistUi.tsx @@ -158,6 +158,7 @@ export function RundownPlaylistUi({ rundownLayouts={rundownLayouts} swapRundownOrder={handleRundownSwap} playlistId={playlist._id} + isOnlyRundownInPlaylist={false} /> ) : null }) From 4f60881e04392b2c2584c144bc32087dade8d20e Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 13 May 2024 13:26:51 +0100 Subject: [PATCH 296/479] fix: tests --- .../topics/__tests__/activePlaylist.spec.ts | 2 +- .../topics/__tests__/segmentsTopic.spec.ts | 32 +++++++++---------- .../src/topics/__tests__/utils.ts | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts index 173bfbb690..cffa88b3d5 100644 --- a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts @@ -123,7 +123,7 @@ describe('ActivePlaylistTopic', () => { }, }, rundownIds: unprotectStringArray(playlist.rundownIdsInOrder), - publicData: null, // { a: 'b' }, + publicData: { a: 'b' }, } // eslint-disable-next-line @typescript-eslint/unbound-method diff --git a/packages/live-status-gateway/src/topics/__tests__/segmentsTopic.spec.ts b/packages/live-status-gateway/src/topics/__tests__/segmentsTopic.spec.ts index db52a980a9..05d7a1649f 100644 --- a/packages/live-status-gateway/src/topics/__tests__/segmentsTopic.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/segmentsTopic.spec.ts @@ -159,28 +159,28 @@ describe('SegmentsTopic', () => { rundownId: RUNDOWN_1_ID, name: 'Segment 1_1', timing: { expectedDurationMs: 0 }, - publicData: null, // undefined, + publicData: undefined, }, { id: '1_2', rundownId: RUNDOWN_1_ID, name: 'Segment 1_2', timing: { expectedDurationMs: 0 }, - publicData: null, // undefined, + publicData: undefined, }, { id: '2_1', rundownId: RUNDOWN_2_ID, name: 'Segment 2_1', timing: { expectedDurationMs: 0 }, - publicData: null, // undefined, + publicData: undefined, }, { id: '2_2', rundownId: RUNDOWN_2_ID, name: 'Segment 2_2', timing: { expectedDurationMs: 0 }, - publicData: null, // undefined, + publicData: undefined, }, ], } @@ -217,28 +217,28 @@ describe('SegmentsTopic', () => { rundownId: RUNDOWN_2_ID, name: 'Segment 2_1', timing: { expectedDurationMs: 0 }, - publicData: null, // undefined, + publicData: undefined, }, { id: '2_2', rundownId: RUNDOWN_2_ID, name: 'Segment 2_2', timing: { expectedDurationMs: 0 }, - publicData: null, // undefined, + publicData: undefined, }, { id: '1_1', rundownId: RUNDOWN_1_ID, name: 'Segment 1_1', timing: { expectedDurationMs: 0 }, - publicData: null, // undefined, + publicData: undefined, }, { id: '1_2', rundownId: RUNDOWN_1_ID, name: 'Segment 1_2', timing: { expectedDurationMs: 0 }, - publicData: null, // undefined, + publicData: undefined, }, ], } @@ -299,28 +299,28 @@ describe('SegmentsTopic', () => { rundownId: RUNDOWN_1_ID, name: 'Segment 1_1', timing: { expectedDurationMs: 0, budgetDurationMs: 5000 }, - publicData: null, // undefined, + publicData: undefined, }, { id: '1_2', rundownId: RUNDOWN_1_ID, name: 'Segment 1_2', timing: { expectedDurationMs: 0, budgetDurationMs: 15000 }, - publicData: null, // undefined, + publicData: undefined, }, { id: '2_1', rundownId: RUNDOWN_2_ID, name: 'Segment 2_1', timing: { expectedDurationMs: 0 }, - publicData: null, // undefined, + publicData: undefined, }, { id: '2_2', rundownId: RUNDOWN_2_ID, name: 'Segment 2_2', timing: { expectedDurationMs: 0, budgetDurationMs: 51000 }, - publicData: null, // undefined, + publicData: undefined, }, ], } @@ -386,28 +386,28 @@ describe('SegmentsTopic', () => { rundownId: RUNDOWN_1_ID, name: 'Segment 1_1', timing: { expectedDurationMs: 5000 }, - publicData: null, // undefined, + publicData: undefined, }, { id: '1_2', rundownId: RUNDOWN_1_ID, name: 'Segment 1_2', timing: { expectedDurationMs: 15000 }, - publicData: null, // undefined, + publicData: undefined, }, { id: '2_1', rundownId: RUNDOWN_2_ID, name: 'Segment 2_1', timing: { expectedDurationMs: 0 }, - publicData: null, // undefined, + publicData: undefined, }, { id: '2_2', rundownId: RUNDOWN_2_ID, name: 'Segment 2_2', timing: { expectedDurationMs: 51000 }, - publicData: null, // undefined, + publicData: undefined, }, ], } diff --git a/packages/live-status-gateway/src/topics/__tests__/utils.ts b/packages/live-status-gateway/src/topics/__tests__/utils.ts index 442712ff2a..55efe90ada 100644 --- a/packages/live-status-gateway/src/topics/__tests__/utils.ts +++ b/packages/live-status-gateway/src/topics/__tests__/utils.ts @@ -32,7 +32,7 @@ export function makeTestPlaylist(id?: string): DBRundownPlaylist { rundownIdsInOrder: [protectString(RUNDOWN_1_ID), protectString(RUNDOWN_2_ID)], studioId: protectString('STUDIO_1'), timing: { type: PlaylistTimingType.None }, - // publicData: { a: 'b' }, + metaData: { a: 'b' }, // tmp, publicData was introduced in R51 } } From 33b19384a77dc81d81ff6d9abe7e17958c16d263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20St=C3=A6rk=C3=A6r?= Date: Tue, 14 May 2024 09:48:14 +0200 Subject: [PATCH 297/479] Docs/sofie 2869/mos plugins (#1185) * feat: adds mos plugin documentation with mermaid.js sequence diagram * fix: more precise wording (from review process) --- .../docs/for-developers/mos-plugins.md | 184 ++++ .../user-guide/concepts-and-architecture.md | 8 +- packages/documentation/docusaurus.config.js | 4 + packages/documentation/package.json | 1 + .../for-developers/shelf-bucket-items.jpg | Bin 0 -> 614336 bytes .../shelf-external_frame-config.png | Bin 0 -> 139128 bytes packages/yarn.lock | 912 +++++++++++++++++- 7 files changed, 1096 insertions(+), 13 deletions(-) create mode 100644 packages/documentation/docs/for-developers/mos-plugins.md create mode 100644 packages/documentation/static/img/docs/for-developers/shelf-bucket-items.jpg create mode 100644 packages/documentation/static/img/docs/for-developers/shelf-external_frame-config.png diff --git a/packages/documentation/docs/for-developers/mos-plugins.md b/packages/documentation/docs/for-developers/mos-plugins.md new file mode 100644 index 0000000000..217118a946 --- /dev/null +++ b/packages/documentation/docs/for-developers/mos-plugins.md @@ -0,0 +1,184 @@ +--- +title: MOS-plugins +--- + +# iFrames MOS-plugins + +**The usage of MOS-plugins allow micro frontends to be injected into Sofie for the purpuse of adding content to the production without turning away from the Sofie UI.** + +Example use cases can be browsing and playing clips straight from a video server, or the creation of lower third graphics without storing it in the NRCS. + +:::note MOS reference +[5.3 MOS Plug-in Communication messages](https://mosprotocol.com/wp-content/MOS-Protocol-Documents/MOSProtocolVersion40/index.html#calibre_link-61) + +The link points at MOS documentations for MOS 4 (for the benefit of having the best documentation), but will be compatible with most older versions too. +::: + +## Bucket items workflow + +MOS-plugins are managed through the Shelf-system. They are added as `external_frame` either as a Tab to a Rundown layout or as a Panel to a Dashboard layout. + +![Video browser MOS Plugin in Shelf tab](/img/docs/for-developers/shelf-bucket-items.jpg) +A video server browser plugin shown as a tab in the rundown layout shelf. + +The user can create one or more Buckets. From the plugin they can drag-and-drop content into the buckets. The user can manage the buckets and their content by creating, renaming, re-arranging and deleting. More details available at the [Bucket concept description.](/docs/user-guide/concepts-and-architecture#buckets) + +## Cross-origin drag-and-drop + +:::note Bucket workflow without drag-and-drop +The plugin iFrame can send a `postMessage` call with an `ncsItem` payload to programatically create an ncsItem without the drag-and-drop interaction. This is a viable solution which avoids cross-origin drag-and-drop problems. +::: + +### The problem + +**Web browsers prevent drops into a webpage if the drag started from a page hosted on another origin.** + +This means that drag-and-drop must happen between pages from the same origin. This is relevant for MOS-plugins, as they are supposed to be displayed in iFrames. Specifically, this means that the plugin in the iFrame must be served from the same origin as the parent page (where the drop will happen). + +There are no properties or options to bypass this from within HTML/Javascript. Bypassing is theoretically possible by overriding the browser's security settings, but this is not recommended. + +:::note Background +The background for the policy is discussed in this Chromium Issue from 2010: [Security: do not allow on-page drag-and-drop from non-same-origin frames (or require an extra gesture)](https://issues.chromium.org/issues/40083787) +::: + +:::note What counts as different origins? +| Sofie Server Domain | Plugin Domain | Cross-origin or Same-origin? | +| ------------------- | ------------- | ---------------------------- | +| `https://mySofie.com:443` | `https://myPlugin.com:443` | cross-origin: different domains | +| | `https://www.mySofie.com:443` | cross-origin: different subdomains | +| | `https://myPlugin.mySofie.com:443` | cross-origin: different subdomains | +| | `http://mySofie.com:443` | cross-origin: different schemes | +| | `https://mySofie.com:80` | cross-origin: different ports | +| | `https://mySofie.com:443/myPlugin` | same-origin: domain, scheme and port match | +| | `https://mySofie.com/myPlugin` | same-origin: domain, scheme and port match (https implies port 443) | + +::: + +#### The "proxy idea" + +As you can tell from the table, you need to exactly match both the protocol, domain and port number. More importantly, different subdomains trigger the cross-origin policy. + +_The proxy idea_ is to use rewrite-rules in a proxy server (e.g. NGINX) to serve the plugin from a path on the Sofie server's domain. As this can't be done as subdomains, that leaves the option of having a folder underneath the top level of the Sofie server's domain. + +An example of this would be to serve Sofie at `https://mysofie.com` and then host the plugin (directly or via a proxy) at `https://mysofie.com/myplugin`. Technically this will work, but this solution is fragile. All links within the plugin will have to be either absolute or truly relative links that take the URL structure into account. This is doable if the plugin is being developed with this in mind. But it leads to a fragile tight coupling between the plugin and the host application (Sofie) which can break with any inconsiderate udate in the future. + +:::note Example of linking from a (potentially proxied) subfolder +**Case:** `https://mysofie.com/myplugin/index.html` wants to acccess `https://mysofie.com/myplugin/static/images/logo.png`. + +Normally the plugin would be developed and bundled to work standalone, resulting in a link relative to its own base path, giving `/static/images/logo.png` which here wrongly resolves to `https://mysofie.com/static/images/logo.png`. + +The plugin would need to use either use the absolute `https://mysofie.com/myplugin/static/images/logo.png` or the relative `images/static/logo.png` or `./images/static/logo.png` or even `/myplugin/static/images/logo.png` to point to the right resource. +::: + +### The solution + +**Sofie proposes a drag-and-drop/postMessage hybrid interface.** +In this model the user interactions of drag-and-drop are targeting a dedicated Drop page served by the plugin-server (same-origin to the plugin). This can be transparently overlaid the real drop region and intercept drop events. The Bucket system has built-in support for this, configured as an additional property to the External frame panel setup in Shelf config. + +![Configuration of External frame with dedicated drop-page](/img/docs/for-developers/shelf-external_frame-config.png) + +The true communication channel between the plugin and Sofie becomes a postMessage protocol where the plugin is managing all drag-and-drop events and converts them into the postMessage protocol. Sofie also handles edge cases such as timeouts, drag leaving the browser etc. + +### Sequence diagram + +#### Post-messages from the Plugin (drag-side) + +| Message | Payload | Description | +| --------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| dragStart | - | Re-sends the DOM event dragStart as a postMessage of the same kind.
                              This is the signal to Sofie to toggle on the Drop-zone and indicate in the UI that a drag is happening. | +| dragEnd | - | Re-sends the DOM event dragEnd as a postMessage of the same kind.
                              This is the signal to Sofie to toggle off the Drop-zone and reset the UI. | + +#### Post-messages from the Plugin Drop-page + +| Message | Payload | Description | +| --------- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| dragEnter | `{event: 'dragEnter', label: string}` | To set the UI to reflect an object is being dragged into a specific bucket.
                              The label property can be used for showing a simple placeholder in the bucket. | +| dragLeave | `{event: 'dragLeave'}` | To reset any UI. | +| drop | `{event: 'drop'}` | To synchronously react to the drop in the UI. | +| data | `{event: 'data', data: ncsItem}` | To (a)synchronously receive the payload.
                              The expected format is an `ncsItem` MOS message (XML string) | +| error | `{event: 'error', message}` | To cancel the drag-operation and handle any errors. | + +:::note Please note +Please note how all interactions are happening over the postMessage interface. +No DOM-driven drag-n-drop events are relevant for Sofie, as they are solely handled between the plugin and its drop-page. +::: + +```mermaid +sequenceDiagram +autonumber + +actor user as User + +participant plugin as Plugin
                              Frontend +participant shelf as Sofie Shelf Component +participant bucket as Sofie Bucket Component +participant drop as Plugin
                              Drop-page + +user->>plugin: Starts dragging from Plugin +plugin->>shelf: postMessage dragStartEvent +shelf--)shelf: 10 000ms timeout to trigger a dragEndEvent
                              if the drag doesn't cancel or successfully drop before that. +shelf->>shelf: Filter for valid Drop Zones
                              based on the optional properties of the dragStartEvent +shelf->>bucket: Sofie React event dragStartEvent +bucket->>drop: Shows iFrame Drop Zone + + + +user->>drop: Drags into the area of a Drop Zone (DOM dragEnter event) +note right of drop: Read payload to provide a title
                              in the dragEnterEvent +drop->>drop: e.dataTransfer.getData('text/plain'); +drop->>bucket: postmessage object dragEnterEvent + +loop dragOver events + user-)drop: Drag moves over drop target (DOM dragover event) + drop->>drop: (re)set timeout 100ms
                              to trigger faux dragLeave +end + +drop--)drop: dragLeave timeout expires +drop->>bucket: postmessage object dragEnterEvent (faux) + + +user->>drop: Drags out of a Drop Zone, or dragOver timeout (DOM dragLeave event) +drop->>drop: cancel dragOver timeout +drop->>bucket: postmessage object dragLeaveEvent + + + +Note over user,drop: Unknown order of events. Handle both outcomes of the race. +par Successful drop or Cancelled drag + user->>plugin: Successful drop
                              or Cancel drag on ESC
                              or drop outside of Drop region
                              (DOM dragEnd event) + plugin->>shelf: postMessage dragEndEvent + shelf->>shelf: Clear the drop-/cancel-timeout. + shelf->>bucket: Sofie React event dragEndEvent + bucket->>drop: Hides iFrame Drop Zone +and Drops in bucket + user->>drop: Drop (DOM drop event) + drop->>bucket: dropEvent + bucket--)bucket: Set timeout to trigger an user-facing error
                              if the data doesn't return in time. + bucket->>bucket: Set loader UI + + drop->>drop: e.dataTransfer.getData('text/plain'); + + + alt Success + drop--)bucket: postmessage object dataEvent + bucket->>bucket: Clear loader UI/Set success UI + else Error + drop--)bucket: postmessage object errorEvent + bucket->>bucket: Clear loader UI + bucket--)user: Error message + else Timeout + bucket->>bucket: Clear loader UI + bucket--)user: Error message + end +end + +``` + +#### Minimal example sequence - happy path + +Don't worry, the sequence diagram shows a lot more detail than you need to think about. Consider this simple happy-path sequence as a representative interaction between the 3 actors (Plugin, Drop-page and Sofie): + +1. Plugin `dragStart` +2. Drop-page `dragEnter` +3. Plugin `dragEnd` and Drop-page `drop` +4. Drop-page `data` diff --git a/packages/documentation/docs/user-guide/concepts-and-architecture.md b/packages/documentation/docs/user-guide/concepts-and-architecture.md index bce331a9f7..ea2fee2b7a 100644 --- a/packages/documentation/docs/user-guide/concepts-and-architecture.md +++ b/packages/documentation/docs/user-guide/concepts-and-architecture.md @@ -28,7 +28,7 @@ To be able to facilitate various workflows and to Here's a short explanation abo - The **Organization** \(only available if user accounts are enabled\) defines things that are common for an organization. An organization consists of: **Users, Studios** and **ShowStyles**. - The **Studio** contains things that are related to the "hardware" or "rig". Technically, a Studio is defined as an entity that can have one \(or none\) rundown active at any given time. In most cases, this will be a representation of your gallery, with cameras, video playback and graphics systems, external inputs, sound mixers, lighting controls and so on. A single System can easily control multiple Studios. - The **Show Style** contains settings for the "show", for example if there's a "Morning Show" and an "Afternoon Show" - produced in the same gallery - they might be two different Show Styles \(played in the same Studio\). Most importantly, the Show Style decides the "look and feel" of the Show towards the producer/director, dictating how data ingested from the NRCS will be interpreted and how the user will interact with the system during playback (see: [Show Style](../configuration/settings-view#show-style) in Settings). - * A **Show Style Variant** is a set of Show Style _Blueprint_ configuration values, that allows to use the same interaction model across multiple Shows with potentially different assets, changing the outward look of the Show: for example news programs with different hosts produced from the same Studio, but with different light setups, backscreen and overlay graphics. + - A **Show Style Variant** is a set of Show Style _Blueprint_ configuration values, that allows to use the same interaction model across multiple Shows with potentially different assets, changing the outward look of the Show: for example news programs with different hosts produced from the same Studio, but with different light setups, backscreen and overlay graphics. ![Sofie Architecture Venn Diagram](/img/docs/main/features/sofie-venn-diagram.png) @@ -88,9 +88,9 @@ An AdLib isn't added to the Part in the GUI until it starts playing, instead you A Bucket is a container for AdLib Pieces created by the producer/operator during production. They exist independently of the Rundowns and associated content created by ingesting data from the NRCS. Users can freely create, modify and remove Buckets. -The primary use-case of these elements is for breaking news formats where quick turnaround video editing may require circumvention of the regular flow of show assets and programming via the NRCS. Currently, one way of creating AdLibs inside Buckets is using a MOS Plugin integration inside the Shelf, where MOS [ncsItem](https://mosprotocol.com/wp-content/MOS-Protocol-Documents/MOS-Protocol-2.8.4-Current.htm#ncsItem) elements can be dragged from the MOS Plugin onto a bucket and ingested. +The primary use-case of these elements is for breaking-news formats where quick turnaround video editing may require circumvention of the regular flow of show assets and programming via the NRCS. Currently, one way of creating AdLibs inside Buckets is using a MOS Plugin integration inside the Shelf, where MOS [ncsItem](https://mosprotocol.com/wp-content/MOS-Protocol-Documents/MOSProtocolVersion40/index.html#calibre_link-72) elements can be dragged from the MOS Plugin onto a bucket and ingested. -The ingest happens via the `getAdlibItem` method: [https://github.com/nrkno/sofie-core/blob/master/packages/blueprints-integration/src/api.ts#L215](https://github.com/nrkno/sofie-core/blob/master/packages/blueprints-integration/src/api.ts#L215) +The ingest happens via the `getAdlibItem` method: [https://github.com/nrkno/sofie-core/blob/6c4edee7f352bb542c8a29317d59c0bf9ac340ba/packages/blueprints-integration/src/api/showStyle.ts#L122](https://github.com/nrkno/sofie-core/blob/6c4edee7f352bb542c8a29317d59c0bf9ac340ba/packages/blueprints-integration/src/api/showStyle.ts#L122) ## Views @@ -127,7 +127,7 @@ Documentation on the interface to be exposed by the Blueprint: ## `PartInstances` and `PieceInstances` -In order to be able to facilitate ingesting changes from the NRCS while continuing to provide a stable and predictable playback of the Rundowns, Sofie internally uses a concept of ["instantiation"](https://en.wikipedia.org/wiki/Instance_(computer_science)) of key Rundown elements. Before playback of a Part can begin, the Part and it's Pieces are copied into an Instance of a Part: a `PartInstance`. This protects the contents of the _Next_ and _On Air_ part, preventing accidental changes that could surprise the producer/director. This also makes it possible to inspect the "as played" state of the Rundown, independently of the "as planned" state ingested from the NRCS. +In order to be able to facilitate ingesting changes from the NRCS while continuing to provide a stable and predictable playback of the Rundowns, Sofie internally uses a concept of ["instantiation"]() of key Rundown elements. Before playback of a Part can begin, the Part and it's Pieces are copied into an Instance of a Part: a `PartInstance`. This protects the contents of the _Next_ and _On Air_ part, preventing accidental changes that could surprise the producer/director. This also makes it possible to inspect the "as played" state of the Rundown, independently of the "as planned" state ingested from the NRCS. The blueprints can optionally allow some changes to the Parts and Pieces to be forwarded onto these `PartInstances`: [https://github.com/nrkno/sofie-core/blob/master/packages/blueprints-integration/src/api.ts#L190](https://github.com/nrkno/sofie-core/blob/master/packages/blueprints-integration/src/api.ts#L190) diff --git a/packages/documentation/docusaurus.config.js b/packages/documentation/docusaurus.config.js index 1d645225ae..ceb68cec4c 100644 --- a/packages/documentation/docusaurus.config.js +++ b/packages/documentation/docusaurus.config.js @@ -14,6 +14,10 @@ module.exports = { favicon: 'img/favicon.ico', organizationName: 'nrkno', projectName: 'sofie-core', + markdown: { + mermaid: true, + }, + themes: ['@docusaurus/theme-mermaid'], themeConfig: { image: 'img/pilot_fredag-05.jpg', colorMode: { diff --git a/packages/documentation/package.json b/packages/documentation/package.json index 887fda9b30..2db1628111 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -21,6 +21,7 @@ "@docusaurus/core": "3.2.1", "@docusaurus/module-type-aliases": "3.2.1", "@docusaurus/preset-classic": "3.2.1", + "@docusaurus/theme-mermaid": "^3.2.1", "@docusaurus/types": "3.2.1", "@mdx-js/react": "^3.0.0", "@svgr/webpack": "^5.5.0", diff --git a/packages/documentation/static/img/docs/for-developers/shelf-bucket-items.jpg b/packages/documentation/static/img/docs/for-developers/shelf-bucket-items.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0312527b9382c33fd5b436c22187bc7f3e6d26ab GIT binary patch literal 614336 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
{sortedOutputLayers.map((item) => item.type === 'deleted' ? ( - + ) : ( toggleExpanded(item.id), [toggleExpanded, item.id]) - const doResetItem = useCallback(() => overrideHelper.resetItem(item.id), [overrideHelper, item.id]) + const doResetItem = useCallback(() => overrideHelper().resetItem(item.id).commit(), [overrideHelper, item.id]) const doChangeItemId = useCallback( (newItemId: string) => { - overrideHelper.changeItemId(item.id, newItemId) + overrideHelper().changeItemId(item.id, newItemId).commit() toggleExpanded(newItemId, true) }, [overrideHelper, toggleExpanded, item.id] @@ -196,7 +198,7 @@ function OutputLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: no: t('Cancel'), yes: t('Delete'), onAccept: () => { - overrideHelper.deleteItem(item.id) + overrideHelper().deleteItem(item.id).commit() }, message: ( @@ -212,7 +214,7 @@ function OutputLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: yes: t('Reset'), no: t('Cancel'), onAccept: () => { - overrideHelper.resetItem(item.id) + overrideHelper().resetItem(item.id).commit() }, message: ( diff --git a/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx b/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx index e04e571d5c..f6f85903f5 100644 --- a/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx +++ b/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx @@ -129,6 +129,8 @@ export function SourceLayerSettings({ showStyleBase }: IStudioSourcesSettingsPro const overrideHelper = useOverrideOpHelper(saveOverrides, showStyleBase.sourceLayersWithOverrides) + const doUndelete = useCallback((itemId: string) => overrideHelper().resetItem(itemId).commit(), [overrideHelper]) + return (

@@ -149,7 +151,7 @@ export function SourceLayerSettings({ showStyleBase }: IStudioSourcesSettingsPro

{sortedSourceLayers.map((item) => item.type === 'deleted' ? ( - + ) : ( toggleExpanded(item.id), [toggleExpanded, item.id]) - const doResetItem = useCallback(() => overrideHelper.resetItem(item.id), [overrideHelper, item.id]) + const doResetItem = useCallback(() => overrideHelper().resetItem(item.id).commit(), [overrideHelper, item.id]) const doChangeItemId = useCallback( (newItemId: string) => { - overrideHelper.changeItemId(item.id, newItemId) + overrideHelper().changeItemId(item.id, newItemId).commit() toggleExpanded(newItemId, true) }, [overrideHelper, toggleExpanded, item.id] @@ -221,7 +223,7 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: no: t('Cancel'), yes: t('Delete'), onAccept: () => { - overrideHelper.deleteItem(item.id) + overrideHelper().deleteItem(item.id).commit() }, message: ( @@ -241,7 +243,7 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: yes: t('Reset'), no: t('Cancel'), onAccept: () => { - overrideHelper.resetItem(item.id) + overrideHelper().resetItem(item.id).commit() }, message: ( diff --git a/meteor/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx b/meteor/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx index a9d149b56c..c8a322f57b 100644 --- a/meteor/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx +++ b/meteor/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx @@ -67,7 +67,7 @@ export function GenericSubDevicesTable({ no: t('Cancel'), yes: t('Remove'), onAccept: () => { - overrideHelper.deleteItem(subdeviceId) + overrideHelper().deleteItem(subdeviceId).commit() }, message: ( @@ -105,6 +105,11 @@ export function GenericSubDevicesTable({ return options }, [peripheralDevicesMap]) + const undeleteItemWithId = useCallback( + (itemId: string) => overrideHelper().resetItem(itemId).commit(), + [overrideHelper] + ) + return (
+ changeSortOrder('name', order)} + /> {filter && ( -
onFilterChange?.('')}> +
+ )} - changeSortOrder('name', order)} - />
G?DNZ?L7mg)#a!s4E{rc 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

GiaEoT;x@teK;4>l7`m} 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 F0weFZ++JOxhPst8?Eg(d~%PLnO|xNGkZ)h=3| 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%0AH`>(}Tj^-yoEm{}e0|&ec+i2k%Hlfm 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 z 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 one^8ycO{*8 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

qQ+fA$-pCy`lNTQv%Gkc$7^l6c6^2< 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 literal 0 HcmV?d00001 diff --git a/packages/yarn.lock b/packages/yarn.lock index 96449a70b2..f07ad0122c 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -3114,6 +3114,13 @@ __metadata: languageName: node linkType: hard +"@braintree/sanitize-url@npm:^6.0.1": + version: 6.0.4 + resolution: "@braintree/sanitize-url@npm:6.0.4" + checksum: f5ec6048973722ea1c46ae555d2e9eb848d7fa258994f8ea7d6db9514ee754ea3ef344ef71b3696d486776bcb839f3124e79f67c6b5b2814ed2da220b340627c + languageName: node + linkType: hard + "@colors/colors@npm:1.5.0": version: 1.5.0 resolution: "@colors/colors@npm:1.5.0" @@ -3602,6 +3609,24 @@ __metadata: languageName: node linkType: hard +"@docusaurus/theme-mermaid@npm:^3.2.1": + version: 3.2.1 + resolution: "@docusaurus/theme-mermaid@npm:3.2.1" + dependencies: + "@docusaurus/core": 3.2.1 + "@docusaurus/module-type-aliases": 3.2.1 + "@docusaurus/theme-common": 3.2.1 + "@docusaurus/types": 3.2.1 + "@docusaurus/utils-validation": 3.2.1 + mermaid: ^10.4.0 + tslib: ^2.6.0 + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: f91000a2da702b2aecdf1326ecfc6279afb283ddf5f1a0b51f1122b7f53fadb8ee8380a52704fd247841010bc33052e23a652820d6f223ed3ee462b0fb2736f1 + languageName: node + linkType: hard + "@docusaurus/theme-search-algolia@npm:3.2.1": version: 3.2.1 resolution: "@docusaurus/theme-search-algolia@npm:3.2.1" @@ -6529,6 +6554,29 @@ __metadata: languageName: node linkType: hard +"@types/d3-scale-chromatic@npm:^3.0.0": + version: 3.0.3 + resolution: "@types/d3-scale-chromatic@npm:3.0.3" + checksum: a465d126a00a71d3824957283580b4b404fe6f6bb52eb2b7303047fffed2bec6e31aeb34bfb30313e72ee1d75243c50ec5a45824eaf547f9c0849a1379527662 + languageName: node + linkType: hard + +"@types/d3-scale@npm:^4.0.3": + version: 4.0.8 + resolution: "@types/d3-scale@npm:4.0.8" + dependencies: + "@types/d3-time": "*" + checksum: 3b1906da895564f73bb3d0415033d9a8aefe7c4f516f970176d5b2ff7a417bd27ae98486e9a9aa0472001dc9885a9204279a1973a985553bdb3ee9bbc1b94018 + languageName: node + linkType: hard + +"@types/d3-time@npm:*": + version: 3.0.3 + resolution: "@types/d3-time@npm:3.0.3" + checksum: a071826c80efdb1999e6406fef2db516d45f3906da3a9a4da8517fa863bae53c4c1056ca5347a20921660607d21ec874fd2febe0e961adb7be6954255587d08f + languageName: node + linkType: hard + "@types/debug@npm:^4.0.0, @types/debug@npm:^4.1.12": version: 4.1.12 resolution: "@types/debug@npm:4.1.12" @@ -6785,6 +6833,15 @@ __metadata: languageName: node linkType: hard +"@types/mdast@npm:^3.0.0": + version: 3.0.15 + resolution: "@types/mdast@npm:3.0.15" + dependencies: + "@types/unist": ^2 + checksum: af85042a4e3af3f879bde4059fa9e76c71cb552dffc896cdcc6cf9dc1fd38e37035c2dbd6245cfa6535b433f1f0478f5549696234ccace47a64055a10c656530 + languageName: node + linkType: hard + "@types/mdast@npm:^4.0.0, @types/mdast@npm:^4.0.2": version: 4.0.3 resolution: "@types/mdast@npm:4.0.3" @@ -7138,6 +7195,13 @@ __metadata: languageName: node linkType: hard +"@types/unist@npm:^2": + version: 2.0.10 + resolution: "@types/unist@npm:2.0.10" + checksum: e2924e18dedf45f68a5c6ccd6015cd62f1643b1b43baac1854efa21ae9e70505db94290434a23da1137d9e31eb58e54ca175982005698ac37300a1c889f6c4aa + languageName: node + linkType: hard + "@types/unist@npm:^2.0.0": version: 2.0.8 resolution: "@types/unist@npm:2.0.8" @@ -9656,6 +9720,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"commander@npm:7, commander@npm:^7.2.0": + version: 7.2.0 + resolution: "commander@npm:7.2.0" + checksum: 53501cbeee61d5157546c0bef0fedb6cdfc763a882136284bed9a07225f09a14b82d2a84e7637edfd1a679fb35ed9502fd58ef1d091e6287f60d790147f68ddc + languageName: node + linkType: hard + "commander@npm:8.3.0, commander@npm:^8.3.0": version: 8.3.0 resolution: "commander@npm:8.3.0" @@ -9691,13 +9762,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"commander@npm:^7.2.0": - version: 7.2.0 - resolution: "commander@npm:7.2.0" - checksum: 53501cbeee61d5157546c0bef0fedb6cdfc763a882136284bed9a07225f09a14b82d2a84e7637edfd1a679fb35ed9502fd58ef1d091e6287f60d790147f68ddc - languageName: node - linkType: hard - "common-ancestor-path@npm:^1.0.1": version: 1.0.1 resolution: "common-ancestor-path@npm:1.0.1" @@ -10093,6 +10157,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"cose-base@npm:^1.0.0": + version: 1.0.3 + resolution: "cose-base@npm:1.0.3" + dependencies: + layout-base: ^1.0.0 + checksum: 3f3d592316df74adb215ca91e430f1c22b6e890bc0025b32ae1f6464c73fdb9614816cb40a8d38b40c6a3e9e7b8c64eda90d53fb9a4a6948abec17dad496f30b + languageName: node + linkType: hard + "cosmiconfig@npm:7.0.0": version: 7.0.0 resolution: "cosmiconfig@npm:7.0.0" @@ -10482,6 +10555,377 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"cytoscape-cose-bilkent@npm:^4.1.0": + version: 4.1.0 + resolution: "cytoscape-cose-bilkent@npm:4.1.0" + dependencies: + cose-base: ^1.0.0 + peerDependencies: + cytoscape: ^3.2.0 + checksum: bea6aa139e21bf4135b01b99f8778eed061e074d1a1689771597e8164a999d66f4075d46be584b0a88a5447f9321f38c90c8821df6a9322faaf5afebf4848d97 + languageName: node + linkType: hard + +"cytoscape@npm:^3.28.1": + version: 3.29.0 + resolution: "cytoscape@npm:3.29.0" + checksum: df8477484799e9c178f4a9b7c9fd66f05cef0ca4fa7d28ff25a662bcca672f32c4462c2bf05d9a3462a9b860cf3e50725b7cd662ca2b58e415672cffd9b4d84a + languageName: node + linkType: hard + +"d3-array@npm:1 - 2": + version: 2.12.1 + resolution: "d3-array@npm:2.12.1" + dependencies: + internmap: ^1.0.0 + checksum: 97853b7b523aded17078f37c67742f45d81e88dda2107ae9994c31b9e36c5fa5556c4c4cf39650436f247813602dfe31bf7ad067ff80f127a16903827f10c6eb + languageName: node + linkType: hard + +"d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3, d3-array@npm:2.5.0 - 3, d3-array@npm:3, d3-array@npm:^3.2.0": + version: 3.2.4 + resolution: "d3-array@npm:3.2.4" + dependencies: + internmap: 1 - 2 + checksum: a5976a6d6205f69208478bb44920dd7ce3e788c9dceb86b304dbe401a4bfb42ecc8b04c20facde486e9adcb488b5d1800d49393a3f81a23902b68158e12cddd0 + languageName: node + linkType: hard + +"d3-axis@npm:3": + version: 3.0.0 + resolution: "d3-axis@npm:3.0.0" + checksum: 227ddaa6d4bad083539c1ec245e2228b4620cca941997a8a650cb0af239375dc20271993127eedac66f0543f331027aca09385e1e16eed023f93eac937cddf0b + languageName: node + linkType: hard + +"d3-brush@npm:3": + version: 3.0.0 + resolution: "d3-brush@npm:3.0.0" + dependencies: + d3-dispatch: 1 - 3 + d3-drag: 2 - 3 + d3-interpolate: 1 - 3 + d3-selection: 3 + d3-transition: 3 + checksum: 1d042167769a02ac76271c71e90376d7184206e489552b7022a8ec2860209fe269db55e0a3430f3dcbe13b6fec2ff65b1adeaccba3218991b38e022390df72e3 + languageName: node + linkType: hard + +"d3-chord@npm:3": + version: 3.0.1 + resolution: "d3-chord@npm:3.0.1" + dependencies: + d3-path: 1 - 3 + checksum: ddf35d41675e0f8738600a8a2f05bf0858def413438c12cba357c5802ecc1014c80a658acbbee63cbad2a8c747912efb2358455d93e59906fe37469f1dc6b78b + languageName: node + linkType: hard + +"d3-color@npm:1 - 3, d3-color@npm:3": + version: 3.1.0 + resolution: "d3-color@npm:3.1.0" + checksum: 4931fbfda5d7c4b5cfa283a13c91a954f86e3b69d75ce588d06cde6c3628cebfc3af2069ccf225e982e8987c612aa7948b3932163ce15eb3c11cd7c003f3ee3b + languageName: node + linkType: hard + +"d3-contour@npm:4": + version: 4.0.2 + resolution: "d3-contour@npm:4.0.2" + dependencies: + d3-array: ^3.2.0 + checksum: 56aa082c1acf62a45b61c8d29fdd307041785aa17d9a07de7d1d848633769887a33fb6823888afa383f31c460d0f21d24756593e84e334ddb92d774214d32f1b + languageName: node + linkType: hard + +"d3-delaunay@npm:6": + version: 6.0.4 + resolution: "d3-delaunay@npm:6.0.4" + dependencies: + delaunator: 5 + checksum: ce6d267d5ef21a8aeadfe4606329fc80a22ab6e7748d47bc220bcc396ee8be84b77a5473033954c5ac4aa522d265ddc45d4165d30fe4787dd60a15ea66b9bbb4 + languageName: node + linkType: hard + +"d3-dispatch@npm:1 - 3, d3-dispatch@npm:3": + version: 3.0.1 + resolution: "d3-dispatch@npm:3.0.1" + checksum: fdfd4a230f46463e28e5b22a45dd76d03be9345b605e1b5dc7d18bd7ebf504e6c00ae123fd6d03e23d9e2711e01f0e14ea89cd0632545b9f0c00b924ba4be223 + languageName: node + linkType: hard + +"d3-drag@npm:2 - 3, d3-drag@npm:3": + version: 3.0.0 + resolution: "d3-drag@npm:3.0.0" + dependencies: + d3-dispatch: 1 - 3 + d3-selection: 3 + checksum: d297231e60ecd633b0d076a63b4052b436ddeb48b5a3a11ff68c7e41a6774565473a6b064c5e9256e88eca6439a917ab9cea76032c52d944ddbf4fd289e31111 + languageName: node + linkType: hard + +"d3-dsv@npm:1 - 3, d3-dsv@npm:3": + version: 3.0.1 + resolution: "d3-dsv@npm:3.0.1" + dependencies: + commander: 7 + iconv-lite: 0.6 + rw: 1 + bin: + csv2json: bin/dsv2json.js + csv2tsv: bin/dsv2dsv.js + dsv2dsv: bin/dsv2dsv.js + dsv2json: bin/dsv2json.js + json2csv: bin/json2dsv.js + json2dsv: bin/json2dsv.js + json2tsv: bin/json2dsv.js + tsv2csv: bin/dsv2dsv.js + tsv2json: bin/dsv2json.js + checksum: 5fc0723647269d5dccd181d74f2265920ab368a2868b0b4f55ffa2fecdfb7814390ea28622cd61ee5d9594ab262879509059544e9f815c54fe76fbfb4ffa4c8a + languageName: node + linkType: hard + +"d3-ease@npm:1 - 3, d3-ease@npm:3": + version: 3.0.1 + resolution: "d3-ease@npm:3.0.1" + checksum: 06e2ee5326d1e3545eab4e2c0f84046a123dcd3b612e68858219aa034da1160333d9ce3da20a1d3486d98cb5c2a06f7d233eee1bc19ce42d1533458bd85dedcd + languageName: node + linkType: hard + +"d3-fetch@npm:3": + version: 3.0.1 + resolution: "d3-fetch@npm:3.0.1" + dependencies: + d3-dsv: 1 - 3 + checksum: 382dcea06549ef82c8d0b719e5dc1d96286352579e3b51b20f71437f5800323315b09cf7dcfd4e1f60a41e1204deb01758470cea257d2285a7abd9dcec806984 + languageName: node + linkType: hard + +"d3-force@npm:3": + version: 3.0.0 + resolution: "d3-force@npm:3.0.0" + dependencies: + d3-dispatch: 1 - 3 + d3-quadtree: 1 - 3 + d3-timer: 1 - 3 + checksum: 6c7e96438cab62fa32aeadb0ade3297b62b51f81b1b38b0a60a5ec9fd627d74090c1189654d92df2250775f31b06812342f089f1d5947de9960a635ee3581def + languageName: node + linkType: hard + +"d3-format@npm:1 - 3, d3-format@npm:3": + version: 3.1.0 + resolution: "d3-format@npm:3.1.0" + checksum: f345ec3b8ad3cab19bff5dead395bd9f5590628eb97a389b1dd89f0b204c7c4fc1d9520f13231c2c7cf14b7c9a8cf10f8ef15bde2befbab41454a569bd706ca2 + languageName: node + linkType: hard + +"d3-geo@npm:3": + version: 3.1.1 + resolution: "d3-geo@npm:3.1.1" + dependencies: + d3-array: 2.5.0 - 3 + checksum: 3cc4bb50af5d2d4858d2df1729a1777b7fd361854079d9faab1166186c988d2cba0d11911da0c4598d5e22fae91d79113ed262a9f98cabdbc6dbf7c30e5c0363 + languageName: node + linkType: hard + +"d3-hierarchy@npm:3": + version: 3.1.2 + resolution: "d3-hierarchy@npm:3.1.2" + checksum: 0fd946a8c5fd4686d43d3e11bbfc2037a145fda29d2261ccd0e36f70b66af6d7638e2c0c7112124d63fc3d3127197a00a6aecf676bd5bd392a94d7235a214263 + languageName: node + linkType: hard + +"d3-interpolate@npm:1 - 3, d3-interpolate@npm:1.2.0 - 3, d3-interpolate@npm:3": + version: 3.0.1 + resolution: "d3-interpolate@npm:3.0.1" + dependencies: + d3-color: 1 - 3 + checksum: a42ba314e295e95e5365eff0f604834e67e4a3b3c7102458781c477bd67e9b24b6bb9d8e41ff5521050a3f2c7c0c4bbbb6e187fd586daa3980943095b267e78b + languageName: node + linkType: hard + +"d3-path@npm:1": + version: 1.0.9 + resolution: "d3-path@npm:1.0.9" + checksum: d4382573baf9509a143f40944baeff9fead136926aed6872f7ead5b3555d68925f8a37935841dd51f1d70b65a294fe35c065b0906fb6e42109295f6598fc16d0 + languageName: node + linkType: hard + +"d3-path@npm:1 - 3, d3-path@npm:3, d3-path@npm:^3.1.0": + version: 3.1.0 + resolution: "d3-path@npm:3.1.0" + checksum: 2306f1bd9191e1eac895ec13e3064f732a85f243d6e627d242a313f9777756838a2215ea11562f0c7630c7c3b16a19ec1fe0948b1c82f3317fac55882f6ee5d8 + languageName: node + linkType: hard + +"d3-polygon@npm:3": + version: 3.0.1 + resolution: "d3-polygon@npm:3.0.1" + checksum: 0b85c532517895544683849768a2c377cee3801ef8ccf3fa9693c8871dd21a0c1a2a0fc75ff54192f0ba2c562b0da2bc27f5bf959dfafc7fa23573b574865d2c + languageName: node + linkType: hard + +"d3-quadtree@npm:1 - 3, d3-quadtree@npm:3": + version: 3.0.1 + resolution: "d3-quadtree@npm:3.0.1" + checksum: 5469d462763811475f34a7294d984f3eb100515b0585ca5b249656f6b1a6e99b20056a2d2e463cc9944b888896d2b1d07859c50f9c0cf23438df9cd2e3146066 + languageName: node + linkType: hard + +"d3-random@npm:3": + version: 3.0.1 + resolution: "d3-random@npm:3.0.1" + checksum: a70ad8d1cabe399ebeb2e482703121ac8946a3b336830b518da6848b9fdd48a111990fc041dc716f16885a72176ffa2898f2a250ca3d363ecdba5ef92b18e131 + languageName: node + linkType: hard + +"d3-sankey@npm:^0.12.3": + version: 0.12.3 + resolution: "d3-sankey@npm:0.12.3" + dependencies: + d3-array: 1 - 2 + d3-shape: ^1.2.0 + checksum: df1cb9c9d02dd8fd14040e89f112f0da58c03bd7529fa001572a6925a51496d1d82ff25d9fedb6c429a91645fbd2476c19891e535ac90c8bc28337c33ee21c87 + languageName: node + linkType: hard + +"d3-scale-chromatic@npm:3": + version: 3.1.0 + resolution: "d3-scale-chromatic@npm:3.1.0" + dependencies: + d3-color: 1 - 3 + d3-interpolate: 1 - 3 + checksum: ab6324bd8e1f708e731e02ab44e09741efda2b174cea1d8ca21e4a87546295e99856bc44e2fd3890f228849c96bccfbcf922328f95be6a7df117453eb5cf22c9 + languageName: node + linkType: hard + +"d3-scale@npm:4": + version: 4.0.2 + resolution: "d3-scale@npm:4.0.2" + dependencies: + d3-array: 2.10.0 - 3 + d3-format: 1 - 3 + d3-interpolate: 1.2.0 - 3 + d3-time: 2.1.1 - 3 + d3-time-format: 2 - 4 + checksum: a9c770d283162c3bd11477c3d9d485d07f8db2071665f1a4ad23eec3e515e2cefbd369059ec677c9ac849877d1a765494e90e92051d4f21111aa56791c98729e + languageName: node + linkType: hard + +"d3-selection@npm:2 - 3, d3-selection@npm:3": + version: 3.0.0 + resolution: "d3-selection@npm:3.0.0" + checksum: f4e60e133309115b99f5b36a79ae0a19d71ee6e2d5e3c7216ef3e75ebd2cb1e778c2ed2fa4c01bef35e0dcbd96c5428f5bd6ca2184fe2957ed582fde6841cbc5 + languageName: node + linkType: hard + +"d3-shape@npm:3": + version: 3.2.0 + resolution: "d3-shape@npm:3.2.0" + dependencies: + d3-path: ^3.1.0 + checksum: de2af5fc9a93036a7b68581ca0bfc4aca2d5a328aa7ba7064c11aedd44d24f310c20c40157cb654359d4c15c3ef369f95ee53d71221017276e34172c7b719cfa + languageName: node + linkType: hard + +"d3-shape@npm:^1.2.0": + version: 1.3.7 + resolution: "d3-shape@npm:1.3.7" + dependencies: + d3-path: 1 + checksum: 46566a3ab64a25023653bf59d64e81e9e6c987e95be985d81c5cedabae5838bd55f4a201a6b69069ca862eb63594cd263cac9034afc2b0e5664dfe286c866129 + languageName: node + linkType: hard + +"d3-time-format@npm:2 - 4, d3-time-format@npm:4": + version: 4.1.0 + resolution: "d3-time-format@npm:4.1.0" + dependencies: + d3-time: 1 - 3 + checksum: 7342bce28355378152bbd4db4e275405439cabba082d9cd01946d40581140481c8328456d91740b0fe513c51ec4a467f4471ffa390c7e0e30ea30e9ec98fcdf4 + languageName: node + linkType: hard + +"d3-time@npm:1 - 3, d3-time@npm:2.1.1 - 3, d3-time@npm:3": + version: 3.1.0 + resolution: "d3-time@npm:3.1.0" + dependencies: + d3-array: 2 - 3 + checksum: 613b435352a78d9f31b7f68540788186d8c331b63feca60ad21c88e9db1989fe888f97f242322ebd6365e45ec3fb206a4324cd4ca0dfffa1d9b5feb856ba00a7 + languageName: node + linkType: hard + +"d3-timer@npm:1 - 3, d3-timer@npm:3": + version: 3.0.1 + resolution: "d3-timer@npm:3.0.1" + checksum: 1cfddf86d7bca22f73f2c427f52dfa35c49f50d64e187eb788dcad6e927625c636aa18ae4edd44d084eb9d1f81d8ca4ec305dae7f733c15846a824575b789d73 + languageName: node + linkType: hard + +"d3-transition@npm:2 - 3, d3-transition@npm:3": + version: 3.0.1 + resolution: "d3-transition@npm:3.0.1" + dependencies: + d3-color: 1 - 3 + d3-dispatch: 1 - 3 + d3-ease: 1 - 3 + d3-interpolate: 1 - 3 + d3-timer: 1 - 3 + peerDependencies: + d3-selection: 2 - 3 + checksum: cb1e6e018c3abf0502fe9ff7b631ad058efb197b5e14b973a410d3935aead6e3c07c67d726cfab258e4936ef2667c2c3d1cd2037feb0765f0b4e1d3b8788c0ea + languageName: node + linkType: hard + +"d3-zoom@npm:3": + version: 3.0.0 + resolution: "d3-zoom@npm:3.0.0" + dependencies: + d3-dispatch: 1 - 3 + d3-drag: 2 - 3 + d3-interpolate: 1 - 3 + d3-selection: 2 - 3 + d3-transition: 2 - 3 + checksum: 8056e3527281cfd1ccbcbc458408f86973b0583e9dac00e51204026d1d36803ca437f970b5736f02fafed9f2b78f145f72a5dbc66397e02d4d95d4c594b8ff54 + languageName: node + linkType: hard + +"d3@npm:^7.4.0, d3@npm:^7.8.2": + version: 7.9.0 + resolution: "d3@npm:7.9.0" + dependencies: + d3-array: 3 + d3-axis: 3 + d3-brush: 3 + d3-chord: 3 + d3-color: 3 + d3-contour: 4 + d3-delaunay: 6 + d3-dispatch: 3 + d3-drag: 3 + d3-dsv: 3 + d3-ease: 3 + d3-fetch: 3 + d3-force: 3 + d3-format: 3 + d3-geo: 3 + d3-hierarchy: 3 + d3-interpolate: 3 + d3-path: 3 + d3-polygon: 3 + d3-quadtree: 3 + d3-random: 3 + d3-scale: 4 + d3-scale-chromatic: 3 + d3-selection: 3 + d3-shape: 3 + d3-time: 3 + d3-time-format: 4 + d3-timer: 3 + d3-transition: 3 + d3-zoom: 3 + checksum: 1c0e9135f1fb78aa32b187fafc8b56ae6346102bd0e4e5e5a5339611a51e6038adbaa293fae373994228100eddd87320e930b1be922baeadc07c9fd43d26d99b + languageName: node + linkType: hard + "d@npm:1, d@npm:^1.0.1": version: 1.0.1 resolution: "d@npm:1.0.1" @@ -10492,6 +10936,16 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"dagre-d3-es@npm:7.0.10": + version: 7.0.10 + resolution: "dagre-d3-es@npm:7.0.10" + dependencies: + d3: ^7.8.2 + lodash-es: ^4.17.21 + checksum: 25194e80dfad48db0dc2e0a273a7c9fcbfdc4cf993b219eaa1e0e0ce0cbb8c63be42fa2aa0c5f9bf9b324c34b8b2e300bb2a1606d5ae35c2de00f9c4ac317d8e + languageName: node + linkType: hard + "dargs@npm:^7.0.0": version: 7.0.0 resolution: "dargs@npm:7.0.0" @@ -10526,6 +10980,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"dayjs@npm:^1.11.7": + version: 1.11.10 + resolution: "dayjs@npm:1.11.10" + checksum: a6b5a3813b8884f5cd557e2e6b7fa569f4c5d0c97aca9558e38534af4f2d60daafd3ff8c2000fed3435cfcec9e805bcebd99f90130c6d1c5ef524084ced588c4 + languageName: node + linkType: hard + "debounce@npm:^1.2.1": version: 1.2.1 resolution: "debounce@npm:1.2.1" @@ -10758,6 +11219,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"delaunator@npm:5": + version: 5.0.1 + resolution: "delaunator@npm:5.0.1" + dependencies: + robust-predicates: ^3.0.2 + checksum: 69ee43ec649b4a13b7f33c8a027fb3e8dfcb09266af324286118da757e04d3d39df619b905dca41421405c311317ccf632ecfa93db44519bacec3303c57c5a0b + languageName: node + linkType: hard + "delayed-stream@npm:~1.0.0": version: 1.0.0 resolution: "delayed-stream@npm:1.0.0" @@ -10901,6 +11371,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"diff@npm:^5.0.0": + version: 5.2.0 + resolution: "diff@npm:5.2.0" + checksum: 12b63ca9c36c72bafa3effa77121f0581b4015df18bc16bac1f8e263597735649f1a173c26f7eba17fb4162b073fee61788abe49610e6c70a2641fe1895443fd + languageName: node + linkType: hard + "dir-glob@npm:^3.0.1": version: 3.0.1 resolution: "dir-glob@npm:3.0.1" @@ -11024,6 +11501,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"dompurify@npm:^3.0.5": + version: 3.1.0 + resolution: "dompurify@npm:3.1.0" + checksum: 06fc76607cd076e394b2ea5479ab6f0407b8fedb6877ae95e94207b878365e5e1cd914055dacce152a5f419818afb8d4cd284b780246cf35363f0747c179a0ba + languageName: node + linkType: hard + "domutils@npm:^1.7.0": version: 1.7.0 resolution: "domutils@npm:1.7.0" @@ -11202,6 +11686,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"elkjs@npm:^0.9.0": + version: 0.9.3 + resolution: "elkjs@npm:0.9.3" + checksum: 1293e42e0ea034b39d3719f3816b7b3cbaceb52a3114f2c1bd5ddd969bb1e36ae0afef58e77864fff7a1018dc5e96c177e9b0a40c16e4aaac26eb87f5785be4b + languageName: node + linkType: hard + "emberplus-connection@npm:^0.2.1": version: 0.2.1 resolution: "emberplus-connection@npm:0.2.1" @@ -13994,7 +14485,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": +"iconv-lite@npm:0.6, iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" dependencies: @@ -14290,6 +14781,20 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"internmap@npm:1 - 2": + version: 2.0.3 + resolution: "internmap@npm:2.0.3" + checksum: 7ca41ec6aba8f0072fc32fa8a023450a9f44503e2d8e403583c55714b25efd6390c38a87161ec456bf42d7bc83aab62eb28f5aef34876b1ac4e60693d5e1d241 + languageName: node + linkType: hard + +"internmap@npm:^1.0.0": + version: 1.0.1 + resolution: "internmap@npm:1.0.1" + checksum: 9d00f8c0cf873a24a53a5a937120dab634c41f383105e066bb318a61864e6292d24eb9516e8e7dccfb4420ec42ca474a0f28ac9a6cc82536898fa09bbbe53813 + languageName: node + linkType: hard + "interpret@npm:^1.0.0": version: 1.4.0 resolution: "interpret@npm:1.4.0" @@ -15812,6 +16317,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"katex@npm:^0.16.9": + version: 0.16.10 + resolution: "katex@npm:0.16.10" + dependencies: + commander: ^8.3.0 + bin: + katex: cli.js + checksum: 108e9d810e17840c43eef8d46171096f4cc97852bfd1e2dd1890d9b3435846816e3e98678a31d38bd064eb97eea83b18ff224cb65d5f9511b54ce7ff4359b591 + languageName: node + linkType: hard + "keyv@npm:^4.0.0, keyv@npm:^4.5.3": version: 4.5.3 resolution: "keyv@npm:4.5.3" @@ -15821,6 +16337,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"khroma@npm:^2.0.0": + version: 2.1.0 + resolution: "khroma@npm:2.1.0" + checksum: b34ba39d3a9a52d388110bded8cb1c12272eb69c249d8eb26feab12d18a96a9bc4ceec4851d2afa43de4569f7d5ea78fa305965a3d0e96a38e02fe77c53677da + languageName: node + linkType: hard + "kind-of@npm:^6.0.0, kind-of@npm:^6.0.2, kind-of@npm:^6.0.3": version: 6.0.3 resolution: "kind-of@npm:6.0.3" @@ -15835,6 +16358,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"kleur@npm:^4.0.3": + version: 4.1.5 + resolution: "kleur@npm:4.1.5" + checksum: 1dc476e32741acf0b1b5b0627ffd0d722e342c1b0da14de3e8ae97821327ca08f9fb944542fb3c126d90ac5f27f9d804edbe7c585bf7d12ef495d115e0f22c12 + languageName: node + linkType: hard + "klona@npm:^2.0.6": version: 2.0.6 resolution: "klona@npm:2.0.6" @@ -15868,6 +16398,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"layout-base@npm:^1.0.0": + version: 1.0.2 + resolution: "layout-base@npm:1.0.2" + checksum: e4c312765ac4fa13b49c940e701461309c7a0aa07f784f81d31f626b945dced90a8abf83222388a5af16b7074271f745501a90ef5a3af676abb2e7eb16d55b2e + languageName: node + linkType: hard + "lerna@npm:^6.6.2": version: 6.6.2 resolution: "lerna@npm:6.6.2" @@ -16215,6 +16752,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"lodash-es@npm:^4.17.21": + version: 4.17.21 + resolution: "lodash-es@npm:4.17.21" + checksum: 05cbffad6e2adbb331a4e16fbd826e7faee403a1a04873b82b42c0f22090f280839f85b95393f487c1303c8a3d2a010048bf06151a6cbe03eee4d388fb0a12d2 + languageName: node + linkType: hard + "lodash.debounce@npm:^4.0.8": version: 4.0.8 resolution: "lodash.debounce@npm:4.0.8" @@ -16613,6 +17157,26 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"mdast-util-from-markdown@npm:^1.3.0": + version: 1.3.1 + resolution: "mdast-util-from-markdown@npm:1.3.1" + dependencies: + "@types/mdast": ^3.0.0 + "@types/unist": ^2.0.0 + decode-named-character-reference: ^1.0.0 + mdast-util-to-string: ^3.1.0 + micromark: ^3.0.0 + micromark-util-decode-numeric-character-reference: ^1.0.0 + micromark-util-decode-string: ^1.0.0 + micromark-util-normalize-identifier: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + unist-util-stringify-position: ^3.0.0 + uvu: ^0.5.0 + checksum: c2fac225167e248d394332a4ea39596e04cbde07d8cdb3889e91e48972c4c3462a02b39fda3855345d90231eb17a90ac6e082fb4f012a77c1d0ddfb9c7446940 + languageName: node + linkType: hard + "mdast-util-from-markdown@npm:^2.0.0": version: 2.0.0 resolution: "mdast-util-from-markdown@npm:2.0.0" @@ -16829,6 +17393,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"mdast-util-to-string@npm:^3.1.0": + version: 3.2.0 + resolution: "mdast-util-to-string@npm:3.2.0" + dependencies: + "@types/mdast": ^3.0.0 + checksum: dc40b544d54339878ae2c9f2b3198c029e1e07291d2126bd00ca28272ee6616d0d2194eb1c9828a7c34d412a79a7e73b26512a734698d891c710a1e73db1e848 + languageName: node + linkType: hard + "mdast-util-to-string@npm:^4.0.0": version: 4.0.0 resolution: "mdast-util-to-string@npm:4.0.0" @@ -16980,6 +17553,34 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"mermaid@npm:^10.4.0": + version: 10.9.0 + resolution: "mermaid@npm:10.9.0" + dependencies: + "@braintree/sanitize-url": ^6.0.1 + "@types/d3-scale": ^4.0.3 + "@types/d3-scale-chromatic": ^3.0.0 + cytoscape: ^3.28.1 + cytoscape-cose-bilkent: ^4.1.0 + d3: ^7.4.0 + d3-sankey: ^0.12.3 + dagre-d3-es: 7.0.10 + dayjs: ^1.11.7 + dompurify: ^3.0.5 + elkjs: ^0.9.0 + katex: ^0.16.9 + khroma: ^2.0.0 + lodash-es: ^4.17.21 + mdast-util-from-markdown: ^1.3.0 + non-layered-tidy-tree-layout: ^2.0.2 + stylis: ^4.1.3 + ts-dedent: ^2.2.0 + uuid: ^9.0.0 + web-worker: ^1.2.0 + checksum: 73ee57b365fbfba8ad72b2ce611a2703753dc155884db3bec4a47951326b9061b31926f84fc0ad67ca203067b62db636f1543fa6f633a6afa24cd4c754b96cf0 + languageName: node + linkType: hard + "methods@npm:~1.1.2": version: 1.1.2 resolution: "methods@npm:1.1.2" @@ -16987,6 +17588,30 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"micromark-core-commonmark@npm:^1.0.1": + version: 1.1.0 + resolution: "micromark-core-commonmark@npm:1.1.0" + dependencies: + decode-named-character-reference: ^1.0.0 + micromark-factory-destination: ^1.0.0 + micromark-factory-label: ^1.0.0 + micromark-factory-space: ^1.0.0 + micromark-factory-title: ^1.0.0 + micromark-factory-whitespace: ^1.0.0 + micromark-util-character: ^1.0.0 + micromark-util-chunked: ^1.0.0 + micromark-util-classify-character: ^1.0.0 + micromark-util-html-tag-name: ^1.0.0 + micromark-util-normalize-identifier: ^1.0.0 + micromark-util-resolve-all: ^1.0.0 + micromark-util-subtokenize: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.1 + uvu: ^0.5.0 + checksum: c6dfedc95889cc73411cb222fc2330b9eda6d849c09c9fd9eb3cd3398af246167e9d3cdb0ae3ce9ae59dd34a14624c8330e380255d41279ad7350cf6c6be6c5b + languageName: node + linkType: hard + "micromark-core-commonmark@npm:^2.0.0": version: 2.0.0 resolution: "micromark-core-commonmark@npm:2.0.0" @@ -17207,6 +17832,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"micromark-factory-destination@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-factory-destination@npm:1.1.0" + dependencies: + micromark-util-character: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + checksum: 9e2b5fb5fedbf622b687e20d51eb3d56ae90c0e7ecc19b37bd5285ec392c1e56f6e21aa7cfcb3c01eda88df88fe528f3acb91a5f57d7f4cba310bc3cd7f824fa + languageName: node + linkType: hard + "micromark-factory-destination@npm:^2.0.0": version: 2.0.0 resolution: "micromark-factory-destination@npm:2.0.0" @@ -17218,6 +17854,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"micromark-factory-label@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-factory-label@npm:1.1.0" + dependencies: + micromark-util-character: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + uvu: ^0.5.0 + checksum: fcda48f1287d9b148c562c627418a2ab759cdeae9c8e017910a0cba94bb759a96611e1fc6df33182e97d28fbf191475237298983bb89ef07d5b02464b1ad28d5 + languageName: node + linkType: hard + "micromark-factory-label@npm:^2.0.0": version: 2.0.0 resolution: "micromark-factory-label@npm:2.0.0" @@ -17266,6 +17914,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"micromark-factory-title@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-factory-title@npm:1.1.0" + dependencies: + micromark-factory-space: ^1.0.0 + micromark-util-character: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + checksum: 4432d3dbc828c81f483c5901b0c6591a85d65a9e33f7d96ba7c3ae821617a0b3237ff5faf53a9152d00aaf9afb3a9f185b205590f40ed754f1d9232e0e9157b1 + languageName: node + linkType: hard + "micromark-factory-title@npm:^2.0.0": version: 2.0.0 resolution: "micromark-factory-title@npm:2.0.0" @@ -17278,6 +17938,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"micromark-factory-whitespace@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-factory-whitespace@npm:1.1.0" + dependencies: + micromark-factory-space: ^1.0.0 + micromark-util-character: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + checksum: ef0fa682c7d593d85a514ee329809dee27d10bc2a2b65217d8ef81173e33b8e83c549049764b1ad851adfe0a204dec5450d9d20a4ca8598f6c94533a73f73fcd + languageName: node + linkType: hard + "micromark-factory-whitespace@npm:^2.0.0": version: 2.0.0 resolution: "micromark-factory-whitespace@npm:2.0.0" @@ -17310,6 +17982,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"micromark-util-chunked@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-util-chunked@npm:1.1.0" + dependencies: + micromark-util-symbol: ^1.0.0 + checksum: c435bde9110cb595e3c61b7f54c2dc28ee03e6a57fa0fc1e67e498ad8bac61ee5a7457a2b6a73022ddc585676ede4b912d28dcf57eb3bd6951e54015e14dc20b + languageName: node + linkType: hard + "micromark-util-chunked@npm:^2.0.0": version: 2.0.0 resolution: "micromark-util-chunked@npm:2.0.0" @@ -17319,6 +18000,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"micromark-util-classify-character@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-util-classify-character@npm:1.1.0" + dependencies: + micromark-util-character: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + checksum: 8499cb0bb1f7fb946f5896285fcca65cd742f66cd3e79ba7744792bd413ec46834f932a286de650349914d02e822946df3b55d03e6a8e1d245d1ddbd5102e5b0 + languageName: node + linkType: hard + "micromark-util-classify-character@npm:^2.0.0": version: 2.0.0 resolution: "micromark-util-classify-character@npm:2.0.0" @@ -17330,6 +18022,16 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"micromark-util-combine-extensions@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-util-combine-extensions@npm:1.1.0" + dependencies: + micromark-util-chunked: ^1.0.0 + micromark-util-types: ^1.0.0 + checksum: ee78464f5d4b61ccb437850cd2d7da4d690b260bca4ca7a79c4bb70291b84f83988159e373b167181b6716cb197e309bc6e6c96a68cc3ba9d50c13652774aba9 + languageName: node + linkType: hard + "micromark-util-combine-extensions@npm:^2.0.0": version: 2.0.0 resolution: "micromark-util-combine-extensions@npm:2.0.0" @@ -17340,6 +18042,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"micromark-util-decode-numeric-character-reference@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-util-decode-numeric-character-reference@npm:1.1.0" + dependencies: + micromark-util-symbol: ^1.0.0 + checksum: 4733fe75146e37611243f055fc6847137b66f0cde74d080e33bd26d0408c1d6f44cabc984063eee5968b133cb46855e729d555b9ff8d744652262b7b51feec73 + languageName: node + linkType: hard + "micromark-util-decode-numeric-character-reference@npm:^2.0.0": version: 2.0.1 resolution: "micromark-util-decode-numeric-character-reference@npm:2.0.1" @@ -17349,6 +18060,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"micromark-util-decode-string@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-util-decode-string@npm:1.1.0" + dependencies: + decode-named-character-reference: ^1.0.0 + micromark-util-character: ^1.0.0 + micromark-util-decode-numeric-character-reference: ^1.0.0 + micromark-util-symbol: ^1.0.0 + checksum: f1625155db452f15aa472918499689ba086b9c49d1322a08b22bfbcabe918c61b230a3002c8bc3ea9b1f52ca7a9bb1c3dd43ccb548c7f5f8b16c24a1ae77a813 + languageName: node + linkType: hard + "micromark-util-decode-string@npm:^2.0.0": version: 2.0.0 resolution: "micromark-util-decode-string@npm:2.0.0" @@ -17361,6 +18084,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"micromark-util-encode@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-util-encode@npm:1.1.0" + checksum: 4ef29d02b12336918cea6782fa87c8c578c67463925221d4e42183a706bde07f4b8b5f9a5e1c7ce8c73bb5a98b261acd3238fecd152e6dd1cdfa2d1ae11b60a0 + languageName: node + linkType: hard + "micromark-util-encode@npm:^2.0.0": version: 2.0.0 resolution: "micromark-util-encode@npm:2.0.0" @@ -17384,6 +18114,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"micromark-util-html-tag-name@npm:^1.0.0": + version: 1.2.0 + resolution: "micromark-util-html-tag-name@npm:1.2.0" + checksum: ccf0fa99b5c58676dc5192c74665a3bfd1b536fafaf94723bd7f31f96979d589992df6fcf2862eba290ef18e6a8efb30ec8e1e910d9f3fc74f208871e9f84750 + languageName: node + linkType: hard + "micromark-util-html-tag-name@npm:^2.0.0": version: 2.0.0 resolution: "micromark-util-html-tag-name@npm:2.0.0" @@ -17391,6 +18128,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"micromark-util-normalize-identifier@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-util-normalize-identifier@npm:1.1.0" + dependencies: + micromark-util-symbol: ^1.0.0 + checksum: 8655bea41ffa4333e03fc22462cb42d631bbef9c3c07b625fd852b7eb442a110f9d2e5902a42e65188d85498279569502bf92f3434a1180fc06f7c37edfbaee2 + languageName: node + linkType: hard + "micromark-util-normalize-identifier@npm:^2.0.0": version: 2.0.0 resolution: "micromark-util-normalize-identifier@npm:2.0.0" @@ -17400,6 +18146,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"micromark-util-resolve-all@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-util-resolve-all@npm:1.1.0" + dependencies: + micromark-util-types: ^1.0.0 + checksum: 1ce6c0237cd3ca061e76fae6602cf95014e764a91be1b9f10d36cb0f21ca88f9a07de8d49ab8101efd0b140a4fbfda6a1efb72027ab3f4d5b54c9543271dc52c + languageName: node + linkType: hard + "micromark-util-resolve-all@npm:^2.0.0": version: 2.0.0 resolution: "micromark-util-resolve-all@npm:2.0.0" @@ -17409,6 +18164,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"micromark-util-sanitize-uri@npm:^1.0.0": + version: 1.2.0 + resolution: "micromark-util-sanitize-uri@npm:1.2.0" + dependencies: + micromark-util-character: ^1.0.0 + micromark-util-encode: ^1.0.0 + micromark-util-symbol: ^1.0.0 + checksum: 6663f365c4fe3961d622a580f4a61e34867450697f6806f027f21cf63c92989494895fcebe2345d52e249fe58a35be56e223a9776d084c9287818b40c779acc1 + languageName: node + linkType: hard + "micromark-util-sanitize-uri@npm:^2.0.0": version: 2.0.0 resolution: "micromark-util-sanitize-uri@npm:2.0.0" @@ -17420,6 +18186,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"micromark-util-subtokenize@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-util-subtokenize@npm:1.1.0" + dependencies: + micromark-util-chunked: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + uvu: ^0.5.0 + checksum: 4a9d780c4d62910e196ea4fd886dc4079d8e424e5d625c0820016da0ed399a281daff39c50f9288045cc4bcd90ab47647e5396aba500f0853105d70dc8b1fc45 + languageName: node + linkType: hard + "micromark-util-subtokenize@npm:^2.0.0": version: 2.0.1 resolution: "micromark-util-subtokenize@npm:2.0.1" @@ -17446,7 +18224,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"micromark-util-types@npm:^1.0.0": +"micromark-util-types@npm:^1.0.0, micromark-util-types@npm:^1.0.1": version: 1.1.0 resolution: "micromark-util-types@npm:1.1.0" checksum: b0ef2b4b9589f15aec2666690477a6a185536927ceb7aa55a0f46475852e012d75a1ab945187e5c7841969a842892164b15d58ff8316b8e0d6cc920cabd5ede7 @@ -17460,6 +18238,31 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"micromark@npm:^3.0.0": + version: 3.2.0 + resolution: "micromark@npm:3.2.0" + dependencies: + "@types/debug": ^4.0.0 + debug: ^4.0.0 + decode-named-character-reference: ^1.0.0 + micromark-core-commonmark: ^1.0.1 + micromark-factory-space: ^1.0.0 + micromark-util-character: ^1.0.0 + micromark-util-chunked: ^1.0.0 + micromark-util-combine-extensions: ^1.0.0 + micromark-util-decode-numeric-character-reference: ^1.0.0 + micromark-util-encode: ^1.0.0 + micromark-util-normalize-identifier: ^1.0.0 + micromark-util-resolve-all: ^1.0.0 + micromark-util-sanitize-uri: ^1.0.0 + micromark-util-subtokenize: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.1 + uvu: ^0.5.0 + checksum: 56c15851ad3eb8301aede65603473443e50c92a54849cac1dadd57e4ec33ab03a0a77f3df03de47133e6e8f695dae83b759b514586193269e98c0bf319ecd5e4 + languageName: node + linkType: hard + "micromark@npm:^4.0.0": version: 4.0.0 resolution: "micromark@npm:4.0.0" @@ -17923,6 +18726,13 @@ asn1@evs-broadcast/node-asn1: languageName: unknown linkType: soft +"mri@npm:^1.1.0": + version: 1.2.0 + resolution: "mri@npm:1.2.0" + checksum: 83f515abbcff60150873e424894a2f65d68037e5a7fcde8a9e2b285ee9c13ac581b63cfc1e6826c4732de3aeb84902f7c1e16b7aff46cd3f897a0f757a894e85 + languageName: node + linkType: hard + "mrmime@npm:^1.0.0": version: 1.0.1 resolution: "mrmime@npm:1.0.1" @@ -18231,6 +19041,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"non-layered-tidy-tree-layout@npm:^2.0.2": + version: 2.0.2 + resolution: "non-layered-tidy-tree-layout@npm:2.0.2" + checksum: 5defc1c459001b22816a4fb8b86259b9b76e7f3090df576122a41c760133ab2061934cacd6f176c98c2ae4fee3879b97941e8897e8882985cbfe830f155cd158 + languageName: node + linkType: hard + "nopt@npm:^4.0.1": version: 4.0.3 resolution: "nopt@npm:4.0.3" @@ -21759,6 +22576,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"robust-predicates@npm:^3.0.2": + version: 3.0.2 + resolution: "robust-predicates@npm:3.0.2" + checksum: 36854c1321548ceca96d36ad9d6e0a5a512986029ec6929ad6ed3ec1612c22cc8b46cc72d2c5674af42e8074a119d793f6f0ea3a5b51373e3ab926c64b172d7a + languageName: node + linkType: hard + "rollup@npm:^2.60.1": version: 2.79.1 resolution: "rollup@npm:2.79.1" @@ -21819,6 +22643,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"rw@npm:1": + version: 1.3.3 + resolution: "rw@npm:1.3.3" + checksum: c20d82421f5a71c86a13f76121b751553a99cd4a70ea27db86f9b23f33db941f3f06019c30f60d50c356d0bd674c8e74764ac146ea55e217c091bde6fba82aa3 + languageName: node + linkType: hard + "rxjs@npm:7.8.1, rxjs@npm:^7.5.5": version: 7.8.1 resolution: "rxjs@npm:7.8.1" @@ -21837,6 +22668,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"sade@npm:^1.7.3": + version: 1.8.1 + resolution: "sade@npm:1.8.1" + dependencies: + mri: ^1.1.0 + checksum: 0756e5b04c51ccdc8221ebffd1548d0ce5a783a44a0fa9017a026659b97d632913e78f7dca59f2496aa996a0be0b0c322afd87ca72ccd909406f49dbffa0f45d + languageName: node + linkType: hard + "safe-array-concat@npm:^1.0.0, safe-array-concat@npm:^1.0.1": version: 1.0.1 resolution: "safe-array-concat@npm:1.0.1" @@ -22458,6 +23298,7 @@ asn1@evs-broadcast/node-asn1: "@docusaurus/core": 3.2.1 "@docusaurus/module-type-aliases": 3.2.1 "@docusaurus/preset-classic": 3.2.1 + "@docusaurus/theme-mermaid": ^3.2.1 "@docusaurus/types": 3.2.1 "@mdx-js/react": ^3.0.0 "@svgr/webpack": ^5.5.0 @@ -23069,6 +23910,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"stylis@npm:^4.1.3": + version: 4.3.1 + resolution: "stylis@npm:4.3.1" + checksum: d365f1b008677b2147e8391e9cf20094a4202a5f9789562e7d9d0a3bd6f0b3067d39e8fd17cce5323903a56f6c45388e3d839e9c0bb5a738c91726992b14966d + languageName: node + linkType: hard + "superfly-timeline@npm:9.0.0, superfly-timeline@npm:^9.0.0": version: 9.0.0 resolution: "superfly-timeline@npm:9.0.0" @@ -23744,6 +24592,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"ts-dedent@npm:^2.2.0": + version: 2.2.0 + resolution: "ts-dedent@npm:2.2.0" + checksum: 93ed8f7878b6d5ed3c08d99b740010eede6bccfe64bce61c5a4da06a2c17d6ddbb80a8c49c2d15251de7594a4f93ffa21dd10e7be75ef66a4dc9951b4a94e2af + languageName: node + linkType: hard + "ts-essentials@npm:^7.0.3": version: 7.0.3 resolution: "ts-essentials@npm:7.0.3" @@ -24348,6 +25203,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"unist-util-stringify-position@npm:^3.0.0": + version: 3.0.3 + resolution: "unist-util-stringify-position@npm:3.0.3" + dependencies: + "@types/unist": ^2.0.0 + checksum: dbd66c15183607ca942a2b1b7a9f6a5996f91c0d30cf8966fb88955a02349d9eefd3974e9010ee67e71175d784c5a9fea915b0aa0b0df99dcb921b95c4c9e124 + languageName: node + linkType: hard + "unist-util-stringify-position@npm:^4.0.0": version: 4.0.0 resolution: "unist-util-stringify-position@npm:4.0.0" @@ -24591,6 +25455,29 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"uuid@npm:^9.0.0": + version: 9.0.1 + resolution: "uuid@npm:9.0.1" + bin: + uuid: dist/bin/uuid + checksum: 39931f6da74e307f51c0fb463dc2462807531dc80760a9bff1e35af4316131b4fc3203d16da60ae33f07fdca5b56f3f1dd662da0c99fea9aaeab2004780cc5f4 + languageName: node + linkType: hard + +"uvu@npm:^0.5.0": + version: 0.5.6 + resolution: "uvu@npm:0.5.6" + dependencies: + dequal: ^2.0.0 + diff: ^5.0.0 + kleur: ^4.0.3 + sade: ^1.7.3 + bin: + uvu: bin.js + checksum: 09460a37975627de9fcad396e5078fb844d01aaf64a6399ebfcfd9e55f1c2037539b47611e8631f89be07656962af0cf48c334993db82b9ae9c3d25ce3862168 + languageName: node + linkType: hard + "v8-compile-cache-lib@npm:^3.0.1": version: 3.0.1 resolution: "v8-compile-cache-lib@npm:3.0.1" @@ -24805,6 +25692,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"web-worker@npm:^1.2.0": + version: 1.3.0 + resolution: "web-worker@npm:1.3.0" + checksum: ed1f869aefd1d81a43d0fbfe7b315a65beb6d7d2486b378c436a7047eed4216be34b2e6afca738b6fa95d016326b765f5f816355db33267dbf43b2b8a1837c0c + languageName: node + linkType: hard + "webapi-parser@npm:^0.5.0": version: 0.5.0 resolution: "webapi-parser@npm:0.5.0" From 158e6603eda370e97e4b2fdaf23b5df229e8ff4a Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Thu, 16 May 2024 07:47:37 +0200 Subject: [PATCH 298/479] feat: add SourceLayer.STUDIO_SCREEN --- meteor/client/styles/_itemTypeColors.scss | 5 ++++- meteor/lib/api/rest.ts | 1 + meteor/server/api/rest/v1/typeConversion.ts | 6 ++++++ packages/shared-lib/src/core/model/ShowStyle.ts | 2 ++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/meteor/client/styles/_itemTypeColors.scss b/meteor/client/styles/_itemTypeColors.scss index e6c364bc76..fbd4853c8f 100644 --- a/meteor/client/styles/_itemTypeColors.scss +++ b/meteor/client/styles/_itemTypeColors.scss @@ -33,11 +33,13 @@ $segment-layer-background-mic: #1e6820; $segment-layer-background-guest: #008a92; $segment-layer-background-local: #9a2bd8; $segment-layer-background-local--second: darken($segment-layer-background-local, 10%); +$segment-layer-background-studio-screen: #641fb3; $segment-item-disabled-background: #898989; $segment-item-disabled-color: #c9c9c9; -$layer-types-solid: 'unknown', 'camera', 'lower-third', 'graphics', 'remote', 'vt', 'local', 'script', 'mic', 'guest'; +$layer-types-solid: 'unknown', 'camera', 'lower-third', 'graphics', 'remote', 'vt', 'local', 'script', 'mic', 'guest', + 'studio-screen'; $layer-types-gradient: 'live-speak'; // $layer-types: join($layer-types-solid, $layer-types-gradient); @@ -457,4 +459,5 @@ $layer-types: join($layer-types-solid, $layer-types-gradient); --segment-layer-background-script: #{$segment-layer-background-script}; --segment-layer-background-mic: #{$segment-layer-background-mic}; --segment-layer-background-guest: #{$segment-layer-background-guest}; + --segment-layer-background-studio-screen: #{$segment-layer-background-studio-screen}; } diff --git a/meteor/lib/api/rest.ts b/meteor/lib/api/rest.ts index 19cb9df2ee..46b52d4822 100644 --- a/meteor/lib/api/rest.ts +++ b/meteor/lib/api/rest.ts @@ -753,6 +753,7 @@ export interface APISourceLayer { | 'live-speak' | 'transition' | 'local' + | 'studio-screen' exclusiveGroup?: string } diff --git a/meteor/server/api/rest/v1/typeConversion.ts b/meteor/server/api/rest/v1/typeConversion.ts index d445174fe6..11c20443b3 100644 --- a/meteor/server/api/rest/v1/typeConversion.ts +++ b/meteor/server/api/rest/v1/typeConversion.ts @@ -169,6 +169,9 @@ export function sourceLayerFrom(apiSourceLayer: APISourceLayer): ISourceLayer { case 'vt': layerType = SourceLayerType.VT break + case 'studio-screen': + layerType = SourceLayerType.STUDIO_SCREEN + break default: layerType = SourceLayerType.UNKNOWN assertNever(apiSourceLayer.layerType) @@ -223,6 +226,9 @@ export function APISourceLayerFrom(sourceLayer: ISourceLayer): APISourceLayer { case SourceLayerType.VT: layerType = 'vt' break + case SourceLayerType.STUDIO_SCREEN: + layerType = 'studio-screen' + break default: layerType = 'unknown' assertNever(sourceLayer.type) diff --git a/packages/shared-lib/src/core/model/ShowStyle.ts b/packages/shared-lib/src/core/model/ShowStyle.ts index 9f791c9f24..fb254f3ae5 100644 --- a/packages/shared-lib/src/core/model/ShowStyle.ts +++ b/packages/shared-lib/src/core/model/ShowStyle.ts @@ -24,6 +24,8 @@ export enum SourceLayerType { // LIGHTS = 14, /** Uncontrolled local sources, such as PowerPoint presentation inputs, Weather systems, EVS replay machines, etc. */ LOCAL = 15, + /** Sources that are intended for a studio screen, such as weather, presentations etc. */ + STUDIO_SCREEN = 16, } /** A single source layer, f.g Cameras, VT, Graphics, Remotes */ From fbdcd03a175f397918e848e36d5ecced9964059b Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 16 May 2024 09:31:26 +0100 Subject: [PATCH 299/479] chore: remove unused code --- .../ui/FloatingInspectors/NoraFloatingInspector.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/meteor/client/ui/FloatingInspectors/NoraFloatingInspector.tsx b/meteor/client/ui/FloatingInspectors/NoraFloatingInspector.tsx index bc882af37c..b77a6f9890 100644 --- a/meteor/client/ui/FloatingInspectors/NoraFloatingInspector.tsx +++ b/meteor/client/ui/FloatingInspectors/NoraFloatingInspector.tsx @@ -148,14 +148,6 @@ export class NoraPreviewRenderer extends React.Component<{}, IStateHeader> { private _setIFrameElement = (e: HTMLIFrameElement | null) => { if (!e) return this.iframeElement = e - - // set up IntersectionObserver to keep the preview inside the viewport - const options = { - threshold: [] as number[], - } - for (let i = 0; i < 50; i++) { - options.threshold.push(i / 50) - } } private _onNoraMessage = (msg: MessageEvent): void => { From c5a6292a85821d67653eebef75bbd929ff0cd44d Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Mon, 7 Aug 2023 11:19:16 +0200 Subject: [PATCH 300/479] feat(Device Triggers): add support for setting shift register operations SOFIE-3136 chore: update documentation fix: ensure that Shift register Ids are only positive integers fix: test feat(Device Triggers): add support for setting shift register operations --- .../actionSelector/ActionSelector.tsx | 89 ++++++++++++++++++- meteor/client/ui/TestTools/DeviceTriggers.tsx | 2 +- meteor/lib/api/triggers/MountedTriggers.ts | 47 +++------- meteor/lib/api/triggers/actionFactory.ts | 8 ++ .../StudioDeviceTriggerManager.ts | 16 ++++ .../blueprints-integration/src/triggers.ts | 12 +++ .../user-guide/configuration/settings-view.md | 8 ++ .../installation/installing-input-gateway.md | 12 +++ .../src/__tests__/index.spec.ts | 2 +- .../shared-lib/src/core/model/ShowStyle.ts | 6 +- .../input-gateway/deviceTriggerPreviews.ts | 12 +++ 11 files changed, 174 insertions(+), 40 deletions(-) diff --git a/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx b/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx index 2e3d5cb67e..7972212a2f 100644 --- a/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx +++ b/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx @@ -11,6 +11,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faAngleRight, faTrash } from '@fortawesome/free-solid-svg-icons' import { AdLibActionEditor } from './actionEditors/AdLibActionEditor' import { catchError } from '../../../../../../lib/lib' +import { DeviceActions } from '@sofie-automation/shared-lib/dist/core/model/ShowStyle' interface IProps { action: SomeAction @@ -93,6 +94,9 @@ function getArguments(t: TFunction, action: SomeAction): string[] { case ClientActions.miniShelfQueueAdLib: result.push(t('Forward: {{forward}}', { forward: action.forward })) break + case DeviceActions.modifyShiftRegister: + result.push(`${action.register ?? '?'}: ${action.operation ?? '?'}${action.value ?? '?'}`) + break default: assertNever(action) return action @@ -134,6 +138,8 @@ function hasArguments(action: SomeAction): boolean { return true case ClientActions.miniShelfQueueAdLib: return true + case DeviceActions.modifyShiftRegister: + return true default: assertNever(action) return action @@ -174,6 +180,8 @@ function actionToLabel(t: TFunction, action: SomeAction['action']): string { return t('Show entire On Air Segment') case ClientActions.miniShelfQueueAdLib: return t('Queue AdLib from Minishelf') + case DeviceActions.modifyShiftRegister: + return t('Modify Shift register') default: assertNever(action) return action @@ -181,7 +189,7 @@ function actionToLabel(t: TFunction, action: SomeAction['action']): string { } function getAvailableActions(t: TFunction): Record { - const actionEnums = [PlayoutActions, ClientActions] + const actionEnums = [PlayoutActions, ClientActions, DeviceActions] const result: Record = {} @@ -387,6 +395,85 @@ function getActionParametersEditor( /> ) + case DeviceActions.modifyShiftRegister: + return ( +
+ + { + onChange({ + ...action, + register: Math.max(0, Number(newVal)), + }) + }} + /> + + { + onChange({ + ...action, + operation: newVal, + }) + }} + /> + + { + onChange({ + ...action, + value: newVal, + }) + }} + /> + + { + onChange({ + ...action, + limitMin: newVal, + }) + }} + /> + + { + onChange({ + ...action, + limitMax: newVal, + }) + }} + /> +
+ ) default: assertNever(action) return action diff --git a/meteor/client/ui/TestTools/DeviceTriggers.tsx b/meteor/client/ui/TestTools/DeviceTriggers.tsx index 6f0d743ee2..8344355d84 100644 --- a/meteor/client/ui/TestTools/DeviceTriggers.tsx +++ b/meteor/client/ui/TestTools/DeviceTriggers.tsx @@ -104,7 +104,7 @@ function DeviceTriggersControls({ peripheralDeviceId }: IDatastoreControlsProps) .filter((preview) => preview.actionId === entry.actionId) .map((preview) => ( - {JSON.stringify(preview.label)}: {preview.type} {preview.sourceLayerType}{' '} + {JSON.stringify(preview.label)}: {String(preview.type)} {preview.sourceLayerType}{' '} {preview.sourceLayerName?.name}{' '} {preview.sourceLayerName?.abbreviation ? `(${preview.sourceLayerName.abbreviation})` : null} diff --git a/meteor/lib/api/triggers/MountedTriggers.ts b/meteor/lib/api/triggers/MountedTriggers.ts index 5fbe29f5e4..580157595f 100644 --- a/meteor/lib/api/triggers/MountedTriggers.ts +++ b/meteor/lib/api/triggers/MountedTriggers.ts @@ -1,16 +1,23 @@ -import { ISourceLayer, ITranslatableMessage, SourceLayerType } from '@sofie-automation/blueprints-integration' +import { ISourceLayer, ITranslatableMessage } from '@sofie-automation/blueprints-integration' import { AdLibActionId, PieceId, RundownBaselineAdLibActionId, - ShowStyleBaseId, - StudioId, TriggeredActionId, } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' -import { ExecutableAction } from './actionFactory' import { IWrappedAdLib } from './actionFilterChainCompilers' +export { + DeviceActionId, + DeviceTriggerMountedActionId, + ShiftRegisterActionArguments, + DeviceTriggerMountedAction, + PreviewWrappedAdLibId, + PreviewWrappedAdLib, + IWrappedAdLibBase, +} from '@sofie-automation/shared-lib/dist/input-gateway/deviceTriggerPreviews' + export type MountedTrigger = (MountedGenericTrigger | MountedAdLibTrigger) & MountedHotkeyMixin export type MountedDeviceTrigger = (MountedGenericTrigger | MountedAdLibTrigger) & MountedDeviceMixin @@ -63,35 +70,3 @@ export interface MountedAdLibTrigger extends MountedTriggerCommon { } export type MountedAdLibTriggerId = ProtectedString<'mountedAdLibTriggerId'> - -export type DeviceTriggerMountedActionId = ProtectedString<'deviceTriggerMountedActionId'> - -export type DeviceActionId = ProtectedString<'DeviceActionId'> - -export interface DeviceTriggerMountedAction { - _id: DeviceTriggerMountedActionId - studioId: StudioId - showStyleBaseId: ShowStyleBaseId - deviceId: string - deviceTriggerId: string - values: DeviceTriggerArguments - actionId: DeviceActionId - actionType: ExecutableAction['action'] - name?: string | ITranslatableMessage -} - -export type PreviewWrappedAdLibId = ProtectedString<'previewWrappedAdLibId'> -export type PreviewWrappedAdLib = Omit & { - _id: PreviewWrappedAdLibId - studioId: StudioId - showStyleBaseId: ShowStyleBaseId - triggeredActionId: TriggeredActionId - actionId: DeviceActionId - sourceLayerType?: SourceLayerType - sourceLayerName?: { - name?: string - abbreviation?: string - } - isCurrent?: boolean - isNext?: boolean -} diff --git a/meteor/lib/api/triggers/actionFactory.ts b/meteor/lib/api/triggers/actionFactory.ts index 3857b09958..e068ed9505 100644 --- a/meteor/lib/api/triggers/actionFactory.ts +++ b/meteor/lib/api/triggers/actionFactory.ts @@ -34,6 +34,7 @@ import { PartId, PartInstanceId, RundownId, RundownPlaylistId } from '@sofie-aut import { PartInstances, Parts } from '../../collections/libCollections' import { RundownPlaylistCollectionUtil } from '../../collections/rundownPlaylistUtil' import { hashSingleUseToken } from '../userActions' +import { DeviceActions } from '@sofie-automation/shared-lib/dist/core/model/ShowStyle' // as described in this issue: https://github.com/Microsoft/TypeScript/issues/14094 type Without = { [P in Exclude]?: never } @@ -577,6 +578,13 @@ export function createAction(action: SomeAction, sourceLayers: SourceLayers): Ex return createShowEntireCurrentSegmentAction(action.filterChain, action.on) case ClientActions.miniShelfQueueAdLib: return createMiniShelfQueueAdLibAction(action.filterChain, action.forward) + case DeviceActions.modifyShiftRegister: + return { + action: action.action, + execute: () => { + // do nothing + }, + } default: assertNever(action) break diff --git a/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts b/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts index 2ed95e7bf4..0ab7df6c2b 100644 --- a/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts +++ b/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts @@ -13,6 +13,7 @@ import { DeviceActionId, DeviceTriggerMountedActionId, PreviewWrappedAdLibId, + ShiftRegisterActionArguments, } from '../../../lib/api/triggers/MountedTriggers' import { isDeviceTrigger } from '../../../lib/api/triggers/triggerTypeSelectors' import { DBTriggeredActions, UITriggeredActionsObj } from '../../../lib/collections/TriggeredActions' @@ -22,6 +23,7 @@ import { DeviceTriggerMountedActionAdlibsPreview, DeviceTriggerMountedActions } import { ContentCache } from './reactiveContentCache' import { logger } from '../../logging' import { SomeAction, SomeBlueprintTrigger } from '@sofie-automation/blueprints-integration' +import { DeviceActions } from '@sofie-automation/shared-lib/dist/core/model/ShowStyle' export class StudioDeviceTriggerManager { #lastShowStyleBaseId: ShowStyleBaseId | null = null @@ -102,6 +104,19 @@ export class StudioDeviceTriggerManager { return } + let deviceActionArguments: ShiftRegisterActionArguments | undefined = undefined + + if (action.action === DeviceActions.modifyShiftRegister) { + deviceActionArguments = { + type: 'modifyRegister', + register: action.register, + operation: action.operation, + value: action.value, + limitMin: action.limitMin, + limitMax: action.limitMax, + } + } + const deviceTriggerMountedActionId = protectString( `${actionId}_${key}` ) @@ -114,6 +129,7 @@ export class StudioDeviceTriggerManager { deviceId: trigger.deviceId, deviceTriggerId: trigger.triggerId, values: trigger.values, + deviceActionArguments, name: triggeredAction.name, }, }) diff --git a/packages/blueprints-integration/src/triggers.ts b/packages/blueprints-integration/src/triggers.ts index d50e6aea8d..46e0b1d534 100644 --- a/packages/blueprints-integration/src/triggers.ts +++ b/packages/blueprints-integration/src/triggers.ts @@ -4,6 +4,7 @@ import { SomeActionIdentifier, ClientActions, PlayoutActions, + DeviceActions, } from '@sofie-automation/shared-lib/dist/core/model/ShowStyle' export enum TriggerType { @@ -281,6 +282,16 @@ export interface IMiniShelfQueueAdLib extends ITriggeredActionBase { forward: boolean // TODO: Change this to use `delta: number`, as opposed to `forward: boolean` } +export interface IModifyShiftRegister extends ITriggeredActionBase { + action: DeviceActions.modifyShiftRegister + filterChain: IGUIContextFilterLink[] + register: number + value: number + operation: '+' | '-' | '=' + limitMin: number + limitMax: number +} + export type SomeAction = | IAdlibPlayoutAction | IRundownPlaylistActivateAction @@ -298,6 +309,7 @@ export type SomeAction = | IRewindSegmentsAction | IShowEntireCurrentSegmentAction | IMiniShelfQueueAdLib + | IModifyShiftRegister export interface IBlueprintTriggeredActions { _id: string diff --git a/packages/documentation/docs/user-guide/configuration/settings-view.md b/packages/documentation/docs/user-guide/configuration/settings-view.md index 2a4a3e6a25..b6ced3ae8c 100644 --- a/packages/documentation/docs/user-guide/configuration/settings-view.md +++ b/packages/documentation/docs/user-guide/configuration/settings-view.md @@ -149,6 +149,8 @@ To edit a given trigger, click on the trigger pill on the left of the Trigger-Ac Device Triggers are valid in the scope of a Studio and will be evaluated on the currently active Rundown in a given Studio. To use Device Triggers, you need to have at least a single [Input Gateway](../installation/installing-input-gateway) attached to a Studio and a Device configured in the Input Gateway. Once that's done, when selecting a **Device** trigger type in the pop-up, you can invoke triggers on your Input Device and you will see a preview of the input events shown at the bottom of the pop-up. You can select which of these events should be the trigger by clicking on one of the previews. Note, that some devices differentiate between _Up_ and _Down_ triggers, while others don't. Some may also have other activites that can be done _to_ a trigger. What they are and how they are identified is device-specific and is best discovered through interaction with the device. +If you would like to set up combination Triggers, using Device Triggers on an Input Device that does not support them natively, you may want to look into [Shift Registers](#shift-registers) + #### Actions The actions are built using a base *action* (such as *Activate a Rundown* or *AdLib*) and a set of *filters*, limiting the scope of the *action*. Optionally, some of these *actions* can take additional *parameters*. These filters can operate on various types of objects, depending on the action in question. All actions currently require that the chain of filters starts with scoping out the Rundown the action is supposed to affect. Currently, there is only one type of Rundown-level filter supported: "The Rundown currently in view". @@ -163,6 +165,12 @@ If the action provides a preview of the triggered items and there is an availabl Clicking on the action and filter pills allows you to edit the action parameters and filter parameters. *Limit* limits the amount of objects to only the first *N* objects matched - this can significantly improve performance on large data sets. *Pick* and *Pick last* filters end the chain of the filters by selecting a single item from the filtered set of objects (the *N-th* object from the beginning or the end, respectively). *Pick* implicitly contains a *Limit* for the performance improvement. This is not true for *Pick last*, though. +##### Shift Registers + +Shift Register modification actions are a special type of an Action, that modifies an internal state memory of the [Input Gateway](../installation/installing-input-gateway.md) and allows combination triggers, pagination, etc. on devices that don't natively support them or combining multiple devices into a single Control Surface. Refer to _Input Gateway_ documentation for more information on Shift Registers. + +Shift Register actions have no effect in the browser, triggered from a _Hotkey_. + ## Migrations The migrations are automatic setup-scripts that help you during initial setup and system upgrades. diff --git a/packages/documentation/docs/user-guide/installation/installing-input-gateway.md b/packages/documentation/docs/user-guide/installation/installing-input-gateway.md index 5d809d74af..0eb54752af 100644 --- a/packages/documentation/docs/user-guide/installation/installing-input-gateway.md +++ b/packages/documentation/docs/user-guide/installation/installing-input-gateway.md @@ -27,6 +27,18 @@ Currently, input gateway supports: * OSC * HTTP +## Input Gateway-specific functions + +### Shift Registers + +Input Gateway supports the concept of _Shift Registers_. A Shift Register is an internal variable/state that can be modified using Actions, from within [Action Triggers](../configuration/settings-view.md#actions). This allows for things such as pagination, _Hold Shift + Another Button_ scenarios, and others on input devices that don't support these features natively. _Shift Registers_ are also global for all devices attached to a single Input Gateway. This allows combining multiple Input devices into a single Control Surface. + +When one of the _Shift Registers_ is set to a value other than `0` (their default state), all triggers sent from that Input Gateway become prefixed with a serialized state of the state registers, making the combination of a _Shift Registers_ state and a trigger unique. + +If you would like to have the same trigger cause the same action in various Shift Register states, add multiple Triggers to the same Action, with different Shift Register combinations. + +Input Gateway supports an unlimited number of Shift Registers, Shift Register numbering starts at 0. + ### Further Reading * [Input Gateway Releases on GitHub](https://github.com/nrkno/sofie-input-gateway/releases) diff --git a/packages/server-core-integration/src/__tests__/index.spec.ts b/packages/server-core-integration/src/__tests__/index.spec.ts index e5850b1e0f..c4a290a5df 100644 --- a/packages/server-core-integration/src/__tests__/index.spec.ts +++ b/packages/server-core-integration/src/__tests__/index.spec.ts @@ -118,7 +118,7 @@ describe('coreConnection', () => { // Subscribe to data: const coll0 = core.getCollection('peripheralDeviceForDevice') expect(coll0.findOne(id)).toBeFalsy() - const subId = await core.subscribe('peripheralDeviceForDevice', id) + const subId = await core.autoSubscribe('peripheralDeviceForDevice', id) const coll1 = core.getCollection('peripheralDeviceForDevice') expect(coll1.findOne(id)).toMatchObject({ _id: id, diff --git a/packages/shared-lib/src/core/model/ShowStyle.ts b/packages/shared-lib/src/core/model/ShowStyle.ts index fb254f3ae5..675c5a1d09 100644 --- a/packages/shared-lib/src/core/model/ShowStyle.ts +++ b/packages/shared-lib/src/core/model/ShowStyle.ts @@ -104,4 +104,8 @@ export enum ClientActions { 'miniShelfQueueAdLib' = 'miniShelfQueueAdLib', } -export type SomeActionIdentifier = PlayoutActions | ClientActions +export enum DeviceActions { + 'modifyShiftRegister' = 'modifyShiftRegister', +} + +export type SomeActionIdentifier = PlayoutActions | ClientActions | DeviceActions diff --git a/packages/shared-lib/src/input-gateway/deviceTriggerPreviews.ts b/packages/shared-lib/src/input-gateway/deviceTriggerPreviews.ts index 49671b70e2..738c486029 100644 --- a/packages/shared-lib/src/input-gateway/deviceTriggerPreviews.ts +++ b/packages/shared-lib/src/input-gateway/deviceTriggerPreviews.ts @@ -22,6 +22,17 @@ export interface IWrappedAdLibBase { item: unknown } +export interface ShiftRegisterActionArguments { + type: 'modifyRegister' + register: number + operation: '=' | '+' | '-' + value: number + limitMin: number + limitMax: number +} + +export type DeviceActionArguments = ShiftRegisterActionArguments + export interface DeviceTriggerMountedAction { _id: DeviceTriggerMountedActionId studioId: StudioId @@ -31,6 +42,7 @@ export interface DeviceTriggerMountedAction { values: DeviceTriggerArguments actionId: DeviceActionId actionType: SomeActionIdentifier + deviceActionArguments?: DeviceActionArguments name?: string | ITranslatableMessage } From 1c4bd0a36885cb582af88c2750b155de9b23fa2a Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Tue, 21 May 2024 08:04:30 +0200 Subject: [PATCH 301/479] fix: add missing SourceLayerType cases --- .../StoryboardPartSecondaryPieces/StoryboardSecondaryPiece.tsx | 1 + .../Renderers/ThumbnailRendererFactory.ts | 1 + meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx | 2 ++ 3 files changed, 4 insertions(+) diff --git a/meteor/client/ui/SegmentStoryboard/StoryboardPartSecondaryPieces/StoryboardSecondaryPiece.tsx b/meteor/client/ui/SegmentStoryboard/StoryboardPartSecondaryPieces/StoryboardSecondaryPiece.tsx index e557eb1efb..b968127429 100644 --- a/meteor/client/ui/SegmentStoryboard/StoryboardPartSecondaryPieces/StoryboardSecondaryPiece.tsx +++ b/meteor/client/ui/SegmentStoryboard/StoryboardPartSecondaryPieces/StoryboardSecondaryPiece.tsx @@ -40,6 +40,7 @@ function renderPieceInside( return ScriptRenderer({ ...props, elementOffset, hovering, typeClass }) case SourceLayerType.GRAPHICS: case SourceLayerType.LOWER_THIRD: + case SourceLayerType.STUDIO_SCREEN: return GraphicsRenderer({ ...props, elementOffset, hovering, typeClass }) case SourceLayerType.SPLITS: return SplitsRenderer({ ...props, elementOffset, hovering, typeClass }) diff --git a/meteor/client/ui/SegmentStoryboard/StoryboardPartThumbnail/Renderers/ThumbnailRendererFactory.ts b/meteor/client/ui/SegmentStoryboard/StoryboardPartThumbnail/Renderers/ThumbnailRendererFactory.ts index a1af0549f1..24a47229c1 100644 --- a/meteor/client/ui/SegmentStoryboard/StoryboardPartThumbnail/Renderers/ThumbnailRendererFactory.ts +++ b/meteor/client/ui/SegmentStoryboard/StoryboardPartThumbnail/Renderers/ThumbnailRendererFactory.ts @@ -40,6 +40,7 @@ export default function renderThumbnail(props: IProps): JSX.Element { return SplitsThumbnailRenderer(props) case SourceLayerType.GRAPHICS: case SourceLayerType.LOWER_THIRD: + case SourceLayerType.STUDIO_SCREEN: return GraphicsThumbnailRenderer(props) case SourceLayerType.LOCAL: return LocalThumbnailRenderer(props) diff --git a/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx b/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx index e04e571d5c..2fa58e93a6 100644 --- a/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx +++ b/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx @@ -61,6 +61,8 @@ function sourceLayerString(t: TFunction<'translation', undefined>, type: SourceL // return t('Lights') case SourceLayerType.LOCAL: return t('Local') + case SourceLayerType.STUDIO_SCREEN: + return t('Studio Screen') default: assertNever(type) return SourceLayerType[type] From 45eefcef737e8e8a5a7e425f9fdd713487ab4a59 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Tue, 21 May 2024 08:18:01 +0200 Subject: [PATCH 302/479] fix: add missing SourceLayerType cases --- meteor/client/ui/SegmentList/PieceHoverInspector.tsx | 1 + meteor/client/ui/SegmentTimeline/SourceLayerItem.tsx | 1 + meteor/client/ui/Shelf/DashboardPieceButton.tsx | 3 ++- meteor/client/ui/Shelf/Renderers/ItemRendererFactory.ts | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/meteor/client/ui/SegmentList/PieceHoverInspector.tsx b/meteor/client/ui/SegmentList/PieceHoverInspector.tsx index 28dbd7c347..3be3985362 100644 --- a/meteor/client/ui/SegmentList/PieceHoverInspector.tsx +++ b/meteor/client/ui/SegmentList/PieceHoverInspector.tsx @@ -62,6 +62,7 @@ export function PieceHoverInspector({ ) case SourceLayerType.GRAPHICS: case SourceLayerType.LOWER_THIRD: + case SourceLayerType.STUDIO_SCREEN: return ( extends MeteorReactComponent< : this.props.layer.type === SourceLayerType.SPLITS ? this.renderSplits(isList && this.props.showThumbnailsInList) : this.props.layer.type === SourceLayerType.GRAPHICS || - this.props.layer.type === SourceLayerType.LOWER_THIRD + this.props.layer.type === SourceLayerType.LOWER_THIRD || + this.props.layer.type === SourceLayerType.STUDIO_SCREEN ? this.renderGraphics(isButtons || (isList && this.props.showThumbnailsInList)) : null} diff --git a/meteor/client/ui/Shelf/Renderers/ItemRendererFactory.ts b/meteor/client/ui/Shelf/Renderers/ItemRendererFactory.ts index 390cf115d6..5de28c9c0b 100644 --- a/meteor/client/ui/Shelf/Renderers/ItemRendererFactory.ts +++ b/meteor/client/ui/Shelf/Renderers/ItemRendererFactory.ts @@ -27,6 +27,7 @@ export default function renderItem(props: ILayerItemRendererProps): JSX.Element return React.createElement(VTListItemRenderer, props) case SourceLayerType.GRAPHICS: case SourceLayerType.LOWER_THIRD: + case SourceLayerType.STUDIO_SCREEN: return React.createElement(L3rdListItemRenderer, props) } From 3daaff052db261abdccdb30e170dbe2f91ecf2d8 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 22 May 2024 14:37:54 +0100 Subject: [PATCH 303/479] feat: add import/export support to config object tables SOFIE-3138 (#1194) --- .../lib/Components/LabelAndOverrides.tsx | 4 +- .../lib/forms/SchemaFormForCollection.tsx | 56 ++++--- meteor/client/lib/forms/SchemaFormInPlace.tsx | 20 ++- .../lib/forms/SchemaFormTable/ArrayTable.tsx | 12 +- .../SchemaFormTable/ArrayTableOpHelper.tsx | 29 +++- .../forms/SchemaFormTable/ArrayTableRow.tsx | 2 +- .../lib/forms/SchemaFormTable/ObjectTable.tsx | 150 +++++++++++++++++- .../SchemaFormTable/ObjectTableOpHelper.tsx | 54 +++++-- .../ui/Settings/ShowStyle/OutputLayer.tsx | 12 +- .../ui/Settings/ShowStyle/SourceLayer.tsx | 12 +- .../Studio/Devices/GenericSubDevices.tsx | 11 +- meteor/client/ui/Settings/Studio/Mappings.tsx | 10 +- .../triggeredActions/TriggeredActionEntry.tsx | 10 +- .../actionEditors/ActionEditor.tsx | 8 +- .../ui/Settings/util/OverrideOpHelper.tsx | 130 +++++++-------- .../docs/for-developers/json-config-schema.md | 4 + packages/shared-lib/src/lib/JSONSchemaUtil.ts | 5 + 17 files changed, 376 insertions(+), 153 deletions(-) diff --git a/meteor/client/lib/Components/LabelAndOverrides.tsx b/meteor/client/lib/Components/LabelAndOverrides.tsx index 0da6eb5970..a25f6477dc 100644 --- a/meteor/client/lib/Components/LabelAndOverrides.tsx +++ b/meteor/client/lib/Components/LabelAndOverrides.tsx @@ -38,11 +38,11 @@ export function LabelAndOverrides({ const { t } = useTranslation() const clearOverride = useCallback(() => { - overrideHelper.clearItemOverrides(opPrefix, String(itemKey)) + overrideHelper().clearItemOverrides(opPrefix, String(itemKey)).commit() }, [overrideHelper, opPrefix, itemKey]) const setValue = useCallback( (newValue: any) => { - overrideHelper.setItemValue(opPrefix, String(itemKey), newValue) + overrideHelper().setItemValue(opPrefix, String(itemKey), newValue).commit() }, [overrideHelper, opPrefix, itemKey] ) diff --git a/meteor/client/lib/forms/SchemaFormForCollection.tsx b/meteor/client/lib/forms/SchemaFormForCollection.tsx index 0f7b23f7fd..d288262177 100644 --- a/meteor/client/lib/forms/SchemaFormForCollection.tsx +++ b/meteor/client/lib/forms/SchemaFormForCollection.tsx @@ -4,11 +4,15 @@ import { ObjectOverrideDeleteOp, ObjectOverrideSetOp, } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import React, { useMemo } from 'react' +import React, { useCallback, useMemo } from 'react' import { MongoCollection } from '../../../lib/collections/lib' -import { WrappedOverridableItemNormal, OverrideOpHelperForItemContents } from '../../ui/Settings/util/OverrideOpHelper' +import { + WrappedOverridableItemNormal, + OverrideOpHelperForItemContentsBatcher, +} from '../../ui/Settings/util/OverrideOpHelper' import { SchemaFormCommonProps } from './schemaFormUtil' import { SchemaFormWithOverrides } from './SchemaFormWithOverrides' +import { MongoModifier } from '@sofie-automation/corelib/dist/mongo' interface SchemaFormForCollectionProps extends Omit { /** The collection to operate on */ @@ -35,7 +39,7 @@ export function SchemaFormForCollection({ partialOverridesForObject, ...commonProps }: SchemaFormForCollectionProps): JSX.Element { - const helper = useMemo( + const helper = useCallback( () => new OverrideOpHelperCollection(collection, objectId, basePath), [collection, objectId, basePath] ) @@ -87,37 +91,47 @@ export function SchemaFormForCollection({ * An alternate OverrideOpHelper designed to directly mutate a collection, instead of using the `ObjectWithOverrides` system. * This allows us to have one SchemaForm implementation that can handle working with `ObjectWithOverrides`, and basic objects in mongodb */ -class OverrideOpHelperCollection implements OverrideOpHelperForItemContents { +class OverrideOpHelperCollection implements OverrideOpHelperForItemContentsBatcher { readonly #collection: MongoCollection readonly #objectId: ProtectedString readonly #basePath: string + #changes: MongoModifier | undefined + constructor(collection: MongoCollection, objectId: ProtectedString, basePath: string) { this.#collection = collection this.#objectId = objectId this.#basePath = basePath } - clearItemOverrides(_itemId: string, subPath: string): void { - this.#collection.update(this.#objectId, { - $unset: { - [`${this.#basePath}.${subPath}`]: 1, - }, - }) + clearItemOverrides(_itemId: string, subPath: string): this { + if (!this.#changes) this.#changes = {} + if (!this.#changes.$unset) this.#changes.$unset = {} + + this.#changes.$unset[`${this.#basePath}.${subPath}`] = 1 + + return this } - setItemValue(_itemId: string, subPath: string, value: any): void { + setItemValue(_itemId: string, subPath: string, value: any): this { + if (!this.#changes) this.#changes = {} + if (value === undefined) { - this.#collection.update(this.#objectId, { - $unset: { - [`${this.#basePath}.${subPath}`]: 1, - }, - }) + if (!this.#changes.$unset) this.#changes.$unset = {} + this.#changes.$unset[`${this.#basePath}.${subPath}`] = 1 } else { - this.#collection.update(this.#objectId, { - $set: { - [`${this.#basePath}.${subPath}`]: value, - }, - }) + if (!this.#changes.$set) this.#changes.$set = {} + ;(this.#changes.$set[`${this.#basePath}.${subPath}`] as any) = value + } + + return this + } + + commit(): void { + if (!this.#changes) { + const changesToSave = this.#changes + this.#changes = undefined + + this.#collection.update(this.#objectId, changesToSave) } } } diff --git a/meteor/client/lib/forms/SchemaFormInPlace.tsx b/meteor/client/lib/forms/SchemaFormInPlace.tsx index a9492acc9b..3771087bf9 100644 --- a/meteor/client/lib/forms/SchemaFormInPlace.tsx +++ b/meteor/client/lib/forms/SchemaFormInPlace.tsx @@ -1,6 +1,9 @@ import { literal, objectPathSet } from '@sofie-automation/corelib/dist/lib' import React, { useCallback, useMemo, useState } from 'react' -import { WrappedOverridableItemNormal, OverrideOpHelperForItemContents } from '../../ui/Settings/util/OverrideOpHelper' +import { + WrappedOverridableItemNormal, + OverrideOpHelperForItemContentsBatcher, +} from '../../ui/Settings/util/OverrideOpHelper' import { SchemaFormCommonProps } from './schemaFormUtil' import { SchemaFormWithOverrides } from './SchemaFormWithOverrides' @@ -13,7 +16,7 @@ export function SchemaFormInPlace({ object, ...commonProps }: SchemaFormInPlaceP const [editCount, setEditCount] = useState(0) const forceRender = useCallback(() => setEditCount((v) => v + 1), []) - const helper = useMemo(() => new OverrideOpHelperInPlace(object, forceRender), [object, forceRender]) + const helper = useCallback(() => new OverrideOpHelperInPlace(object, forceRender), [object, forceRender]) const wrappedItem = useMemo( () => @@ -34,7 +37,7 @@ export function SchemaFormInPlace({ object, ...commonProps }: SchemaFormInPlaceP * An alternate OverrideOpHelper designed to directly mutate an object, instead of using the `ObjectWithOverrides` system. * This allows us to have one SchemaForm implementation that can handle working with `ObjectWithOverrides`, and simpler options */ -class OverrideOpHelperInPlace implements OverrideOpHelperForItemContents { +class OverrideOpHelperInPlace implements OverrideOpHelperForItemContentsBatcher { readonly #object: any readonly #forceRender: () => void @@ -43,11 +46,18 @@ class OverrideOpHelperInPlace implements OverrideOpHelperForItemContents { this.#forceRender = forceRender } - clearItemOverrides(_itemId: string, _subPath: string): void { + clearItemOverrides(_itemId: string, _subPath: string): this { // Not supported as this is faking an item with overrides + + return this } - setItemValue(_itemId: string, subPath: string, value: any): void { + setItemValue(_itemId: string, subPath: string, value: any): this { objectPathSet(this.#object, subPath, value) + + return this + } + + commit(): void { this.#forceRender() } } diff --git a/meteor/client/lib/forms/SchemaFormTable/ArrayTable.tsx b/meteor/client/lib/forms/SchemaFormTable/ArrayTable.tsx index 751992dd1a..af028346ec 100644 --- a/meteor/client/lib/forms/SchemaFormTable/ArrayTable.tsx +++ b/meteor/client/lib/forms/SchemaFormTable/ArrayTable.tsx @@ -63,16 +63,16 @@ export const SchemaFormArrayTable = ({ newObj.push(getSchemaDefaultValues(schema.items)) // Send it onwards - overrideHelper.setItemValue(item.id, attr, newObj) + overrideHelper().setItemValue(item.id, attr, newObj).commit() }, [schema.items, overrideHelper, rowsArray, item.id, attr]) const resyncTable = useCallback( - () => overrideHelper.clearItemOverrides(item.id, attr), + () => overrideHelper().clearItemOverrides(item.id, attr).commit(), [overrideHelper, item.id, attr] ) - const tableOverrideHelper = useMemo( - () => new OverrideOpHelperArrayTable(overrideHelper, item.id, rowsArray, attr), + const tableOverrideHelper = useCallback( + () => new OverrideOpHelperArrayTable(overrideHelper(), item.id, rowsArray, attr), [overrideHelper, item.id, rows, attr] ) @@ -89,7 +89,9 @@ export const SchemaFormArrayTable = ({ no: t('Cancel'), yes: t('Remove'), onAccept: () => { - tableOverrideHelper.deleteRow(rowId + '') + tableOverrideHelper() + .deleteRow(rowId + '') + .commit() }, message: ( diff --git a/meteor/client/lib/forms/SchemaFormTable/ArrayTableOpHelper.tsx b/meteor/client/lib/forms/SchemaFormTable/ArrayTableOpHelper.tsx index ef12bcafb5..7790813efc 100644 --- a/meteor/client/lib/forms/SchemaFormTable/ArrayTableOpHelper.tsx +++ b/meteor/client/lib/forms/SchemaFormTable/ArrayTableOpHelper.tsx @@ -1,41 +1,56 @@ import { clone, objectPathSet } from '@sofie-automation/corelib/dist/lib' -import { OverrideOpHelperForItemContents } from '../../../ui/Settings/util/OverrideOpHelper' +import { OverrideOpHelperForItemContentsBatcher } from '../../../ui/Settings/util/OverrideOpHelper' /** * The OverrideOp system does not support Arrays currently. * This is intended to be a break point to avoid tables from attempting to define operations on arrays, * and instead make the table be treated as a single blob. */ -export class OverrideOpHelperArrayTable implements OverrideOpHelperForItemContents { - readonly #baseHelper: OverrideOpHelperForItemContents +export class OverrideOpHelperArrayTable implements OverrideOpHelperForItemContentsBatcher { + readonly #baseHelper: OverrideOpHelperForItemContentsBatcher readonly #itemId: string readonly #currentRows: unknown[] readonly #path: string - constructor(baseHelper: OverrideOpHelperForItemContents, itemId: string, currentRows: unknown[], path: string) { + constructor( + baseHelper: OverrideOpHelperForItemContentsBatcher, + itemId: string, + currentRows: unknown[], + path: string + ) { this.#baseHelper = baseHelper this.#itemId = itemId this.#currentRows = currentRows this.#path = path } - clearItemOverrides(_itemId: string, _subPath: string): void { + clearItemOverrides(_itemId: string, _subPath: string): this { // Not supported as this is faking an item with overrides + + return this } - deleteRow(rowId: string): void { + deleteRow(rowId: string): this { // Delete the row const newObj = clone(this.#currentRows) newObj.splice(Number(rowId), 1) // Send it onwards this.#baseHelper.setItemValue(this.#itemId, this.#path, newObj) + + return this } - setItemValue(rowId: string, subPath: string, value: unknown): void { + setItemValue(rowId: string, subPath: string, value: unknown): this { // Build the new object const newObj = clone(this.#currentRows) objectPathSet(newObj, `${rowId}.${subPath}`, value) // Send it onwards this.#baseHelper.setItemValue(this.#itemId, this.#path, newObj) + + return this + } + + commit(): void { + this.#baseHelper.commit() } } diff --git a/meteor/client/lib/forms/SchemaFormTable/ArrayTableRow.tsx b/meteor/client/lib/forms/SchemaFormTable/ArrayTableRow.tsx index 49355a6b1c..6ce78d6975 100644 --- a/meteor/client/lib/forms/SchemaFormTable/ArrayTableRow.tsx +++ b/meteor/client/lib/forms/SchemaFormTable/ArrayTableRow.tsx @@ -15,7 +15,7 @@ interface ArrayTableRowProps { translationNamespaces: string[] sofieEnumDefinitons: Record | undefined - overrideHelper: OverrideOpHelperArrayTable + overrideHelper: () => OverrideOpHelperArrayTable rowId: number rowObject: any diff --git a/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx b/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx index 45bb8e349c..22c0a3183e 100644 --- a/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx +++ b/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx @@ -1,12 +1,13 @@ -import { faPlus } from '@fortawesome/free-solid-svg-icons' +import { faDownload, faPlus, faUpload } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { getRandomString, joinObjectPathFragments, objectPathGet } from '@sofie-automation/corelib/dist/lib' -import React, { useCallback, useMemo } from 'react' +import React, { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { WrappedOverridableItemNormal, OverrideOpHelperForItemContents, getAllCurrentAndDeletedItemsFromOverrides, + WrappedOverridableItem, } from '../../../ui/Settings/util/OverrideOpHelper' import { useToggleExpandHelper } from '../../../ui/Settings/util/ToggleExpandedHelper' import { doModalDialog } from '../../ModalDialog' @@ -23,6 +24,8 @@ import { SchemaTableSummaryRow } from '../SchemaTableSummaryRow' import { OverrideOpHelperObjectTable } from './ObjectTableOpHelper' import { ObjectTableDeletedRow } from './ObjectTableDeletedRow' import { SchemaFormSectionHeader } from '../SchemaFormSectionHeader' +import { UploadButton } from '../../uploadButton' +import Tooltip from 'rc-tooltip' interface SchemaFormObjectTableProps { /** Schema for each row in the table */ @@ -85,18 +88,20 @@ export const SchemaFormObjectTable = ({ const rowSchema = schema.patternProperties?.[''] const addNewItem = useCallback(() => { - overrideHelper.setItemValue(item.id, `${attr}.${getRandomString()}`, getSchemaDefaultValues(rowSchema)) + const newRowId = getRandomString() + overrideHelper().setItemValue(item.id, `${attr}.${newRowId}`, getSchemaDefaultValues(rowSchema)).commit() + toggleExpanded(newRowId, true) }, [rowSchema, overrideHelper, item.id, attr]) const doUndeleteRow = useCallback( (rowId: string) => { - overrideHelper.clearItemOverrides(item.id, joinObjectPathFragments(attr, rowId)) + overrideHelper().clearItemOverrides(item.id, joinObjectPathFragments(attr, rowId)).commit() }, [overrideHelper, item.id, attr] ) - const tableOverrideHelper = useMemo( - () => new OverrideOpHelperObjectTable(overrideHelper, item.id, wrappedRows, attr), + const tableOverrideHelper = useCallback( + () => new OverrideOpHelperObjectTable(overrideHelper(), item, wrappedRows, attr), [overrideHelper, item.id, wrappedRows, attr] ) @@ -112,7 +117,9 @@ export const SchemaFormObjectTable = ({ no: t('Cancel'), yes: t('Remove'), onAccept: () => { - tableOverrideHelper.deleteRow(rowId + '') + tableOverrideHelper() + .deleteRow(rowId + '') + .commit() }, message: ( @@ -209,8 +216,137 @@ export const SchemaFormObjectTable = ({ + {schema[SchemaFormUIField.SupportsImportExport] ? ( + + ) : ( + '' + )} ) } } + +interface ImportExportButtonsProps { + schema: JSONSchema + overrideHelper: () => OverrideOpHelperObjectTable + wrappedRows: WrappedOverridableItem[] +} + +function ImportExportButtons({ schema, overrideHelper, wrappedRows }: Readonly) { + const { t } = useTranslation() + + console.log(schema) + + const [uploadFileKey, setUploadFileKey] = useState(0) + + const exportTable = () => { + const exportObject: Record = {} + for (const obj of wrappedRows) { + exportObject[obj.id] = obj.computed + } + + const exportContents = JSON.stringify(exportObject, undefined, 2) + + const file = new File([exportContents], `${encodeURIComponent(`${schema.title}`)}.json`, { + type: 'application/json', + }) + + const link = document.createElement('a') + const url = URL.createObjectURL(file) + + link.href = url + link.download = file.name + document.body.appendChild(link) + link.click() + + document.body.removeChild(link) + URL.revokeObjectURL(url) + } + + const importTable = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (!file) return + + const reader = new FileReader() + reader.onload = (e2) => { + // On file upload + setUploadFileKey(Date.now()) + + const uploadFileContents = e2.target?.result + if (!uploadFileContents) return + + const newRows = JSON.parse(uploadFileContents as string) + if (!newRows || typeof newRows !== 'object') return + + doModalDialog({ + title: t('Import file?'), + yes: t('Replace rows'), + no: t('Cancel'), + message: ( +

+ {t('Are you sure you want to import the contents of the file "{{fileName}}"?', { + fileName: file.name, + })} +

+ ), + onAccept: () => { + const batch = overrideHelper() + + for (const row of wrappedRows) { + batch.deleteRow(row.id) + } + + for (const [rowId, row] of Object.entries(newRows)) { + batch.insertRow(rowId, row) + } + + batch.commit() + }, + actions: [ + { + label: t('Append rows'), + on: () => { + const batch = overrideHelper() + + for (const [rowId, row] of Object.entries(newRows)) { + batch.insertRow(rowId, row) + } + + batch.commit() + }, + classNames: 'btn-secondary', + }, + ], + }) + } + reader.readAsText(file) + } + + return ( + <> + + + + + + + + + + + + + ) +} diff --git a/meteor/client/lib/forms/SchemaFormTable/ObjectTableOpHelper.tsx b/meteor/client/lib/forms/SchemaFormTable/ObjectTableOpHelper.tsx index 3e78ba8c63..a34129ea20 100644 --- a/meteor/client/lib/forms/SchemaFormTable/ObjectTableOpHelper.tsx +++ b/meteor/client/lib/forms/SchemaFormTable/ObjectTableOpHelper.tsx @@ -1,58 +1,78 @@ import { clone, joinObjectPathFragments, objectPathSet } from '@sofie-automation/corelib/dist/lib' -import { OverrideOpHelperForItemContents, WrappedOverridableItem } from '../../../ui/Settings/util/OverrideOpHelper' +import { + OverrideOpHelperForItemContentsBatcher, + WrappedOverridableItem, +} from '../../../ui/Settings/util/OverrideOpHelper' /** * The OverrideOp system does not support tables of objects currently. * This is intended to be a break point to allow tables to provide their nested schemaForms with a way to work with this */ -export class OverrideOpHelperObjectTable implements OverrideOpHelperForItemContents { - readonly #baseHelper: OverrideOpHelperForItemContents - readonly #itemId: string +export class OverrideOpHelperObjectTable implements OverrideOpHelperForItemContentsBatcher { + readonly #baseHelper: OverrideOpHelperForItemContentsBatcher + readonly #parentItem: WrappedOverridableItem readonly #currentRows: WrappedOverridableItem[] readonly #path: string constructor( - baseHelper: OverrideOpHelperForItemContents, - itemId: string, + baseHelper: OverrideOpHelperForItemContentsBatcher, + parentItem: WrappedOverridableItem, currentRows: WrappedOverridableItem[], path: string ) { this.#baseHelper = baseHelper - this.#itemId = itemId + this.#parentItem = parentItem this.#currentRows = currentRows this.#path = path } - clearItemOverrides(rowId: string, subPath: string): void { - this.#baseHelper.clearItemOverrides(this.#itemId, joinObjectPathFragments(this.#path, rowId, subPath)) + clearItemOverrides(rowId: string, subPath: string): this { + this.#baseHelper.clearItemOverrides(this.#parentItem.id, joinObjectPathFragments(this.#path, rowId, subPath)) + + return this + } + insertRow(rowId: string, value: unknown): this { + this.#baseHelper.setItemValue(this.#parentItem.id, joinObjectPathFragments(this.#path, rowId), value) + + return this } - deleteRow(rowId: string): void { + deleteRow(rowId: string): this { + const rowPath = joinObjectPathFragments(this.#path, rowId) + // Clear any existing overrides - this.#baseHelper.clearItemOverrides(this.#itemId, joinObjectPathFragments(this.#path, rowId)) + this.#baseHelper.clearItemOverrides(this.#parentItem.id, rowPath) // If row was not user created (it has defaults), then don't store `undefined` const row = this.#currentRows.find((r) => r.id === rowId) if (row && row.defaults) { // This is a bit of a hack, but it isn't possible to create a delete op from here - this.#baseHelper.setItemValue(this.#itemId, joinObjectPathFragments(this.#path, rowId), undefined) + this.#baseHelper.setItemValue(this.#parentItem.id, rowPath, undefined) } + + return this } - setItemValue(rowId: string, subPath: string, value: unknown): void { + setItemValue(rowId: string, subPath: string, value: unknown): this { const currentRow = this.#currentRows.find((r) => r.id === rowId) - if (!currentRow || currentRow.type === 'deleted') return // Unable to set value + if (!currentRow || currentRow.type === 'deleted') return this // Unable to set value if (currentRow.defaults) { // has defaults, so override the single value - this.#baseHelper.setItemValue(this.#itemId, joinObjectPathFragments(this.#path, rowId, subPath), value) + this.#baseHelper.setItemValue(this.#parentItem.id, joinObjectPathFragments(this.#path, rowId, subPath), value) } else { // no defaults, replace the whole object // Ensure there arent existing overrides - this.#baseHelper.clearItemOverrides(this.#itemId, joinObjectPathFragments(this.#path, rowId)) + this.#baseHelper.clearItemOverrides(this.#parentItem.id, joinObjectPathFragments(this.#path, rowId)) // replace with a new override const newObj = clone(currentRow.computed) objectPathSet(newObj, subPath, value) - this.#baseHelper.setItemValue(this.#itemId, joinObjectPathFragments(this.#path, rowId), newObj) + this.#baseHelper.setItemValue(this.#parentItem.id, joinObjectPathFragments(this.#path, rowId), newObj) } + + return this + } + + commit(): void { + this.#baseHelper.commit() } } diff --git a/meteor/client/ui/Settings/ShowStyle/OutputLayer.tsx b/meteor/client/ui/Settings/ShowStyle/OutputLayer.tsx index e56580f557..741d7aeccb 100644 --- a/meteor/client/ui/Settings/ShowStyle/OutputLayer.tsx +++ b/meteor/client/ui/Settings/ShowStyle/OutputLayer.tsx @@ -95,6 +95,8 @@ export function OutputLayerSettings({ showStyleBase }: IOutputSettingsProps): JS const overrideHelper = useOverrideOpHelper(saveOverrides, showStyleBase.outputLayersWithOverrides) + const doUndelete = useCallback((itemId: string) => overrideHelper().resetItem(itemId).commit(), [overrideHelper]) + return (

@@ -120,7 +122,7 @@ export function OutputLayerSettings({ showStyleBase }: IOutputSettingsProps): JS

@@ -125,7 +130,7 @@ export function GenericSubDevicesTable({ key={item.id} item={item} peripheralDevice={peripheralDevice} - undeleteItemWithId={overrideHelper.resetItem} + undeleteItemWithId={undeleteItemWithId} /> ) } else { @@ -259,7 +264,7 @@ function SubDeviceEditRow({ (newId: string) => { if (item.id === newId) return - overrideHelper.changeItemId(item.id, newId) + overrideHelper().changeItemId(item.id, newId).commit() // toggle ui visibility editItemWithId(item.id, false) diff --git a/meteor/client/ui/Settings/Studio/Mappings.tsx b/meteor/client/ui/Settings/Studio/Mappings.tsx index 2be3c66ae5..6353541dd0 100644 --- a/meteor/client/ui/Settings/Studio/Mappings.tsx +++ b/meteor/client/ui/Settings/Studio/Mappings.tsx @@ -125,6 +125,8 @@ export function StudioMappings({ manifest, translationNamespaces, studio }: IStu const overrideHelper = useOverrideOpHelper(saveOverrides, studio.mappingsWithOverrides) + const doUndelete = useCallback((itemId: string) => overrideHelper().resetItem(itemId).commit(), [overrideHelper]) + return (

{t('Layer Mappings')}

@@ -144,7 +146,7 @@ export function StudioMappings({ manifest, translationNamespaces, studio }: IStu manifestNames={manifestNames} translationNamespaces={translationNamespaces} mapping={item.defaults} - doUndelete={overrideHelper.resetItem} + doUndelete={doUndelete} /> ) : ( { - overrideHelper.deleteItem(item.id) + overrideHelper().deleteItem(item.id).commit() }, message: ( @@ -280,7 +282,7 @@ function StudioMappingsEntry({ yes: t('Reset'), no: t('Cancel'), onAccept: () => { - overrideHelper.resetItem(item.id) + overrideHelper().resetItem(item.id).commit() }, message: ( @@ -297,7 +299,7 @@ function StudioMappingsEntry({ const doChangeItemId = useCallback( (newItemId: string) => { - overrideHelper.changeItemId(item.id, newItemId) + overrideHelper().changeItemId(item.id, newItemId).commit() toggleExpanded(newItemId, true) }, [overrideHelper, toggleExpanded, item.id] diff --git a/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionEntry.tsx b/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionEntry.tsx index a4d400b274..8e9653d441 100644 --- a/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionEntry.tsx +++ b/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionEntry.tsx @@ -248,7 +248,7 @@ export const TriggeredActionEntry: React.FC = React.memo(function Trigge const removeTrigger = useCallback( (id: string) => { - triggersOverridesHelper.deleteItem(id) + triggersOverridesHelper().deleteItem(id).commit() setSelectedTrigger(null) }, @@ -273,7 +273,7 @@ export const TriggeredActionEntry: React.FC = React.memo(function Trigge LAST_UP_SETTING = !!newVal.up } - triggersOverridesHelper.replaceItem(id, newVal) + triggersOverridesHelper().replaceItem(id, newVal).commit() setSelectedTrigger(null) }, @@ -284,7 +284,7 @@ export const TriggeredActionEntry: React.FC = React.memo(function Trigge (id: string) => { if (!triggeredAction?._id) return - triggersOverridesHelper.resetItem(id) + triggersOverridesHelper().resetItem(id).commit() setSelectedTrigger(null) }, @@ -344,7 +344,7 @@ export const TriggeredActionEntry: React.FC = React.memo(function Trigge (id: string) => { if (!triggeredAction?._id) return - actionsOverridesHelper.deleteItem(id) + actionsOverridesHelper().deleteItem(id).commit() setSelectedAction(null) }, @@ -357,7 +357,7 @@ export const TriggeredActionEntry: React.FC = React.memo(function Trigge const restoreAction = useCallback( (id: string) => { - actionsOverridesHelper.resetItem(id) + actionsOverridesHelper().resetItem(id).commit() }, [actionsOverridesHelper] ) diff --git a/meteor/client/ui/Settings/components/triggeredActions/actionEditors/ActionEditor.tsx b/meteor/client/ui/Settings/components/triggeredActions/actionEditors/ActionEditor.tsx index 979aeaaf67..94463ec2da 100644 --- a/meteor/client/ui/Settings/components/triggeredActions/actionEditors/ActionEditor.tsx +++ b/meteor/client/ui/Settings/components/triggeredActions/actionEditors/ActionEditor.tsx @@ -70,7 +70,7 @@ export const ActionEditor: React.FC = function ActionEditor({ (filterIndex: number, newVal: ChainLink) => { action.filterChain.splice(filterIndex, 1, newVal) - overrideHelper.replaceItem(actionId, action) + overrideHelper().replaceItem(actionId, action).commit() }, [action, overrideHelper] ) @@ -90,7 +90,7 @@ export const ActionEditor: React.FC = function ActionEditor({ action.filterChain.splice(filterIndex + 1, 0, obj) - overrideHelper.replaceItem(actionId, action) + overrideHelper().replaceItem(actionId, action).commit() } setOpenFilterIndex(filterIndex + 1) @@ -100,11 +100,11 @@ export const ActionEditor: React.FC = function ActionEditor({ function onFilterRemove(filterIndex: number) { action.filterChain.splice(filterIndex, 1) - overrideHelper.replaceItem(actionId, action) + overrideHelper().replaceItem(actionId, action).commit() } function onChange(newVal: SomeAction) { - overrideHelper.replaceItem(actionId, newVal) + overrideHelper().replaceItem(actionId, newVal).commit() } function isFinished(): boolean { diff --git a/meteor/client/ui/Settings/util/OverrideOpHelper.tsx b/meteor/client/ui/Settings/util/OverrideOpHelper.tsx index 20aca135ed..99e647d408 100644 --- a/meteor/client/ui/Settings/util/OverrideOpHelper.tsx +++ b/meteor/client/ui/Settings/util/OverrideOpHelper.tsx @@ -6,7 +6,7 @@ import { ObjectOverrideSetOp, applyAndValidateOverrides, } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { useRef, useMemo, useEffect, MutableRefObject } from 'react' +import { useRef, useEffect, useCallback } from 'react' import { ReadonlyDeep } from 'type-fest' /** @@ -109,80 +109,92 @@ export function getAllCurrentAndDeletedItemsFromOverrides( type SaveOverridesFunction = (newOps: SomeObjectOverrideOp[]) => void -export interface OverrideOpHelperForItemContents { +export type OverrideOpHelperForItemContents = () => OverrideOpHelperForItemContentsBatcher + +export interface OverrideOpHelperForItemContentsBatcher { /** * Clear all of the overrides for an value inside of an item * This acts as a reset of property of its child properties * Has no effect if there are no `overrideOps` on the `WrappedOverridableItemNormal` */ - clearItemOverrides(itemId: string, subPath: string): void + clearItemOverrides(itemId: string, subPath: string): this /** * Set the value of a property of an item. * Note: the id cannot be changed in this way */ - setItemValue(itemId: string, subPath: string, value: unknown): void + setItemValue(itemId: string, subPath: string, value: unknown): this + + /** + * Finish the batch operation + */ + commit(): void } -export interface OverrideOpHelper extends OverrideOpHelperForItemContents { +export interface OverrideOpHelperBatcher extends OverrideOpHelperForItemContentsBatcher { /** * Clear all of the overrides for an item * This acts as a reset to defaults or undelete * Has no effect if there are no `overrideOps` on the `WrappedOverridableItemNormal` */ - resetItem(itemId: string): void + resetItem(itemId: string): this /** * Delete an item from the object */ - deleteItem(itemId: string): void + deleteItem(itemId: string): this /** * Change the id of an item. * This is only possible for ones which were created by an override, and does not exist in the defaults * Only possible when the item being renamed does not exist in the defaults */ - changeItemId(oldItemId: string, newItemId: string): void + changeItemId(oldItemId: string, newItemId: string): this /** * Replace a whole item with a new object * Note: the id cannot be changed in this way */ - replaceItem(itemId: string, value: any): void + replaceItem(itemId: string, value: any): this + + /** + * Finish the batch operation + */ + commit(): void } -class OverrideOpHelperImpl implements OverrideOpHelper { + +export type OverrideOpHelper = () => OverrideOpHelperBatcher + +class OverrideOpHelperImpl implements OverrideOpHelperBatcher { readonly #saveOverrides: SaveOverridesFunction - readonly #objectWithOverridesRef: MutableRefObject> + readonly #object: ObjectWithOverrides - constructor( - saveOverrides: SaveOverridesFunction, - objectWithOverridesRef: MutableRefObject> - ) { + constructor(saveOverrides: SaveOverridesFunction, object: ObjectWithOverrides) { this.#saveOverrides = saveOverrides - this.#objectWithOverridesRef = objectWithOverridesRef + this.#object = { ...object } } - clearItemOverrides = (itemId: string, subPath: string): void => { - if (!this.#objectWithOverridesRef.current) return - + clearItemOverrides = (itemId: string, subPath: string): this => { const opPath = `${itemId}.${subPath}` - const newOps = this.#objectWithOverridesRef.current.overrides.filter((op) => op.path !== opPath) + const newOps = this.#object.overrides.filter((op) => op.path !== opPath) + + this.#object.overrides = newOps - this.#saveOverrides(newOps) + return this } - resetItem = (itemId: string): void => { - if (!this.#objectWithOverridesRef.current) return + resetItem = (itemId: string): this => { + const newOps = filterOverrideOpsForPrefix(this.#object.overrides, itemId).otherOps - const newOps = filterOverrideOpsForPrefix(this.#objectWithOverridesRef.current.overrides, itemId).otherOps + this.#object.overrides = newOps - this.#saveOverrides(newOps) + return this } - deleteItem = (itemId: string): void => { - const newOps = filterOverrideOpsForPrefix(this.#objectWithOverridesRef.current.overrides, itemId).otherOps - if (this.#objectWithOverridesRef.current.defaults[itemId]) { + deleteItem = (itemId: string): this => { + const newOps = filterOverrideOpsForPrefix(this.#object.overrides, itemId).otherOps + if (this.#object.defaults[itemId]) { // If it was from the defaults, we need to mark it deleted newOps.push( literal({ @@ -192,26 +204,19 @@ class OverrideOpHelperImpl implements OverrideOpHelper { ) } - this.#saveOverrides(newOps) - } + this.#object.overrides = newOps - changeItemId = (oldItemId: string, newItemId: string): void => { - if (!this.#objectWithOverridesRef.current) return + return this + } - const { otherOps: newOps, opsForPrefix: opsForId } = filterOverrideOpsForPrefix( - this.#objectWithOverridesRef.current.overrides, - oldItemId - ) + changeItemId = (oldItemId: string, newItemId: string): this => { + const { otherOps: newOps, opsForPrefix: opsForId } = filterOverrideOpsForPrefix(this.#object.overrides, oldItemId) - if ( - !newItemId || - newOps.find((op) => op.path === newItemId) || - this.#objectWithOverridesRef.current.defaults[newItemId] - ) { + if (!newItemId || newOps.find((op) => op.path === newItemId) || this.#object.defaults[newItemId]) { throw new Error('Id is invalid or already in use') } - if (this.#objectWithOverridesRef.current.defaults[oldItemId]) { + if (this.#object.defaults[oldItemId]) { // Future: should we be able to handle this? throw new Error("Can't change id of object with defaults") } else { @@ -230,21 +235,18 @@ class OverrideOpHelperImpl implements OverrideOpHelper { } } - this.#saveOverrides(newOps) + this.#object.overrides = newOps } - } - setItemValue = (itemId: string, subPath: string, value: unknown): void => { - if (!this.#objectWithOverridesRef.current) return + return this + } + setItemValue = (itemId: string, subPath: string, value: unknown): this => { if (subPath === '_id') { throw new Error('Item id cannot be changed through this helper') } else { // Set a property - const { otherOps: newOps, opsForPrefix: opsForId } = filterOverrideOpsForPrefix( - this.#objectWithOverridesRef.current.overrides, - itemId - ) + const { otherOps: newOps, opsForPrefix: opsForId } = filterOverrideOpsForPrefix(this.#object.overrides, itemId) const setRootOp = opsForId.find((op) => op.path === itemId) if (setRootOp && setRootOp.op === 'set') { @@ -288,15 +290,15 @@ class OverrideOpHelperImpl implements OverrideOpHelper { } } - this.#saveOverrides(newOps) + this.#object.overrides = newOps } - } - replaceItem = (itemId: string, value: unknown): void => { - if (!this.#objectWithOverridesRef.current) return + return this + } + replaceItem = (itemId: string, value: unknown): this => { // Set a property - const { otherOps: newOps } = filterOverrideOpsForPrefix(this.#objectWithOverridesRef.current.overrides, itemId) + const { otherOps: newOps } = filterOverrideOpsForPrefix(this.#object.overrides, itemId) // TODO - is this too naive? @@ -308,7 +310,13 @@ class OverrideOpHelperImpl implements OverrideOpHelper { }) ) - this.#saveOverrides(newOps) + this.#object.overrides = newOps + + return this + } + + commit = () => { + this.#saveOverrides(this.#object.overrides) } } @@ -321,17 +329,15 @@ export function useOverrideOpHelper( ): OverrideOpHelper { const objectWithOverridesRef = useRef(objectWithOverrides) - const helper = useMemo( - () => new OverrideOpHelperImpl(saveOverrides, objectWithOverridesRef), - [saveOverrides, objectWithOverridesRef] - ) - // Use a ref to minimise reactivity when it changes useEffect(() => { objectWithOverridesRef.current = objectWithOverrides }, [objectWithOverrides]) - return helper + return useCallback(() => { + if (!objectWithOverridesRef.current) throw new Error('No current object!') + return new OverrideOpHelperImpl(saveOverrides, objectWithOverridesRef.current) + }, [saveOverrides, objectWithOverridesRef]) } function findParentOpToUpdate( diff --git a/packages/documentation/docs/for-developers/json-config-schema.md b/packages/documentation/docs/for-developers/json-config-schema.md index 1d6df1db25..b56e6e6ee7 100644 --- a/packages/documentation/docs/for-developers/json-config-schema.md +++ b/packages/documentation/docs/for-developers/json-config-schema.md @@ -73,6 +73,10 @@ This will provide a dropdown of all source-layers in the show-style. Setting `ui:sofie-enum:filter` to an array of numbers will filter the dropdown by the specified SourceLayerType. +### `ui:import-export` + +Valid only for tables, this allows for importing and exporting the contents of the table. + ## Supported types Any JSON Schema property or type is allowed, but will be ignored if it is not supported. diff --git a/packages/shared-lib/src/lib/JSONSchemaUtil.ts b/packages/shared-lib/src/lib/JSONSchemaUtil.ts index 958ab122ab..992ad75eb0 100644 --- a/packages/shared-lib/src/lib/JSONSchemaUtil.ts +++ b/packages/shared-lib/src/lib/JSONSchemaUtil.ts @@ -44,6 +44,11 @@ export enum SchemaFormUIField { * When using `ui:sofie-enum`, filter the options by type */ SofieEnumFilter = 'ui:sofie-enum:filter', + /** + * Whether a table supports being imported and exported + * Valid only for tables + */ + SupportsImportExport = 'ui:import-export', } export function getSchemaDefaultValues(schema: JSONSchema | undefined): any { From 132effc3d2c8a23ea5ea17044a65dcac620d8b01 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 22 May 2024 14:41:58 +0100 Subject: [PATCH 304/479] chore: fix typo --- meteor/client/lib/forms/SchemaFormForCollection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meteor/client/lib/forms/SchemaFormForCollection.tsx b/meteor/client/lib/forms/SchemaFormForCollection.tsx index d288262177..9e3562508c 100644 --- a/meteor/client/lib/forms/SchemaFormForCollection.tsx +++ b/meteor/client/lib/forms/SchemaFormForCollection.tsx @@ -127,7 +127,7 @@ class OverrideOpHelperCollection implements OverrideOpHelperForItemContentsBatch } commit(): void { - if (!this.#changes) { + if (this.#changes) { const changesToSave = this.#changes this.#changes = undefined From 306d31e03a63539035fce9d8f3f303122c13cbb3 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 22 May 2024 14:37:54 +0100 Subject: [PATCH 305/479] feat: add import/export support to config object tables SOFIE-3138 (#1194) --- .../lib/Components/LabelAndOverrides.tsx | 4 +- .../lib/forms/SchemaFormForCollection.tsx | 56 ++++--- meteor/client/lib/forms/SchemaFormInPlace.tsx | 20 ++- .../lib/forms/SchemaFormTable/ArrayTable.tsx | 12 +- .../SchemaFormTable/ArrayTableOpHelper.tsx | 29 +++- .../forms/SchemaFormTable/ArrayTableRow.tsx | 2 +- .../lib/forms/SchemaFormTable/ObjectTable.tsx | 150 +++++++++++++++++- .../SchemaFormTable/ObjectTableOpHelper.tsx | 54 +++++-- .../ui/Settings/ShowStyle/OutputLayer.tsx | 12 +- .../ui/Settings/ShowStyle/SourceLayer.tsx | 12 +- .../Studio/Devices/GenericSubDevices.tsx | 11 +- meteor/client/ui/Settings/Studio/Mappings.tsx | 10 +- .../triggeredActions/TriggeredActionEntry.tsx | 10 +- .../actionEditors/ActionEditor.tsx | 8 +- .../ui/Settings/util/OverrideOpHelper.tsx | 130 +++++++-------- .../docs/for-developers/json-config-schema.md | 4 + packages/shared-lib/src/lib/JSONSchemaUtil.ts | 5 + 17 files changed, 376 insertions(+), 153 deletions(-) diff --git a/meteor/client/lib/Components/LabelAndOverrides.tsx b/meteor/client/lib/Components/LabelAndOverrides.tsx index 7b91fe5507..785da347c6 100644 --- a/meteor/client/lib/Components/LabelAndOverrides.tsx +++ b/meteor/client/lib/Components/LabelAndOverrides.tsx @@ -38,11 +38,11 @@ export function LabelAndOverrides({ const { t } = useTranslation() const clearOverride = useCallback(() => { - overrideHelper.clearItemOverrides(opPrefix, String(itemKey)) + overrideHelper().clearItemOverrides(opPrefix, String(itemKey)).commit() }, [overrideHelper, opPrefix, itemKey]) const setValue = useCallback( (newValue: any) => { - overrideHelper.setItemValue(opPrefix, String(itemKey), newValue) + overrideHelper().setItemValue(opPrefix, String(itemKey), newValue).commit() }, [overrideHelper, opPrefix, itemKey] ) diff --git a/meteor/client/lib/forms/SchemaFormForCollection.tsx b/meteor/client/lib/forms/SchemaFormForCollection.tsx index e54d98d70a..e70c205f7e 100644 --- a/meteor/client/lib/forms/SchemaFormForCollection.tsx +++ b/meteor/client/lib/forms/SchemaFormForCollection.tsx @@ -4,11 +4,15 @@ import { ObjectOverrideDeleteOp, ObjectOverrideSetOp, } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import React, { useMemo } from 'react' +import React, { useCallback, useMemo } from 'react' import { MongoCollection } from '../../../lib/collections/lib' -import { WrappedOverridableItemNormal, OverrideOpHelperForItemContents } from '../../ui/Settings/util/OverrideOpHelper' +import { + WrappedOverridableItemNormal, + OverrideOpHelperForItemContentsBatcher, +} from '../../ui/Settings/util/OverrideOpHelper' import { SchemaFormCommonProps } from './schemaFormUtil' import { SchemaFormWithOverrides } from './SchemaFormWithOverrides' +import { MongoModifier } from '@sofie-automation/corelib/dist/mongo' interface SchemaFormForCollectionProps extends Omit { /** The collection to operate on */ @@ -35,7 +39,7 @@ export function SchemaFormForCollection({ partialOverridesForObject, ...commonProps }: Readonly): JSX.Element { - const helper = useMemo( + const helper = useCallback( () => new OverrideOpHelperCollection(collection, objectId, basePath), [collection, objectId, basePath] ) @@ -87,37 +91,47 @@ export function SchemaFormForCollection({ * An alternate OverrideOpHelper designed to directly mutate a collection, instead of using the `ObjectWithOverrides` system. * This allows us to have one SchemaForm implementation that can handle working with `ObjectWithOverrides`, and basic objects in mongodb */ -class OverrideOpHelperCollection implements OverrideOpHelperForItemContents { +class OverrideOpHelperCollection implements OverrideOpHelperForItemContentsBatcher { readonly #collection: MongoCollection readonly #objectId: ProtectedString readonly #basePath: string + #changes: MongoModifier | undefined + constructor(collection: MongoCollection, objectId: ProtectedString, basePath: string) { this.#collection = collection this.#objectId = objectId this.#basePath = basePath } - clearItemOverrides(_itemId: string, subPath: string): void { - this.#collection.update(this.#objectId, { - $unset: { - [`${this.#basePath}.${subPath}`]: 1, - }, - }) + clearItemOverrides(_itemId: string, subPath: string): this { + if (!this.#changes) this.#changes = {} + if (!this.#changes.$unset) this.#changes.$unset = {} + + this.#changes.$unset[`${this.#basePath}.${subPath}`] = 1 + + return this } - setItemValue(_itemId: string, subPath: string, value: any): void { + setItemValue(_itemId: string, subPath: string, value: any): this { + if (!this.#changes) this.#changes = {} + if (value === undefined) { - this.#collection.update(this.#objectId, { - $unset: { - [`${this.#basePath}.${subPath}`]: 1, - }, - }) + if (!this.#changes.$unset) this.#changes.$unset = {} + this.#changes.$unset[`${this.#basePath}.${subPath}`] = 1 } else { - this.#collection.update(this.#objectId, { - $set: { - [`${this.#basePath}.${subPath}`]: value, - }, - }) + if (!this.#changes.$set) this.#changes.$set = {} + ;(this.#changes.$set[`${this.#basePath}.${subPath}`] as any) = value + } + + return this + } + + commit(): void { + if (this.#changes) { + const changesToSave = this.#changes + this.#changes = undefined + + this.#collection.update(this.#objectId, changesToSave) } } } diff --git a/meteor/client/lib/forms/SchemaFormInPlace.tsx b/meteor/client/lib/forms/SchemaFormInPlace.tsx index 947b102263..9d7be60ec1 100644 --- a/meteor/client/lib/forms/SchemaFormInPlace.tsx +++ b/meteor/client/lib/forms/SchemaFormInPlace.tsx @@ -1,6 +1,9 @@ import { literal, objectPathSet } from '@sofie-automation/corelib/dist/lib' import React, { useCallback, useMemo, useState } from 'react' -import { WrappedOverridableItemNormal, OverrideOpHelperForItemContents } from '../../ui/Settings/util/OverrideOpHelper' +import { + WrappedOverridableItemNormal, + OverrideOpHelperForItemContentsBatcher, +} from '../../ui/Settings/util/OverrideOpHelper' import { SchemaFormCommonProps } from './schemaFormUtil' import { SchemaFormWithOverrides } from './SchemaFormWithOverrides' @@ -13,7 +16,7 @@ export function SchemaFormInPlace({ object, ...commonProps }: Readonly setEditCount((v) => v + 1), []) - const helper = useMemo(() => new OverrideOpHelperInPlace(object, forceRender), [object, forceRender]) + const helper = useCallback(() => new OverrideOpHelperInPlace(object, forceRender), [object, forceRender]) const wrappedItem = useMemo( () => @@ -34,7 +37,7 @@ export function SchemaFormInPlace({ object, ...commonProps }: Readonly void @@ -43,11 +46,18 @@ class OverrideOpHelperInPlace implements OverrideOpHelperForItemContents { this.#forceRender = forceRender } - clearItemOverrides(_itemId: string, _subPath: string): void { + clearItemOverrides(_itemId: string, _subPath: string): this { // Not supported as this is faking an item with overrides + + return this } - setItemValue(_itemId: string, subPath: string, value: any): void { + setItemValue(_itemId: string, subPath: string, value: any): this { objectPathSet(this.#object, subPath, value) + + return this + } + + commit(): void { this.#forceRender() } } diff --git a/meteor/client/lib/forms/SchemaFormTable/ArrayTable.tsx b/meteor/client/lib/forms/SchemaFormTable/ArrayTable.tsx index 5fd6e10e8c..af9bc02031 100644 --- a/meteor/client/lib/forms/SchemaFormTable/ArrayTable.tsx +++ b/meteor/client/lib/forms/SchemaFormTable/ArrayTable.tsx @@ -67,16 +67,16 @@ export const SchemaFormArrayTable = ({ newObj.push(getSchemaDefaultValues(schema.items)) // Send it onwards - overrideHelper.setItemValue(item.id, attr, newObj) + overrideHelper().setItemValue(item.id, attr, newObj).commit() }, [schema.items, overrideHelper, rowsArray, item.id, attr]) const resyncTable = useCallback( - () => overrideHelper.clearItemOverrides(item.id, attr), + () => overrideHelper().clearItemOverrides(item.id, attr).commit(), [overrideHelper, item.id, attr] ) - const tableOverrideHelper = useMemo( - () => new OverrideOpHelperArrayTable(overrideHelper, item.id, rowsArray, attr), + const tableOverrideHelper = useCallback( + () => new OverrideOpHelperArrayTable(overrideHelper(), item.id, rowsArray, attr), [overrideHelper, item.id, rows, attr] ) @@ -93,7 +93,9 @@ export const SchemaFormArrayTable = ({ no: t('Cancel'), yes: t('Remove'), onAccept: () => { - tableOverrideHelper.deleteRow(rowId + '') + tableOverrideHelper() + .deleteRow(rowId + '') + .commit() }, message: ( diff --git a/meteor/client/lib/forms/SchemaFormTable/ArrayTableOpHelper.tsx b/meteor/client/lib/forms/SchemaFormTable/ArrayTableOpHelper.tsx index ef12bcafb5..7790813efc 100644 --- a/meteor/client/lib/forms/SchemaFormTable/ArrayTableOpHelper.tsx +++ b/meteor/client/lib/forms/SchemaFormTable/ArrayTableOpHelper.tsx @@ -1,41 +1,56 @@ import { clone, objectPathSet } from '@sofie-automation/corelib/dist/lib' -import { OverrideOpHelperForItemContents } from '../../../ui/Settings/util/OverrideOpHelper' +import { OverrideOpHelperForItemContentsBatcher } from '../../../ui/Settings/util/OverrideOpHelper' /** * The OverrideOp system does not support Arrays currently. * This is intended to be a break point to avoid tables from attempting to define operations on arrays, * and instead make the table be treated as a single blob. */ -export class OverrideOpHelperArrayTable implements OverrideOpHelperForItemContents { - readonly #baseHelper: OverrideOpHelperForItemContents +export class OverrideOpHelperArrayTable implements OverrideOpHelperForItemContentsBatcher { + readonly #baseHelper: OverrideOpHelperForItemContentsBatcher readonly #itemId: string readonly #currentRows: unknown[] readonly #path: string - constructor(baseHelper: OverrideOpHelperForItemContents, itemId: string, currentRows: unknown[], path: string) { + constructor( + baseHelper: OverrideOpHelperForItemContentsBatcher, + itemId: string, + currentRows: unknown[], + path: string + ) { this.#baseHelper = baseHelper this.#itemId = itemId this.#currentRows = currentRows this.#path = path } - clearItemOverrides(_itemId: string, _subPath: string): void { + clearItemOverrides(_itemId: string, _subPath: string): this { // Not supported as this is faking an item with overrides + + return this } - deleteRow(rowId: string): void { + deleteRow(rowId: string): this { // Delete the row const newObj = clone(this.#currentRows) newObj.splice(Number(rowId), 1) // Send it onwards this.#baseHelper.setItemValue(this.#itemId, this.#path, newObj) + + return this } - setItemValue(rowId: string, subPath: string, value: unknown): void { + setItemValue(rowId: string, subPath: string, value: unknown): this { // Build the new object const newObj = clone(this.#currentRows) objectPathSet(newObj, `${rowId}.${subPath}`, value) // Send it onwards this.#baseHelper.setItemValue(this.#itemId, this.#path, newObj) + + return this + } + + commit(): void { + this.#baseHelper.commit() } } diff --git a/meteor/client/lib/forms/SchemaFormTable/ArrayTableRow.tsx b/meteor/client/lib/forms/SchemaFormTable/ArrayTableRow.tsx index ccd6440ac9..9a905ec7ef 100644 --- a/meteor/client/lib/forms/SchemaFormTable/ArrayTableRow.tsx +++ b/meteor/client/lib/forms/SchemaFormTable/ArrayTableRow.tsx @@ -15,7 +15,7 @@ interface ArrayTableRowProps { translationNamespaces: string[] sofieEnumDefinitons: Record | undefined - overrideHelper: OverrideOpHelperArrayTable + overrideHelper: () => OverrideOpHelperArrayTable rowId: number rowObject: any diff --git a/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx b/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx index 710d628d1c..8c5992e300 100644 --- a/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx +++ b/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx @@ -1,12 +1,13 @@ -import { faPlus } from '@fortawesome/free-solid-svg-icons' +import { faDownload, faPlus, faUpload } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { getRandomString, joinObjectPathFragments, objectPathGet } from '@sofie-automation/corelib/dist/lib' -import React, { useCallback, useMemo } from 'react' +import React, { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { WrappedOverridableItemNormal, OverrideOpHelperForItemContents, getAllCurrentAndDeletedItemsFromOverrides, + WrappedOverridableItem, } from '../../../ui/Settings/util/OverrideOpHelper' import { useToggleExpandHelper } from '../../../ui/util/useToggleExpandHelper' import { doModalDialog } from '../../ModalDialog' @@ -27,6 +28,8 @@ import { SchemaTableSummaryRow } from '../SchemaTableSummaryRow' import { OverrideOpHelperObjectTable } from './ObjectTableOpHelper' import { ObjectTableDeletedRow } from './ObjectTableDeletedRow' import { SchemaFormSectionHeader } from '../SchemaFormSectionHeader' +import { UploadButton } from '../../uploadButton' +import Tooltip from 'rc-tooltip' interface SchemaFormObjectTableProps { /** Schema for each row in the table */ @@ -89,18 +92,20 @@ export const SchemaFormObjectTable = ({ const rowSchema = schema.patternProperties?.[''] const addNewItem = useCallback(() => { - overrideHelper.setItemValue(item.id, `${attr}.${getRandomString()}`, getSchemaDefaultValues(rowSchema)) + const newRowId = getRandomString() + overrideHelper().setItemValue(item.id, `${attr}.${newRowId}`, getSchemaDefaultValues(rowSchema)).commit() + toggleExpanded(newRowId, true) }, [rowSchema, overrideHelper, item.id, attr]) const doUndeleteRow = useCallback( (rowId: string) => { - overrideHelper.clearItemOverrides(item.id, joinObjectPathFragments(attr, rowId)) + overrideHelper().clearItemOverrides(item.id, joinObjectPathFragments(attr, rowId)).commit() }, [overrideHelper, item.id, attr] ) - const tableOverrideHelper = useMemo( - () => new OverrideOpHelperObjectTable(overrideHelper, item.id, wrappedRows, attr), + const tableOverrideHelper = useCallback( + () => new OverrideOpHelperObjectTable(overrideHelper(), item, wrappedRows, attr), [overrideHelper, item.id, wrappedRows, attr] ) @@ -116,7 +121,9 @@ export const SchemaFormObjectTable = ({ no: t('Cancel'), yes: t('Remove'), onAccept: () => { - tableOverrideHelper.deleteRow(rowId + '') + tableOverrideHelper() + .deleteRow(rowId + '') + .commit() }, message: ( @@ -213,8 +220,137 @@ export const SchemaFormObjectTable = ({ + {getSchemaUIField(schema, SchemaFormUIField.SupportsImportExport) ? ( + + ) : ( + '' + )}
) } } + +interface ImportExportButtonsProps { + schema: JSONSchema + overrideHelper: () => OverrideOpHelperObjectTable + wrappedRows: WrappedOverridableItem[] +} + +function ImportExportButtons({ schema, overrideHelper, wrappedRows }: Readonly) { + const { t } = useTranslation() + + console.log(schema) + + const [uploadFileKey, setUploadFileKey] = useState(0) + + const exportTable = () => { + const exportObject: Record = {} + for (const obj of wrappedRows) { + exportObject[obj.id] = obj.computed + } + + const exportContents = JSON.stringify(exportObject, undefined, 2) + + const file = new File([exportContents], `${encodeURIComponent(`${schema.title}`)}.json`, { + type: 'application/json', + }) + + const link = document.createElement('a') + const url = URL.createObjectURL(file) + + link.href = url + link.download = file.name + document.body.appendChild(link) + link.click() + + document.body.removeChild(link) + URL.revokeObjectURL(url) + } + + const importTable = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (!file) return + + const reader = new FileReader() + reader.onload = (e2) => { + // On file upload + setUploadFileKey(Date.now()) + + const uploadFileContents = e2.target?.result + if (!uploadFileContents) return + + const newRows = JSON.parse(uploadFileContents as string) + if (!newRows || typeof newRows !== 'object') return + + doModalDialog({ + title: t('Import file?'), + yes: t('Replace rows'), + no: t('Cancel'), + message: ( +

+ {t('Are you sure you want to import the contents of the file "{{fileName}}"?', { + fileName: file.name, + })} +

+ ), + onAccept: () => { + const batch = overrideHelper() + + for (const row of wrappedRows) { + batch.deleteRow(row.id) + } + + for (const [rowId, row] of Object.entries(newRows)) { + batch.insertRow(rowId, row) + } + + batch.commit() + }, + actions: [ + { + label: t('Append rows'), + on: () => { + const batch = overrideHelper() + + for (const [rowId, row] of Object.entries(newRows)) { + batch.insertRow(rowId, row) + } + + batch.commit() + }, + classNames: 'btn-secondary', + }, + ], + }) + } + reader.readAsText(file) + } + + return ( + <> + + + + + + + + + + + + + ) +} diff --git a/meteor/client/lib/forms/SchemaFormTable/ObjectTableOpHelper.tsx b/meteor/client/lib/forms/SchemaFormTable/ObjectTableOpHelper.tsx index 3e78ba8c63..a34129ea20 100644 --- a/meteor/client/lib/forms/SchemaFormTable/ObjectTableOpHelper.tsx +++ b/meteor/client/lib/forms/SchemaFormTable/ObjectTableOpHelper.tsx @@ -1,58 +1,78 @@ import { clone, joinObjectPathFragments, objectPathSet } from '@sofie-automation/corelib/dist/lib' -import { OverrideOpHelperForItemContents, WrappedOverridableItem } from '../../../ui/Settings/util/OverrideOpHelper' +import { + OverrideOpHelperForItemContentsBatcher, + WrappedOverridableItem, +} from '../../../ui/Settings/util/OverrideOpHelper' /** * The OverrideOp system does not support tables of objects currently. * This is intended to be a break point to allow tables to provide their nested schemaForms with a way to work with this */ -export class OverrideOpHelperObjectTable implements OverrideOpHelperForItemContents { - readonly #baseHelper: OverrideOpHelperForItemContents - readonly #itemId: string +export class OverrideOpHelperObjectTable implements OverrideOpHelperForItemContentsBatcher { + readonly #baseHelper: OverrideOpHelperForItemContentsBatcher + readonly #parentItem: WrappedOverridableItem readonly #currentRows: WrappedOverridableItem[] readonly #path: string constructor( - baseHelper: OverrideOpHelperForItemContents, - itemId: string, + baseHelper: OverrideOpHelperForItemContentsBatcher, + parentItem: WrappedOverridableItem, currentRows: WrappedOverridableItem[], path: string ) { this.#baseHelper = baseHelper - this.#itemId = itemId + this.#parentItem = parentItem this.#currentRows = currentRows this.#path = path } - clearItemOverrides(rowId: string, subPath: string): void { - this.#baseHelper.clearItemOverrides(this.#itemId, joinObjectPathFragments(this.#path, rowId, subPath)) + clearItemOverrides(rowId: string, subPath: string): this { + this.#baseHelper.clearItemOverrides(this.#parentItem.id, joinObjectPathFragments(this.#path, rowId, subPath)) + + return this + } + insertRow(rowId: string, value: unknown): this { + this.#baseHelper.setItemValue(this.#parentItem.id, joinObjectPathFragments(this.#path, rowId), value) + + return this } - deleteRow(rowId: string): void { + deleteRow(rowId: string): this { + const rowPath = joinObjectPathFragments(this.#path, rowId) + // Clear any existing overrides - this.#baseHelper.clearItemOverrides(this.#itemId, joinObjectPathFragments(this.#path, rowId)) + this.#baseHelper.clearItemOverrides(this.#parentItem.id, rowPath) // If row was not user created (it has defaults), then don't store `undefined` const row = this.#currentRows.find((r) => r.id === rowId) if (row && row.defaults) { // This is a bit of a hack, but it isn't possible to create a delete op from here - this.#baseHelper.setItemValue(this.#itemId, joinObjectPathFragments(this.#path, rowId), undefined) + this.#baseHelper.setItemValue(this.#parentItem.id, rowPath, undefined) } + + return this } - setItemValue(rowId: string, subPath: string, value: unknown): void { + setItemValue(rowId: string, subPath: string, value: unknown): this { const currentRow = this.#currentRows.find((r) => r.id === rowId) - if (!currentRow || currentRow.type === 'deleted') return // Unable to set value + if (!currentRow || currentRow.type === 'deleted') return this // Unable to set value if (currentRow.defaults) { // has defaults, so override the single value - this.#baseHelper.setItemValue(this.#itemId, joinObjectPathFragments(this.#path, rowId, subPath), value) + this.#baseHelper.setItemValue(this.#parentItem.id, joinObjectPathFragments(this.#path, rowId, subPath), value) } else { // no defaults, replace the whole object // Ensure there arent existing overrides - this.#baseHelper.clearItemOverrides(this.#itemId, joinObjectPathFragments(this.#path, rowId)) + this.#baseHelper.clearItemOverrides(this.#parentItem.id, joinObjectPathFragments(this.#path, rowId)) // replace with a new override const newObj = clone(currentRow.computed) objectPathSet(newObj, subPath, value) - this.#baseHelper.setItemValue(this.#itemId, joinObjectPathFragments(this.#path, rowId), newObj) + this.#baseHelper.setItemValue(this.#parentItem.id, joinObjectPathFragments(this.#path, rowId), newObj) } + + return this + } + + commit(): void { + this.#baseHelper.commit() } } diff --git a/meteor/client/ui/Settings/ShowStyle/OutputLayer.tsx b/meteor/client/ui/Settings/ShowStyle/OutputLayer.tsx index a187da9d51..c0c331ab7d 100644 --- a/meteor/client/ui/Settings/ShowStyle/OutputLayer.tsx +++ b/meteor/client/ui/Settings/ShowStyle/OutputLayer.tsx @@ -95,6 +95,8 @@ export function OutputLayerSettings({ showStyleBase }: Readonly overrideHelper().resetItem(itemId).commit(), [overrideHelper]) + return (

@@ -120,7 +122,7 @@ export function OutputLayerSettings({ showStyleBase }: Readonly {sortedOutputLayers.map((item) => item.type === 'deleted' ? ( - + ) : ( toggleExpanded(item.id), [toggleExpanded, item.id]) - const doResetItem = useCallback(() => overrideHelper.resetItem(item.id), [overrideHelper, item.id]) + const doResetItem = useCallback(() => overrideHelper().resetItem(item.id).commit(), [overrideHelper, item.id]) const doChangeItemId = useCallback( (newItemId: string) => { - overrideHelper.changeItemId(item.id, newItemId) + overrideHelper().changeItemId(item.id, newItemId).commit() toggleExpanded(newItemId, true) }, [overrideHelper, toggleExpanded, item.id] @@ -196,7 +198,7 @@ function OutputLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: no: t('Cancel'), yes: t('Delete'), onAccept: () => { - overrideHelper.deleteItem(item.id) + overrideHelper().deleteItem(item.id).commit() }, message: ( @@ -212,7 +214,7 @@ function OutputLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: yes: t('Reset'), no: t('Cancel'), onAccept: () => { - overrideHelper.resetItem(item.id) + overrideHelper().resetItem(item.id).commit() }, message: ( diff --git a/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx b/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx index d1c9e79b6e..61bb07e963 100644 --- a/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx +++ b/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx @@ -129,6 +129,8 @@ export function SourceLayerSettings({ showStyleBase }: Readonly overrideHelper().resetItem(itemId).commit(), [overrideHelper]) + return (

@@ -149,7 +151,7 @@ export function SourceLayerSettings({ showStyleBase }: Readonly {sortedSourceLayers.map((item) => item.type === 'deleted' ? ( - + ) : ( toggleExpanded(item.id), [toggleExpanded, item.id]) - const doResetItem = useCallback(() => overrideHelper.resetItem(item.id), [overrideHelper, item.id]) + const doResetItem = useCallback(() => overrideHelper().resetItem(item.id).commit(), [overrideHelper, item.id]) const doChangeItemId = useCallback( (newItemId: string) => { - overrideHelper.changeItemId(item.id, newItemId) + overrideHelper().changeItemId(item.id, newItemId).commit() toggleExpanded(newItemId, true) }, [overrideHelper, toggleExpanded, item.id] @@ -221,7 +223,7 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: no: t('Cancel'), yes: t('Delete'), onAccept: () => { - overrideHelper.deleteItem(item.id) + overrideHelper().deleteItem(item.id).commit() }, message: ( @@ -241,7 +243,7 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: yes: t('Reset'), no: t('Cancel'), onAccept: () => { - overrideHelper.resetItem(item.id) + overrideHelper().resetItem(item.id).commit() }, message: ( diff --git a/meteor/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx b/meteor/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx index 762e63de97..b3967bf83e 100644 --- a/meteor/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx +++ b/meteor/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx @@ -67,7 +67,7 @@ export function GenericSubDevicesTable({ no: t('Cancel'), yes: t('Remove'), onAccept: () => { - overrideHelper.deleteItem(subdeviceId) + overrideHelper().deleteItem(subdeviceId).commit() }, message: ( @@ -105,6 +105,11 @@ export function GenericSubDevicesTable({ return options }, [peripheralDevicesMap]) + const undeleteItemWithId = useCallback( + (itemId: string) => overrideHelper().resetItem(itemId).commit(), + [overrideHelper] + ) + return (

@@ -125,7 +130,7 @@ export function GenericSubDevicesTable({ key={item.id} item={item} peripheralDevice={peripheralDevice} - undeleteItemWithId={overrideHelper.resetItem} + undeleteItemWithId={undeleteItemWithId} /> ) } else { @@ -263,7 +268,7 @@ function SubDeviceEditRow({ (newId: string) => { if (item.id === newId) return - overrideHelper.changeItemId(item.id, newId) + overrideHelper().changeItemId(item.id, newId).commit() // toggle ui visibility editItemWithId(item.id, false) diff --git a/meteor/client/ui/Settings/Studio/Mappings.tsx b/meteor/client/ui/Settings/Studio/Mappings.tsx index 84bfb74afd..b8e2106700 100644 --- a/meteor/client/ui/Settings/Studio/Mappings.tsx +++ b/meteor/client/ui/Settings/Studio/Mappings.tsx @@ -130,6 +130,8 @@ export function StudioMappings({ const overrideHelper = useOverrideOpHelper(saveOverrides, studio.mappingsWithOverrides) + const doUndelete = useCallback((itemId: string) => overrideHelper().resetItem(itemId).commit(), [overrideHelper]) + return (

{t('Layer Mappings')}

@@ -149,7 +151,7 @@ export function StudioMappings({ manifestNames={manifestNames} translationNamespaces={translationNamespaces} mapping={item.defaults} - doUndelete={overrideHelper.resetItem} + doUndelete={doUndelete} /> ) : ( { - overrideHelper.deleteItem(item.id) + overrideHelper().deleteItem(item.id).commit() }, message: ( @@ -285,7 +287,7 @@ function StudioMappingsEntry({ yes: t('Reset'), no: t('Cancel'), onAccept: () => { - overrideHelper.resetItem(item.id) + overrideHelper().resetItem(item.id).commit() }, message: ( @@ -302,7 +304,7 @@ function StudioMappingsEntry({ const doChangeItemId = useCallback( (newItemId: string) => { - overrideHelper.changeItemId(item.id, newItemId) + overrideHelper().changeItemId(item.id, newItemId).commit() toggleExpanded(newItemId, true) }, [overrideHelper, toggleExpanded, item.id] diff --git a/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionEntry.tsx b/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionEntry.tsx index a4d400b274..8e9653d441 100644 --- a/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionEntry.tsx +++ b/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionEntry.tsx @@ -248,7 +248,7 @@ export const TriggeredActionEntry: React.FC = React.memo(function Trigge const removeTrigger = useCallback( (id: string) => { - triggersOverridesHelper.deleteItem(id) + triggersOverridesHelper().deleteItem(id).commit() setSelectedTrigger(null) }, @@ -273,7 +273,7 @@ export const TriggeredActionEntry: React.FC = React.memo(function Trigge LAST_UP_SETTING = !!newVal.up } - triggersOverridesHelper.replaceItem(id, newVal) + triggersOverridesHelper().replaceItem(id, newVal).commit() setSelectedTrigger(null) }, @@ -284,7 +284,7 @@ export const TriggeredActionEntry: React.FC = React.memo(function Trigge (id: string) => { if (!triggeredAction?._id) return - triggersOverridesHelper.resetItem(id) + triggersOverridesHelper().resetItem(id).commit() setSelectedTrigger(null) }, @@ -344,7 +344,7 @@ export const TriggeredActionEntry: React.FC = React.memo(function Trigge (id: string) => { if (!triggeredAction?._id) return - actionsOverridesHelper.deleteItem(id) + actionsOverridesHelper().deleteItem(id).commit() setSelectedAction(null) }, @@ -357,7 +357,7 @@ export const TriggeredActionEntry: React.FC = React.memo(function Trigge const restoreAction = useCallback( (id: string) => { - actionsOverridesHelper.resetItem(id) + actionsOverridesHelper().resetItem(id).commit() }, [actionsOverridesHelper] ) diff --git a/meteor/client/ui/Settings/components/triggeredActions/actionEditors/ActionEditor.tsx b/meteor/client/ui/Settings/components/triggeredActions/actionEditors/ActionEditor.tsx index 6f2996c40c..df1d2426c9 100644 --- a/meteor/client/ui/Settings/components/triggeredActions/actionEditors/ActionEditor.tsx +++ b/meteor/client/ui/Settings/components/triggeredActions/actionEditors/ActionEditor.tsx @@ -70,7 +70,7 @@ export const ActionEditor: React.FC = function ActionEditor({ (filterIndex: number, newVal: ChainLink) => { action.filterChain.splice(filterIndex, 1, newVal) - overrideHelper.replaceItem(actionId, action) + overrideHelper().replaceItem(actionId, action).commit() }, [action, overrideHelper] ) @@ -90,7 +90,7 @@ export const ActionEditor: React.FC = function ActionEditor({ action.filterChain.splice(filterIndex + 1, 0, obj) - overrideHelper.replaceItem(actionId, action) + overrideHelper().replaceItem(actionId, action).commit() } setOpenFilterIndex(filterIndex + 1) @@ -100,11 +100,11 @@ export const ActionEditor: React.FC = function ActionEditor({ function onFilterRemove(filterIndex: number) { action.filterChain.splice(filterIndex, 1) - overrideHelper.replaceItem(actionId, action) + overrideHelper().replaceItem(actionId, action).commit() } function onChange(newVal: SomeAction) { - overrideHelper.replaceItem(actionId, newVal) + overrideHelper().replaceItem(actionId, newVal).commit() } function isFinished(): boolean { diff --git a/meteor/client/ui/Settings/util/OverrideOpHelper.tsx b/meteor/client/ui/Settings/util/OverrideOpHelper.tsx index b3589435b7..a6108dab16 100644 --- a/meteor/client/ui/Settings/util/OverrideOpHelper.tsx +++ b/meteor/client/ui/Settings/util/OverrideOpHelper.tsx @@ -8,7 +8,7 @@ import { filterOverrideOpsForPrefix, findParentOpToUpdate, } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { useRef, useMemo, useEffect, MutableRefObject } from 'react' +import { useRef, useEffect, useCallback } from 'react' import { ReadonlyDeep } from 'type-fest' export interface WrappedOverridableItemDeleted { @@ -86,80 +86,92 @@ export function getAllCurrentAndDeletedItemsFromOverrides( type SaveOverridesFunction = (newOps: SomeObjectOverrideOp[]) => void -export interface OverrideOpHelperForItemContents { +export type OverrideOpHelperForItemContents = () => OverrideOpHelperForItemContentsBatcher + +export interface OverrideOpHelperForItemContentsBatcher { /** * Clear all of the overrides for an value inside of an item * This acts as a reset of property of its child properties * Has no effect if there are no `overrideOps` on the `WrappedOverridableItemNormal` */ - clearItemOverrides(itemId: string, subPath: string): void + clearItemOverrides(itemId: string, subPath: string): this /** * Set the value of a property of an item. * Note: the id cannot be changed in this way */ - setItemValue(itemId: string, subPath: string, value: unknown): void + setItemValue(itemId: string, subPath: string, value: unknown): this + + /** + * Finish the batch operation + */ + commit(): void } -export interface OverrideOpHelper extends OverrideOpHelperForItemContents { +export interface OverrideOpHelperBatcher extends OverrideOpHelperForItemContentsBatcher { /** * Clear all of the overrides for an item * This acts as a reset to defaults or undelete * Has no effect if there are no `overrideOps` on the `WrappedOverridableItemNormal` */ - resetItem(itemId: string): void + resetItem(itemId: string): this /** * Delete an item from the object */ - deleteItem(itemId: string): void + deleteItem(itemId: string): this /** * Change the id of an item. * This is only possible for ones which were created by an override, and does not exist in the defaults * Only possible when the item being renamed does not exist in the defaults */ - changeItemId(oldItemId: string, newItemId: string): void + changeItemId(oldItemId: string, newItemId: string): this /** * Replace a whole item with a new object * Note: the id cannot be changed in this way */ - replaceItem(itemId: string, value: any): void + replaceItem(itemId: string, value: any): this + + /** + * Finish the batch operation + */ + commit(): void } -class OverrideOpHelperImpl implements OverrideOpHelper { + +export type OverrideOpHelper = () => OverrideOpHelperBatcher + +class OverrideOpHelperImpl implements OverrideOpHelperBatcher { readonly #saveOverrides: SaveOverridesFunction - readonly #objectWithOverridesRef: MutableRefObject> + readonly #object: ObjectWithOverrides - constructor( - saveOverrides: SaveOverridesFunction, - objectWithOverridesRef: MutableRefObject> - ) { + constructor(saveOverrides: SaveOverridesFunction, object: ObjectWithOverrides) { this.#saveOverrides = saveOverrides - this.#objectWithOverridesRef = objectWithOverridesRef + this.#object = { ...object } } - clearItemOverrides = (itemId: string, subPath: string): void => { - if (!this.#objectWithOverridesRef.current) return - + clearItemOverrides = (itemId: string, subPath: string): this => { const opPath = `${itemId}.${subPath}` - const newOps = this.#objectWithOverridesRef.current.overrides.filter((op) => op.path !== opPath) + const newOps = this.#object.overrides.filter((op) => op.path !== opPath) + + this.#object.overrides = newOps - this.#saveOverrides(newOps) + return this } - resetItem = (itemId: string): void => { - if (!this.#objectWithOverridesRef.current) return + resetItem = (itemId: string): this => { + const newOps = filterOverrideOpsForPrefix(this.#object.overrides, itemId).otherOps - const newOps = filterOverrideOpsForPrefix(this.#objectWithOverridesRef.current.overrides, itemId).otherOps + this.#object.overrides = newOps - this.#saveOverrides(newOps) + return this } - deleteItem = (itemId: string): void => { - const newOps = filterOverrideOpsForPrefix(this.#objectWithOverridesRef.current.overrides, itemId).otherOps - if (this.#objectWithOverridesRef.current.defaults[itemId]) { + deleteItem = (itemId: string): this => { + const newOps = filterOverrideOpsForPrefix(this.#object.overrides, itemId).otherOps + if (this.#object.defaults[itemId]) { // If it was from the defaults, we need to mark it deleted newOps.push( literal({ @@ -169,26 +181,19 @@ class OverrideOpHelperImpl implements OverrideOpHelper { ) } - this.#saveOverrides(newOps) - } + this.#object.overrides = newOps - changeItemId = (oldItemId: string, newItemId: string): void => { - if (!this.#objectWithOverridesRef.current) return + return this + } - const { otherOps: newOps, opsForPrefix: opsForId } = filterOverrideOpsForPrefix( - this.#objectWithOverridesRef.current.overrides, - oldItemId - ) + changeItemId = (oldItemId: string, newItemId: string): this => { + const { otherOps: newOps, opsForPrefix: opsForId } = filterOverrideOpsForPrefix(this.#object.overrides, oldItemId) - if ( - !newItemId || - newOps.find((op) => op.path === newItemId) || - this.#objectWithOverridesRef.current.defaults[newItemId] - ) { + if (!newItemId || newOps.find((op) => op.path === newItemId) || this.#object.defaults[newItemId]) { throw new Error('Id is invalid or already in use') } - if (this.#objectWithOverridesRef.current.defaults[oldItemId]) { + if (this.#object.defaults[oldItemId]) { // Future: should we be able to handle this? throw new Error("Can't change id of object with defaults") } else { @@ -207,21 +212,18 @@ class OverrideOpHelperImpl implements OverrideOpHelper { } } - this.#saveOverrides(newOps) + this.#object.overrides = newOps } - } - setItemValue = (itemId: string, subPath: string, value: unknown): void => { - if (!this.#objectWithOverridesRef.current) return + return this + } + setItemValue = (itemId: string, subPath: string, value: unknown): this => { if (subPath === '_id') { throw new Error('Item id cannot be changed through this helper') } else { // Set a property - const { otherOps: newOps, opsForPrefix: opsForId } = filterOverrideOpsForPrefix( - this.#objectWithOverridesRef.current.overrides, - itemId - ) + const { otherOps: newOps, opsForPrefix: opsForId } = filterOverrideOpsForPrefix(this.#object.overrides, itemId) const setRootOp = opsForId.find((op) => op.path === itemId) if (setRootOp && setRootOp.op === 'set') { @@ -261,15 +263,15 @@ class OverrideOpHelperImpl implements OverrideOpHelper { } } - this.#saveOverrides(newOps) + this.#object.overrides = newOps } - } - replaceItem = (itemId: string, value: unknown): void => { - if (!this.#objectWithOverridesRef.current) return + return this + } + replaceItem = (itemId: string, value: unknown): this => { // Set a property - const { otherOps: newOps } = filterOverrideOpsForPrefix(this.#objectWithOverridesRef.current.overrides, itemId) + const { otherOps: newOps } = filterOverrideOpsForPrefix(this.#object.overrides, itemId) // TODO - is this too naive? @@ -281,7 +283,13 @@ class OverrideOpHelperImpl implements OverrideOpHelper { }) ) - this.#saveOverrides(newOps) + this.#object.overrides = newOps + + return this + } + + commit = () => { + this.#saveOverrides(this.#object.overrides) } } @@ -294,15 +302,13 @@ export function useOverrideOpHelper( ): OverrideOpHelper { const objectWithOverridesRef = useRef(objectWithOverrides) - const helper = useMemo( - () => new OverrideOpHelperImpl(saveOverrides, objectWithOverridesRef), - [saveOverrides, objectWithOverridesRef] - ) - // Use a ref to minimise reactivity when it changes useEffect(() => { objectWithOverridesRef.current = objectWithOverrides }, [objectWithOverrides]) - return helper + return useCallback(() => { + if (!objectWithOverridesRef.current) throw new Error('No current object!') + return new OverrideOpHelperImpl(saveOverrides, objectWithOverridesRef.current) + }, [saveOverrides, objectWithOverridesRef]) } diff --git a/packages/documentation/docs/for-developers/json-config-schema.md b/packages/documentation/docs/for-developers/json-config-schema.md index 1d6df1db25..b56e6e6ee7 100644 --- a/packages/documentation/docs/for-developers/json-config-schema.md +++ b/packages/documentation/docs/for-developers/json-config-schema.md @@ -73,6 +73,10 @@ This will provide a dropdown of all source-layers in the show-style. Setting `ui:sofie-enum:filter` to an array of numbers will filter the dropdown by the specified SourceLayerType. +### `ui:import-export` + +Valid only for tables, this allows for importing and exporting the contents of the table. + ## Supported types Any JSON Schema property or type is allowed, but will be ignored if it is not supported. diff --git a/packages/shared-lib/src/lib/JSONSchemaUtil.ts b/packages/shared-lib/src/lib/JSONSchemaUtil.ts index 740368922a..3be7f98375 100644 --- a/packages/shared-lib/src/lib/JSONSchemaUtil.ts +++ b/packages/shared-lib/src/lib/JSONSchemaUtil.ts @@ -44,6 +44,11 @@ export enum SchemaFormUIField { * When using `ui:sofie-enum`, filter the options by type */ SofieEnumFilter = 'ui:sofie-enum:filter', + /** + * Whether a table supports being imported and exported + * Valid only for tables + */ + SupportsImportExport = 'ui:import-export', } export function getSchemaUIField(schema: JSONSchema, field: SchemaFormUIField): any { From d076381a1885052b4a8e643cd3890aa80f8c09d4 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 22 May 2024 14:54:46 +0100 Subject: [PATCH 306/479] chore: enable node20 in ci --- .github/workflows/node.yaml | 4 +++- .github/workflows/prerelease-libs.yml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index 3995554f64..cc3655a953 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -467,7 +467,7 @@ jobs: - blueprints-integration - server-core-integration - shared-lib - node-version: [14.x, 16.x, 18.x] + node-version: [14.x, 16.x, 18.x, 20.x] include: # include additional configs, to run certain packages only for a certain version of node - node-version: 14.x @@ -481,6 +481,8 @@ jobs: package-name: openapi - node-version: 18.x package-name: openapi + - node-version: 20.x + package-name: openapi # No tests for the gateways yet # - node-version: 18.x # package-name: playout-gateway diff --git a/.github/workflows/prerelease-libs.yml b/.github/workflows/prerelease-libs.yml index cee7b42fd6..bac83c4b71 100644 --- a/.github/workflows/prerelease-libs.yml +++ b/.github/workflows/prerelease-libs.yml @@ -53,7 +53,7 @@ jobs: - blueprints-integration - server-core-integration - shared-lib - node-version: [14.x, 16.x, 18.x] + node-version: [14.x, 16.x, 18.x, 20.x] steps: - uses: actions/checkout@v4 From 3a8ef454b3675a56b1f05ce08c19d48eb3eacce1 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 22 May 2024 14:55:02 +0100 Subject: [PATCH 307/479] feat: base64 image input control SOFIE-3138 (#1191) Co-authored-by: Jan Starzak --- .../lib/Components/Base64ImageInput.tsx | 63 ++++++++++++++++ .../lib/Components/LabelAndOverrides.tsx | 18 ++++- .../forms/SchemaFormTable/ObjectTable.scss | 6 ++ .../lib/forms/SchemaFormTable/ObjectTable.tsx | 74 +++++++++++++------ .../lib/forms/SchemaFormWithOverrides.tsx | 16 +++- meteor/client/lib/forms/schemaFormUtil.tsx | 11 ++- meteor/client/lib/uploadButton.tsx | 2 + .../ui/Settings/util/OverrideOpHelper.tsx | 8 +- packages/shared-lib/src/lib/JSONSchemaUtil.ts | 4 +- 9 files changed, 170 insertions(+), 32 deletions(-) create mode 100644 meteor/client/lib/Components/Base64ImageInput.tsx diff --git a/meteor/client/lib/Components/Base64ImageInput.tsx b/meteor/client/lib/Components/Base64ImageInput.tsx new file mode 100644 index 0000000000..dfdd79d0d7 --- /dev/null +++ b/meteor/client/lib/Components/Base64ImageInput.tsx @@ -0,0 +1,63 @@ +import React, { useCallback, useState } from 'react' +import { UploadButton } from '../uploadButton' +import { faUpload } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { useTranslation } from 'react-i18next' + +interface IBase64ImageInputControlProps { + classNames?: string + disabled?: boolean + + value: string + handleUpdate: (value: string) => void +} +export function Base64ImageInputControl({ + classNames, + value, + disabled, + handleUpdate, +}: Readonly): JSX.Element { + const { t } = useTranslation() + + const [uploadFileKey, setUploadFileKey] = useState(() => Date.now()) + + const handleSelectFile = useCallback( + (event: React.ChangeEvent) => { + // Clear the field + setUploadFileKey(Date.now()) + + const file = event.target.files?.[0] + if (!file) return + + const reader = new FileReader() + reader.onload = (readEvent) => { + // On file upload + + const uploadResult = readEvent.target?.result + if (typeof uploadResult !== 'string' || !uploadResult) return + + console.log('selected', readEvent.target?.result) + + handleUpdate(uploadResult.toString()) + } + reader.readAsDataURL(file) + }, + [handleUpdate] + ) + + return ( +
+ + + {t('Select image')} + + {value ? : null} +
+ ) +} diff --git a/meteor/client/lib/Components/LabelAndOverrides.tsx b/meteor/client/lib/Components/LabelAndOverrides.tsx index a25f6477dc..9e70e1b7c8 100644 --- a/meteor/client/lib/Components/LabelAndOverrides.tsx +++ b/meteor/client/lib/Components/LabelAndOverrides.tsx @@ -16,7 +16,7 @@ export interface LabelAndOverridesProps { opPrefix: string overrideHelper: OverrideOpHelperForItemContents - formatDefaultValue?: (value: any) => string + formatDefaultValue?: (value: any) => JSX.Element | string | null children: (value: TValue, setValue: (value: TValue) => void) => React.ReactNode } @@ -49,7 +49,7 @@ export function LabelAndOverrides({ const isOverridden = hasOpWithPath(item.overrideOps, opPrefix, String(itemKey)) - let displayValue = '""' + let displayValue: JSX.Element | string | null = '""' if (item.defaults) { const defaultValue: any = item.defaults[itemKey] // Special cases for formatting of the default @@ -174,3 +174,17 @@ export function LabelAndOverridesForInt( export function LabelActual(props: LabelActualProps): JSX.Element { return
{props.label}
} + +export function LabelAndOverridesForBase64Image( + props: Readonly, 'formatDefaultValue'>> +): JSX.Element { + const formatter = useCallback((defaultValue: any) => { + if (defaultValue && defaultValue.startsWith('data:image/')) { + return + } else { + return '-' + } + }, []) + + return {...props} formatDefaultValue={formatter} /> +} diff --git a/meteor/client/lib/forms/SchemaFormTable/ObjectTable.scss b/meteor/client/lib/forms/SchemaFormTable/ObjectTable.scss index 83fac8951c..5ec883fd32 100644 --- a/meteor/client/lib/forms/SchemaFormTable/ObjectTable.scss +++ b/meteor/client/lib/forms/SchemaFormTable/ObjectTable.scss @@ -27,3 +27,9 @@ background-color: white; } } + +.settings-config-table img.table-image-preview { + // Match the line height of the text + max-height: 1.5rem; + max-width: 3rem; +} diff --git a/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx b/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx index 22c0a3183e..cb92b6b733 100644 --- a/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx +++ b/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx @@ -1,6 +1,6 @@ import { faDownload, faPlus, faUpload } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { getRandomString, joinObjectPathFragments, objectPathGet } from '@sofie-automation/corelib/dist/lib' +import { getRandomString, joinObjectPathFragments, literal, objectPathGet } from '@sofie-automation/corelib/dist/lib' import React, { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { @@ -58,31 +58,59 @@ export const SchemaFormObjectTable = ({ const { t } = useTranslation() const wrappedRows = useMemo(() => { - const itemValue = item.defaults ?? item.computed // If this is sourced from an override, there are no defaults so we need to use the computed instead - const rawRows = (attr ? objectPathGet(itemValue, attr) : itemValue) || {} - - const prefix = joinObjectPathFragments(item.id, attr) + '.' - - // Filter and strip the ops to be local to the row object - const ops: SomeObjectOverrideOp[] = [] - for (const op of item.overrideOps) { - if (op.path.startsWith(prefix)) { - ops.push({ - ...op, - path: op.path.slice(prefix.length), - }) + if (item.defaults) { + // Table can be overriden with granularity + + const rawRows = (attr ? objectPathGet(item.defaults, attr) : item.defaults) || {} + + const prefix = joinObjectPathFragments(item.id, attr) + '.' + + // Filter and strip the ops to be local to the row object + const ops: SomeObjectOverrideOp[] = [] + for (const op of item.overrideOps) { + if (op.path.startsWith(prefix)) { + ops.push({ + ...op, + path: op.path.slice(prefix.length), + }) + } } - } - const wrappedRows = getAllCurrentAndDeletedItemsFromOverrides( - { - defaults: rawRows, - overrides: ops, - }, - (a, b) => a[0].localeCompare(b[0]) // TODO - better comparitor? - ) + const wrappedRows = getAllCurrentAndDeletedItemsFromOverrides( + { + defaults: rawRows, + overrides: ops, + }, + (a, b) => a[0].localeCompare(b[0]) // TODO - better comparitor? + ) + + console.log('obj', item, rawRows, ops, wrappedRows) + + return wrappedRows + } else { + // Table is formed of purely of an override, so ignore any defaults - return wrappedRows + const rawRows = (attr ? objectPathGet(item.computed, attr) : item.computed) || {} + + // Convert the items into an array + const validItems: Array<[id: string, obj: object]> = [] + for (const [id, obj] of Object.entries(rawRows)) { + if (obj) validItems.push([id, obj]) + } + + validItems.sort((a, b) => a[0].localeCompare(b[0])) // TODO - better comparitor? + + // Sort and wrap in the return type + return validItems.map(([id, obj]) => + literal>({ + type: 'normal', + id: id, + computed: obj, + defaults: undefined, + overrideOps: [], + }) + ) + } }, [attr, item]) const rowSchema = schema.patternProperties?.[''] diff --git a/meteor/client/lib/forms/SchemaFormWithOverrides.tsx b/meteor/client/lib/forms/SchemaFormWithOverrides.tsx index 868ebb6fc6..c6acb13940 100644 --- a/meteor/client/lib/forms/SchemaFormWithOverrides.tsx +++ b/meteor/client/lib/forms/SchemaFormWithOverrides.tsx @@ -9,6 +9,7 @@ import { IntInputControl } from '../Components/IntInput' import { JsonTextInputControl } from '../Components/JsonTextInput' import { LabelAndOverrides, + LabelAndOverridesForBase64Image, LabelAndOverridesForCheckbox, LabelAndOverridesForDropdown, LabelAndOverridesForInt, @@ -22,6 +23,7 @@ import { MultiSelectInputControl } from '../Components/MultiSelectInput' import { SchemaFormObjectTable } from './SchemaFormTable/ObjectTable' import { SchemaFormUIField } from '@sofie-automation/blueprints-integration' import { SchemaFormSectionHeader } from './SchemaFormSectionHeader' +import { Base64ImageInputControl } from '../Components/Base64ImageInput' interface SchemaFormWithOverridesProps extends SchemaFormCommonProps { /** Base path of the schema within the document */ @@ -102,7 +104,9 @@ export function SchemaFormWithOverrides(props: SchemaFormWithOverridesProps): JS case TypeName.Boolean: return case TypeName.String: - if (props.schema[SchemaFormUIField.SofieEnum]) { + if (props.schema[SchemaFormUIField.DisplayType] === 'base64-image') { + return + } else if (props.schema[SchemaFormUIField.SofieEnum]) { return ( { ) } + +const Base64ImagePickerWithOverrides = ({ commonAttrs }: FormComponentProps) => { + return ( + + {(value, handleUpdate) => ( + + )} + + ) +} diff --git a/meteor/client/lib/forms/schemaFormUtil.tsx b/meteor/client/lib/forms/schemaFormUtil.tsx index aaac9d3d3a..0116a76e19 100644 --- a/meteor/client/lib/forms/schemaFormUtil.tsx +++ b/meteor/client/lib/forms/schemaFormUtil.tsx @@ -1,3 +1,4 @@ +import React from 'react' import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import { i18nTranslator } from '../../ui/i18n' import { JSONSchema, TypeName } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes' @@ -38,7 +39,7 @@ export function translateStringIfHasNamespaces(str: string, translationNamespace export interface SchemaSummaryField { attr: string name: string - transform?: (val: any) => string + transform?: (val: any) => string | JSX.Element } export function getSchemaSummaryFieldsForObject( @@ -97,6 +98,14 @@ export function getSchemaSummaryFields(schema: JSONSchema, prefix?: string): Sch return val } } + } else if (schema.type === 'string' && schema[SchemaFormUIField.DisplayType] === 'base64-image') { + transform = (val) => { + if (val && val.startsWith('data:image/')) { + return + } else { + return '-' + } + } } return [ diff --git a/meteor/client/lib/uploadButton.tsx b/meteor/client/lib/uploadButton.tsx index c6ede0c814..bd1d0b3255 100644 --- a/meteor/client/lib/uploadButton.tsx +++ b/meteor/client/lib/uploadButton.tsx @@ -5,6 +5,7 @@ interface IProps { accept?: string onChange?: (e: React.ChangeEvent) => void children?: React.ReactNode + disabled?: boolean } export const UploadButton: React.FunctionComponent = function (props: IProps) { @@ -22,6 +23,7 @@ export const UploadButton: React.FunctionComponent = function (props: IP display: 'inline', position: 'absolute', }} + disabled={props.disabled} /> ) diff --git a/meteor/client/ui/Settings/util/OverrideOpHelper.tsx b/meteor/client/ui/Settings/util/OverrideOpHelper.tsx index 99e647d408..c1e28ba8a7 100644 --- a/meteor/client/ui/Settings/util/OverrideOpHelper.tsx +++ b/meteor/client/ui/Settings/util/OverrideOpHelper.tsx @@ -84,13 +84,13 @@ export function getAllCurrentAndDeletedItemsFromOverrides( }) ) - const removedOutputLayers: WrappedOverridableItemDeleted[] = [] + const removedItems: WrappedOverridableItemDeleted[] = [] // Find the items which have been deleted with an override const computedOutputLayerIds = new Set(sortedItems.map((l) => l.id)) for (const [id, output] of Object.entries>(rawObject.defaults)) { if (!computedOutputLayerIds.has(id) && output) { - removedOutputLayers.push( + removedItems.push( literal>({ type: 'deleted', id: id, @@ -102,9 +102,9 @@ export function getAllCurrentAndDeletedItemsFromOverrides( } } - if (comparitor) removedOutputLayers.sort((a, b) => comparitor([a.id, a.defaults], [b.id, b.defaults])) + if (comparitor) removedItems.sort((a, b) => comparitor([a.id, a.defaults], [b.id, b.defaults])) - return [...sortedItems, ...removedOutputLayers] + return [...sortedItems, ...removedItems] } type SaveOverridesFunction = (newOps: SomeObjectOverrideOp[]) => void diff --git a/packages/shared-lib/src/lib/JSONSchemaUtil.ts b/packages/shared-lib/src/lib/JSONSchemaUtil.ts index 992ad75eb0..372bbd982e 100644 --- a/packages/shared-lib/src/lib/JSONSchemaUtil.ts +++ b/packages/shared-lib/src/lib/JSONSchemaUtil.ts @@ -26,7 +26,9 @@ export enum SchemaFormUIField { ZeroBased = 'ui:zeroBased', /** * Override the presentation with a special mode. - * Currently only valid for string properties. Valid values are 'json'. + * Currently only valid for: + * - object properties. Valid values are 'json'. + * - string properties. Valid values are 'base64-image'. */ DisplayType = 'ui:displayType', /** From a51e9f7e6d0a3eb9b12b7d0c2073fba2892fd90e Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 22 May 2024 15:57:04 +0200 Subject: [PATCH 308/479] feat(live-status-gateway): add partId and segmentId to adLibs --- .../api/schemas/adLibs.yaml | 14 ++++++- .../src/liveStatusServer.ts | 1 + .../src/topics/adLibsTopic.ts | 41 ++++++++++++++++--- 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/packages/live-status-gateway/api/schemas/adLibs.yaml b/packages/live-status-gateway/api/schemas/adLibs.yaml index 2d66ec8c0b..b98a84e17f 100644 --- a/packages/live-status-gateway/api/schemas/adLibs.yaml +++ b/packages/live-status-gateway/api/schemas/adLibs.yaml @@ -21,7 +21,7 @@ $defs: description: The available Global AdLibs for this playlist type: array items: - $ref: '#/$defs/adLib' + $ref: '#/$defs/globalAdLib' required: [event, rundownPlaylistId, adLibs, globalAdLibs] additionalProperties: false examples: @@ -32,6 +32,18 @@ $defs: globalAdLibs: $ref: '#/$defs/adLib/examples' adLib: + allOf: + - $ref: '#/$defs/adLibBase' + - type: object + properties: + segmentId: + description: Unique id of the segment this adLib belongs to + partId: + description: Unique id of the part this adLib belongs to + required: [segmentId, partId] + globalAdLib: + $ref: '#/$defs/adLibBase' + adLibBase: type: object properties: id: diff --git a/packages/live-status-gateway/src/liveStatusServer.ts b/packages/live-status-gateway/src/liveStatusServer.ts index addc8c3934..ea18611f8a 100644 --- a/packages/live-status-gateway/src/liveStatusServer.ts +++ b/packages/live-status-gateway/src/liveStatusServer.ts @@ -116,6 +116,7 @@ export class LiveStatusServer { await partsHandler.subscribe(segmentsTopic) await showStyleBaseHandler.subscribe(adLibsTopic) + await partsHandler.subscribe(adLibsTopic) await playlistHandler.subscribe(adLibsTopic) await adLibActionsHandler.subscribe(adLibsTopic) await adLibsHandler.subscribe(adLibsTopic) diff --git a/packages/live-status-gateway/src/topics/adLibsTopic.ts b/packages/live-status-gateway/src/topics/adLibsTopic.ts index d2c93871bc..e2e29efc55 100644 --- a/packages/live-status-gateway/src/topics/adLibsTopic.ts +++ b/packages/live-status-gateway/src/topics/adLibsTopic.ts @@ -17,6 +17,9 @@ import { ShowStyleBaseExt, ShowStyleBaseHandler } from '../collections/showStyle import { interpollateTranslation } from '@sofie-automation/corelib/dist/TranslatableMessage' import { AdLibsHandler } from '../collections/adLibsHandler' import { GlobalAdLibsHandler } from '../collections/globalAdLibsHandler' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import { PartsHandler } from '../collections/partsHandler' +import { PartId } from '@sofie-automation/corelib/dist/dataModel/Ids' const THROTTLE_PERIOD_MS = 100 @@ -24,7 +27,7 @@ export interface AdLibsStatus { event: 'adLibs' rundownPlaylistId: string | null adLibs: AdLibStatus[] - globalAdLibs: AdLibStatus[] + globalAdLibs: GlobalAdLibStatus[] } interface AdLibActionType { @@ -32,7 +35,14 @@ interface AdLibActionType { label: string } -interface AdLibStatus { +interface AdLibStatus extends AdLibStatusBase { + segmentId: string + partId: string +} + +type GlobalAdLibStatus = AdLibStatusBase + +interface AdLibStatusBase { id: string name: string sourceLayer: string @@ -49,7 +59,8 @@ export class AdLibsTopic CollectionObserver, CollectionObserver, CollectionObserver, - CollectionObserver + CollectionObserver, + CollectionObserver { public observerName = AdLibsTopic.name private _activePlaylist: DBRundownPlaylist | undefined @@ -57,6 +68,7 @@ export class AdLibsTopic private _outputLayersMap: ReadonlyMap = new Map() private _adLibActions: AdLibAction[] | undefined private _abLibs: AdLibPiece[] | undefined + private _parts: ReadonlyMap = new Map() private _globalAdLibActions: RundownBaselineAdLibAction[] | undefined private _globalAdLibs: RundownBaselineAdLibItem[] | undefined private throttledSendStatusToAll: () => void @@ -76,7 +88,7 @@ export class AdLibsTopic sendStatus(subscribers: Iterable): void { const adLibs: AdLibStatus[] = [] - const globalAdLibs: AdLibStatus[] = [] + const globalAdLibs: GlobalAdLibStatus[] = [] if (this._adLibActions) { adLibs.push( @@ -95,9 +107,12 @@ export class AdLibsTopic }) ) : [] + const segmentId = this._parts.get(action.partId)?.segmentId return literal({ id: unprotectString(action._id), name: interpollateTranslation(action.display.label.key, action.display.label.args), + partId: unprotectString(action.partId), + segmentId: unprotectString(segmentId) ?? 'invalid', sourceLayer: sourceLayerName ?? 'invalid', outputLayer: outputLayerName ?? 'invalid', actionType: triggerModes, @@ -113,9 +128,12 @@ export class AdLibsTopic ...this._abLibs.map((adLib) => { const sourceLayerName = this._sourceLayersMap.get(adLib.sourceLayerId) const outputLayerName = this._outputLayersMap.get(adLib.outputLayerId) + const segmentId = adLib.partId ? this._parts.get(adLib.partId)?.segmentId : undefined return literal({ id: unprotectString(adLib._id), name: adLib.name, + partId: unprotectString(adLib.partId) ?? 'invalid', + segmentId: unprotectString(segmentId) ?? 'invalid', sourceLayer: sourceLayerName ?? 'invalid', outputLayer: outputLayerName ?? 'invalid', actionType: [], @@ -143,7 +161,7 @@ export class AdLibsTopic }) ) : [] - return literal({ + return literal({ id: unprotectString(action._id), name: interpollateTranslation(action.display.label.key, action.display.label.args), sourceLayer: sourceLayerName ?? 'invalid', @@ -161,7 +179,7 @@ export class AdLibsTopic ...this._globalAdLibs.map((adLib) => { const sourceLayerName = this._sourceLayersMap.get(adLib.sourceLayerId) const outputLayerName = this._outputLayersMap.get(adLib.outputLayerId) - return literal({ + return literal({ id: unprotectString(adLib._id), name: adLib.name, sourceLayer: sourceLayerName ?? 'invalid', @@ -197,6 +215,7 @@ export class AdLibsTopic | RundownBaselineAdLibAction[] | AdLibPiece[] | RundownBaselineAdLibItem[] + | DBPart[] | undefined ): Promise { switch (source) { @@ -238,6 +257,16 @@ export class AdLibsTopic this._outputLayersMap = showStyleBaseExt?.outputLayerNamesById ?? new Map() break } + case PartsHandler.name: { + const parts = data ? (data as DBPart[]) : [] + this.logUpdateReceived('parts', source) + const newParts = new Map() + parts.forEach((part) => { + newParts.set(part._id, part) + }) + this._parts = newParts + break + } default: throw new Error(`${this._name} received unsupported update from ${source}}`) } From 1a6f2ff51c98a80a6049959a68a15f5a4bbf7739 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 22 May 2024 14:55:02 +0100 Subject: [PATCH 309/479] feat: base64 image input control SOFIE-3138 (#1191) Co-authored-by: Jan Starzak --- .../lib/Components/Base64ImageInput.tsx | 63 ++++++++++++++++ .../lib/Components/LabelAndOverrides.tsx | 18 ++++- .../forms/SchemaFormTable/ObjectTable.scss | 6 ++ .../lib/forms/SchemaFormTable/ObjectTable.tsx | 74 +++++++++++++------ .../lib/forms/SchemaFormWithOverrides.tsx | 16 +++- meteor/client/lib/forms/schemaFormUtil.tsx | 14 +++- meteor/client/lib/uploadButton.tsx | 2 + .../ui/Settings/util/OverrideOpHelper.tsx | 8 +- packages/shared-lib/src/lib/JSONSchemaUtil.ts | 4 +- 9 files changed, 173 insertions(+), 32 deletions(-) create mode 100644 meteor/client/lib/Components/Base64ImageInput.tsx diff --git a/meteor/client/lib/Components/Base64ImageInput.tsx b/meteor/client/lib/Components/Base64ImageInput.tsx new file mode 100644 index 0000000000..dfdd79d0d7 --- /dev/null +++ b/meteor/client/lib/Components/Base64ImageInput.tsx @@ -0,0 +1,63 @@ +import React, { useCallback, useState } from 'react' +import { UploadButton } from '../uploadButton' +import { faUpload } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { useTranslation } from 'react-i18next' + +interface IBase64ImageInputControlProps { + classNames?: string + disabled?: boolean + + value: string + handleUpdate: (value: string) => void +} +export function Base64ImageInputControl({ + classNames, + value, + disabled, + handleUpdate, +}: Readonly): JSX.Element { + const { t } = useTranslation() + + const [uploadFileKey, setUploadFileKey] = useState(() => Date.now()) + + const handleSelectFile = useCallback( + (event: React.ChangeEvent) => { + // Clear the field + setUploadFileKey(Date.now()) + + const file = event.target.files?.[0] + if (!file) return + + const reader = new FileReader() + reader.onload = (readEvent) => { + // On file upload + + const uploadResult = readEvent.target?.result + if (typeof uploadResult !== 'string' || !uploadResult) return + + console.log('selected', readEvent.target?.result) + + handleUpdate(uploadResult.toString()) + } + reader.readAsDataURL(file) + }, + [handleUpdate] + ) + + return ( +
+ + + {t('Select image')} + + {value ? : null} +
+ ) +} diff --git a/meteor/client/lib/Components/LabelAndOverrides.tsx b/meteor/client/lib/Components/LabelAndOverrides.tsx index 785da347c6..25cb48894a 100644 --- a/meteor/client/lib/Components/LabelAndOverrides.tsx +++ b/meteor/client/lib/Components/LabelAndOverrides.tsx @@ -16,7 +16,7 @@ export interface LabelAndOverridesProps { opPrefix: string overrideHelper: OverrideOpHelperForItemContents - formatDefaultValue?: (value: any) => string + formatDefaultValue?: (value: any) => JSX.Element | string | null children: (value: TValue, setValue: (value: TValue) => void) => React.ReactNode } @@ -49,7 +49,7 @@ export function LabelAndOverrides({ const isOverridden = hasOpWithPath(item.overrideOps, opPrefix, String(itemKey)) - let displayValue = '""' + let displayValue: JSX.Element | string | null = '""' if (item.defaults) { const defaultValue: any = item.defaults[itemKey] // Special cases for formatting of the default @@ -174,3 +174,17 @@ export function LabelAndOverridesForInt( export function LabelActual(props: Readonly): JSX.Element { return
{props.label}
} + +export function LabelAndOverridesForBase64Image( + props: Readonly, 'formatDefaultValue'>> +): JSX.Element { + const formatter = useCallback((defaultValue: any) => { + if (defaultValue && defaultValue.startsWith('data:image/')) { + return + } else { + return '-' + } + }, []) + + return {...props} formatDefaultValue={formatter} /> +} diff --git a/meteor/client/lib/forms/SchemaFormTable/ObjectTable.scss b/meteor/client/lib/forms/SchemaFormTable/ObjectTable.scss index 83fac8951c..5ec883fd32 100644 --- a/meteor/client/lib/forms/SchemaFormTable/ObjectTable.scss +++ b/meteor/client/lib/forms/SchemaFormTable/ObjectTable.scss @@ -27,3 +27,9 @@ background-color: white; } } + +.settings-config-table img.table-image-preview { + // Match the line height of the text + max-height: 1.5rem; + max-width: 3rem; +} diff --git a/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx b/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx index 8c5992e300..5c76136015 100644 --- a/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx +++ b/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx @@ -1,6 +1,6 @@ import { faDownload, faPlus, faUpload } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { getRandomString, joinObjectPathFragments, objectPathGet } from '@sofie-automation/corelib/dist/lib' +import { getRandomString, joinObjectPathFragments, literal, objectPathGet } from '@sofie-automation/corelib/dist/lib' import React, { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { @@ -62,31 +62,59 @@ export const SchemaFormObjectTable = ({ const { t } = useTranslation() const wrappedRows = useMemo(() => { - const itemValue = item.defaults ?? item.computed // If this is sourced from an override, there are no defaults so we need to use the computed instead - const rawRows = (attr ? objectPathGet(itemValue, attr) : itemValue) || {} - - const prefix = joinObjectPathFragments(item.id, attr) + '.' - - // Filter and strip the ops to be local to the row object - const ops: SomeObjectOverrideOp[] = [] - for (const op of item.overrideOps) { - if (op.path.startsWith(prefix)) { - ops.push({ - ...op, - path: op.path.slice(prefix.length), - }) + if (item.defaults) { + // Table can be overriden with granularity + + const rawRows = (attr ? objectPathGet(item.defaults, attr) : item.defaults) || {} + + const prefix = joinObjectPathFragments(item.id, attr) + '.' + + // Filter and strip the ops to be local to the row object + const ops: SomeObjectOverrideOp[] = [] + for (const op of item.overrideOps) { + if (op.path.startsWith(prefix)) { + ops.push({ + ...op, + path: op.path.slice(prefix.length), + }) + } } - } - const wrappedRows = getAllCurrentAndDeletedItemsFromOverrides( - { - defaults: rawRows, - overrides: ops, - }, - (a, b) => a[0].localeCompare(b[0]) // TODO - better comparitor? - ) + const wrappedRows = getAllCurrentAndDeletedItemsFromOverrides( + { + defaults: rawRows, + overrides: ops, + }, + (a, b) => a[0].localeCompare(b[0]) // TODO - better comparitor? + ) + + console.log('obj', item, rawRows, ops, wrappedRows) + + return wrappedRows + } else { + // Table is formed of purely of an override, so ignore any defaults - return wrappedRows + const rawRows = (attr ? objectPathGet(item.computed, attr) : item.computed) || {} + + // Convert the items into an array + const validItems: Array<[id: string, obj: object]> = [] + for (const [id, obj] of Object.entries(rawRows)) { + if (obj) validItems.push([id, obj]) + } + + validItems.sort((a, b) => a[0].localeCompare(b[0])) // TODO - better comparitor? + + // Sort and wrap in the return type + return validItems.map(([id, obj]) => + literal>({ + type: 'normal', + id: id, + computed: obj, + defaults: undefined, + overrideOps: [], + }) + ) + } }, [attr, item]) const rowSchema = schema.patternProperties?.[''] diff --git a/meteor/client/lib/forms/SchemaFormWithOverrides.tsx b/meteor/client/lib/forms/SchemaFormWithOverrides.tsx index 1e6aa7baec..d55d193dd8 100644 --- a/meteor/client/lib/forms/SchemaFormWithOverrides.tsx +++ b/meteor/client/lib/forms/SchemaFormWithOverrides.tsx @@ -9,6 +9,7 @@ import { IntInputControl } from '../Components/IntInput' import { JsonTextInputControl } from '../Components/JsonTextInput' import { LabelAndOverrides, + LabelAndOverridesForBase64Image, LabelAndOverridesForCheckbox, LabelAndOverridesForDropdown, LabelAndOverridesForInt, @@ -22,6 +23,7 @@ import { MultiSelectInputControl } from '../Components/MultiSelectInput' import { SchemaFormObjectTable } from './SchemaFormTable/ObjectTable' import { getSchemaUIField, SchemaFormUIField } from '@sofie-automation/blueprints-integration' import { SchemaFormSectionHeader } from './SchemaFormSectionHeader' +import { Base64ImageInputControl } from '../Components/Base64ImageInput' interface SchemaFormWithOverridesProps extends SchemaFormCommonProps { /** Base path of the schema within the document */ @@ -102,7 +104,9 @@ export function SchemaFormWithOverrides(props: Readonly case TypeName.String: - if (getSchemaUIField(props.schema, SchemaFormUIField.SofieEnum)) { + if (getSchemaUIField(props.schema, SchemaFormUIField.DisplayType) === 'base64-image') { + return + } else if (getSchemaUIField(props.schema, SchemaFormUIField.SofieEnum)) { return ( ) } + +const Base64ImagePickerWithOverrides = ({ commonAttrs }: FormComponentProps) => { + return ( + + {(value, handleUpdate) => ( + + )} + + ) +} diff --git a/meteor/client/lib/forms/schemaFormUtil.tsx b/meteor/client/lib/forms/schemaFormUtil.tsx index 49f35acb9e..a603d6eb1e 100644 --- a/meteor/client/lib/forms/schemaFormUtil.tsx +++ b/meteor/client/lib/forms/schemaFormUtil.tsx @@ -1,3 +1,4 @@ +import React from 'react' import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import { i18nTranslator } from '../../ui/i18n' import { JSONSchema, TypeName } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes' @@ -38,7 +39,7 @@ export function translateStringIfHasNamespaces(str: string, translationNamespace export interface SchemaSummaryField { attr: string name: string - transform?: (val: any) => string + transform?: (val: any) => string | JSX.Element } export function getSchemaSummaryFieldsForObject( @@ -97,6 +98,17 @@ export function getSchemaSummaryFields(schema: JSONSchema, prefix?: string): Sch return val } } + } else if ( + schema.type === 'string' && + getSchemaUIField(schema, SchemaFormUIField.DisplayType) === 'base64-image' + ) { + transform = (val) => { + if (val && val.startsWith('data:image/')) { + return + } else { + return '-' + } + } } return [ diff --git a/meteor/client/lib/uploadButton.tsx b/meteor/client/lib/uploadButton.tsx index c6ede0c814..bd1d0b3255 100644 --- a/meteor/client/lib/uploadButton.tsx +++ b/meteor/client/lib/uploadButton.tsx @@ -5,6 +5,7 @@ interface IProps { accept?: string onChange?: (e: React.ChangeEvent) => void children?: React.ReactNode + disabled?: boolean } export const UploadButton: React.FunctionComponent = function (props: IProps) { @@ -22,6 +23,7 @@ export const UploadButton: React.FunctionComponent = function (props: IP display: 'inline', position: 'absolute', }} + disabled={props.disabled} /> ) diff --git a/meteor/client/ui/Settings/util/OverrideOpHelper.tsx b/meteor/client/ui/Settings/util/OverrideOpHelper.tsx index a6108dab16..34918ecc5b 100644 --- a/meteor/client/ui/Settings/util/OverrideOpHelper.tsx +++ b/meteor/client/ui/Settings/util/OverrideOpHelper.tsx @@ -61,13 +61,13 @@ export function getAllCurrentAndDeletedItemsFromOverrides( }) ) - const removedOutputLayers: WrappedOverridableItemDeleted[] = [] + const removedItems: WrappedOverridableItemDeleted[] = [] // Find the items which have been deleted with an override const computedOutputLayerIds = new Set(sortedItems.map((l) => l.id)) for (const [id, output] of Object.entries>(rawObject.defaults)) { if (!computedOutputLayerIds.has(id) && output) { - removedOutputLayers.push( + removedItems.push( literal>({ type: 'deleted', id: id, @@ -79,9 +79,9 @@ export function getAllCurrentAndDeletedItemsFromOverrides( } } - if (comparitor) removedOutputLayers.sort((a, b) => comparitor([a.id, a.defaults], [b.id, b.defaults])) + if (comparitor) removedItems.sort((a, b) => comparitor([a.id, a.defaults], [b.id, b.defaults])) - return [...sortedItems, ...removedOutputLayers] + return [...sortedItems, ...removedItems] } type SaveOverridesFunction = (newOps: SomeObjectOverrideOp[]) => void diff --git a/packages/shared-lib/src/lib/JSONSchemaUtil.ts b/packages/shared-lib/src/lib/JSONSchemaUtil.ts index 3be7f98375..02feb95420 100644 --- a/packages/shared-lib/src/lib/JSONSchemaUtil.ts +++ b/packages/shared-lib/src/lib/JSONSchemaUtil.ts @@ -26,7 +26,9 @@ export enum SchemaFormUIField { ZeroBased = 'ui:zeroBased', /** * Override the presentation with a special mode. - * Currently only valid for string properties. Valid values are 'json'. + * Currently only valid for: + * - object properties. Valid values are 'json'. + * - string properties. Valid values are 'base64-image'. */ DisplayType = 'ui:displayType', /** From 020d3cfd2c112b57667d6612d19472570f8455b9 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 22 May 2024 16:15:32 +0200 Subject: [PATCH 310/479] fix: build errors --- .../StoryboardPartSecondaryPieces/StoryboardSecondaryPiece.tsx | 1 + .../Renderers/ThumbnailRendererFactory.ts | 1 + meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx | 2 ++ 3 files changed, 4 insertions(+) diff --git a/meteor/client/ui/SegmentStoryboard/StoryboardPartSecondaryPieces/StoryboardSecondaryPiece.tsx b/meteor/client/ui/SegmentStoryboard/StoryboardPartSecondaryPieces/StoryboardSecondaryPiece.tsx index e557eb1efb..b968127429 100644 --- a/meteor/client/ui/SegmentStoryboard/StoryboardPartSecondaryPieces/StoryboardSecondaryPiece.tsx +++ b/meteor/client/ui/SegmentStoryboard/StoryboardPartSecondaryPieces/StoryboardSecondaryPiece.tsx @@ -40,6 +40,7 @@ function renderPieceInside( return ScriptRenderer({ ...props, elementOffset, hovering, typeClass }) case SourceLayerType.GRAPHICS: case SourceLayerType.LOWER_THIRD: + case SourceLayerType.STUDIO_SCREEN: return GraphicsRenderer({ ...props, elementOffset, hovering, typeClass }) case SourceLayerType.SPLITS: return SplitsRenderer({ ...props, elementOffset, hovering, typeClass }) diff --git a/meteor/client/ui/SegmentStoryboard/StoryboardPartThumbnail/Renderers/ThumbnailRendererFactory.ts b/meteor/client/ui/SegmentStoryboard/StoryboardPartThumbnail/Renderers/ThumbnailRendererFactory.ts index a1af0549f1..24a47229c1 100644 --- a/meteor/client/ui/SegmentStoryboard/StoryboardPartThumbnail/Renderers/ThumbnailRendererFactory.ts +++ b/meteor/client/ui/SegmentStoryboard/StoryboardPartThumbnail/Renderers/ThumbnailRendererFactory.ts @@ -40,6 +40,7 @@ export default function renderThumbnail(props: IProps): JSX.Element { return SplitsThumbnailRenderer(props) case SourceLayerType.GRAPHICS: case SourceLayerType.LOWER_THIRD: + case SourceLayerType.STUDIO_SCREEN: return GraphicsThumbnailRenderer(props) case SourceLayerType.LOCAL: return LocalThumbnailRenderer(props) diff --git a/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx b/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx index e04e571d5c..5d48cbbaf3 100644 --- a/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx +++ b/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx @@ -43,6 +43,8 @@ function sourceLayerString(t: TFunction<'translation', undefined>, type: SourceL return t('Lower Third') // case SourceLayerType.MIC: // return t('Studio Microphone') + case SourceLayerType.STUDIO_SCREEN: + return t('Studio Screen Graphics') case SourceLayerType.REMOTE: return t('Remote Source') case SourceLayerType.SCRIPT: From f51bbbfbe3127bbaa71d9b85eeeb8c94dd029a3b Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Fri, 24 May 2024 09:08:43 +0200 Subject: [PATCH 311/479] chore:add missing SourceLayerType cases --- meteor/client/ui/PieceIcons/PieceCountdown.tsx | 1 + meteor/client/ui/SegmentContainer/PieceMultistepChevron.tsx | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/meteor/client/ui/PieceIcons/PieceCountdown.tsx b/meteor/client/ui/PieceIcons/PieceCountdown.tsx index 94c0d0c1fd..75832a1911 100644 --- a/meteor/client/ui/PieceIcons/PieceCountdown.tsx +++ b/meteor/client/ui/PieceIcons/PieceCountdown.tsx @@ -24,6 +24,7 @@ export interface IPropsHeader { const supportedLayers = new Set([ SourceLayerType.GRAPHICS, + SourceLayerType.STUDIO_SCREEN, SourceLayerType.LIVE_SPEAK, SourceLayerType.REMOTE, SourceLayerType.SPLITS, diff --git a/meteor/client/ui/SegmentContainer/PieceMultistepChevron.tsx b/meteor/client/ui/SegmentContainer/PieceMultistepChevron.tsx index b09472f752..4edb82d9a0 100644 --- a/meteor/client/ui/SegmentContainer/PieceMultistepChevron.tsx +++ b/meteor/client/ui/SegmentContainer/PieceMultistepChevron.tsx @@ -12,7 +12,9 @@ export const PieceMultistepChevron = function PieceMultistepChevron({ const noraContent = piece.instance.piece.content as NoraContent | undefined const hasStepChevron = - (piece.sourceLayer?.type === SourceLayerType.GRAPHICS || piece.sourceLayer?.type === SourceLayerType.LOWER_THIRD) && + (piece.sourceLayer?.type === SourceLayerType.GRAPHICS || + piece.sourceLayer?.type === SourceLayerType.LOWER_THIRD || + piece.sourceLayer?.type === SourceLayerType.STUDIO_SCREEN) && noraContent?.payload?.step?.enabled if (!hasStepChevron) return null From 927d2986352ddb81f4a406c6269f91630d299855 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Fri, 24 May 2024 09:24:25 +0200 Subject: [PATCH 312/479] chore: fix build error --- .../live-status-gateway/src/topics/__tests__/adLibs.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts b/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts index 424816a581..0aad832465 100644 --- a/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts @@ -85,6 +85,8 @@ describe('ActivePlaylistTopic', () => { sourceLayer: 'Layer 0', tags: ['adlib_tag'], publicData: { a: 'b' }, + segmentId: 'segment0', + partId: 'part0', }, ], globalAdLibs: [ From 49274d64bdc0532f294be34b0a4413d848321e40 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 24 May 2024 11:06:58 +0100 Subject: [PATCH 313/479] feat: action triggers styleClassNames SOFIE-3138 (#1192) --- .../lib/Components/Base64ImageInput.tsx | 2 - .../lib/forms/SchemaFormTable/ObjectTable.tsx | 4 -- .../SchemaFormTable/ObjectTableOpHelper.tsx | 2 +- .../triggeredActions/TriggeredActionEntry.tsx | 14 ++++ meteor/lib/api/triggeredActions.ts | 1 + meteor/lib/collections/TriggeredActions.ts | 6 ++ .../server/api/blueprints/migrationContext.ts | 1 + .../StudioDeviceTriggerManager.ts | 66 +++++++++++++------ meteor/server/api/triggeredActions.ts | 2 + .../migration/upgrades/showStyleBase.ts | 1 + .../server/publications/triggeredActionsUI.ts | 2 + .../blueprints-integration/src/triggers.ts | 2 + .../input-gateway/deviceTriggerPreviews.ts | 15 +++-- 13 files changed, 85 insertions(+), 33 deletions(-) diff --git a/meteor/client/lib/Components/Base64ImageInput.tsx b/meteor/client/lib/Components/Base64ImageInput.tsx index dfdd79d0d7..e8f44d50d9 100644 --- a/meteor/client/lib/Components/Base64ImageInput.tsx +++ b/meteor/client/lib/Components/Base64ImageInput.tsx @@ -36,8 +36,6 @@ export function Base64ImageInputControl({ const uploadResult = readEvent.target?.result if (typeof uploadResult !== 'string' || !uploadResult) return - console.log('selected', readEvent.target?.result) - handleUpdate(uploadResult.toString()) } reader.readAsDataURL(file) diff --git a/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx b/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx index cb92b6b733..97d27b842a 100644 --- a/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx +++ b/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx @@ -84,8 +84,6 @@ export const SchemaFormObjectTable = ({ (a, b) => a[0].localeCompare(b[0]) // TODO - better comparitor? ) - console.log('obj', item, rawRows, ops, wrappedRows) - return wrappedRows } else { // Table is formed of purely of an override, so ignore any defaults @@ -268,8 +266,6 @@ interface ImportExportButtonsProps { function ImportExportButtons({ schema, overrideHelper, wrappedRows }: Readonly) { const { t } = useTranslation() - console.log(schema) - const [uploadFileKey, setUploadFileKey] = useState(0) const exportTable = () => { diff --git a/meteor/client/lib/forms/SchemaFormTable/ObjectTableOpHelper.tsx b/meteor/client/lib/forms/SchemaFormTable/ObjectTableOpHelper.tsx index a34129ea20..933c738bfb 100644 --- a/meteor/client/lib/forms/SchemaFormTable/ObjectTableOpHelper.tsx +++ b/meteor/client/lib/forms/SchemaFormTable/ObjectTableOpHelper.tsx @@ -44,7 +44,7 @@ export class OverrideOpHelperObjectTable implements OverrideOpHelperForItemConte // If row was not user created (it has defaults), then don't store `undefined` const row = this.#currentRows.find((r) => r.id === rowId) - if (row && row.defaults) { + if (!this.#parentItem.defaults || row?.defaults) { // This is a bit of a hack, but it isn't possible to create a delete op from here this.#baseHelper.setItemValue(this.#parentItem.id, rowPath, undefined) } diff --git a/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionEntry.tsx b/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionEntry.tsx index 8e9653d441..09ab381cf8 100644 --- a/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionEntry.tsx +++ b/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionEntry.tsx @@ -543,6 +543,20 @@ export const TriggeredActionEntry: React.FC = React.memo(function Trigge /> {t('Optional description of the action')} + ) : null} diff --git a/meteor/lib/api/triggeredActions.ts b/meteor/lib/api/triggeredActions.ts index fc7c1ea801..2f6c0828fb 100644 --- a/meteor/lib/api/triggeredActions.ts +++ b/meteor/lib/api/triggeredActions.ts @@ -15,6 +15,7 @@ export interface CreateTriggeredActionsContent { name?: ITranslatableMessage | string triggers?: Record actions?: Record + styleClassNames?: string } export enum TriggeredActionsAPIMethods { diff --git a/meteor/lib/collections/TriggeredActions.ts b/meteor/lib/collections/TriggeredActions.ts index b66ce92dab..e3ef994e8e 100644 --- a/meteor/lib/collections/TriggeredActions.ts +++ b/meteor/lib/collections/TriggeredActions.ts @@ -21,6 +21,9 @@ export interface UITriggeredActionsObj { /** A list of actions to execute */ actions: Record + + /** Space separated list of class names to use when displaying this triggered actions */ + styleClassNames?: string } export interface DBTriggeredActions { @@ -42,6 +45,9 @@ export interface DBTriggeredActions { /** A list of actions to execute */ actionsWithOverrides: ObjectWithOverrides> + + /** Space separated list of class names to use when displaying this triggered actions */ + styleClassNames?: string } /** Note: Use DBTriggeredActions instead */ diff --git a/meteor/server/api/blueprints/migrationContext.ts b/meteor/server/api/blueprints/migrationContext.ts index 842954be31..af08e6f4ad 100644 --- a/meteor/server/api/blueprints/migrationContext.ts +++ b/meteor/server/api/blueprints/migrationContext.ts @@ -48,6 +48,7 @@ function convertTriggeredActionToBlueprints(triggeredAction: TriggeredActionsObj name: triggeredAction.name, triggers: clone(triggeredAction.triggersWithOverrides.defaults), actions: clone(triggeredAction.actionsWithOverrides.defaults), + styleClassNames: triggeredAction.styleClassNames, } return obj diff --git a/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts b/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts index 0ab7df6c2b..03282c892b 100644 --- a/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts +++ b/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts @@ -12,6 +12,7 @@ import { import { DeviceActionId, DeviceTriggerMountedActionId, + PreviewWrappedAdLib, PreviewWrappedAdLibId, ShiftRegisterActionArguments, } from '../../../lib/api/triggers/MountedTriggers' @@ -136,33 +137,58 @@ export class StudioDeviceTriggerManager { upsertedDeviceTriggerMountedActionIds.push(deviceTriggerMountedActionId) }) - if (!isPreviewableAction(thisAction)) return - - const previewedAdLibs = thisAction.preview(context) - - previewedAdLibs.forEach((adLib) => { - const adLibPreviewId = protectString( - `${triggeredAction._id}_${studioId}_${key}_${adLib._id}` - ) + if (!isPreviewableAction(thisAction)) { + const adLibPreviewId = protectString(`${actionId}_preview`) DeviceTriggerMountedActionAdlibsPreview.upsert(adLibPreviewId, { - $set: { - ...adLib, + $set: literal({ _id: adLibPreviewId, + _rank: 0, + partId: null, + type: undefined, + label: thisAction.action, + item: null, triggeredActionId: triggeredAction._id, actionId, studioId, showStyleBaseId, - sourceLayerType: adLib.sourceLayerId ? sourceLayers[adLib.sourceLayerId]?.type : undefined, - sourceLayerName: adLib.sourceLayerId - ? { - name: sourceLayers[adLib.sourceLayerId]?.name, - abbreviation: sourceLayers[adLib.sourceLayerId]?.abbreviation, - } - : undefined, - }, + sourceLayerType: undefined, + sourceLayerName: undefined, + styleClassNames: triggeredAction.styleClassNames, + }), }) + addedPreviewIds.push(adLibPreviewId) - }) + } else { + const previewedAdLibs = thisAction.preview(context) + + previewedAdLibs.forEach((adLib) => { + const adLibPreviewId = protectString( + `${triggeredAction._id}_${studioId}_${key}_${adLib._id}` + ) + DeviceTriggerMountedActionAdlibsPreview.upsert(adLibPreviewId, { + $set: literal({ + ...adLib, + _id: adLibPreviewId, + triggeredActionId: triggeredAction._id, + actionId, + studioId, + showStyleBaseId, + sourceLayerType: adLib.sourceLayerId + ? sourceLayers[adLib.sourceLayerId]?.type + : undefined, + sourceLayerName: adLib.sourceLayerId + ? { + name: sourceLayers[adLib.sourceLayerId]?.name, + abbreviation: sourceLayers[adLib.sourceLayerId]?.abbreviation, + } + : undefined, + styleClassNames: triggeredAction.styleClassNames, + }), + }) + + addedPreviewIds.push(adLibPreviewId) + }) + } }) DeviceTriggerMountedActionAdlibsPreview.remove({ @@ -234,6 +260,8 @@ function convertDocument(doc: ReadonlyObjectDeep): UITrigger actions: applyAndValidateOverrides>(doc.actionsWithOverrides).obj, triggers: applyAndValidateOverrides>(doc.triggersWithOverrides).obj, + + styleClassNames: doc.styleClassNames, }) } diff --git a/meteor/server/api/triggeredActions.ts b/meteor/server/api/triggeredActions.ts index 790cdd8654..043ed7a600 100644 --- a/meteor/server/api/triggeredActions.ts +++ b/meteor/server/api/triggeredActions.ts @@ -37,6 +37,8 @@ export async function createTriggeredActions( // User source objects should be formed purely of overrides actionsWithOverrides: convertObjectIntoOverrides(base?.actions), triggersWithOverrides: convertObjectIntoOverrides(base?.triggers), + + styleClassNames: base?.styleClassNames, }) ) return id diff --git a/meteor/server/migration/upgrades/showStyleBase.ts b/meteor/server/migration/upgrades/showStyleBase.ts index f762d098bc..f92eb12059 100644 --- a/meteor/server/migration/upgrades/showStyleBase.ts +++ b/meteor/server/migration/upgrades/showStyleBase.ts @@ -160,6 +160,7 @@ export async function runUpgradeForShowStyleBase(showStyleBaseId: ShowStyleBaseI blueprintUniqueId: newTriggeredAction._id, triggersWithOverrides: wrapDefaultObject(newTriggeredAction.triggers), actionsWithOverrides: wrapDefaultObject(newTriggeredAction.actions), + styleClassNames: newTriggeredAction.styleClassNames, }), }, }) diff --git a/meteor/server/publications/triggeredActionsUI.ts b/meteor/server/publications/triggeredActionsUI.ts index da80b87519..0a63d4436e 100644 --- a/meteor/server/publications/triggeredActionsUI.ts +++ b/meteor/server/publications/triggeredActionsUI.ts @@ -53,6 +53,8 @@ function convertDocument(doc: DBTriggeredActions): UITriggeredActionsObj { actions: applyAndValidateOverrides(doc.actionsWithOverrides).obj, triggers: applyAndValidateOverrides(doc.triggersWithOverrides).obj, + + styleClassNames: doc.styleClassNames, }) } diff --git a/packages/blueprints-integration/src/triggers.ts b/packages/blueprints-integration/src/triggers.ts index 46e0b1d534..8420aeff2f 100644 --- a/packages/blueprints-integration/src/triggers.ts +++ b/packages/blueprints-integration/src/triggers.ts @@ -321,6 +321,8 @@ export interface IBlueprintTriggeredActions { triggers: Record /** A list of actions to execute */ actions: Record + /** Space separated list of class names to use when displaying this triggered actions */ + styleClassNames?: string } export { SomeActionIdentifier, ClientActions, PlayoutActions } diff --git a/packages/shared-lib/src/input-gateway/deviceTriggerPreviews.ts b/packages/shared-lib/src/input-gateway/deviceTriggerPreviews.ts index 738c486029..fdcab3d567 100644 --- a/packages/shared-lib/src/input-gateway/deviceTriggerPreviews.ts +++ b/packages/shared-lib/src/input-gateway/deviceTriggerPreviews.ts @@ -53,11 +53,12 @@ export type PreviewWrappedAdLib = Omit & { showStyleBaseId: ShowStyleBaseId triggeredActionId: TriggeredActionId actionId: DeviceActionId - sourceLayerType?: SourceLayerType - sourceLayerName?: { - name?: string - abbreviation?: string - } - isCurrent?: boolean - isNext?: boolean + sourceLayerType: SourceLayerType | undefined + sourceLayerName: + | { + name?: string + abbreviation?: string + } + | undefined + styleClassNames: string | undefined } From fa306dc16d5f6ee31418e282180d4c704f5d9feb Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 24 May 2024 11:06:58 +0100 Subject: [PATCH 314/479] feat: action triggers styleClassNames SOFIE-3138 (#1192) --- .../lib/Components/Base64ImageInput.tsx | 2 - .../lib/forms/SchemaFormTable/ObjectTable.tsx | 4 -- .../SchemaFormTable/ObjectTableOpHelper.tsx | 2 +- .../triggeredActions/TriggeredActionEntry.tsx | 14 ++++ meteor/lib/api/triggeredActions.ts | 1 + meteor/lib/collections/TriggeredActions.ts | 6 ++ .../server/api/blueprints/migrationContext.ts | 1 + .../StudioDeviceTriggerManager.ts | 66 +++++++++++++------ meteor/server/api/triggeredActions.ts | 2 + .../migration/upgrades/showStyleBase.ts | 1 + .../server/publications/triggeredActionsUI.ts | 2 + .../blueprints-integration/src/triggers.ts | 2 + .../input-gateway/deviceTriggerPreviews.ts | 15 +++-- 13 files changed, 85 insertions(+), 33 deletions(-) diff --git a/meteor/client/lib/Components/Base64ImageInput.tsx b/meteor/client/lib/Components/Base64ImageInput.tsx index dfdd79d0d7..e8f44d50d9 100644 --- a/meteor/client/lib/Components/Base64ImageInput.tsx +++ b/meteor/client/lib/Components/Base64ImageInput.tsx @@ -36,8 +36,6 @@ export function Base64ImageInputControl({ const uploadResult = readEvent.target?.result if (typeof uploadResult !== 'string' || !uploadResult) return - console.log('selected', readEvent.target?.result) - handleUpdate(uploadResult.toString()) } reader.readAsDataURL(file) diff --git a/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx b/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx index 5c76136015..b70e6e0f57 100644 --- a/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx +++ b/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx @@ -88,8 +88,6 @@ export const SchemaFormObjectTable = ({ (a, b) => a[0].localeCompare(b[0]) // TODO - better comparitor? ) - console.log('obj', item, rawRows, ops, wrappedRows) - return wrappedRows } else { // Table is formed of purely of an override, so ignore any defaults @@ -272,8 +270,6 @@ interface ImportExportButtonsProps { function ImportExportButtons({ schema, overrideHelper, wrappedRows }: Readonly) { const { t } = useTranslation() - console.log(schema) - const [uploadFileKey, setUploadFileKey] = useState(0) const exportTable = () => { diff --git a/meteor/client/lib/forms/SchemaFormTable/ObjectTableOpHelper.tsx b/meteor/client/lib/forms/SchemaFormTable/ObjectTableOpHelper.tsx index a34129ea20..933c738bfb 100644 --- a/meteor/client/lib/forms/SchemaFormTable/ObjectTableOpHelper.tsx +++ b/meteor/client/lib/forms/SchemaFormTable/ObjectTableOpHelper.tsx @@ -44,7 +44,7 @@ export class OverrideOpHelperObjectTable implements OverrideOpHelperForItemConte // If row was not user created (it has defaults), then don't store `undefined` const row = this.#currentRows.find((r) => r.id === rowId) - if (row && row.defaults) { + if (!this.#parentItem.defaults || row?.defaults) { // This is a bit of a hack, but it isn't possible to create a delete op from here this.#baseHelper.setItemValue(this.#parentItem.id, rowPath, undefined) } diff --git a/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionEntry.tsx b/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionEntry.tsx index 8e9653d441..09ab381cf8 100644 --- a/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionEntry.tsx +++ b/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionEntry.tsx @@ -543,6 +543,20 @@ export const TriggeredActionEntry: React.FC = React.memo(function Trigge /> {t('Optional description of the action')} + ) : null} diff --git a/meteor/lib/api/triggeredActions.ts b/meteor/lib/api/triggeredActions.ts index fc7c1ea801..2f6c0828fb 100644 --- a/meteor/lib/api/triggeredActions.ts +++ b/meteor/lib/api/triggeredActions.ts @@ -15,6 +15,7 @@ export interface CreateTriggeredActionsContent { name?: ITranslatableMessage | string triggers?: Record actions?: Record + styleClassNames?: string } export enum TriggeredActionsAPIMethods { diff --git a/meteor/lib/collections/TriggeredActions.ts b/meteor/lib/collections/TriggeredActions.ts index b66ce92dab..e3ef994e8e 100644 --- a/meteor/lib/collections/TriggeredActions.ts +++ b/meteor/lib/collections/TriggeredActions.ts @@ -21,6 +21,9 @@ export interface UITriggeredActionsObj { /** A list of actions to execute */ actions: Record + + /** Space separated list of class names to use when displaying this triggered actions */ + styleClassNames?: string } export interface DBTriggeredActions { @@ -42,6 +45,9 @@ export interface DBTriggeredActions { /** A list of actions to execute */ actionsWithOverrides: ObjectWithOverrides> + + /** Space separated list of class names to use when displaying this triggered actions */ + styleClassNames?: string } /** Note: Use DBTriggeredActions instead */ diff --git a/meteor/server/api/blueprints/migrationContext.ts b/meteor/server/api/blueprints/migrationContext.ts index e398586ca7..2efa6221ca 100644 --- a/meteor/server/api/blueprints/migrationContext.ts +++ b/meteor/server/api/blueprints/migrationContext.ts @@ -51,6 +51,7 @@ function convertTriggeredActionToBlueprints(triggeredAction: TriggeredActionsObj name: triggeredAction.name, triggers: clone(triggeredAction.triggersWithOverrides.defaults), actions: clone(triggeredAction.actionsWithOverrides.defaults), + styleClassNames: triggeredAction.styleClassNames, } return obj diff --git a/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts b/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts index 0ab7df6c2b..03282c892b 100644 --- a/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts +++ b/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts @@ -12,6 +12,7 @@ import { import { DeviceActionId, DeviceTriggerMountedActionId, + PreviewWrappedAdLib, PreviewWrappedAdLibId, ShiftRegisterActionArguments, } from '../../../lib/api/triggers/MountedTriggers' @@ -136,33 +137,58 @@ export class StudioDeviceTriggerManager { upsertedDeviceTriggerMountedActionIds.push(deviceTriggerMountedActionId) }) - if (!isPreviewableAction(thisAction)) return - - const previewedAdLibs = thisAction.preview(context) - - previewedAdLibs.forEach((adLib) => { - const adLibPreviewId = protectString( - `${triggeredAction._id}_${studioId}_${key}_${adLib._id}` - ) + if (!isPreviewableAction(thisAction)) { + const adLibPreviewId = protectString(`${actionId}_preview`) DeviceTriggerMountedActionAdlibsPreview.upsert(adLibPreviewId, { - $set: { - ...adLib, + $set: literal({ _id: adLibPreviewId, + _rank: 0, + partId: null, + type: undefined, + label: thisAction.action, + item: null, triggeredActionId: triggeredAction._id, actionId, studioId, showStyleBaseId, - sourceLayerType: adLib.sourceLayerId ? sourceLayers[adLib.sourceLayerId]?.type : undefined, - sourceLayerName: adLib.sourceLayerId - ? { - name: sourceLayers[adLib.sourceLayerId]?.name, - abbreviation: sourceLayers[adLib.sourceLayerId]?.abbreviation, - } - : undefined, - }, + sourceLayerType: undefined, + sourceLayerName: undefined, + styleClassNames: triggeredAction.styleClassNames, + }), }) + addedPreviewIds.push(adLibPreviewId) - }) + } else { + const previewedAdLibs = thisAction.preview(context) + + previewedAdLibs.forEach((adLib) => { + const adLibPreviewId = protectString( + `${triggeredAction._id}_${studioId}_${key}_${adLib._id}` + ) + DeviceTriggerMountedActionAdlibsPreview.upsert(adLibPreviewId, { + $set: literal({ + ...adLib, + _id: adLibPreviewId, + triggeredActionId: triggeredAction._id, + actionId, + studioId, + showStyleBaseId, + sourceLayerType: adLib.sourceLayerId + ? sourceLayers[adLib.sourceLayerId]?.type + : undefined, + sourceLayerName: adLib.sourceLayerId + ? { + name: sourceLayers[adLib.sourceLayerId]?.name, + abbreviation: sourceLayers[adLib.sourceLayerId]?.abbreviation, + } + : undefined, + styleClassNames: triggeredAction.styleClassNames, + }), + }) + + addedPreviewIds.push(adLibPreviewId) + }) + } }) DeviceTriggerMountedActionAdlibsPreview.remove({ @@ -234,6 +260,8 @@ function convertDocument(doc: ReadonlyObjectDeep): UITrigger actions: applyAndValidateOverrides>(doc.actionsWithOverrides).obj, triggers: applyAndValidateOverrides>(doc.triggersWithOverrides).obj, + + styleClassNames: doc.styleClassNames, }) } diff --git a/meteor/server/api/triggeredActions.ts b/meteor/server/api/triggeredActions.ts index 790cdd8654..043ed7a600 100644 --- a/meteor/server/api/triggeredActions.ts +++ b/meteor/server/api/triggeredActions.ts @@ -37,6 +37,8 @@ export async function createTriggeredActions( // User source objects should be formed purely of overrides actionsWithOverrides: convertObjectIntoOverrides(base?.actions), triggersWithOverrides: convertObjectIntoOverrides(base?.triggers), + + styleClassNames: base?.styleClassNames, }) ) return id diff --git a/meteor/server/migration/upgrades/showStyleBase.ts b/meteor/server/migration/upgrades/showStyleBase.ts index 9d96c8d115..7cbab024cd 100644 --- a/meteor/server/migration/upgrades/showStyleBase.ts +++ b/meteor/server/migration/upgrades/showStyleBase.ts @@ -189,6 +189,7 @@ export async function runUpgradeForShowStyleBase(showStyleBaseId: ShowStyleBaseI blueprintUniqueId: newTriggeredAction._id, triggersWithOverrides: wrapDefaultObject(newTriggeredAction.triggers), actionsWithOverrides: wrapDefaultObject(newTriggeredAction.actions), + styleClassNames: newTriggeredAction.styleClassNames, }), }, }) diff --git a/meteor/server/publications/triggeredActionsUI.ts b/meteor/server/publications/triggeredActionsUI.ts index 97d6dc5798..95d4af2687 100644 --- a/meteor/server/publications/triggeredActionsUI.ts +++ b/meteor/server/publications/triggeredActionsUI.ts @@ -53,6 +53,8 @@ function convertDocument(doc: DBTriggeredActions): UITriggeredActionsObj { actions: applyAndValidateOverrides(doc.actionsWithOverrides).obj, triggers: applyAndValidateOverrides(doc.triggersWithOverrides).obj, + + styleClassNames: doc.styleClassNames, }) } diff --git a/packages/blueprints-integration/src/triggers.ts b/packages/blueprints-integration/src/triggers.ts index e427ae3923..0aec70b2df 100644 --- a/packages/blueprints-integration/src/triggers.ts +++ b/packages/blueprints-integration/src/triggers.ts @@ -327,6 +327,8 @@ export interface IBlueprintTriggeredActions { triggers: Record /** A list of actions to execute */ actions: Record + /** Space separated list of class names to use when displaying this triggered actions */ + styleClassNames?: string } export { SomeActionIdentifier, ClientActions, PlayoutActions } diff --git a/packages/shared-lib/src/input-gateway/deviceTriggerPreviews.ts b/packages/shared-lib/src/input-gateway/deviceTriggerPreviews.ts index 738c486029..fdcab3d567 100644 --- a/packages/shared-lib/src/input-gateway/deviceTriggerPreviews.ts +++ b/packages/shared-lib/src/input-gateway/deviceTriggerPreviews.ts @@ -53,11 +53,12 @@ export type PreviewWrappedAdLib = Omit & { showStyleBaseId: ShowStyleBaseId triggeredActionId: TriggeredActionId actionId: DeviceActionId - sourceLayerType?: SourceLayerType - sourceLayerName?: { - name?: string - abbreviation?: string - } - isCurrent?: boolean - isNext?: boolean + sourceLayerType: SourceLayerType | undefined + sourceLayerName: + | { + name?: string + abbreviation?: string + } + | undefined + styleClassNames: string | undefined } From 348a6e60b23e7ef6ed8117740915f32654ce8c1e Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 28 May 2024 15:16:52 +0100 Subject: [PATCH 315/479] fix: emit ddp publciation remove messages before add SOFIE-3172 --- meteor/server/lib/customPublication/publish.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/meteor/server/lib/customPublication/publish.ts b/meteor/server/lib/customPublication/publish.ts index 59677c857b..9c362254a6 100644 --- a/meteor/server/lib/customPublication/publish.ts +++ b/meteor/server/lib/customPublication/publish.ts @@ -55,6 +55,10 @@ export class CustomPublish }> { changed(changes: CustomPublishChanges): void { if (!this.#isReady) throw new Meteor.Error(500, 'CustomPublish has not been initialised') + for (const id of changes.removed.values()) { + this._meteorSubscription.removed(this._collectionName, unprotectString(id)) + } + for (const doc of changes.added.values()) { this._meteorSubscription.added(this._collectionName, unprotectString(doc._id), doc) } @@ -62,10 +66,6 @@ export class CustomPublish }> { for (const doc of changes.changed.values()) { this._meteorSubscription.changed(this._collectionName, unprotectString(doc._id), doc) } - - for (const id of changes.removed.values()) { - this._meteorSubscription.removed(this._collectionName, unprotectString(id)) - } } } From 082f3344fe19f080489dbde8ff155513a1c203b8 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 28 May 2024 15:18:11 +0100 Subject: [PATCH 316/479] fix: invalidate deviceTriggerPreviews when the filterChain changes SOFIE-3172 --- .../StudioDeviceTriggerManager.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts b/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts index 03282c892b..c7eed9a955 100644 --- a/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts +++ b/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts @@ -71,22 +71,23 @@ export class StudioDeviceTriggerManager { showStyleBaseId: { $in: [showStyleBaseId, null], }, - }).map((pair) => convertDocument(pair)) - const triggeredActions = allTriggeredActions.filter((pair) => - Object.values(pair.triggers).find((trigger) => isDeviceTrigger(trigger)) - ) + }) const upsertedDeviceTriggerMountedActionIds: DeviceTriggerMountedActionId[] = [] const touchedActionIds: DeviceActionId[] = [] - for (const triggeredAction of triggeredActions) { + for (const rawTriggeredAction of allTriggeredActions) { + const triggeredAction = convertDocument(rawTriggeredAction) + + if (!Object.values(triggeredAction.triggers).find(isDeviceTrigger)) continue + const addedPreviewIds: PreviewWrappedAdLibId[] = [] Object.entries(triggeredAction.actions).forEach(([key, action]) => { - // Since the compiled aciton is cached using this actionId as a key, having the action - // and the filterChain length allows for a quicker invalidation without doing a deepEquals + // Since the compiled action is cached using this actionId as a key, having the action + // and the filterChain allows for a quicker invalidation without doing a deepEquals const actionId = protectString( - `${studioId}_${triggeredAction._id}_${key}_${action.action}_${action.filterChain.length}` + `${studioId}_${triggeredAction._id}_${key}_${action.action}_${JSON.stringify(action.filterChain)}` ) const existingAction = actionManager.getAction(actionId) let thisAction: ExecutableAction @@ -191,6 +192,8 @@ export class StudioDeviceTriggerManager { } }) + console.log(`updating ${triggeredAction._id}, gen ${addedPreviewIds}`) + DeviceTriggerMountedActionAdlibsPreview.remove({ triggeredActionId: triggeredAction._id, _id: { From 108b7be65f748c7fd4ee5eb6d37cc08ec0c23501 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Thu, 30 May 2024 09:50:05 +0200 Subject: [PATCH 317/479] chore: fix unit test --- .../src/topics/__tests__/adLibs.spec.ts | 6 +++++- .../src/topics/__tests__/utils.ts | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts b/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts index 0aad832465..27d2f8f29f 100644 --- a/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/adLibs.spec.ts @@ -1,5 +1,5 @@ import { protectString, unprotectString } from '@sofie-automation/server-core-integration' -import { makeMockLogger, makeMockSubscriber, makeTestPlaylist, makeTestShowStyleBase } from './utils' +import { makeMockLogger, makeMockSubscriber, makeTestParts, makeTestPlaylist, makeTestShowStyleBase } from './utils' import { AdLibsStatus, AdLibsTopic } from '../adLibsTopic' import { PlaylistHandler } from '../../collections/playlistHandler' import { ShowStyleBaseExt, ShowStyleBaseHandler } from '../../collections/showStyleBaseHandler' @@ -7,6 +7,7 @@ import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibActio import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' import { AdLibActionsHandler } from '../../collections/adLibActionsHandler' import { GlobalAdLibActionsHandler } from '../../collections/globalAdLibActionsHandler' +import { PartsHandler } from '../../collections/partsHandler' function makeTestAdLibActions(): AdLibAction[] { return [ @@ -60,6 +61,9 @@ describe('ActivePlaylistTopic', () => { playlist.activationId = protectString('somethingRandom') await topic.update(PlaylistHandler.name, playlist) + const parts = makeTestParts() + await topic.update(PartsHandler.name, parts) + const testShowStyleBase = makeTestShowStyleBase() await topic.update(ShowStyleBaseHandler.name, testShowStyleBase as ShowStyleBaseExt) diff --git a/packages/live-status-gateway/src/topics/__tests__/utils.ts b/packages/live-status-gateway/src/topics/__tests__/utils.ts index 55efe90ada..45dedd21fd 100644 --- a/packages/live-status-gateway/src/topics/__tests__/utils.ts +++ b/packages/live-status-gateway/src/topics/__tests__/utils.ts @@ -6,6 +6,7 @@ import { mock, MockProxy } from 'jest-mock-extended' import { ShowStyleBaseExt } from '../../collections/showStyleBaseHandler' import { Logger } from 'winston' import { WebSocket } from 'ws' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' const RUNDOWN_1_ID = 'RUNDOWN_1' const RUNDOWN_2_ID = 'RUNDOWN_2' @@ -42,3 +43,18 @@ export function makeTestShowStyleBase(): Pick Date: Thu, 30 May 2024 10:35:58 +0200 Subject: [PATCH 318/479] chore: duplicate case --- meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx b/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx index b354763624..ffe7694032 100644 --- a/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx +++ b/meteor/client/ui/Settings/ShowStyle/SourceLayer.tsx @@ -43,8 +43,6 @@ function sourceLayerString(t: TFunction<'translation', undefined>, type: SourceL return t('Lower Third') // case SourceLayerType.MIC: // return t('Studio Microphone') - case SourceLayerType.STUDIO_SCREEN: - return t('Studio Screen Graphics') case SourceLayerType.REMOTE: return t('Remote Source') case SourceLayerType.SCRIPT: @@ -64,7 +62,7 @@ function sourceLayerString(t: TFunction<'translation', undefined>, type: SourceL case SourceLayerType.LOCAL: return t('Local') case SourceLayerType.STUDIO_SCREEN: - return t('Studio Screen') + return t('Studio Screen Graphics') default: assertNever(type) return SourceLayerType[type] From 251b1ccd83c0f3686fa3dc698002e758c00488bd Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 30 May 2024 14:31:37 +0100 Subject: [PATCH 319/479] feat: replace user facing 'scratchpad' references to 'rehearsal mode' and 'adlib testing' SOFIE-3015 --- meteor/client/ui/RundownView.tsx | 2 +- meteor/client/ui/SegmentScratchpad/SegmentScratchpad.tsx | 2 +- .../actionEditors/actionSelector/ActionSelector.tsx | 2 +- meteor/lib/clientUserAction.ts | 2 +- packages/corelib/src/error.ts | 4 ++-- packages/corelib/src/worker/studio.ts | 2 +- packages/job-worker/src/playout/scratchpad.ts | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx index da60fdb3b9..e8be4c91a9 100644 --- a/meteor/client/ui/RundownView.tsx +++ b/meteor/client/ui/RundownView.tsx @@ -1016,7 +1016,7 @@ const RundownHeader = withTranslation()( this.deactivate(e)}>{t('Deactivate')} ) : null} {this.props.studio.settings.allowScratchpad && this.props.playlist.activationId ? ( - this.activateScratchpad(e)}>{t('Activate Scratchpad')} + this.activateScratchpad(e)}>{t('Rehearsal Mode')} ) : null} {this.props.playlist.activationId ? ( this.take(e)}>{t('Take')} diff --git a/meteor/client/ui/SegmentScratchpad/SegmentScratchpad.tsx b/meteor/client/ui/SegmentScratchpad/SegmentScratchpad.tsx index 2ba3413605..302984e8e5 100644 --- a/meteor/client/ui/SegmentScratchpad/SegmentScratchpad.tsx +++ b/meteor/client/ui/SegmentScratchpad/SegmentScratchpad.tsx @@ -462,7 +462,7 @@ export const SegmentScratchpad = React.memo( className={'segment-timeline__title__label' + (props.segment.identifier ? ' identifier' : '')} data-identifier={props.segment.identifier} > - {t('Scratchpad')} + {t('Adlib Testing')} {(criticalNotes > 0 || warningNotes > 0) && (
diff --git a/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx b/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx index 9f96e97a72..6d826f069c 100644 --- a/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx +++ b/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx @@ -161,7 +161,7 @@ function actionToLabel(t: TFunction, action: SomeAction['action']): string { case PlayoutActions.deactivateRundownPlaylist: return t('Deactivate Rundown') case PlayoutActions.activateScratchpadMode: - return t('Activate Scratchpad') + return t('Rehearsal Mode') case PlayoutActions.disableNextPiece: return t('Disable next Piece') case PlayoutActions.hold: diff --git a/meteor/lib/clientUserAction.ts b/meteor/lib/clientUserAction.ts index 609126dfe9..3532db07bd 100644 --- a/meteor/lib/clientUserAction.ts +++ b/meteor/lib/clientUserAction.ts @@ -113,7 +113,7 @@ function userActionToLabel(userAction: UserAction, t: i18next.TFunction) { case UserAction.PERIPHERAL_DEVICE_REFRESH_DEBUG_STATES: return t('Refreshing debug states') case UserAction.ACTIVATE_SCRATCHPAD: - return t('Activate Scratchpad') + return t('Rehearsal Mode') default: assertNever(userAction) } diff --git a/packages/corelib/src/error.ts b/packages/corelib/src/error.ts index 2b176752db..85503e4912 100644 --- a/packages/corelib/src/error.ts +++ b/packages/corelib/src/error.ts @@ -110,8 +110,8 @@ const UserErrorMessagesTranslations: { [key in UserErrorMessage]: string } = { [UserErrorMessage.ShowStyleBaseNotFound]: t(`ShowStyleBase not found!`), [UserErrorMessage.NoMigrationsToApply]: t(`No migrations to apply`), [UserErrorMessage.ValidationFailed]: t('Validation failed!'), - [UserErrorMessage.ScratchpadNotAllowed]: t(`Scratchpad mode is not allowed`), - [UserErrorMessage.ScratchpadAlreadyActive]: t(`Scratchpad mode is already active`), + [UserErrorMessage.ScratchpadNotAllowed]: t(`Rehearsal mode is not allowed`), + [UserErrorMessage.ScratchpadAlreadyActive]: t(`Rehearsal mode is already active`), [UserErrorMessage.BucketNotFound]: t(`Bucket not found!`), } diff --git a/packages/corelib/src/worker/studio.ts b/packages/corelib/src/worker/studio.ts index 0ec51a7c09..173c64cd94 100644 --- a/packages/corelib/src/worker/studio.ts +++ b/packages/corelib/src/worker/studio.ts @@ -184,7 +184,7 @@ export enum StudioJobs { BlueprintIgnoreFixUpConfigForStudio = 'blueprintIgnoreFixUpConfigForStudio', /** - * Activate scratchpad mode for the Rundown containing the nexted Part. + * Activate scratchpad (Rehearsal Mode) mode for the Rundown containing the nexted Part. */ ActivateScratchpad = 'activateScratchpad', } diff --git a/packages/job-worker/src/playout/scratchpad.ts b/packages/job-worker/src/playout/scratchpad.ts index 3205351b9c..283ce6278a 100644 --- a/packages/job-worker/src/playout/scratchpad.ts +++ b/packages/job-worker/src/playout/scratchpad.ts @@ -36,7 +36,7 @@ export async function handleActivateScratchpad(context: JobContext, data: Activa _id: getRandomId(), _rank: 0, externalId: '', - title: 'Scratchpad', + title: 'Adlib Testing', expectedDuration: 0, expectedDurationWithPreroll: 0, // Filled in later untimed: true, From afb2f43c8f199ff5da4ea6c3ccc3bd1e97a80f4f Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 31 May 2024 16:34:18 +0100 Subject: [PATCH 320/479] feat: rename internal 'scratchpad' naming to 'adlib testing' SOFIE-3015 --- meteor/client/ui/RundownView.tsx | 18 +-- .../SegmentAdlibTesting.scss | 9 ++ .../SegmentAdlibTesting.tsx} | 8 +- .../SegmentAdlibTestingContainer.tsx} | 6 +- .../SegmentContainer/withResolvedSegment.ts | 8 +- .../SegmentScratchpad/SegmentScratchpad.scss | 9 -- .../ui/SegmentTimeline/SegmentContextMenu.tsx | 2 +- meteor/client/ui/Settings/Studio/Generic.tsx | 6 +- .../actionSelector/ActionSelector.tsx | 8 +- meteor/lib/Rundown.ts | 4 +- meteor/lib/api/triggers/actionFactory.ts | 6 +- meteor/lib/api/userActions.ts | 4 +- meteor/lib/clientUserAction.ts | 2 +- meteor/lib/userAction.ts | 2 +- meteor/server/api/userActions.ts | 4 +- .../generateNotesForSegment.ts | 2 +- .../blueprints-integration/src/triggers.ts | 6 +- packages/corelib/src/dataModel/Segment.ts | 4 +- packages/corelib/src/dataModel/Studio.ts | 4 +- packages/corelib/src/error.ts | 8 +- .../src/playout/__tests__/infinites.test.ts | 26 ++-- packages/corelib/src/playout/infinites.ts | 6 +- packages/corelib/src/worker/studio.ts | 8 +- .../src/__mocks__/defaultCollectionObjects.ts | 2 +- .../PartAndPieceInstanceActionService.ts | 6 +- packages/job-worker/src/ingest/commit.ts | 6 +- .../src/ingest/ingestSegmentJobs.ts | 2 +- packages/job-worker/src/ingest/lib.ts | 6 +- .../model/implementation/LoadIngestModel.ts | 2 +- .../src/ingest/syncChangesToPartInstance.ts | 4 +- .../src/playout/__tests__/actions.test.ts | 12 +- .../src/playout/activePlaylistActions.ts | 4 +- .../{scratchpad.ts => adlibTesting.ts} | 21 +-- packages/job-worker/src/playout/infinites.ts | 8 +- packages/job-worker/src/playout/lib.ts | 4 +- .../src/playout/model/PlayoutModel.ts | 4 +- .../playout/model/PlayoutPartInstanceModel.ts | 4 +- .../src/playout/model/PlayoutRundownModel.ts | 16 +-- .../model/implementation/LoadPlayoutModel.ts | 4 +- .../model/implementation/PlayoutModelImpl.ts | 24 ++-- .../PlayoutPartInstanceModelImpl.ts | 4 +- .../implementation/PlayoutRundownModelImpl.ts | 50 +++---- .../implementation/PlayoutSegmentModelImpl.ts | 8 +- .../model/implementation/SavePlayoutModel.ts | 24 ++-- .../__tests__/PlayoutRundownModelImpl.spec.ts | 124 +++++++++--------- .../__tests__/PlayoutSegmentModelImpl.spec.ts | 20 +-- .../__tests__/SavePlayoutModel.spec.ts | 38 +++--- .../job-worker/src/playout/moveNextPart.ts | 2 +- packages/job-worker/src/playout/setNext.ts | 6 +- .../job-worker/src/workers/studio/jobs.ts | 4 +- .../shared-lib/src/core/model/ShowStyle.ts | 2 +- 51 files changed, 288 insertions(+), 283 deletions(-) create mode 100644 meteor/client/ui/SegmentAdlibTesting/SegmentAdlibTesting.scss rename meteor/client/ui/{SegmentScratchpad/SegmentScratchpad.tsx => SegmentAdlibTesting/SegmentAdlibTesting.tsx} (98%) rename meteor/client/ui/{SegmentScratchpad/SegmentScratchpadContainer.tsx => SegmentAdlibTesting/SegmentAdlibTestingContainer.tsx} (97%) delete mode 100644 meteor/client/ui/SegmentScratchpad/SegmentScratchpad.scss rename packages/job-worker/src/playout/{scratchpad.ts => adlibTesting.ts} (73%) diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx index e8be4c91a9..796e9482c8 100644 --- a/meteor/client/ui/RundownView.tsx +++ b/meteor/client/ui/RundownView.tsx @@ -147,7 +147,7 @@ import { } from '../collections' import { UIShowStyleBase } from '../../lib/api/showStyles' import { RundownPlaylistCollectionUtil } from '../../lib/collections/rundownPlaylistUtil' -import { SegmentScratchpadContainer } from './SegmentScratchpad/SegmentScratchpadContainer' +import { SegmentAdlibTestingContainer } from './SegmentAdlibTesting/SegmentAdlibTestingContainer' import { PromiseButton } from '../lib/Components/PromiseButton' import { logger } from '../../lib/logging' import { isTranslatableMessage, translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' @@ -823,19 +823,19 @@ const RundownHeader = withTranslation()( } } } - private activateScratchpad = (e: any) => { + private activateAdlibTesting = (e: any) => { const { t } = this.props if (e.persist) e.persist() if ( this.props.studioMode && - this.props.studio.settings.allowScratchpad && + this.props.studio.settings.allowAdlibTestingSegment && this.props.playlist.activationId && this.props.currentRundown ) { const rundownId = this.props.currentRundown._id - doUserAction(t, e, UserAction.ACTIVATE_SCRATCHPAD, (e, ts) => - MeteorCall.userAction.activateScratchpadMode(e, ts, this.props.playlist._id, rundownId) + doUserAction(t, e, UserAction.ACTIVATE_ADLIB_TESTING, (e, ts) => + MeteorCall.userAction.activateAdlibTestingMode(e, ts, this.props.playlist._id, rundownId) ) } } @@ -1015,8 +1015,8 @@ const RundownHeader = withTranslation()( {this.props.playlist.activationId ? ( this.deactivate(e)}>{t('Deactivate')} ) : null} - {this.props.studio.settings.allowScratchpad && this.props.playlist.activationId ? ( - this.activateScratchpad(e)}>{t('Rehearsal Mode')} + {this.props.studio.settings.allowAdlibTestingSegment && this.props.playlist.activationId ? ( + this.activateAdlibTesting(e)}>{t('Rehearsal Mode')} ) : null} {this.props.playlist.activationId ? ( this.take(e)}>{t('Take')} @@ -2628,8 +2628,8 @@ const RundownViewContent = translateWithTracker + if (segment.orphaned === SegmentOrphanedReason.ADLIB_TESTING) { + return } switch (displayMode) { diff --git a/meteor/client/ui/SegmentAdlibTesting/SegmentAdlibTesting.scss b/meteor/client/ui/SegmentAdlibTesting/SegmentAdlibTesting.scss new file mode 100644 index 0000000000..7d3543a118 --- /dev/null +++ b/meteor/client/ui/SegmentAdlibTesting/SegmentAdlibTesting.scss @@ -0,0 +1,9 @@ +$segment-adlib-testing-header-color: #fff; + +.segment-timeline.segment-adlib-testing { + .segment-timeline__title { + background: $segment-adlib-testing-header-color; + color: #000; + text-shadow: none; + } +} diff --git a/meteor/client/ui/SegmentScratchpad/SegmentScratchpad.tsx b/meteor/client/ui/SegmentAdlibTesting/SegmentAdlibTesting.tsx similarity index 98% rename from meteor/client/ui/SegmentScratchpad/SegmentScratchpad.tsx rename to meteor/client/ui/SegmentAdlibTesting/SegmentAdlibTesting.tsx index 302984e8e5..829d689bbc 100644 --- a/meteor/client/ui/SegmentScratchpad/SegmentScratchpad.tsx +++ b/meteor/client/ui/SegmentAdlibTesting/SegmentAdlibTesting.tsx @@ -63,8 +63,8 @@ const PART_WIDTH = 166 // Must match SCSS: $segment-storyboard-part-width const PART_LIST_LEAD_IN = 0 // Must match SCSS: .segment-storyboard__part-list(padding-left) const PART_SHADE_WIDTH = 100 -export const SegmentScratchpad = React.memo( - React.forwardRef(function SegmentScratchpad(props: IProps, ref) { +export const SegmentAdlibTesting = React.memo( + React.forwardRef(function SegmentAdlibTesting(props: IProps, ref) { const innerRef = useRef(null) const combinedRef = useCombinedRefs(null, ref, innerRef) const listRef = useRef(null) @@ -426,7 +426,7 @@ export const SegmentScratchpad = React.memo( return (
(function SegmentScratchpadContainer({ +export const SegmentAdlibTestingContainer = withResolvedSegment(function SegmentAdlibTestingContainer({ rundownId, rundownIdsBefore, segmentId, @@ -200,7 +200,7 @@ export const SegmentScratchpadContainer = withResolvedSegment(function S } return ( - {part && timecode === null && ( diff --git a/meteor/client/ui/Settings/Studio/Generic.tsx b/meteor/client/ui/Settings/Studio/Generic.tsx index ec5c041050..5f11c85b2e 100644 --- a/meteor/client/ui/Settings/Studio/Generic.tsx +++ b/meteor/client/ui/Settings/Studio/Generic.tsx @@ -235,10 +235,12 @@ export const StudioGenericProperties = withTranslation()(
({ {showRowId && } {summaryFields.map((field) => { - const rawValue = objectPathGet(object, field.attr) + const rawValue = objectPathGet(rowItem.computed, field.attr) let value = field.transform ? field.transform(rawValue) : rawValue if (Array.isArray(value)) value = value.join(', ') @@ -47,6 +54,16 @@ export function SchemaTableSummaryRow({ })} ({ {showRowId && } {summaryFields.map((field) => { - const rawValue = objectPathGet(object, field.attr) + const rawValue = objectPathGet(rowItem.computed, field.attr) let value = field.transform ? field.transform(rawValue) : rawValue if (Array.isArray(value)) value = value.join(', ') @@ -47,6 +54,16 @@ export function SchemaTableSummaryRow({ })} From 598b93201c09ef0a8860ebf2b0886e0f3c0d3519 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 29 Jul 2024 18:06:00 +0100 Subject: [PATCH 396/479] fix: remove remaining usages of `withMediaObjectStatus` --- .../SourceLayerItemContainer.tsx | 10 +- .../SegmentTimeline/withMediaObjectStatus.tsx | 82 +-------- meteor/client/ui/Shelf/AdLibListItem.tsx | 111 ++++++------ meteor/client/ui/Shelf/AdLibListView.tsx | 3 - meteor/client/ui/Shelf/AdLibPanel.tsx | 1 - meteor/client/ui/Shelf/AdLibRegionPanel.tsx | 15 +- meteor/client/ui/Shelf/BucketPieceButton.tsx | 167 +++++++++--------- .../client/ui/Shelf/DashboardPieceButton.tsx | 10 +- .../ItemRenderers/InspectorTitle.tsx | 14 +- .../ui/Shelf/Inspector/ShelfInspector.tsx | 50 +++--- 10 files changed, 192 insertions(+), 271 deletions(-) diff --git a/meteor/client/ui/SegmentTimeline/SourceLayerItemContainer.tsx b/meteor/client/ui/SegmentTimeline/SourceLayerItemContainer.tsx index 3cd05cbade..00db22bf05 100644 --- a/meteor/client/ui/SegmentTimeline/SourceLayerItemContainer.tsx +++ b/meteor/client/ui/SegmentTimeline/SourceLayerItemContainer.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { ISourceLayerItemProps, SourceLayerItem } from './SourceLayerItem' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { withMediaObjectStatus, WithMediaObjectStatusProps } from './withMediaObjectStatus' +import { useContentStatusForPieceInstance } from './withMediaObjectStatus' import { UIStudio } from '../../../lib/api/studios' interface IPropsHeader extends Omit { @@ -9,6 +9,8 @@ interface IPropsHeader extends Omit { studio: UIStudio } -export const SourceLayerItemContainer = withMediaObjectStatus()( - (props: IPropsHeader & WithMediaObjectStatusProps) => -) +export function SourceLayerItemContainer(props: IPropsHeader): JSX.Element { + const contentStatus = useContentStatusForPieceInstance(props.piece.instance) + + return +} diff --git a/meteor/client/ui/SegmentTimeline/withMediaObjectStatus.tsx b/meteor/client/ui/SegmentTimeline/withMediaObjectStatus.tsx index ca5359a759..6641cefd83 100644 --- a/meteor/client/ui/SegmentTimeline/withMediaObjectStatus.tsx +++ b/meteor/client/ui/SegmentTimeline/withMediaObjectStatus.tsx @@ -1,50 +1,17 @@ -import React, { useEffect, useState } from 'react' import { PieceUi } from './SegmentTimelineContainer' -import { ISourceLayer } from '@sofie-automation/blueprints-integration' import { RundownUtils } from '../../lib/rundown' import { IAdLibListItem } from '../Shelf/AdLibListItem' import { BucketAdLibUi, BucketAdLibActionUi } from '../Shelf/RundownViewBuckets' import { AdLibPieceUi } from '../../lib/shelf' -import { UIStudio } from '../../../lib/api/studios' import { UIBucketContentStatuses, UIPieceContentStatuses } from '../Collections' -import { Piece, PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' +import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { PieceContentStatusObj } from '../../../lib/api/pieceContentStatus' -import { deepFreeze } from '@sofie-automation/corelib/dist/lib' import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { UIBucketContentStatus, UIPieceContentStatus } from '../../../lib/api/rundownNotifications' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { ReadonlyDeep } from 'type-fest' -type AnyPiece = { - piece?: BucketAdLibUi | IAdLibListItem | AdLibPieceUi | PieceUi | BucketAdLibActionUi | undefined - layer?: ISourceLayer | undefined - isLiveLine?: boolean - studio: UIStudio | undefined -} - -type IWrappedComponent = new (props: IProps, state: IState) => React.Component< - IProps, - IState -> - -const DEFAULT_STATUS = deepFreeze({ - status: PieceStatusCode.UNKNOWN, - messages: [], - progress: undefined, - - blacks: [], - freezes: [], - scenes: [], - - thumbnailUrl: undefined, - previewUrl: undefined, - - packageName: null, - - contentDuration: undefined, -}) - function unwrapPieceInstance(piece: BucketAdLibUi | IAdLibListItem | AdLibPieceUi | PieceUi | BucketAdLibActionUi) { if (RundownUtils.isPieceInstance(piece)) { return piece.instance.piece @@ -82,52 +49,11 @@ function getStatusDocForPiece( }) } +/** @deprecated */ export interface WithMediaObjectStatusProps { contentStatus: ReadonlyDeep | undefined } -/** - * @deprecated This can now be achieved by a simple minimongo query against either UIPieceContentStatuses or UIBucketContentStatuses - */ -export function withMediaObjectStatus(): ( - WrappedComponent: - | IWrappedComponent - | React.FC -) => React.FC { - return (WrappedComponent) => { - return function WithMediaObjectStatusHOCComponent(props: IProps) { - const [invalidationToken, setInvalidationToken] = useState(Date.now()) - useEffect(() => { - // Force an invalidation shortly after mounting - const callback = window.requestIdleCallback( - () => { - setInvalidationToken(Date.now()) - }, - { - timeout: 500, - } - ) - return () => { - window.cancelIdleCallback(callback) - } - }, []) - - const statusObj: ReadonlyDeep | undefined = useTracker(() => { - const { piece, studio, layer } = props - - // Check item status - if (piece && (piece.sourceLayer || layer) && studio) { - // Extract the status or populate some default values - return getStatusDocForPiece(piece)?.status ?? DEFAULT_STATUS - } - return undefined - }, [props.piece, props.studio, props.isLiveLine, invalidationToken]) - - return - } - } -} - export function useContentStatusForAdlibPiece( piece: Pick | undefined ): PieceContentStatusObj | undefined { @@ -183,7 +109,7 @@ export function useContentStatusForPieceInstance( } export function useContentStatusForItem( - piece: BucketAdLibUi | IAdLibListItem | AdLibPieceUi | PieceUi | BucketAdLibActionUi + piece: BucketAdLibUi | IAdLibListItem | AdLibPieceUi | PieceUi | BucketAdLibActionUi | undefined ): ReadonlyDeep | undefined { - return useTracker(() => getStatusDocForPiece(piece)?.status, [piece]) + return useTracker(() => piece && getStatusDocForPiece(piece)?.status, [piece]) } diff --git a/meteor/client/ui/Shelf/AdLibListItem.tsx b/meteor/client/ui/Shelf/AdLibListItem.tsx index e7aa2d7678..6291caa39f 100644 --- a/meteor/client/ui/Shelf/AdLibListItem.tsx +++ b/meteor/client/ui/Shelf/AdLibListItem.tsx @@ -1,10 +1,9 @@ import * as React from 'react' import ClassNames from 'classnames' import { ISourceLayer, IOutputLayer, IBlueprintActionTriggerMode } from '@sofie-automation/blueprints-integration' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { unprotectString } from '../../../lib/lib' import renderItem from './Renderers/ItemRendererFactory' -import { withMediaObjectStatus, WithMediaObjectStatusProps } from '../SegmentTimeline/withMediaObjectStatus' +import { useContentStatusForAdlibPiece } from '../SegmentTimeline/withMediaObjectStatus' import { ContextMenuTrigger } from '@jstarpl/react-contextmenu' import { contextMenuHoldToDisplayTime } from '../../lib/lib' import { setShelfContextMenuContext, ContextType as MenuContextType } from './ShelfContextMenu' @@ -27,61 +26,59 @@ interface IListViewItemProps { disabled?: boolean onSelectAdLib?: (aSLine: IAdLibListItem) => void onToggleAdLib?: (aSLine: IAdLibListItem, queue: boolean, context: any, mode?: IBlueprintActionTriggerMode) => void - playlist: DBRundownPlaylist } -export const AdLibListItem = withMediaObjectStatus()( - class AdLibListItem extends React.Component { - constructor(props: IListViewItemProps & WithMediaObjectStatusProps) { - super(props) - } +export function AdLibListItem({ + piece, + studio, + layer, + selected, + disabled, + onSelectAdLib, + onToggleAdLib, +}: IListViewItemProps): JSX.Element { + const contentStatus = useContentStatusForAdlibPiece(piece) - render(): JSX.Element { - return ( - this.props.onSelectAdLib && this.props.onSelectAdLib(this.props.piece), - onContextMenu: () => this.props.onSelectAdLib && this.props.onSelectAdLib(this.props.piece), - onDoubleClick: (e) => - !this.props.disabled && - this.props.onToggleAdLib && - this.props.onToggleAdLib(this.props.piece, e.shiftKey, e), - }} - collect={() => - setShelfContextMenuContext({ - type: MenuContextType.ADLIB, - details: { - adLib: this.props.piece, - onToggle: !this.props.disabled ? this.props.onToggleAdLib : undefined, - disabled: this.props.disabled, - }, - }) - } - holdToDisplay={contextMenuHoldToDisplayTime()} - renderTag="tr" - key={unprotectString(this.props.piece._id)} - > - {renderItem({ - adLibListItem: this.props.piece, - contentStatus: this.props.contentStatus, - layer: this.props.layer, - outputLayer: this.props.piece.outputLayer, - selected: this.props.selected, - status: this.props.contentStatus?.status, - messages: this.props.contentStatus?.messages, - studio: this.props.studio, - })} - - ) - } - } -) + return ( + onSelectAdLib && onSelectAdLib(piece), + onContextMenu: () => onSelectAdLib && onSelectAdLib(piece), + onDoubleClick: (e) => !disabled && onToggleAdLib && onToggleAdLib(piece, e.shiftKey, e), + }} + collect={() => + setShelfContextMenuContext({ + type: MenuContextType.ADLIB, + details: { + adLib: piece, + onToggle: !disabled ? onToggleAdLib : undefined, + disabled: disabled, + }, + }) + } + holdToDisplay={contextMenuHoldToDisplayTime()} + renderTag="tr" + key={unprotectString(piece._id)} + > + {renderItem({ + adLibListItem: piece, + contentStatus: contentStatus, + layer: layer, + outputLayer: piece.outputLayer, + selected: selected, + status: contentStatus?.status, + messages: contentStatus?.messages, + studio: studio, + })} + + ) +} diff --git a/meteor/client/ui/Shelf/AdLibListView.tsx b/meteor/client/ui/Shelf/AdLibListView.tsx index 78bc745899..310522d12d 100644 --- a/meteor/client/ui/Shelf/AdLibListView.tsx +++ b/meteor/client/ui/Shelf/AdLibListView.tsx @@ -5,7 +5,6 @@ import { RundownUtils } from '../../lib/rundown' import { AdLibListItem, IAdLibListItem } from './AdLibListItem' import { AdLibPieceUi, AdlibSegmentUi } from '../../lib/shelf' import { RundownLayoutFilter, RundownLayoutFilterBase } from '../../../lib/collections/RundownLayouts' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { BucketAdLibActionUi, BucketAdLibUi } from './RundownViewBuckets' import { PieceUi } from '../SegmentContainer/withResolvedSegment' import { IBlueprintActionTriggerMode } from '@sofie-automation/blueprints-integration' @@ -24,7 +23,6 @@ interface IListViewPropsHeader { noSegments: boolean filter: RundownLayoutFilter | undefined rundownAdLibs?: Array - playlist: DBRundownPlaylist studio: UIStudio } @@ -208,7 +206,6 @@ export function AdLibListView(props: Readonly): JSX.Elemen disabled={adLibPiece.disabled ?? false} onToggleAdLib={props.onToggleAdLib} onSelectAdLib={props.onSelectAdLib} - playlist={props.playlist} /> ) } diff --git a/meteor/client/ui/Shelf/AdLibPanel.tsx b/meteor/client/ui/Shelf/AdLibPanel.tsx index 1491b5b3ca..095345896a 100644 --- a/meteor/client/ui/Shelf/AdLibPanel.tsx +++ b/meteor/client/ui/Shelf/AdLibPanel.tsx @@ -760,7 +760,6 @@ export function AdLibPanel({ showStyleBase={showStyleBase} searchFilter={searchFilter} filter={filter as RundownLayoutFilter} - playlist={playlist} studio={studio} noSegments={!withSegments} /> diff --git a/meteor/client/ui/Shelf/AdLibRegionPanel.tsx b/meteor/client/ui/Shelf/AdLibRegionPanel.tsx index b37d2664c6..ad9678926d 100644 --- a/meteor/client/ui/Shelf/AdLibRegionPanel.tsx +++ b/meteor/client/ui/Shelf/AdLibRegionPanel.tsx @@ -25,7 +25,7 @@ import { } from '../../lib/shelf' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { PieceUi } from '../SegmentTimeline/SegmentTimelineContainer' -import { withMediaObjectStatus, WithMediaObjectStatusProps } from '../SegmentTimeline/withMediaObjectStatus' +import { useContentStatusForPieceInstance, WithMediaObjectStatusProps } from '../SegmentTimeline/withMediaObjectStatus' import { ISourceLayer } from '@sofie-automation/blueprints-integration' import { UIStudios } from '../Collections' import { Meteor } from 'meteor/meteor' @@ -233,10 +233,15 @@ class AdLibRegionPanelBase extends React.Component< } } -export const AdLibRegionPanelWithStatus = withMediaObjectStatus< - Translated, - {} ->()(AdLibRegionPanelBase) +function AdLibRegionPanelWithStatus( + props: Translated< + IAdLibPanelProps & IAdLibRegionPanelProps & AdLibFetchAndFilterProps & IAdLibRegionPanelTrackedProps + > +) { + const contentStatus = useContentStatusForPieceInstance(props.piece?.instance) + + return +} export const AdLibRegionPanel = translateWithTracker< Translated, diff --git a/meteor/client/ui/Shelf/BucketPieceButton.tsx b/meteor/client/ui/Shelf/BucketPieceButton.tsx index b95eb66464..299d4e0981 100644 --- a/meteor/client/ui/Shelf/BucketPieceButton.tsx +++ b/meteor/client/ui/Shelf/BucketPieceButton.tsx @@ -1,24 +1,22 @@ +import React from 'react' import { IDashboardButtonProps, DashboardPieceButtonBase } from './DashboardPieceButton' import { - DragSource, - DropTarget, ConnectDragSource, ConnectDropTarget, DragSourceMonitor, - DropTargetMonitor, ConnectDragPreview, ConnectableElement, + useDrop, + useDrag, } from 'react-dnd' import { DragDropItemTypes } from '../DragDropItemTypes' import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' -import { withMediaObjectStatus } from '../SegmentTimeline/withMediaObjectStatus' +import { useContentStatusForAdlibPiece } from '../SegmentTimeline/withMediaObjectStatus' import { BucketAdLibActionUi, BucketAdLibItem } from './RundownViewBuckets' import { IBlueprintActionTriggerMode } from '@sofie-automation/blueprints-integration' import { BucketId, PieceId } from '@sofie-automation/corelib/dist/dataModel/Ids' -type IDashboardButtonPropsCombined = BucketPieceButtonBaseProps & IDashboardButtonProps - interface IBucketPieceDragObject { id: PieceId bucketId: BucketId @@ -31,69 +29,6 @@ export interface IBucketPieceDropResult { action: 'reorder' | 'move' | undefined } -const buttonSource = { - beginDrag(props: IDashboardButtonPropsCombined, _monitor: DragSourceMonitor, _component: any) { - return { - id: props.piece._id, - originalIndex: props.findAdLib(props.piece._id).index, - bucketId: props.bucketId, - } - }, - - endDrag( - props: IDashboardButtonPropsCombined, - monitor: DragSourceMonitor - ) { - const { id: droppedId, originalIndex } = monitor.getItem() - const didDrop = monitor.didDrop() - - if (!didDrop) { - props.moveAdLib(droppedId, originalIndex) - } else { - const dropResult = monitor.getDropResult() - if (!dropResult) return - - const { action } = dropResult - if (action === 'reorder') { - const { index: newIndex } = props.findAdLib(droppedId) - props.onAdLibReorder(droppedId, newIndex, originalIndex) - } else if (action === 'move') { - const { bucketId } = dropResult - props.onAdLibMove(droppedId, bucketId) - } - } - }, -} - -const buttonTarget = { - canDrop(_props: IDashboardButtonPropsCombined, _monitor: DropTargetMonitor) { - return true - }, - - hover( - props: IDashboardButtonPropsCombined, - monitor: DropTargetMonitor, - _component: any - ) { - const { id: draggedId } = monitor.getItem() - const overId = props.piece._id - - if (draggedId !== overId) { - const { index: overIndex } = props.findAdLib(overId) - props.moveAdLib(draggedId, overIndex) - } - }, - - drop(props: IDashboardButtonPropsCombined, _monitor: DropTargetMonitor) { - const { index } = props.findAdLib(props.piece._id) - - return { - index, - action: 'reorder', - } - }, -} - export interface BucketPieceButtonBaseProps { moveAdLib: (id: PieceId, atIndex: number) => void findAdLib: (id: PieceId) => { piece: BucketAdLib | BucketAdLibActionUi | undefined; index: number } @@ -122,17 +57,85 @@ class BucketPieceButtonBase extends DashboardPieceButtonBase & BucketPieceButtonBaseProps, - {} ->()( - DropTarget(DragDropItemTypes.BUCKET_ADLIB_PIECE, buttonTarget, (connect) => ({ - connectDropTarget: connect.dropTarget(), - }))( - DragSource(DragDropItemTypes.BUCKET_ADLIB_PIECE, buttonSource, (connect, monitor) => ({ - connectDragSource: connect.dragSource(), - connectDragPreview: connect.dragPreview(), +export function BucketPieceButton( + props: React.PropsWithChildren & BucketPieceButtonBaseProps +): JSX.Element { + const contentStatus = useContentStatusForAdlibPiece(props.piece) + + const [, connectDropTarget] = useDrop({ + accept: DragDropItemTypes.BUCKET_ADLIB_PIECE, + canDrop(_props, _monitor) { + return true + }, + + hover(item, _monitor) { + const { id: draggedId } = item + const overId = props.piece._id + + if (draggedId !== overId) { + const { index: overIndex } = props.findAdLib(overId) + props.moveAdLib(draggedId, overIndex) + } + }, + + drop(item, _monitor) { + const { index } = props.findAdLib(item.id) + + return { + index, + action: 'reorder', + } + }, + }) + + const [collectedProps, connectDragSource, connectDragPreview] = useDrag< + IBucketPieceDragObject, + {}, + { isDragging: boolean } + >({ + type: DragDropItemTypes.BUCKET_ADLIB_PIECE, + item: (_monitor: DragSourceMonitor) => { + return { + id: props.piece._id, + originalIndex: props.findAdLib(props.piece._id).index, + bucketId: props.bucketId, + } + }, + + end: (item, monitor: DragSourceMonitor) => { + const { id: droppedId, originalIndex } = item + const didDrop = monitor.didDrop() + + if (!didDrop) { + props.moveAdLib(droppedId, originalIndex) + } else { + const dropResult = monitor.getDropResult() + if (!dropResult) return + + const { action } = dropResult + if (action === 'reorder') { + const { index: newIndex } = props.findAdLib(droppedId) + props.onAdLibReorder(droppedId, newIndex, originalIndex) + } else if (action === 'move') { + const { bucketId } = dropResult + props.onAdLibMove(droppedId, bucketId) + } + } + }, + + collect: (monitor) => ({ isDragging: monitor.isDragging(), - }))(BucketPieceButtonBase) - ) as any -) + }), + }) + + return ( + + ) +} diff --git a/meteor/client/ui/Shelf/DashboardPieceButton.tsx b/meteor/client/ui/Shelf/DashboardPieceButton.tsx index 06569ae188..811c424c92 100644 --- a/meteor/client/ui/Shelf/DashboardPieceButton.tsx +++ b/meteor/client/ui/Shelf/DashboardPieceButton.tsx @@ -19,7 +19,7 @@ import { StyledTimecode } from '../../lib/StyledTimecode' import { VTFloatingInspector } from '../FloatingInspectors/VTFloatingInspector' import { getNoticeLevelForPieceStatus } from '../../../lib/notifications/notifications' import { L3rdFloatingInspector } from '../FloatingInspectors/L3rdFloatingInspector' -import { withMediaObjectStatus, WithMediaObjectStatusProps } from '../SegmentTimeline/withMediaObjectStatus' +import { useContentStatusForAdlibPiece, WithMediaObjectStatusProps } from '../SegmentTimeline/withMediaObjectStatus' import { isTouchDevice } from '../../lib/lib' import { AdLibPieceUi } from '../../lib/shelf' @@ -489,6 +489,8 @@ export class DashboardPieceButtonBase extends React.Component< } } -export const DashboardPieceButton = withMediaObjectStatus, {}>()( - DashboardPieceButtonBase -) +export function DashboardPieceButton(props: React.PropsWithChildren): JSX.Element { + const contentStatus = useContentStatusForAdlibPiece(props.piece) + + return +} diff --git a/meteor/client/ui/Shelf/Inspector/ItemRenderers/InspectorTitle.tsx b/meteor/client/ui/Shelf/Inspector/ItemRenderers/InspectorTitle.tsx index 07fe44151b..6f127d8424 100644 --- a/meteor/client/ui/Shelf/Inspector/ItemRenderers/InspectorTitle.tsx +++ b/meteor/client/ui/Shelf/Inspector/ItemRenderers/InspectorTitle.tsx @@ -4,7 +4,7 @@ import { PieceUi } from '../../../SegmentTimeline/SegmentTimelineContainer' import { BucketAdLibUi, BucketAdLibActionUi } from '../../RundownViewBuckets' import { RundownUtils } from '../../../../lib/rundown' import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' -import { withMediaObjectStatus, WithMediaObjectStatusProps } from '../../../SegmentTimeline/withMediaObjectStatus' +import { useContentStatusForItem } from '../../../SegmentTimeline/withMediaObjectStatus' import { IAdLibListItem } from '../../AdLibListItem' import { AdLibPieceUi } from '../../../../lib/shelf' import { UIShowStyleBase } from '../../../../../lib/api/showStyles' @@ -16,15 +16,13 @@ interface IProps { studio: UIStudio } -const InspectorTitle = withMediaObjectStatus()(function InspectorTitle( - props: IProps & WithMediaObjectStatusProps -) { +function InspectorTitle(props: IProps): JSX.Element { + const contentStatus = useContentStatusForItem(props.piece) + const piece = RundownUtils.isPieceInstance(props.piece) ? (props.piece.instance.piece as Piece) : (props.piece as AdLibPieceUi) - const status = props.contentStatus?.status - const layer = props.showStyleBase.sourceLayers[piece.sourceLayerId] return ( @@ -33,7 +31,7 @@ const InspectorTitle = withMediaObjectStatus()(function InspectorTit className={ClassNames( 'shelf-inspector__title__icon', layer && RundownUtils.getSourceLayerClassName(layer.type), - RundownUtils.getPieceStatusClassName(status) + RundownUtils.getPieceStatusClassName(contentStatus?.status) )} >
{layer && (layer.abbreviation || layer.name)}
@@ -41,6 +39,6 @@ const InspectorTitle = withMediaObjectStatus()(function InspectorTit {piece.name} ) -}) +} export default InspectorTitle diff --git a/meteor/client/ui/Shelf/Inspector/ShelfInspector.tsx b/meteor/client/ui/Shelf/Inspector/ShelfInspector.tsx index cb81e210ae..bc34c096ed 100644 --- a/meteor/client/ui/Shelf/Inspector/ShelfInspector.tsx +++ b/meteor/client/ui/Shelf/Inspector/ShelfInspector.tsx @@ -9,9 +9,7 @@ import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/Rund import { IAdLibListItem } from '../AdLibListItem' import { UIShowStyleBase } from '../../../../lib/api/showStyles' import { UIStudio } from '../../../../lib/api/studios' -import { withMediaObjectStatus, WithMediaObjectStatusProps } from '../../SegmentTimeline/withMediaObjectStatus' - -export { ShelfInspector } +import { useContentStatusForItem } from '../../SegmentTimeline/withMediaObjectStatus' interface IShelfInspectorProps { selected: BucketAdLibItem | IAdLibListItem | PieceUi | undefined @@ -21,33 +19,27 @@ interface IShelfInspectorProps { onSelectPiece: (piece: BucketAdLibItem | IAdLibListItem | PieceUi | undefined) => void } -const ShelfInspector = withMediaObjectStatus()( - class ShelfInspector extends React.Component { - constructor(props: IShelfInspectorProps & WithMediaObjectStatusProps) { - super(props) - } - - shouldComponentUpdate(nextProps: IShelfInspectorProps & WithMediaObjectStatusProps): boolean { - if (_.isEqual(nextProps, this.props)) return false - return true - } +export const ShelfInspector = React.memo( + function ShelfInspector({ selected, showStyleBase, studio, rundownPlaylist, onSelectPiece }: IShelfInspectorProps) { + const contentStatus = useContentStatusForItem(selected) - render(): JSX.Element { - const { selected, contentStatus, showStyleBase, studio, rundownPlaylist, onSelectPiece } = this.props - const content = - selected && renderItem(selected, contentStatus, showStyleBase, studio, rundownPlaylist, onSelectPiece) + const content = + selected && renderItem(selected, contentStatus, showStyleBase, studio, rundownPlaylist, onSelectPiece) - return ( - - {content || false} - - ) - } + return ( + + {content || false} + + ) + }, + (prevProps, nextProps) => { + if (_.isEqual(nextProps, prevProps)) return false + return true } ) From a0f693e431ad74b88dda682bc751c5cc571ff2c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20St=C3=A6rk=C3=A6r?= Date: Tue, 30 Jul 2024 11:26:50 +0200 Subject: [PATCH 397/479] =?UTF-8?q?SOFIE-3367=20feat:=20allows=20to=20cofi?= =?UTF-8?q?gure=20an=20offset-factor=20between=20the=20L=20and=20R=20joyco?= =?UTF-8?q?n=20=E2=80=A6=20(#1231)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: allows to cofigure an offset-factor between the L and R joycon joysticks to compensate for different rates. New URL param is called joycon_rightHandOffset. Used to be hardcoded to 1.4 which now is a default. (cherry picked from commit 9d186ae622df925d2bd3a6b789571c3d6f000ed9) * feat: adds docs --- meteor/client/ui/Prompter/PrompterView.tsx | 2 ++ meteor/client/ui/Prompter/controller/joycon-device.ts | 10 ++++++---- .../docs/user-guide/features/prompter.md | 11 ++++++----- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/meteor/client/ui/Prompter/PrompterView.tsx b/meteor/client/ui/Prompter/PrompterView.tsx index a0bb3a7659..fc1c9af9a9 100644 --- a/meteor/client/ui/Prompter/PrompterView.tsx +++ b/meteor/client/ui/Prompter/PrompterView.tsx @@ -44,6 +44,7 @@ interface PrompterConfig { joycon_rangeNeutralMin?: number joycon_rangeNeutralMax?: number joycon_rangeFwdMax?: number + joycon_rightHandOffset?: number pedal_speedMap?: number[] pedal_reverseSpeedMap?: number[] pedal_rangeRevMin?: number @@ -144,6 +145,7 @@ export class PrompterViewInner extends MeteorReactComponent this.deadBand) { lastSeenSpeed = joycon.axes[1] * -1 // in this mode, we are "negative" on both sticks.... } else if (Math.abs(joycon.axes[3]) > this.deadBand) { - lastSeenSpeed = joycon.axes[3] * -1.4 // in this mode, we are "negative" on both sticks.... - // factor increased by 1.4 to account for the R joystick being less sensitive than L + lastSeenSpeed = joycon.axes[3] * this.rightHandOffset * -1 // in this mode, we are "negative" on both sticks.... + // rightHandOffset allows for different rates between the two controllers } this.timestampOfLastUsedJoyconInput = joycon.timestamp } diff --git a/packages/documentation/docs/user-guide/features/prompter.md b/packages/documentation/docs/user-guide/features/prompter.md index 893e43f7af..a64cd7e641 100644 --- a/packages/documentation/docs/user-guide/features/prompter.md +++ b/packages/documentation/docs/user-guide/features/prompter.md @@ -28,7 +28,7 @@ The prompter UI can be configured using query parameters: | `showmarker` | 0 / 1 | If the marker is not set to "hide", control if the marker is hidden or not | `1` | | `showscroll` | 0 / 1 | Whether the scroll bar should be shown | `1` | | `followtake` | 0 / 1 | Whether the prompter should automatically scroll to current segment when the operator TAKE:s it | `1` | -| `showoverunder` | 0 / 1 | The timer in the top-right of the prompter, showing the overtime/undertime of the current show. | `1` | +| `showoverunder` | 0 / 1 | The timer in the top-right of the prompter, showing the overtime/undertime of the current show. | `1` | | `debug` | 0 / 1 | Whether to display a debug box showing controller input values and the calculated speed the prompter is currently scrolling at. Used to tweak speedMaps and ranges. | `0` | Example: [http://127.0.0.1/prompter/studio0/?mode=mouse&followtake=0&fontsize=20](http://127.0.0.1/prompter/studio0/?mode=mouse&followtake=0&fontsize=20) @@ -37,9 +37,9 @@ Example: [http://127.0.0.1/prompter/studio0/?mode=mouse&followtake=0&fontsize=20 The prompter can be controlled by different types of controllers. The control mode is set by a query parameter, like so: `?mode=mouse`. -| Query parameter | Description | -| :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Default | Controlled by both mouse and keyboard | +| Query parameter | Description | +| :---------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Default | Controlled by both mouse and keyboard | | `?mode=mouse` | Controlled by mouse only. [See configuration details](prompter.md#control-using-mouse-scroll-wheel) | | `?mode=keyboard` | Controlled by keyboard only. [See configuration details](prompter.md#control-using-keyboard) | | `?mode=shuttlekeyboard` | Controlled by a Contour Design ShuttleXpress, X-keys Jog and Shuttle or any compatible, configured as keyboard-ish device. [See configuration details](prompter.md#control-using-contour-shuttlexpress-or-x-keys) | @@ -117,7 +117,7 @@ If you want to use traditional analogue pedals with 5 volt TRS connection, a con - `pedal_rangeNeutralMax` has to be greater than `pedal_rangeNeutralMin` - `pedal_rangeFwdMax` has to be greater than `pedal_rangeNeutralMax` -![Yamaha FC7 mapped for both a forward \(80-127\) and backwards \(0-35\) range.](/img/docs/main/features/yamaha-fc7.jpg) +![Yamaha FC7 mapped for both a forward (80-127) and backwards (0-35) range.](/img/docs/main/features/yamaha-fc7.jpg) The default values allow for both going forwards and backwards. This matches the _Yamaha FC7_ expression pedal. The default values create a forward-range from 80-127, a neutral zone from 35-80 and a reverse-range from 0-35. @@ -158,6 +158,7 @@ The Joycons can operate in 3 modes, the L-stick, the R-stick or both L+R sticks | `joycon_rangeNeutralMin` | number | The beginning of the backwards-range. | `-0.25` | | `joycon_rangeNeutralMax` | number | The minimum input to run forward, the start of the forward-range \(min speed\). This is also the end of any "deadband" you want filter out before starting moving forwards. | `0.25` | | `joycon_rangeFwdMax` | number | The maximum input, the end of the forward-range \(max speed\) | `1` | +| `joycon_rightHandOffset` | number | A ratio to increase or decrease the R Joycon joystick sensitivity relative to the L Joycon. | `1.4` | - `joycon_rangeNeutralMin` has to be greater than `joycon_rangeRevMin` - `joycon_rangeNeutralMax` has to be greater than `joycon_rangeNeutralMin` From f0224c9b89d6e0b6bf07c88b113a7ab070ed3e33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20St=C3=A6rk=C3=A6r?= Date: Tue, 30 Jul 2024 11:37:08 +0200 Subject: [PATCH 398/479] chore(release): 1.50.4 --- meteor/CHANGELOG.md | 11 ++++++ meteor/package.json | 2 +- meteor/yarn.lock | 12 +++--- packages/blueprints-integration/CHANGELOG.md | 8 ++++ packages/blueprints-integration/package.json | 4 +- packages/corelib/package.json | 6 +-- packages/documentation/package.json | 2 +- packages/job-worker/package.json | 8 ++-- packages/lerna.json | 2 +- packages/live-status-gateway/package.json | 10 ++--- packages/mos-gateway/CHANGELOG.md | 11 ++++++ packages/mos-gateway/package.json | 6 +-- packages/openapi/package.json | 2 +- packages/playout-gateway/CHANGELOG.md | 11 ++++++ packages/playout-gateway/package.json | 6 +-- packages/server-core-integration/CHANGELOG.md | 8 ++++ packages/server-core-integration/package.json | 4 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 38 +++++++++---------- 19 files changed, 101 insertions(+), 52 deletions(-) diff --git a/meteor/CHANGELOG.md b/meteor/CHANGELOG.md index d2c3a9d3ca..ab5960933e 100644 --- a/meteor/CHANGELOG.md +++ b/meteor/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [1.50.4](https://github.com/nrkno/sofie-core/compare/v1.50.3-LSG-updates...v1.50.4) (2024-07-30) + + +### Bug Fixes + +* further improve stringifyError ([0233073](https://github.com/nrkno/sofie-core/commit/02330735fd4f3b03f9bff84e4c41fb9199a4623e)) +* improve error logging: use stringifyError() ([8da63de](https://github.com/nrkno/sofie-core/commit/8da63dec44915439ea436eee9697f3774241537b)) +* **LSG:** Token "examples" does not exist when running `yarn gendocs` ([a0ea9b4](https://github.com/nrkno/sofie-core/commit/a0ea9b48997fe39e8db5ababef744c103a68509f)) +* make stringifyError handle UserError better ([e2ecc7e](https://github.com/nrkno/sofie-core/commit/e2ecc7eb48b9ad6c0d95b299d954abac577e70a7)) +* refactor VirtualElement to be a FC ([bf81baf](https://github.com/nrkno/sofie-core/commit/bf81baf9ff520ad6a9fc9b6378ce6ceeac320645)) + ### [1.50.3](https://github.com/nrkno/sofie-core/compare/v1.50.2...v1.50.3) (2024-06-24) diff --git a/meteor/package.json b/meteor/package.json index 1a565620e0..db512d753d 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -1,6 +1,6 @@ { "name": "automation-core", - "version": "1.50.3", + "version": "1.50.4", "private": true, "engines": { "node": ">=14.19.1" diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 62207c18bf..b959fdb83c 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -2082,7 +2082,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/blueprints-integration@portal:../packages/blueprints-integration::locator=automation-core%40workspace%3A." dependencies: - "@sofie-automation/shared-lib": 1.50.3 + "@sofie-automation/shared-lib": 1.50.4 tslib: ^2.4.0 type-fest: ^2.19.0 languageName: node @@ -2123,8 +2123,8 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/corelib@portal:../packages/corelib::locator=automation-core%40workspace%3A." dependencies: - "@sofie-automation/blueprints-integration": 1.50.3 - "@sofie-automation/shared-lib": 1.50.3 + "@sofie-automation/blueprints-integration": 1.50.4 + "@sofie-automation/shared-lib": 1.50.4 fast-clone: ^1.5.13 i18next: ^21.9.1 influx: ^5.9.3 @@ -2155,9 +2155,9 @@ __metadata: resolution: "@sofie-automation/job-worker@portal:../packages/job-worker::locator=automation-core%40workspace%3A." dependencies: "@slack/webhook": ^6.1.0 - "@sofie-automation/blueprints-integration": 1.50.3 - "@sofie-automation/corelib": 1.50.3 - "@sofie-automation/shared-lib": 1.50.3 + "@sofie-automation/blueprints-integration": 1.50.4 + "@sofie-automation/corelib": 1.50.4 + "@sofie-automation/shared-lib": 1.50.4 amqplib: ^0.10.3 deepmerge: ^4.3.1 elastic-apm-node: ^3.43.0 diff --git a/packages/blueprints-integration/CHANGELOG.md b/packages/blueprints-integration/CHANGELOG.md index b547bc181f..ca99a53bf2 100644 --- a/packages/blueprints-integration/CHANGELOG.md +++ b/packages/blueprints-integration/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.50.4](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.3-LSG-updates...v1.50.4) (2024-07-30) + +**Note:** Version bump only for package @sofie-automation/blueprints-integration + + + + + ## [1.50.3](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.2...v1.50.3) (2024-06-24) **Note:** Version bump only for package @sofie-automation/blueprints-integration diff --git a/packages/blueprints-integration/package.json b/packages/blueprints-integration/package.json index 16bbb4b312..fde026582b 100644 --- a/packages/blueprints-integration/package.json +++ b/packages/blueprints-integration/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/blueprints-integration", - "version": "1.50.3", + "version": "1.50.4", "description": "Library to define the interaction between core and the blueprints.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -38,7 +38,7 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/shared-lib": "1.50.3", + "@sofie-automation/shared-lib": "1.50.4", "tslib": "^2.4.0", "type-fest": "^2.19.0" }, diff --git a/packages/corelib/package.json b/packages/corelib/package.json index 9287cce58f..0d26799e42 100644 --- a/packages/corelib/package.json +++ b/packages/corelib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/corelib", - "version": "1.50.3", + "version": "1.50.4", "private": true, "description": "Internal library for some types shared by core and workers", "main": "dist/index.js", @@ -39,8 +39,8 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/blueprints-integration": "1.50.3", - "@sofie-automation/shared-lib": "1.50.3", + "@sofie-automation/blueprints-integration": "1.50.4", + "@sofie-automation/shared-lib": "1.50.4", "fast-clone": "^1.5.13", "i18next": "^21.9.1", "influx": "^5.9.3", diff --git a/packages/documentation/package.json b/packages/documentation/package.json index f4dedc4eb5..d95532143e 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -1,6 +1,6 @@ { "name": "sofie-documentation", - "version": "1.50.3", + "version": "1.50.4", "private": true, "scripts": { "docusaurus": "docusaurus", diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index 7dd8b13a1c..5ce5041061 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/job-worker", - "version": "1.50.3", + "version": "1.50.4", "description": "Worker for things", "main": "dist/index.js", "license": "MIT", @@ -41,9 +41,9 @@ ], "dependencies": { "@slack/webhook": "^6.1.0", - "@sofie-automation/blueprints-integration": "1.50.3", - "@sofie-automation/corelib": "1.50.3", - "@sofie-automation/shared-lib": "1.50.3", + "@sofie-automation/blueprints-integration": "1.50.4", + "@sofie-automation/corelib": "1.50.4", + "@sofie-automation/shared-lib": "1.50.4", "amqplib": "^0.10.3", "deepmerge": "^4.3.1", "elastic-apm-node": "^3.43.0", diff --git a/packages/lerna.json b/packages/lerna.json index 6b4655c5b3..d9f6ff6141 100644 --- a/packages/lerna.json +++ b/packages/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.50.3", + "version": "1.50.4", "npmClient": "yarn", "useWorkspaces": true } diff --git a/packages/live-status-gateway/package.json b/packages/live-status-gateway/package.json index 06f772a959..e0cf67276e 100644 --- a/packages/live-status-gateway/package.json +++ b/packages/live-status-gateway/package.json @@ -1,6 +1,6 @@ { "name": "live-status-gateway", - "version": "1.50.3", + "version": "1.50.4", "private": true, "description": "Provides state from Sofie over sockets", "license": "MIT", @@ -53,10 +53,10 @@ "production" ], "dependencies": { - "@sofie-automation/blueprints-integration": "1.50.3", - "@sofie-automation/corelib": "1.50.3", - "@sofie-automation/server-core-integration": "1.50.3", - "@sofie-automation/shared-lib": "1.50.3", + "@sofie-automation/blueprints-integration": "1.50.4", + "@sofie-automation/corelib": "1.50.4", + "@sofie-automation/server-core-integration": "1.50.4", + "@sofie-automation/shared-lib": "1.50.4", "debug": "^4.3.2", "fast-clone": "^1.5.13", "influx": "^5.9.2", diff --git a/packages/mos-gateway/CHANGELOG.md b/packages/mos-gateway/CHANGELOG.md index f0dba62d33..a0ba68997e 100644 --- a/packages/mos-gateway/CHANGELOG.md +++ b/packages/mos-gateway/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.50.4](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.3-LSG-updates...v1.50.4) (2024-07-30) + + +### Bug Fixes + +* improve error logging: use stringifyError() ([8da63de](https://github.com/nrkno/tv-automation-server-core/commit/8da63dec44915439ea436eee9697f3774241537b)) + + + + + ## [1.50.3](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.2...v1.50.3) (2024-06-24) **Note:** Version bump only for package mos-gateway diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index ccaccf062b..76c9eadd35 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -1,6 +1,6 @@ { "name": "mos-gateway", - "version": "1.50.3", + "version": "1.50.4", "private": true, "description": "MOS-Gateway for the Sofie project", "license": "MIT", @@ -66,8 +66,8 @@ ], "dependencies": { "@mos-connection/connector": "^3.0.4", - "@sofie-automation/server-core-integration": "1.50.3", - "@sofie-automation/shared-lib": "1.50.3", + "@sofie-automation/server-core-integration": "1.50.4", + "@sofie-automation/shared-lib": "1.50.4", "tslib": "^2.4.0", "type-fest": "^2.19.0", "underscore": "^1.13.4", diff --git a/packages/openapi/package.json b/packages/openapi/package.json index 9ae630b5eb..ca2649a852 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/openapi", - "version": "1.50.3", + "version": "1.50.4", "private": true, "license": "MIT", "repository": { diff --git a/packages/playout-gateway/CHANGELOG.md b/packages/playout-gateway/CHANGELOG.md index a06ce42990..51ce2aa8e7 100644 --- a/packages/playout-gateway/CHANGELOG.md +++ b/packages/playout-gateway/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.50.4](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.3-LSG-updates...v1.50.4) (2024-07-30) + + +### Bug Fixes + +* improve error logging: use stringifyError() ([8da63de](https://github.com/nrkno/tv-automation-server-core/commit/8da63dec44915439ea436eee9697f3774241537b)) + + + + + ## [1.50.3](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.2...v1.50.3) (2024-06-24) **Note:** Version bump only for package playout-gateway diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index a994f01ef1..8183a95b88 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -1,6 +1,6 @@ { "name": "playout-gateway", - "version": "1.50.3", + "version": "1.50.4", "private": true, "description": "Connect to Core, play stuff", "license": "MIT", @@ -56,8 +56,8 @@ "production" ], "dependencies": { - "@sofie-automation/server-core-integration": "1.50.3", - "@sofie-automation/shared-lib": "1.50.3", + "@sofie-automation/server-core-integration": "1.50.4", + "@sofie-automation/shared-lib": "1.50.4", "debug": "^4.3.3", "influx": "^5.9.3", "timeline-state-resolver": "9.0.1", diff --git a/packages/server-core-integration/CHANGELOG.md b/packages/server-core-integration/CHANGELOG.md index 0aaa4932e2..85a829f08a 100644 --- a/packages/server-core-integration/CHANGELOG.md +++ b/packages/server-core-integration/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.50.4](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.3-LSG-updates...v1.50.4) (2024-07-30) + +**Note:** Version bump only for package @sofie-automation/server-core-integration + + + + + ## [1.50.3](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.2...v1.50.3) (2024-06-24) **Note:** Version bump only for package @sofie-automation/server-core-integration diff --git a/packages/server-core-integration/package.json b/packages/server-core-integration/package.json index 7728a8df01..ce03404bc0 100644 --- a/packages/server-core-integration/package.json +++ b/packages/server-core-integration/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/server-core-integration", - "version": "1.50.3", + "version": "1.50.4", "description": "Library for connecting to Core", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -70,7 +70,7 @@ "production" ], "dependencies": { - "@sofie-automation/shared-lib": "1.50.3", + "@sofie-automation/shared-lib": "1.50.4", "ejson": "^2.2.3", "eventemitter3": "^4.0.7", "faye-websocket": "^0.11.4", diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 54e3cd630f..13bcbdd014 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/shared-lib", - "version": "1.50.3", + "version": "1.50.4", "description": "Library for types & values shared by core, workers and gateways", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/packages/yarn.lock b/packages/yarn.lock index a05ca1fe53..47a05b8d8b 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -5030,11 +5030,11 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/blueprints-integration@1.50.3, @sofie-automation/blueprints-integration@workspace:blueprints-integration": +"@sofie-automation/blueprints-integration@1.50.4, @sofie-automation/blueprints-integration@workspace:blueprints-integration": version: 0.0.0-use.local resolution: "@sofie-automation/blueprints-integration@workspace:blueprints-integration" dependencies: - "@sofie-automation/shared-lib": 1.50.3 + "@sofie-automation/shared-lib": 1.50.4 tslib: ^2.4.0 type-fest: ^2.19.0 languageName: unknown @@ -5071,12 +5071,12 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/corelib@1.50.3, @sofie-automation/corelib@workspace:corelib": +"@sofie-automation/corelib@1.50.4, @sofie-automation/corelib@workspace:corelib": version: 0.0.0-use.local resolution: "@sofie-automation/corelib@workspace:corelib" dependencies: - "@sofie-automation/blueprints-integration": 1.50.3 - "@sofie-automation/shared-lib": 1.50.3 + "@sofie-automation/blueprints-integration": 1.50.4 + "@sofie-automation/shared-lib": 1.50.4 fast-clone: ^1.5.13 i18next: ^21.9.1 influx: ^5.9.3 @@ -5107,9 +5107,9 @@ __metadata: resolution: "@sofie-automation/job-worker@workspace:job-worker" dependencies: "@slack/webhook": ^6.1.0 - "@sofie-automation/blueprints-integration": 1.50.3 - "@sofie-automation/corelib": 1.50.3 - "@sofie-automation/shared-lib": 1.50.3 + "@sofie-automation/blueprints-integration": 1.50.4 + "@sofie-automation/corelib": 1.50.4 + "@sofie-automation/shared-lib": 1.50.4 amqplib: ^0.10.3 deepmerge: ^4.3.1 elastic-apm-node: ^3.43.0 @@ -5139,11 +5139,11 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/server-core-integration@1.50.3, @sofie-automation/server-core-integration@workspace:server-core-integration": +"@sofie-automation/server-core-integration@1.50.4, @sofie-automation/server-core-integration@workspace:server-core-integration": version: 0.0.0-use.local resolution: "@sofie-automation/server-core-integration@workspace:server-core-integration" dependencies: - "@sofie-automation/shared-lib": 1.50.3 + "@sofie-automation/shared-lib": 1.50.4 ejson: ^2.2.3 eventemitter3: ^4.0.7 faye-websocket: ^0.11.4 @@ -5153,7 +5153,7 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/shared-lib@1.50.3, @sofie-automation/shared-lib@workspace:shared-lib": +"@sofie-automation/shared-lib@1.50.4, @sofie-automation/shared-lib@workspace:shared-lib": version: 0.0.0-use.local resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: @@ -14992,10 +14992,10 @@ asn1@evs-broadcast/node-asn1: "@asyncapi/generator": 1.9.13 "@asyncapi/html-template": 0.26.0 "@asyncapi/nodejs-ws-template": 0.9.25 - "@sofie-automation/blueprints-integration": 1.50.3 - "@sofie-automation/corelib": 1.50.3 - "@sofie-automation/server-core-integration": 1.50.3 - "@sofie-automation/shared-lib": 1.50.3 + "@sofie-automation/blueprints-integration": 1.50.4 + "@sofie-automation/corelib": 1.50.4 + "@sofie-automation/server-core-integration": 1.50.4 + "@sofie-automation/shared-lib": 1.50.4 debug: ^4.3.2 fast-clone: ^1.5.13 influx: ^5.9.2 @@ -16076,8 +16076,8 @@ asn1@evs-broadcast/node-asn1: resolution: "mos-gateway@workspace:mos-gateway" dependencies: "@mos-connection/connector": ^3.0.4 - "@sofie-automation/server-core-integration": 1.50.3 - "@sofie-automation/shared-lib": 1.50.3 + "@sofie-automation/server-core-integration": 1.50.4 + "@sofie-automation/shared-lib": 1.50.4 tslib: ^2.4.0 type-fest: ^2.19.0 underscore: ^1.13.4 @@ -18053,8 +18053,8 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "playout-gateway@workspace:playout-gateway" dependencies: - "@sofie-automation/server-core-integration": 1.50.3 - "@sofie-automation/shared-lib": 1.50.3 + "@sofie-automation/server-core-integration": 1.50.4 + "@sofie-automation/shared-lib": 1.50.4 debug: ^4.3.3 influx: ^5.9.3 timeline-state-resolver: 9.0.1 From beee11a21a44a3e3e75c8189061b74ae45f9068a Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 1 Aug 2024 13:46:01 +0100 Subject: [PATCH 399/479] fix: rundown timing drifting when playing parts with preroll SOFIE-3291 (#1234) --- meteor/__mocks__/defaultCollectionObjects.ts | 2 +- meteor/__mocks__/helpers/database.ts | 10 +-- .../lib/__tests__/rundownTiming.test.ts | 2 +- meteor/client/lib/rundown.ts | 8 +-- meteor/client/lib/rundownTiming.ts | 35 +++++----- .../client/ui/ClockView/PresenterScreen.tsx | 4 +- .../RundownTiming/SegmentDuration.tsx | 4 +- .../Renderers/MicSourceRenderer.tsx | 6 +- .../ui/SegmentTimeline/SegmentTimeline.tsx | 2 +- meteor/server/__tests__/cronjobs.test.ts | 4 +- .../api/__tests__/peripheralDevice.test.ts | 4 +- meteor/server/migration/1_39_0.ts | 10 +-- meteor/server/migration/1_50_0.ts | 67 +++++++++++++++++++ packages/corelib/src/dataModel/Part.ts | 4 +- packages/corelib/src/playout/timings.ts | 28 ++++---- .../src/__mocks__/defaultCollectionObjects.ts | 2 +- .../src/__mocks__/presetCollections.ts | 10 +-- .../rundown-updatePartInstanceRanks.test.ts | 10 +-- .../src/blueprints/context/adlibActions.ts | 2 +- .../cache/__tests__/DatabaseCaches.test.ts | 6 +- .../src/ingest/__tests__/ingest.test.ts | 2 +- .../src/ingest/__tests__/updateNext.test.ts | 28 ++++---- .../src/ingest/generationSegment.ts | 6 +- .../src/ingest/syncChangesToPartInstance.ts | 4 +- .../src/playout/__tests__/helpers/rundowns.ts | 4 +- .../job-worker/src/playout/adlibAction.ts | 4 +- packages/job-worker/src/playout/adlibUtils.ts | 6 +- packages/job-worker/src/playout/lib.ts | 12 ++-- .../lookahead/__tests__/lookahead.test.ts | 2 +- .../playout/lookahead/__tests__/util.test.ts | 2 +- .../topics/__tests__/activePlaylist.spec.ts | 2 +- .../topics/__tests__/segmentsTopic.spec.ts | 16 ++--- .../src/topics/helpers/partTiming.ts | 2 +- .../src/topics/helpers/segmentTiming.ts | 4 +- 34 files changed, 189 insertions(+), 125 deletions(-) diff --git a/meteor/__mocks__/defaultCollectionObjects.ts b/meteor/__mocks__/defaultCollectionObjects.ts index 3179bde198..373c7ac2c6 100644 --- a/meteor/__mocks__/defaultCollectionObjects.ts +++ b/meteor/__mocks__/defaultCollectionObjects.ts @@ -138,7 +138,7 @@ export function defaultPart(_id: PartId, rundownId: RundownId, segmentId: Segmen _rank: 0, externalId: unprotectString(_id), title: 'Default Part', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, } } export function defaultPiece(_id: PieceId, rundownId: RundownId, segmentId: SegmentId, partId: PartId): Piece { diff --git a/meteor/__mocks__/helpers/database.ts b/meteor/__mocks__/helpers/database.ts index 23c08c2b98..6acc45f8bd 100644 --- a/meteor/__mocks__/helpers/database.ts +++ b/meteor/__mocks__/helpers/database.ts @@ -662,7 +662,7 @@ export async function setupDefaultRundown( _rank: 0, externalId: 'MOCK_PART_0_0', title: 'Part 0 0', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, } await Parts.mutableCollection.insertAsync(part00) @@ -730,7 +730,7 @@ export async function setupDefaultRundown( _rank: 1, externalId: 'MOCK_PART_0_1', title: 'Part 0 1', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, } await Parts.mutableCollection.insertAsync(part01) @@ -771,7 +771,7 @@ export async function setupDefaultRundown( _rank: 0, externalId: 'MOCK_PART_1_0', title: 'Part 1 0', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, } await Parts.mutableCollection.insertAsync(part10) @@ -782,7 +782,7 @@ export async function setupDefaultRundown( _rank: 1, externalId: 'MOCK_PART_1_1', title: 'Part 1 1', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, } await Parts.mutableCollection.insertAsync(part11) @@ -793,7 +793,7 @@ export async function setupDefaultRundown( _rank: 2, externalId: 'MOCK_PART_1_2', title: 'Part 1 2', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, } await Parts.mutableCollection.insertAsync(part12) diff --git a/meteor/client/lib/__tests__/rundownTiming.test.ts b/meteor/client/lib/__tests__/rundownTiming.test.ts index a36aa68f77..c223adb5ce 100644 --- a/meteor/client/lib/__tests__/rundownTiming.test.ts +++ b/meteor/client/lib/__tests__/rundownTiming.test.ts @@ -50,7 +50,7 @@ function makeMockPart( _rank: rank, rundownId: protectString(rundownId), ...durations, - expectedDurationWithPreroll: durations.expectedDuration, + expectedDurationWithTransition: durations.expectedDuration, }) } diff --git a/meteor/client/lib/rundown.ts b/meteor/client/lib/rundown.ts index 0eb58e3043..d3a06e6aec 100644 --- a/meteor/client/lib/rundown.ts +++ b/meteor/client/lib/rundown.ts @@ -36,7 +36,7 @@ import { FindOptions } from '../../lib/collections/lib' import { getShowHiddenSourceLayers } from './localStorage' import { Rundown } from '../../lib/collections/Rundowns' import { IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' -import { calculatePartInstanceExpectedDurationWithPreroll } from '@sofie-automation/corelib/dist/playout/timings' +import { calculatePartInstanceExpectedDurationWithTransition } from '@sofie-automation/corelib/dist/playout/timings' import { AdLibPieceUi } from './shelf' import { UIShowStyleBase } from '../../lib/api/showStyles' import { PartId, PieceId, RundownId, SegmentId, ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids' @@ -63,7 +63,7 @@ export namespace RundownUtils { return ( memo + (part.instance.timings?.duration || - calculatePartInstanceExpectedDurationWithPreroll(part.instance) || + calculatePartInstanceExpectedDurationWithTransition(part.instance) || part.renderedDuration || (display ? Settings.defaultDisplayDuration : 0)) ) @@ -218,7 +218,7 @@ export namespace RundownUtils { ? part.instance.timings.duration + (part.instance.timings?.playOffset || 0) : (partDuration || part.renderedDuration || - calculatePartInstanceExpectedDurationWithPreroll(part.instance) || + calculatePartInstanceExpectedDurationWithTransition(part.instance) || 0) - (piece.renderedInPoint || 0))) : part.instance.timings?.duration !== undefined ? part.instance.timings.duration + (part.instance.timings?.playOffset || 0) @@ -422,7 +422,7 @@ export namespace RundownUtils { partsE = segmentInfo.partInstances.map((partInstance, itIndex) => { const partTimeline: SuperTimeline.TimelineObject[] = [] - const partExpectedDuration = calculatePartInstanceExpectedDurationWithPreroll(partInstance) + const partExpectedDuration = calculatePartInstanceExpectedDurationWithTransition(partInstance) // extend objects to match the Extended interface const partE = literal({ diff --git a/meteor/client/lib/rundownTiming.ts b/meteor/client/lib/rundownTiming.ts index 5fb0747e0d..421b9b0945 100644 --- a/meteor/client/lib/rundownTiming.ts +++ b/meteor/client/lib/rundownTiming.ts @@ -14,7 +14,7 @@ import { PartId, PartInstanceId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { literal } from '@sofie-automation/corelib/dist/lib' import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' -import { calculatePartInstanceExpectedDurationWithPreroll } from '@sofie-automation/corelib/dist/playout/timings' +import { calculatePartInstanceExpectedDurationWithTransition } from '@sofie-automation/corelib/dist/playout/timings' import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { findPartInstanceInMapOrWrapToTemporary, @@ -211,7 +211,7 @@ export class RundownTimingCalculator { // if the Part is using budgetDuration, this budget is calculated when going through all the segments // in the Rundown (see further down) if (!segmentUsesBudget && !partIsUntimed) { - totalRundownDuration += calculatePartInstanceExpectedDurationWithPreroll(partInstance) || 0 + totalRundownDuration += calculatePartInstanceExpectedDurationWithTransition(partInstance) || 0 } const lastStartedPlayback = partInstance.timings?.plannedStartedPlayback @@ -225,7 +225,7 @@ export class RundownTimingCalculator { let displayDurationFromGroup = 0 partExpectedDuration = - calculatePartInstanceExpectedDurationWithPreroll(partInstance) || + calculatePartInstanceExpectedDurationWithTransition(partInstance) || partInstance.timings?.duration || 0 @@ -248,7 +248,7 @@ export class RundownTimingCalculator { ) { this.displayDurationGroups[partInstance.part.displayDurationGroup] = (this.displayDurationGroups[partInstance.part.displayDurationGroup] || 0) + - (calculatePartInstanceExpectedDurationWithPreroll(partInstance) || 0) + (calculatePartInstanceExpectedDurationWithTransition(partInstance) || 0) displayDurationFromGroup = partInstance.part.displayDuration || Math.max( @@ -272,7 +272,7 @@ export class RundownTimingCalculator { : undefined) partDuration = Math.max( - duration || calculatePartInstanceExpectedDurationWithPreroll(partInstance) || 0, + duration || calculatePartInstanceExpectedDurationWithTransition(partInstance) || 0, now - lastStartedPlayback ) - playOffset // because displayDurationGroups have no actual timing on them, we need to have a copy of the @@ -282,7 +282,7 @@ export class RundownTimingCalculator { duration || (memberOfDisplayDurationGroup ? displayDurationFromGroup - : calculatePartInstanceExpectedDurationWithPreroll(partInstance)) || + : calculatePartInstanceExpectedDurationWithTransition(partInstance)) || defaultDuration partDisplayDuration = Math.max(partDisplayDurationNoPlayback, now - lastStartedPlayback) this.partPlayed[unprotectString(partInstance.part._id)] = now - lastStartedPlayback @@ -304,7 +304,7 @@ export class RundownTimingCalculator { (duration || (memberOfDisplayDurationGroup ? displayDurationFromGroup - : calculatePartInstanceExpectedDurationWithPreroll(partInstance)) || + : calculatePartInstanceExpectedDurationWithTransition(partInstance)) || 0) - (now - lastStartedPlayback) ) @@ -312,7 +312,7 @@ export class RundownTimingCalculator { } else { partDuration = (partInstance.timings?.duration || - calculatePartInstanceExpectedDurationWithPreroll(partInstance) || + calculatePartInstanceExpectedDurationWithTransition(partInstance) || 0) - playOffset partDisplayDurationNoPlayback = Math.max( 0, @@ -320,7 +320,7 @@ export class RundownTimingCalculator { displayDurationFromGroup || ensureMinimumDefaultDurationIfNotAuto( partInstance, - calculatePartInstanceExpectedDurationWithPreroll(partInstance), + calculatePartInstanceExpectedDurationWithTransition(partInstance), defaultDuration ) ) @@ -344,7 +344,7 @@ export class RundownTimingCalculator { valToAddToAsPlayedDuration = partInstance.timings.duration } else if (partCounts) { valToAddToAsPlayedDuration = - calculatePartInstanceExpectedDurationWithPreroll(partInstance) || 0 + calculatePartInstanceExpectedDurationWithTransition(partInstance) || 0 } asPlayedRundownDuration += valToAddToAsPlayedDuration @@ -381,15 +381,15 @@ export class RundownTimingCalculator { memberOfDisplayDurationGroup ? Math.max( partExpectedDuration, - calculatePartInstanceExpectedDurationWithPreroll(partInstance) || 0 + calculatePartInstanceExpectedDurationWithTransition(partInstance) || 0 ) - : calculatePartInstanceExpectedDurationWithPreroll(partInstance) || 0, + : calculatePartInstanceExpectedDurationWithTransition(partInstance) || 0, now - lastStartedPlayback ) } else { asDisplayedRundownDuration += partInstance.timings?.duration || - calculatePartInstanceExpectedDurationWithPreroll(partInstance) || + calculatePartInstanceExpectedDurationWithTransition(partInstance) || 0 } @@ -433,12 +433,12 @@ export class RundownTimingCalculator { waitDuration = partInstance.timings?.duration || partDisplayDuration || - calculatePartInstanceExpectedDurationWithPreroll(partInstance) || + calculatePartInstanceExpectedDurationWithTransition(partInstance) || 0 } else { waitDuration = partInstance.timings?.duration || - calculatePartInstanceExpectedDurationWithPreroll(partInstance) || + calculatePartInstanceExpectedDurationWithTransition(partInstance) || 0 } if (segmentUsesBudget) { @@ -462,7 +462,8 @@ export class RundownTimingCalculator { // this needs to use partInstance.part.expectedDuration as opposed to partExpectedDuration, because // partExpectedDuration is affected by displayGroups, and if it hasn't played yet then it shouldn't // add any duration to the "remaining" time pool - remainingRundownDuration += calculatePartInstanceExpectedDurationWithPreroll(partInstance) || 0 + remainingRundownDuration += + calculatePartInstanceExpectedDurationWithTransition(partInstance) || 0 // item is onAir right now, and it's is currently shorter than expectedDuration } else if ( lastStartedPlayback && @@ -599,7 +600,7 @@ export class RundownTimingCalculator { let onAirPartDuration = currentLivePartInstance.timings?.duration || - calculatePartInstanceExpectedDurationWithPreroll(currentLivePartInstance) || + calculatePartInstanceExpectedDurationWithTransition(currentLivePartInstance) || 0 if ( currentLivePart.displayDurationGroup && diff --git a/meteor/client/ui/ClockView/PresenterScreen.tsx b/meteor/client/ui/ClockView/PresenterScreen.tsx index d16bd23d02..928e899177 100644 --- a/meteor/client/ui/ClockView/PresenterScreen.tsx +++ b/meteor/client/ui/ClockView/PresenterScreen.tsx @@ -32,7 +32,7 @@ import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/Sho import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts' import { ShelfDashboardLayout } from '../Shelf/ShelfDashboardLayout' import { parse as queryStringParse } from 'query-string' -import { calculatePartInstanceExpectedDurationWithPreroll } from '@sofie-automation/corelib/dist/playout/timings' +import { calculatePartInstanceExpectedDurationWithTransition } from '@sofie-automation/corelib/dist/playout/timings' import { getPlaylistTimingDiff } from '../../lib/rundownTiming' import { UIShowStyleBase } from '../../../lib/api/showStyles' import { UIShowStyleBases, UIStudios } from '../Collections' @@ -467,7 +467,7 @@ export class PresenterScreenBase extends MeteorReactComponent< showStyleBaseId={currentShowStyleBaseId} rundownIds={this.props.rundownIds} partAutoNext={currentPart.instance.part.autoNext || false} - partExpectedDuration={calculatePartInstanceExpectedDurationWithPreroll(currentPart.instance)} + partExpectedDuration={calculatePartInstanceExpectedDurationWithTransition(currentPart.instance)} partStartedPlayback={currentPart.instance.timings?.plannedStartedPlayback} playlistActivationId={this.props.playlist?.activationId} /> diff --git a/meteor/client/ui/RundownView/RundownTiming/SegmentDuration.tsx b/meteor/client/ui/RundownView/RundownTiming/SegmentDuration.tsx index f802e2ac13..1bae98c3bb 100644 --- a/meteor/client/ui/RundownView/RundownTiming/SegmentDuration.tsx +++ b/meteor/client/ui/RundownView/RundownTiming/SegmentDuration.tsx @@ -4,7 +4,7 @@ import { withTiming, WithTiming } from './withTiming' import { unprotectString } from '../../../../lib/lib' import { RundownUtils } from '../../../lib/rundown' import { PartUi } from '../../SegmentTimeline/SegmentTimelineContainer' -import { calculatePartInstanceExpectedDurationWithPreroll } from '@sofie-automation/corelib/dist/playout/timings' +import { calculatePartInstanceExpectedDurationWithTransition } from '@sofie-automation/corelib/dist/playout/timings' import { SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' interface ISegmentDurationProps { @@ -45,7 +45,7 @@ export const SegmentDuration = withTiming()(function budget += part.instance.orphaned || part.instance.part.untimed ? 0 - : calculatePartInstanceExpectedDurationWithPreroll(part.instance) || 0 + : calculatePartInstanceExpectedDurationWithTransition(part.instance) || 0 }) } props.parts.forEach((part) => { diff --git a/meteor/client/ui/SegmentTimeline/Renderers/MicSourceRenderer.tsx b/meteor/client/ui/SegmentTimeline/Renderers/MicSourceRenderer.tsx index fc9c84ad06..527f2da2df 100644 --- a/meteor/client/ui/SegmentTimeline/Renderers/MicSourceRenderer.tsx +++ b/meteor/client/ui/SegmentTimeline/Renderers/MicSourceRenderer.tsx @@ -8,7 +8,7 @@ import * as _ from 'underscore' import { getElementWidth } from '../../../utils/dimensions' import { MicFloatingInspector } from '../../FloatingInspectors/MicFloatingInspector' -import { calculatePartInstanceExpectedDurationWithPreroll } from '@sofie-automation/corelib/dist/playout/timings' +import { calculatePartInstanceExpectedDurationWithTransition } from '@sofie-automation/corelib/dist/playout/timings' import { unprotectString } from '../../../../lib/lib' import { IFloatingInspectorPosition } from '../../FloatingInspectors/IFloatingInspectorPosition' import { logger } from '../../../../lib/logging' @@ -135,8 +135,8 @@ export const MicSourceRenderer = withTranslation()( _forceSizingRecheck = true } - const expectedDuration = calculatePartInstanceExpectedDurationWithPreroll(this.props.part.instance) - const prevExpectedDuration = calculatePartInstanceExpectedDurationWithPreroll(prevProps.part.instance) + const expectedDuration = calculatePartInstanceExpectedDurationWithTransition(this.props.part.instance) + const prevExpectedDuration = calculatePartInstanceExpectedDurationWithTransition(prevProps.part.instance) if ( !_forceSizingRecheck && diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx index a8432be189..230c07bcda 100644 --- a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx +++ b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx @@ -227,7 +227,7 @@ export const BUDGET_GAP_PART = { gap: true, title: 'gap', invalid: true, - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }), pieces: [], renderedDuration: 0, diff --git a/meteor/server/__tests__/cronjobs.test.ts b/meteor/server/__tests__/cronjobs.test.ts index 90288cff7b..4d6798e60e 100644 --- a/meteor/server/__tests__/cronjobs.test.ts +++ b/meteor/server/__tests__/cronjobs.test.ts @@ -220,7 +220,7 @@ describe('cronjobs', () => { segmentId: segment0._id, externalId: '', title: '', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, } await Parts.mutableCollection.insertAsync(part0) const part1: DBPart = { @@ -230,7 +230,7 @@ describe('cronjobs', () => { segmentId: getRandomId(), // non-existent externalId: '', title: '', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, } await Parts.mutableCollection.insertAsync(part1) diff --git a/meteor/server/api/__tests__/peripheralDevice.test.ts b/meteor/server/api/__tests__/peripheralDevice.test.ts index 81d3d6dee4..d1ef86c0d1 100644 --- a/meteor/server/api/__tests__/peripheralDevice.test.ts +++ b/meteor/server/api/__tests__/peripheralDevice.test.ts @@ -134,7 +134,7 @@ describe('test peripheralDevice general API methods', () => { segmentId: segmentID, rundownId: rundownID, title: 'Part 000', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }) await Pieces.mutableCollection.insertAsync({ _id: protectString('piece0001'), @@ -161,7 +161,7 @@ describe('test peripheralDevice general API methods', () => { segmentId: segmentID, rundownId: rundownID, title: 'Part 001', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }) await Segments.mutableCollection.insertAsync({ _id: protectString('segment1'), diff --git a/meteor/server/migration/1_39_0.ts b/meteor/server/migration/1_39_0.ts index 6f754eea0d..382063cdd9 100644 --- a/meteor/server/migration/1_39_0.ts +++ b/meteor/server/migration/1_39_0.ts @@ -4,19 +4,19 @@ import { addMigrationSteps } from './databaseMigration' // Release 39 export const addSteps = addMigrationSteps('1.39.0', [ { - id: `Parts.expectedDurationWithPreroll`, + id: `Parts.expectedDurationWithTransition`, canBeRunAutomatically: true, validate: async () => { const objects = await Parts.countDocuments({ expectedDuration: { $exists: true, }, - expectedDurationWithPreroll: { + expectedDurationWithTransition: { $exists: false, }, }) if (objects > 0) { - return `timing is expectedDurationWithPreroll on ${objects} objects` + return `timing is expectedDurationWithTransition on ${objects} objects` } return false }, @@ -25,14 +25,14 @@ export const addSteps = addMigrationSteps('1.39.0', [ expectedDuration: { $exists: true, }, - expectedDurationWithPreroll: { + expectedDurationWithTransition: { $exists: false, }, }) for (const obj of objects) { await Parts.mutableCollection.updateAsync(obj._id, { $set: { - expectedDurationWithPreroll: obj.expectedDuration, + expectedDurationWithTransition: obj.expectedDuration, }, }) } diff --git a/meteor/server/migration/1_50_0.ts b/meteor/server/migration/1_50_0.ts index 6ad55eeeb2..2de2e5c44e 100644 --- a/meteor/server/migration/1_50_0.ts +++ b/meteor/server/migration/1_50_0.ts @@ -3,6 +3,8 @@ import { AdLibActions, AdLibPieces, ExpectedPackages, + PartInstances, + Parts, PeripheralDevices, Pieces, RundownPlaylists, @@ -928,6 +930,71 @@ export const addSteps = addMigrationSteps('1.50.0', [ } }, }, + + { + id: `Part rename 'expectedDurationWithPreroll' to 'expectedDurationWithTransition'`, + canBeRunAutomatically: true, + validate: async () => { + const objectCount = await Parts.countDocuments({ + expectedDurationWithTransition: { $exists: false }, + expectedDurationWithPreroll: { $exists: true }, + }) + + if (objectCount) { + return `object needs to be updated` + } + return false + }, + migrate: async () => { + const objects = await Parts.findFetchAsync({ + expectedDurationWithTransition: { $exists: false }, + expectedDurationWithPreroll: { $exists: true }, + }) + + for (const part of objects) { + await Parts.mutableCollection.updateAsync(part._id, { + $set: { + expectedDurationWithTransition: (part as any).expectedDurationWithPreroll, + }, + $unset: { + expectedDurationWithPreroll: 1, + }, + }) + } + }, + }, + { + id: `PartInstance rename 'part.expectedDurationWithPreroll' to 'part.expectedDurationWithTransition'`, + canBeRunAutomatically: true, + validate: async () => { + const objectCount = await PartInstances.countDocuments({ + 'part.expectedDurationWithTransition': { $exists: false }, + 'part.expectedDurationWithPreroll': { $exists: true }, + }) + + if (objectCount) { + return `object needs to be updated` + } + return false + }, + migrate: async () => { + const objects = await PartInstances.findFetchAsync({ + 'part.expectedDurationWithTransition': { $exists: false }, + 'part.expectedDurationWithPreroll': { $exists: true }, + }) + + for (const partInstance of objects) { + await PartInstances.mutableCollection.updateAsync(partInstance._id, { + $set: { + 'part.expectedDurationWithTransition': (partInstance.part as any).expectedDurationWithPreroll, + }, + $unset: { + 'part.expectedDurationWithPreroll': 1, + }, + }) + } + }, + }, ]) function updatePlayoutDeviceTypeInOverride(op: SomeObjectOverrideOp): ObjectOverrideSetOp | undefined { diff --git a/packages/corelib/src/dataModel/Part.ts b/packages/corelib/src/dataModel/Part.ts index 62c722a7dc..e67642fba1 100644 --- a/packages/corelib/src/dataModel/Part.ts +++ b/packages/corelib/src/dataModel/Part.ts @@ -33,8 +33,8 @@ export interface DBPart extends ProtectedStringProperties // pieces: CalculateTimingsPiece[] ): number | undefined { if (partInstance.part.expectedDuration === undefined) return undefined if (partInstance.partPlayoutTimings) { - return calculateExpectedDurationWithPreroll(partInstance.part.expectedDuration, partInstance.partPlayoutTimings) - } else { - const timings = calculatePartTimings(undefined, {}, [], partInstance.part, []) // TODO: Piece timings should be taken into account - - return calculateExpectedDurationWithPreroll( - partInstance.part.expectedDurationWithPreroll ?? partInstance.part.expectedDuration, - timings + // The timings needed are known, we can ensure that live data is used + return calculateExpectedDurationWithTransition( + partInstance.part.expectedDuration, + partInstance.partPlayoutTimings ) + } else { + return partInstance.part.expectedDurationWithTransition } } diff --git a/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts b/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts index 6931adad17..bad4e09c26 100644 --- a/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts +++ b/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts @@ -135,7 +135,7 @@ export function defaultPart(_id: PartId, rundownId: RundownId, segmentId: Segmen _rank: 0, externalId: unprotectString(_id), title: 'Default Part', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, } } export function defaultPiece(_id: PieceId, rundownId: RundownId, segmentId: SegmentId, partId: PartId): Piece { diff --git a/packages/job-worker/src/__mocks__/presetCollections.ts b/packages/job-worker/src/__mocks__/presetCollections.ts index b8e1aeb881..3f76d2a0e2 100644 --- a/packages/job-worker/src/__mocks__/presetCollections.ts +++ b/packages/job-worker/src/__mocks__/presetCollections.ts @@ -232,7 +232,7 @@ export async function setupDefaultRundown( _rank: 0, externalId: 'MOCK_PART_0_0', title: 'Part 0 0', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, } await context.mockCollections.Parts.insertOne(part00) @@ -300,7 +300,7 @@ export async function setupDefaultRundown( _rank: 1, externalId: 'MOCK_PART_0_1', title: 'Part 0 1', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, } await context.mockCollections.Parts.insertOne(part01) @@ -340,7 +340,7 @@ export async function setupDefaultRundown( _rank: 0, externalId: 'MOCK_PART_1_0', title: 'Part 1 0', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, } await context.mockCollections.Parts.insertOne(part10) @@ -351,7 +351,7 @@ export async function setupDefaultRundown( _rank: 1, externalId: 'MOCK_PART_1_1', title: 'Part 1 1', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, } await context.mockCollections.Parts.insertOne(part11) @@ -362,7 +362,7 @@ export async function setupDefaultRundown( _rank: 2, externalId: 'MOCK_PART_1_2', title: 'Part 1 2', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, } await context.mockCollections.Parts.insertOne(part12) diff --git a/packages/job-worker/src/__tests__/rundown-updatePartInstanceRanks.test.ts b/packages/job-worker/src/__tests__/rundown-updatePartInstanceRanks.test.ts index 5064c6a68c..b42a40b68d 100644 --- a/packages/job-worker/src/__tests__/rundown-updatePartInstanceRanks.test.ts +++ b/packages/job-worker/src/__tests__/rundown-updatePartInstanceRanks.test.ts @@ -53,7 +53,7 @@ describe('updatePartInstanceRanks', () => { segmentId, externalId: id, title: id, - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }) } @@ -264,7 +264,7 @@ describe('updatePartInstanceRanks', () => { segmentId, externalId: adlibId, title: adlibId, - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }, 'adlib-part' ) @@ -376,7 +376,7 @@ describe('updatePartInstanceRanks', () => { segmentId, externalId: adlibId, title: adlibId, - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }, 'adlib-part' ) @@ -419,7 +419,7 @@ describe('updatePartInstanceRanks', () => { segmentId, externalId: adlibId0, title: adlibId0, - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }, 'deleted' ) @@ -432,7 +432,7 @@ describe('updatePartInstanceRanks', () => { segmentId, externalId: adlibId1, title: adlibId1, - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }, 'adlib-part' ) diff --git a/packages/job-worker/src/blueprints/context/adlibActions.ts b/packages/job-worker/src/blueprints/context/adlibActions.ts index ae35a16786..8c33c5f9a3 100644 --- a/packages/job-worker/src/blueprints/context/adlibActions.ts +++ b/packages/job-worker/src/blueprints/context/adlibActions.ts @@ -464,7 +464,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct invalid: false, invalidReason: undefined, floated: false, - expectedDurationWithPreroll: undefined, // Filled in later + expectedDurationWithTransition: undefined, // Filled in later }, } diff --git a/packages/job-worker/src/cache/__tests__/DatabaseCaches.test.ts b/packages/job-worker/src/cache/__tests__/DatabaseCaches.test.ts index 26bd7389d1..71df9fd10d 100644 --- a/packages/job-worker/src/cache/__tests__/DatabaseCaches.test.ts +++ b/packages/job-worker/src/cache/__tests__/DatabaseCaches.test.ts @@ -33,7 +33,7 @@ describe('DatabaseCaches', () => { rundownId: protectString(''), segmentId: protectString(''), externalId: 'test', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }) cache.Parts.updateOne(id, (p) => { p.title = 'Test2' @@ -145,7 +145,7 @@ describe('DatabaseCaches', () => { rundownId: protectString(''), segmentId: protectString(''), externalId: 'test', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }) const deferFcn0 = jest.fn(async () => { await expect(context.directCollections.Parts.findOne(id)).resolves.toBeFalsy() @@ -277,7 +277,7 @@ describe('DatabaseCaches', () => { rundownId: protectString(''), segmentId: protectString(''), externalId: 'test', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }) cache.Parts.updateOne(id, (p) => { p.title = 'insertthenupdate' diff --git a/packages/job-worker/src/ingest/__tests__/ingest.test.ts b/packages/job-worker/src/ingest/__tests__/ingest.test.ts index d46de0e0f1..7011bbbc12 100644 --- a/packages/job-worker/src/ingest/__tests__/ingest.test.ts +++ b/packages/job-worker/src/ingest/__tests__/ingest.test.ts @@ -2028,7 +2028,7 @@ describe('Test ingest actions for rundowns and segments', () => { segmentId: currentPartInstance.segmentId, externalId: `${partInstanceId}_externalId`, title: 'New part', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }, } diff --git a/packages/job-worker/src/ingest/__tests__/updateNext.test.ts b/packages/job-worker/src/ingest/__tests__/updateNext.test.ts index 300eb75457..756897a4e4 100644 --- a/packages/job-worker/src/ingest/__tests__/updateNext.test.ts +++ b/packages/job-worker/src/ingest/__tests__/updateNext.test.ts @@ -114,7 +114,7 @@ async function createMockRO(context: MockJobContext): Promise { segmentId: protectString('mock_segment1'), externalId: 'p1', title: 'Part 1', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }), }), literal({ @@ -132,7 +132,7 @@ async function createMockRO(context: MockJobContext): Promise { segmentId: protectString('mock_segment1'), externalId: 'p2', title: 'Part 2', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }), }), literal({ @@ -150,7 +150,7 @@ async function createMockRO(context: MockJobContext): Promise { segmentId: protectString('mock_segment1'), externalId: 'p3', title: 'Part 3', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }), }), // Segment 2 @@ -169,7 +169,7 @@ async function createMockRO(context: MockJobContext): Promise { segmentId: protectString('mock_segment2'), externalId: 'p4', title: 'Part 4', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }), }), literal({ @@ -187,7 +187,7 @@ async function createMockRO(context: MockJobContext): Promise { segmentId: protectString('mock_segment2'), externalId: 'p5', title: 'Part 5', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }), }), // Segment 3 @@ -206,7 +206,7 @@ async function createMockRO(context: MockJobContext): Promise { segmentId: protectString('mock_segment3'), externalId: 'p6', title: 'Part 6', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }), }), // Segment 4 @@ -225,7 +225,7 @@ async function createMockRO(context: MockJobContext): Promise { segmentId: protectString('mock_segment4'), externalId: 'p7', title: 'Part 7', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }), }), literal({ @@ -244,7 +244,7 @@ async function createMockRO(context: MockJobContext): Promise { externalId: 'p8', title: 'Part 8', floated: true, - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }), }), literal({ @@ -262,7 +262,7 @@ async function createMockRO(context: MockJobContext): Promise { segmentId: protectString('mock_segment4'), externalId: 'p9', title: 'Part 9', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }), }), @@ -281,7 +281,7 @@ async function createMockRO(context: MockJobContext): Promise { segmentId: protectString('mock_segment4'), externalId: 'o1', title: 'Orphan 1', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }), orphaned: 'adlib-part', }), @@ -482,7 +482,7 @@ describe('ensureNextPartIsValid', () => { segmentId: protectString('mock_segment1'), externalId: 'o1', title: 'Orphan 1', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }), orphaned: 'deleted', }) @@ -519,7 +519,7 @@ describe('ensureNextPartIsValid', () => { segmentId: protectString('mock_segment1'), externalId: 'o1', title: 'Orphan 1', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }), orphaned: 'deleted', }) @@ -547,7 +547,7 @@ describe('ensureNextPartIsValid', () => { segmentId: protectString('mock_segment1'), externalId: 'o1', title: 'Orphan 1', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }) await context.mockCollections.PartInstances.insertOne( literal({ @@ -582,7 +582,7 @@ describe('ensureNextPartIsValid', () => { segmentId: protectString('mock_segment4'), externalId: 'tmp1', title: 'Tmp Part 1', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }) await context.mockCollections.PartInstances.insertOne( literal({ diff --git a/packages/job-worker/src/ingest/generationSegment.ts b/packages/job-worker/src/ingest/generationSegment.ts index 6520d88800..67e49a40ef 100644 --- a/packages/job-worker/src/ingest/generationSegment.ts +++ b/packages/job-worker/src/ingest/generationSegment.ts @@ -18,7 +18,7 @@ import { getSegmentId, getPartId, getRundown, canSegmentBeUpdated } from './lib' import { JobContext } from '../jobs' import { CommitIngestData } from './lock' import { BlueprintResultSegment, NoteSeverity } from '@sofie-automation/blueprints-integration' -import { calculatePartExpectedDurationWithPreroll } from '@sofie-automation/corelib/dist/playout/timings' +import { calculatePartExpectedDurationWithTransition } from '@sofie-automation/corelib/dist/playout/timings' import { wrapTranslatableMessageFromBlueprints } from '@sofie-automation/corelib/dist/TranslatableMessage' import { ReadOnlyCache } from '../cache/CacheBase' @@ -216,7 +216,7 @@ export async function calculateSegmentsFromIngestData( } : undefined, - expectedDurationWithPreroll: undefined, // Below + expectedDurationWithTransition: undefined, // Below }) res.parts.push(part) @@ -250,7 +250,7 @@ export async function calculateSegmentsFromIngestData( ) ) - part.expectedDurationWithPreroll = calculatePartExpectedDurationWithPreroll(part, processedPieces) + part.expectedDurationWithTransition = calculatePartExpectedDurationWithTransition(part, processedPieces) }) preserveOrphanedSegmentPositionInRundown(context, cache, newSegment) diff --git a/packages/job-worker/src/ingest/syncChangesToPartInstance.ts b/packages/job-worker/src/ingest/syncChangesToPartInstance.ts index 23dd0a62b6..a60e150915 100644 --- a/packages/job-worker/src/ingest/syncChangesToPartInstance.ts +++ b/packages/job-worker/src/ingest/syncChangesToPartInstance.ts @@ -15,7 +15,7 @@ import { getPieceInstancesForPart, syncPlayheadInfinitesForNextPartInstance, } from '../playout/infinites' -import { isTooCloseToAutonext, updateExpectedDurationWithPrerollForPartInstance } from '../playout/lib' +import { isTooCloseToAutonext, updateExpectedDurationWithTransitionForPartInstance } from '../playout/lib' import _ = require('underscore') import { SyncIngestUpdateToPartInstanceContext } from '../blueprints/context' import { @@ -185,7 +185,7 @@ export async function syncChangesToPartInstances( } if (playStatus === 'next') { - updateExpectedDurationWithPrerollForPartInstance(cache, existingPartInstance._id) + updateExpectedDurationWithTransitionForPartInstance(cache, existingPartInstance._id) } // Save notes: diff --git a/packages/job-worker/src/playout/__tests__/helpers/rundowns.ts b/packages/job-worker/src/playout/__tests__/helpers/rundowns.ts index a67c06110b..e296006033 100644 --- a/packages/job-worker/src/playout/__tests__/helpers/rundowns.ts +++ b/packages/job-worker/src/playout/__tests__/helpers/rundowns.ts @@ -69,7 +69,7 @@ export async function setupRundownBase( _rank: 0, externalId: 'MOCK_PART_0_0', title: 'Part 0 0', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, ...partPropsOverride, } @@ -139,7 +139,7 @@ export async function setupPart2( _rank: 1, externalId: 'MOCK_PART_0_1', title: 'Part 0 1', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, ...partPropsOverride, } diff --git a/packages/job-worker/src/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index 6b474aaf6d..c7943fba18 100644 --- a/packages/job-worker/src/playout/adlibAction.ts +++ b/packages/job-worker/src/playout/adlibAction.ts @@ -12,7 +12,7 @@ import { getCurrentTime } from '../lib' import { ReadonlyDeep } from 'type-fest' import { CacheForPlayoutPreInit, CacheForPlayout } from './cache' import { syncPlayheadInfinitesForNextPartInstance } from './infinites' -import { updateExpectedDurationWithPrerollForPartInstance } from './lib' +import { updateExpectedDurationWithTransitionForPartInstance } from './lib' import { runJobWithPlaylistLock } from './lock' import { updateTimeline } from './timeline/generate' import { performTakeToNextedPart } from './take' @@ -192,7 +192,7 @@ async function applyAnyExecutionSideEffects( if (actionContext.nextPartState !== ActionPartChange.NONE) { const nextPartInstanceId = cache.Playlist.doc.nextPartInfo?.partInstanceId if (nextPartInstanceId) { - updateExpectedDurationWithPrerollForPartInstance(cache, nextPartInstanceId) + updateExpectedDurationWithTransitionForPartInstance(cache, nextPartInstanceId) } } diff --git a/packages/job-worker/src/playout/adlibUtils.ts b/packages/job-worker/src/playout/adlibUtils.ts index f785fecc0d..00e30d7d56 100644 --- a/packages/job-worker/src/playout/adlibUtils.ts +++ b/packages/job-worker/src/playout/adlibUtils.ts @@ -7,7 +7,7 @@ import { PieceInstance, rewrapPieceToInstance } from '@sofie-automation/corelib/ import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { assertNever, getRandomId, getRank } from '@sofie-automation/corelib/dist/lib' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' -import { calculatePartExpectedDurationWithPreroll } from '@sofie-automation/corelib/dist/playout/timings' +import { calculatePartExpectedDurationWithTransition } from '@sofie-automation/corelib/dist/playout/timings' import { getCurrentTime } from '../lib' import { JobContext } from '../jobs' import { CacheForPlayout, getRundownIDsFromCache } from './cache' @@ -56,7 +56,7 @@ export async function innerStartOrQueueAdLibPiece( rundownId: rundown._id, title: adLibPiece.name, expectedDuration: adLibPiece.expectedDuration, - expectedDurationWithPreroll: adLibPiece.expectedDuration, // Filled in later + expectedDurationWithTransition: adLibPiece.expectedDuration, // Filled in later }, } const newPieceInstance = convertAdLibToPieceInstance( @@ -67,7 +67,7 @@ export async function innerStartOrQueueAdLibPiece( queue ) - newPartInstance.part.expectedDurationWithPreroll = calculatePartExpectedDurationWithPreroll( + newPartInstance.part.expectedDurationWithTransition = calculatePartExpectedDurationWithTransition( newPartInstance.part, [newPieceInstance.piece] ) diff --git a/packages/job-worker/src/playout/lib.ts b/packages/job-worker/src/playout/lib.ts index 8f6aef5c1f..69eb1ad8b7 100644 --- a/packages/job-worker/src/playout/lib.ts +++ b/packages/job-worker/src/playout/lib.ts @@ -9,7 +9,7 @@ import { ReadonlyDeep } from 'type-fest' import { CacheForPlayout, getOrderedSegmentsAndPartsFromPlayoutCache, getRundownIDsFromCache } from './cache' import { logger } from '../logging' import { getCurrentTime } from '../lib' -import { calculatePartExpectedDurationWithPreroll } from '@sofie-automation/corelib/dist/playout/timings' +import { calculatePartExpectedDurationWithTransition } from '@sofie-automation/corelib/dist/playout/timings' import { MongoQuery } from '../db' import { mongoWhere } from '@sofie-automation/corelib/dist/mongo' import _ = require('underscore') @@ -305,10 +305,10 @@ export function isTooCloseToAutonext( } /** - * Update the expectedDurationWithPreroll on the specified PartInstance. + * Update the expectedDurationWithTransition on the specified PartInstance. * The value is used by the UI to approximate the duration of a PartInstance as it will be played out */ -export function updateExpectedDurationWithPrerollForPartInstance( +export function updateExpectedDurationWithTransitionForPartInstance( cache: CacheForPlayout, partInstanceId: PartInstanceId ): void { @@ -316,14 +316,14 @@ export function updateExpectedDurationWithPrerollForPartInstance( if (nextPartInstance) { const pieceInstances = cache.PieceInstances.findAll((p) => p.partInstanceId === nextPartInstance._id) - // Update expectedDurationWithPreroll of the next part instance, as it may have changed and is used by the ui until it is taken - const expectedDurationWithPreroll = calculatePartExpectedDurationWithPreroll( + // Update expectedDurationWithTransition of the next part instance, as it may have changed and is used by the ui until it is taken + const expectedDurationWithTransition = calculatePartExpectedDurationWithTransition( nextPartInstance.part, pieceInstances.map((p) => p.piece) ) cache.PartInstances.updateOne(nextPartInstance._id, (doc) => { - doc.part.expectedDurationWithPreroll = expectedDurationWithPreroll + doc.part.expectedDurationWithTransition = expectedDurationWithTransition return doc }) } diff --git a/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts b/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts index d03c78b4ce..740122012e 100644 --- a/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts +++ b/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts @@ -97,7 +97,7 @@ describe('Lookahead', () => { _rank: index, externalId: 'MOCK_PART_' + index, title: 'Part ' + index, - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, } } diff --git a/packages/job-worker/src/playout/lookahead/__tests__/util.test.ts b/packages/job-worker/src/playout/lookahead/__tests__/util.test.ts index e63e1b5173..1702eae1cd 100644 --- a/packages/job-worker/src/playout/lookahead/__tests__/util.test.ts +++ b/packages/job-worker/src/playout/lookahead/__tests__/util.test.ts @@ -115,7 +115,7 @@ describe('getOrderedPartsAfterPlayhead', () => { _rank: index, externalId: 'MOCK_PART_' + index, title: 'Part ' + index, - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, } } diff --git a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts index 09f9220fe6..51eded94fd 100644 --- a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts @@ -77,7 +77,7 @@ describe('ActivePlaylistTopic', () => { _id: protectString('PART_1'), title: 'Test Part', segmentId: protectString('SEGMENT_1'), - expectedDurationWithPreroll: 10000, + expectedDurationWithTransition: 10000, expectedDuration: 10000, metaData: { b: 'c' }, // tmp, publicData was introduced in R51 } diff --git a/packages/live-status-gateway/src/topics/__tests__/segmentsTopic.spec.ts b/packages/live-status-gateway/src/topics/__tests__/segmentsTopic.spec.ts index 05d7a1649f..9fe1029f8b 100644 --- a/packages/live-status-gateway/src/topics/__tests__/segmentsTopic.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/segmentsTopic.spec.ts @@ -36,7 +36,7 @@ function makeTestPart( _rank: rank, rundownId: protectString(rundownId), segmentId: protectString(segmentId), - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, ...partProps, } } @@ -354,25 +354,25 @@ describe('SegmentsTopic', () => { mockSubscriber.send.mockClear() await topic.update(PartsHandler.name, [ makeTestPart('1_2_1', 1, RUNDOWN_1_ID, segment_1_2_id, { - expectedDurationWithPreroll: 10000, + expectedDurationWithTransition: 10000, }), makeTestPart('2_2_1', 1, RUNDOWN_1_ID, segment_2_2_id, { - expectedDurationWithPreroll: 40000, + expectedDurationWithTransition: 40000, }), makeTestPart('1_2_2', 2, RUNDOWN_1_ID, segment_1_2_id, { - expectedDurationWithPreroll: 5000, + expectedDurationWithTransition: 5000, }), makeTestPart('1_1_2', 2, RUNDOWN_1_ID, segment_1_1_id, { - expectedDurationWithPreroll: 1000, + expectedDurationWithTransition: 1000, }), makeTestPart('1_1_1', 1, RUNDOWN_1_ID, segment_1_1_id, { - expectedDurationWithPreroll: 3000, + expectedDurationWithTransition: 3000, }), makeTestPart('2_2_2', 2, RUNDOWN_1_ID, segment_2_2_id, { - expectedDurationWithPreroll: 11000, + expectedDurationWithTransition: 11000, }), makeTestPart('1_1_2', 2, RUNDOWN_1_ID, segment_1_1_id, { - expectedDurationWithPreroll: 1000, + expectedDurationWithTransition: 1000, }), ]) jest.advanceTimersByTime(THROTTLE_PERIOD_MS) diff --git a/packages/live-status-gateway/src/topics/helpers/partTiming.ts b/packages/live-status-gateway/src/topics/helpers/partTiming.ts index 6ef75c355e..949ba89bfb 100644 --- a/packages/live-status-gateway/src/topics/helpers/partTiming.ts +++ b/packages/live-status-gateway/src/topics/helpers/partTiming.ts @@ -21,7 +21,7 @@ export function calculateCurrentPartTiming( (partInstance) => partInstance.part.displayDurationGroup === currentPartInstance.part.displayDurationGroup ) const groupDuration = displayDurationGroup.reduce((sum, partInstance) => { - return sum + (partInstance.part.expectedDurationWithPreroll ?? 0) + return sum + (partInstance.part.expectedDurationWithTransition ?? 0) }, 0) const groupPlayed = displayDurationGroup.reduce((sum, partInstance) => { return (partInstance.timings?.duration ?? 0) + sum diff --git a/packages/live-status-gateway/src/topics/helpers/segmentTiming.ts b/packages/live-status-gateway/src/topics/helpers/segmentTiming.ts index 966354e6d9..bf6c58c0eb 100644 --- a/packages/live-status-gateway/src/topics/helpers/segmentTiming.ts +++ b/packages/live-status-gateway/src/topics/helpers/segmentTiming.ts @@ -42,8 +42,8 @@ export function calculateSegmentTiming(segmentParts: DBPart[]): SegmentTiming { return part.budgetDuration != null && !part.untimed ? (sum ?? 0) + part.budgetDuration : sum }, undefined), expectedDurationMs: segmentParts.reduce((sum, part): number => { - return part.expectedDurationWithPreroll != null && !part.untimed - ? sum + part.expectedDurationWithPreroll + return part.expectedDurationWithTransition != null && !part.untimed + ? sum + part.expectedDurationWithTransition : sum }, 0), } From 76cdaa12dd03c7a7b59252677e68391db6ae4559 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 1 Aug 2024 14:07:04 +0100 Subject: [PATCH 400/479] chore: fix missed property --- packages/live-status-gateway/src/topics/__tests__/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/live-status-gateway/src/topics/__tests__/utils.ts b/packages/live-status-gateway/src/topics/__tests__/utils.ts index 45dedd21fd..d85ad1b165 100644 --- a/packages/live-status-gateway/src/topics/__tests__/utils.ts +++ b/packages/live-status-gateway/src/topics/__tests__/utils.ts @@ -53,7 +53,7 @@ export function makeTestParts(): DBPart[] { segmentId: protectString('segment0'), notes: [], externalId: 'NCS_PART_0', - expectedDurationWithPreroll: 1000, + expectedDurationWithTransition: 1000, title: 'Part 0', }, ] From 195a7d98242d6377dac9f1efc69f861fc3b554e5 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 2 Aug 2024 09:41:34 +0100 Subject: [PATCH 401/479] fix: compensate for piece preroll for adlibbed pieces SOFIE-3369 (#1236) --- packages/corelib/src/playout/processAndPrune.ts | 16 ++++++++++++---- .../src/playout/__tests__/timeline.test.ts | 6 ++++-- .../job-worker/src/playout/timeline/piece.ts | 12 +++++++++--- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/packages/corelib/src/playout/processAndPrune.ts b/packages/corelib/src/playout/processAndPrune.ts index 8fc4d73f90..1b85bde41e 100644 --- a/packages/corelib/src/playout/processAndPrune.ts +++ b/packages/corelib/src/playout/processAndPrune.ts @@ -10,10 +10,18 @@ import { getPieceControlObjectId } from './ids' /** * Get the `enable: { start: ?? }` for the new piece in terms that can be used as an `end` for another object */ -function getPieceStartTime(newPieceStart: number | 'now', newPiece: PieceInstance): number | string { +function getPieceStartTimeAsReference(newPieceStart: number | 'now', newPiece: PieceInstance): number | string { return typeof newPieceStart === 'number' ? newPieceStart : `#${getPieceControlObjectId(newPiece)}.start` } +function getPieceStartTimeWithinPart(p: PieceInstance): 'now' | number { + if (p.piece.enable.start === 'now' || !p.dynamicallyInserted) { + return p.piece.enable.start + } else { + return p.piece.enable.start + (p.piece.prerollDuration ?? 0) + } +} + function isClear(piece?: PieceInstance): boolean { return !!piece?.piece.virtual } @@ -85,7 +93,7 @@ export function processAndPrunePieceInstanceTimings( for (const pieces of groupedPieces.values()) { // Group and sort the pieces so that we can step through each point in time const piecesByStart: Array<[number | 'now', PieceInstance[]]> = _.sortBy( - Array.from(groupByToMapFunc(pieces, (p) => p.piece.enable.start).entries()).map(([k, v]) => + Array.from(groupByToMapFunc(pieces, (p) => getPieceStartTimeWithinPart(p)).entries()).map(([k, v]) => literal<[number | 'now', PieceInstance[]]>([k === 'now' ? 'now' : Number(k), v]) ), ([k]) => (k === 'now' ? nowInPart : k) @@ -120,7 +128,7 @@ function updateWithNewPieces( if (newPiece) { const activePiece = activePieces[key] if (activePiece) { - activePiece.resolvedEndCap = getPieceStartTime(newPiecesStart, newPiece) + activePiece.resolvedEndCap = getPieceStartTimeAsReference(newPiecesStart, newPiece) } // track the new piece activePieces[key] = newPiece @@ -145,7 +153,7 @@ function updateWithNewPieces( (newPiecesStart !== 0 || isCandidateBetterToBeContinued(activePieces.other, newPiece)) ) { // These modes should stop the 'other' when they start if not hidden behind a higher priority onEnd - activePieces.other.resolvedEndCap = getPieceStartTime(newPiecesStart, newPiece) + activePieces.other.resolvedEndCap = getPieceStartTimeAsReference(newPiecesStart, newPiece) activePieces.other = undefined } } diff --git a/packages/job-worker/src/playout/__tests__/timeline.test.ts b/packages/job-worker/src/playout/__tests__/timeline.test.ts index 5c939920b3..f44dd90fbb 100644 --- a/packages/job-worker/src/playout/__tests__/timeline.test.ts +++ b/packages/job-worker/src/playout/__tests__/timeline.test.ts @@ -1439,6 +1439,8 @@ describe('Timeline', () => { // Simulate the piece timing confirmation from playout-gateway await doSimulatePiecePlaybackTimings(playlistId, pieceOffset, 1) + const pieceOffsetWithPreroll = pieceOffset + 340 + // Now we have a concrete time await checkTimings({ previousPart: null, @@ -1446,7 +1448,7 @@ describe('Timeline', () => { piece000: { controlObj: { start: 500, // This one gave the preroll - end: pieceOffset, + end: pieceOffsetWithPreroll, }, childGroup: { preroll: 500, @@ -1465,7 +1467,7 @@ describe('Timeline', () => { [adlibbedPieceId]: { // Our adlibbed piece controlObj: { - start: pieceOffset, + start: pieceOffsetWithPreroll, }, childGroup: { preroll: 340, diff --git a/packages/job-worker/src/playout/timeline/piece.ts b/packages/job-worker/src/playout/timeline/piece.ts index 69f5d3ad15..72c354d8e9 100644 --- a/packages/job-worker/src/playout/timeline/piece.ts +++ b/packages/job-worker/src/playout/timeline/piece.ts @@ -93,10 +93,16 @@ export function getPieceEnableInsidePart( partGroupId: string ): TSR.Timeline.TimelineEnable { const pieceEnable: TSR.Timeline.TimelineEnable = { ...pieceInstance.piece.enable } - if (typeof pieceEnable.start === 'number' && !pieceInstance.dynamicallyInserted) { - // timed pieces should be offset based on the preroll of the part - pieceEnable.start += partTimings.toPartDelay + if (typeof pieceEnable.start === 'number') { + if (pieceInstance.dynamicallyInserted) { + // timed adlibbed pieces needs to factor in their preroll + pieceEnable.start += pieceInstance.piece.prerollDuration || 0 + } else { + // timed planned pieces should be offset based on the preroll of the part + pieceEnable.start += partTimings.toPartDelay + } } + if (partTimings.toPartPostroll) { if (!pieceEnable.duration) { // make sure that the control object is shortened correctly From cf9fa159b7385614a879ee215b3093a7e708bb7d Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 5 Aug 2024 11:41:34 +0100 Subject: [PATCH 402/479] fix: refactor Rundown orphaned property SOFIE-2963 #1210 (#1217) fix: refactor Rundown orphaned property SOFIE-2963 #1210 --- meteor/server/migration/X_X_X.ts | 31 ++++++++++++++++++++- packages/corelib/src/dataModel/Rundown.ts | 12 ++++---- packages/job-worker/src/playout/snapshot.ts | 2 +- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/meteor/server/migration/X_X_X.ts b/meteor/server/migration/X_X_X.ts index 7a68db2c2c..cb88eb6813 100644 --- a/meteor/server/migration/X_X_X.ts +++ b/meteor/server/migration/X_X_X.ts @@ -1,7 +1,7 @@ import { addMigrationSteps } from './databaseMigration' import { CURRENT_SYSTEM_VERSION } from './currentSystemVersion' import { Rundowns } from '../collections' -import { RundownSource } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { RundownOrphanedReason, RundownSource } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { PeripheralDeviceId, RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' /* @@ -71,4 +71,33 @@ export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [ } }, }, + { + id: `Rundowns remove orphaned FROM_SNAPSHOT`, + canBeRunAutomatically: true, + validate: async () => { + const objects = await Rundowns.findFetchAsync({ + orphaned: 'from-snapshot' as any, + }) + + if (objects.length > 0) { + return `object needs to be updated` + } + return false + }, + migrate: async () => { + await Rundowns.mutableCollection.updateAsync( + { + orphaned: 'from-snapshot' as any, + }, + { + $set: { + orphaned: RundownOrphanedReason.DELETED, + }, + }, + { + multi: true, + } + ) + }, + }, ]) diff --git a/packages/corelib/src/dataModel/Rundown.ts b/packages/corelib/src/dataModel/Rundown.ts index c0e47ca652..1907fb8725 100644 --- a/packages/corelib/src/dataModel/Rundown.ts +++ b/packages/corelib/src/dataModel/Rundown.ts @@ -12,10 +12,8 @@ import { RundownNote } from './Notes' import { ReadonlyDeep } from 'type-fest' export enum RundownOrphanedReason { - /** Rundown is deleted from the NRCS but we still need it */ + /** Rundown is deleted from the source but we still need it */ DELETED = 'deleted', - /** Rundown was restored from a snapshot and does not correspond with a rundown in the NRCS */ - FROM_SNAPSHOT = 'from-snapshot', /** Rundown was unsynced by the user */ MANUAL = 'manual', } @@ -29,7 +27,6 @@ export interface RundownImportVersions { core: string } -/** This is a very uncomplete mock-up of the Rundown object */ export interface Rundown { _id: RundownId /** ID of the organization that owns the rundown */ @@ -53,9 +50,10 @@ export interface Rundown { /** Air-status, comes from NCS, examples: "READY" | "NOT READY" */ airStatus?: string - // There should be something like a Owner user here somewhere? - - /** Is the rundown in an unsynced (has been unpublished from ENPS) state? */ + /** + * Is the rundown in an unsynced state? + * This can be because the rundown was deleted from the source, or because the user manually unsynced it + */ orphaned?: RundownOrphanedReason /** Last sent storyStatus to ingestDevice (MOS) */ diff --git a/packages/job-worker/src/playout/snapshot.ts b/packages/job-worker/src/playout/snapshot.ts index ded023007b..9b479f0968 100644 --- a/packages/job-worker/src/playout/snapshot.ts +++ b/packages/job-worker/src/playout/snapshot.ts @@ -163,7 +163,7 @@ export async function handleRestorePlaylistSnapshot( for (const rd of snapshot.rundowns) { if (!rd.orphaned) { - rd.orphaned = RundownOrphanedReason.FROM_SNAPSHOT + rd.orphaned = RundownOrphanedReason.MANUAL } rd.playlistId = playlistId From 0fe74b45d2c09e2e08e942c49dae55ceb1ef698c Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 7 Aug 2024 13:57:59 +0100 Subject: [PATCH 403/479] fix: unexpected timeline updates while playing final part in rundown SOFIE-3371 (#1237) --- .../src/ingest/__tests__/updateNext.test.ts | 54 ++++++--- packages/job-worker/src/ingest/commit.ts | 5 +- packages/job-worker/src/ingest/updateNext.ts | 113 ++++++++++-------- 3 files changed, 104 insertions(+), 68 deletions(-) diff --git a/packages/job-worker/src/ingest/__tests__/updateNext.test.ts b/packages/job-worker/src/ingest/__tests__/updateNext.test.ts index 756897a4e4..619dd8f9cd 100644 --- a/packages/job-worker/src/ingest/__tests__/updateNext.test.ts +++ b/packages/job-worker/src/ingest/__tests__/updateNext.test.ts @@ -344,7 +344,7 @@ describe('ensureNextPartIsValid', () => { }) } async function ensureNextPartIsValid() { - await runJobWithPlayoutCache(context, { playlistId: rundownPlaylistId }, null, async (cache) => + return runJobWithPlayoutCache(context, { playlistId: rundownPlaylistId }, null, async (cache) => ensureNextPartIsValidRaw(context, cache) ) } @@ -352,7 +352,7 @@ describe('ensureNextPartIsValid', () => { test('Start with null', async () => { await resetPartIds(null, null) - await ensureNextPartIsValid() + await expect(ensureNextPartIsValid()).resolves.toBeTruthy() expect(setNextPartMock).toHaveBeenCalledTimes(1) expect(setNextPartMock).toHaveBeenCalledWith( @@ -365,7 +365,7 @@ describe('ensureNextPartIsValid', () => { test('Missing next PartInstance', async () => { await resetPartIds('mock_part_instance3', 'fake_part') - await ensureNextPartIsValid() + await expect(ensureNextPartIsValid()).resolves.toBeTruthy() expect(setNextPartMock).toHaveBeenCalledTimes(1) expect(setNextPartMock).toHaveBeenCalledWith( @@ -380,14 +380,14 @@ describe('ensureNextPartIsValid', () => { test('Missing current PartInstance with valid next', async () => { await resetPartIds('fake_part', 'mock_part_instance4') - await ensureNextPartIsValid() + await expect(ensureNextPartIsValid()).resolves.toBeFalsy() expect(setNextPartMock).toHaveBeenCalledTimes(0) }) test('Missing current and next PartInstance', async () => { await resetPartIds('fake_part', 'not_real_either') - await ensureNextPartIsValid() + await expect(ensureNextPartIsValid()).resolves.toBeTruthy() expect(setNextPartMock).toHaveBeenCalledTimes(1) expect(setNextPartMock).toHaveBeenCalledWith( @@ -400,21 +400,21 @@ describe('ensureNextPartIsValid', () => { test('Ensure correct PartInstance doesnt change', async () => { await resetPartIds('mock_part_instance3', 'mock_part_instance4') - await ensureNextPartIsValid() + await expect(ensureNextPartIsValid()).resolves.toBeFalsy() expect(setNextPartMock).not.toHaveBeenCalled() }) test('Ensure manual PartInstance doesnt change', async () => { await resetPartIds('mock_part_instance3', 'mock_part_instance5', true) - await ensureNextPartIsValid() + await expect(ensureNextPartIsValid()).resolves.toBeFalsy() expect(setNextPartMock).not.toHaveBeenCalled() }) test('Ensure non-manual PartInstance does change', async () => { await resetPartIds('mock_part_instance3', 'mock_part_instance5', false) - await ensureNextPartIsValid() + await expect(ensureNextPartIsValid()).resolves.toBeTruthy() expect(setNextPartMock).toHaveBeenCalledTimes(1) expect(setNextPartMock).toHaveBeenCalledWith( @@ -427,7 +427,7 @@ describe('ensureNextPartIsValid', () => { test('Ensure manual but missing PartInstance does change', async () => { await resetPartIds('mock_part_instance3', 'fake_part', true) - await ensureNextPartIsValid() + await expect(ensureNextPartIsValid()).resolves.toBeTruthy() expect(setNextPartMock).toHaveBeenCalledTimes(1) expect(setNextPartMock).toHaveBeenCalledWith( @@ -440,7 +440,7 @@ describe('ensureNextPartIsValid', () => { test('Ensure manual but floated PartInstance does change', async () => { await resetPartIds('mock_part_instance7', 'mock_part_instance8', true) - await ensureNextPartIsValid() + await expect(ensureNextPartIsValid()).resolves.toBeTruthy() expect(setNextPartMock).toHaveBeenCalledTimes(1) expect(setNextPartMock).toHaveBeenCalledWith( @@ -453,7 +453,7 @@ describe('ensureNextPartIsValid', () => { test('Ensure floated PartInstance does change', async () => { await resetPartIds('mock_part_instance7', 'mock_part_instance8', false) - await ensureNextPartIsValid() + await expect(ensureNextPartIsValid()).resolves.toBeTruthy() expect(setNextPartMock).toHaveBeenCalledTimes(1) expect(setNextPartMock).toHaveBeenCalledWith( @@ -490,7 +490,7 @@ describe('ensureNextPartIsValid', () => { await resetPartIds(null, instanceId, false) - await ensureNextPartIsValid() + await expect(ensureNextPartIsValid()).resolves.toBeTruthy() expect(setNextPartMock).toHaveBeenCalledTimes(1) expect(setNextPartMock).toHaveBeenCalledWith( @@ -527,7 +527,7 @@ describe('ensureNextPartIsValid', () => { await resetPartIds(null, instanceId, true) - await ensureNextPartIsValid() + await expect(ensureNextPartIsValid()).resolves.toBeTruthy() expect(setNextPartMock).toHaveBeenCalledTimes(1) expect(setNextPartMock).toHaveBeenCalledWith( @@ -568,7 +568,7 @@ describe('ensureNextPartIsValid', () => { await resetPartIds('mock_part_instance1', instanceId, false) - await ensureNextPartIsValid() + await expect(ensureNextPartIsValid()).resolves.toBeFalsy() expect(setNextPartMock).toHaveBeenCalledTimes(0) }) @@ -601,7 +601,7 @@ describe('ensureNextPartIsValid', () => { try { // make sure it finds the part we expect await resetPartIds('mock_part_instance9', null, false) - await ensureNextPartIsValid() + await expect(ensureNextPartIsValid()).resolves.toBeTruthy() expect(setNextPartMock).toHaveBeenCalledTimes(1) expect(setNextPartMock).toHaveBeenCalledWith( @@ -619,7 +619,7 @@ describe('ensureNextPartIsValid', () => { await context.mockCollections.Parts.remove(part._id) // make sure the next part gets cleared - await ensureNextPartIsValid() + await expect(ensureNextPartIsValid()).resolves.toBeTruthy() expect(setNextPartMock).toHaveBeenCalledTimes(1) expect(setNextPartMock).toHaveBeenCalledWith( @@ -634,4 +634,26 @@ describe('ensureNextPartIsValid', () => { await context.mockCollections.Parts.remove(part._id) } }) + + test('Current part is last in rundown, next is missing', async () => { + await resetPartIds('mock_part_instance9', 'fake_part_instance', false) + + await expect(ensureNextPartIsValid()).resolves.toBeTruthy() + + expect(setNextPartMock).toHaveBeenCalledTimes(1) + expect(setNextPartMock).toHaveBeenCalledWith( + expect.objectContaining({}), + expect.objectContaining({ PlaylistId: rundownPlaylistId }), + null, + false + ) + }) + + test('Current part is last in rundown, no-op to update', async () => { + await resetPartIds('mock_part_instance9', null, false) + + await expect(ensureNextPartIsValid()).resolves.toBeFalsy() + + expect(setNextPartMock).toHaveBeenCalledTimes(0) + }) }) diff --git a/packages/job-worker/src/ingest/commit.ts b/packages/job-worker/src/ingest/commit.ts index fae4a9c90b..884ad7c048 100644 --- a/packages/job-worker/src/ingest/commit.ts +++ b/packages/job-worker/src/ingest/commit.ts @@ -242,6 +242,7 @@ export async function CommitIngestOperation( await pSaveIngest // do some final playout checks, which may load back some Parts data + // Note: This should trigger a timeline update, one is already queued in the `deferAfterSave` above await ensureNextPartIsValid(context, playoutCache) // save the final playout changes @@ -511,9 +512,9 @@ export async function updatePlayoutAfterChangingRundownInPlaylist( ) } - await ensureNextPartIsValid(context, playoutCache) + const shouldUpdateTimeline = await ensureNextPartIsValid(context, playoutCache) - if (playoutCache.Playlist.doc.activationId) { + if (playoutCache.Playlist.doc.activationId || shouldUpdateTimeline) { triggerUpdateTimelineAfterIngestData(context, playoutCache.PlaylistId) } }) diff --git a/packages/job-worker/src/ingest/updateNext.ts b/packages/job-worker/src/ingest/updateNext.ts index 435ee314f5..d5fd8bb8bc 100644 --- a/packages/job-worker/src/ingest/updateNext.ts +++ b/packages/job-worker/src/ingest/updateNext.ts @@ -8,78 +8,91 @@ import { import { JobContext } from '../jobs' import { setNextPart } from '../playout/setNext' import { isPartPlayable } from '@sofie-automation/corelib/dist/dataModel/Part' -import { updateTimeline } from '../playout/timeline/generate' /** * Make sure that the nextPartInstance for the current Playlist is still correct * This will often change the nextPartInstance * @param context Context of the job being run * @param cache Playout Cache to operate on + * @returns Whether the timeline should be updated following this operation */ -export async function ensureNextPartIsValid(context: JobContext, cache: CacheForPlayout): Promise { +export async function ensureNextPartIsValid(context: JobContext, cache: CacheForPlayout): Promise { const span = context.startSpan('api.ingest.ensureNextPartIsValid') // Ensure the next-id is still valid const playlist = cache.Playlist.doc - if (playlist?.activationId) { - const { currentPartInstance, nextPartInstance } = getSelectedPartInstancesFromCache(cache) + if (!playlist?.activationId) { + span?.end() + return false + } + + const { currentPartInstance, nextPartInstance } = getSelectedPartInstancesFromCache(cache) + + if ( + playlist.nextPartInfo?.manuallySelected && + nextPartInstance?.part && + isPartPlayable(nextPartInstance.part) && + nextPartInstance.orphaned !== 'deleted' + ) { + // Manual next part is almost always valid. This includes orphaned (adlib-part) partinstances + span?.end() + return false + } + + // If we are close to an autonext, then leave it to avoid glitches + if (isTooCloseToAutonext(currentPartInstance) && nextPartInstance) { + span?.end() + return false + } + + const allPartsAndSegments = getOrderedSegmentsAndPartsFromPlayoutCache(cache) + + if (currentPartInstance && nextPartInstance) { + // Check if the part is the same + const newNextPart = selectNextPart( + context, + playlist, + currentPartInstance, + nextPartInstance, + allPartsAndSegments + ) if ( - playlist.nextPartInfo?.manuallySelected && - nextPartInstance?.part && - isPartPlayable(nextPartInstance.part) && - nextPartInstance.orphaned !== 'deleted' + // Nothing should be nexted + !newNextPart || + // The nexted-part should be different to what is selected + newNextPart.part._id !== nextPartInstance.part._id || + // The nexted-part Instance is no longer playable + !isPartPlayable(nextPartInstance.part) ) { - // Manual next part is almost always valid. This includes orphaned (adlib-part) partinstances + // The 'new' next part is before the current next, so move the next point + await setNextPart(context, cache, newNextPart ?? null, false) + span?.end() - return + return true } + } else if (!nextPartInstance || nextPartInstance.orphaned === 'deleted') { + // Don't have a nextPart or it has been deleted, so autoselect something + const newNextPart = selectNextPart( + context, + playlist, + currentPartInstance ?? null, + nextPartInstance ?? null, + allPartsAndSegments + ) - // If we are close to an autonext, then leave it to avoid glitches - if (isTooCloseToAutonext(currentPartInstance) && nextPartInstance) { + if (!newNextPart && !cache.Playlist.doc?.nextPartInfo) { + // No currently nexted part, and nothing was selected, so nothing to update span?.end() - return + return false } - const allPartsAndSegments = getOrderedSegmentsAndPartsFromPlayoutCache(cache) - - if (currentPartInstance && nextPartInstance) { - // Check if the part is the same - const newNextPart = selectNextPart( - context, - playlist, - currentPartInstance, - nextPartInstance, - allPartsAndSegments - ) - - if ( - // Nothing should be nexted - !newNextPart || - // The nexted-part should be different to what is selected - newNextPart.part._id !== nextPartInstance.part._id || - // The nexted-part Instance is no longer playable - !isPartPlayable(nextPartInstance.part) - ) { - // The 'new' next part is before the current next, so move the next point - await setNextPart(context, cache, newNextPart ?? null, false) + await setNextPart(context, cache, newNextPart ?? null, false) - await updateTimeline(context, cache) - } - } else if (!nextPartInstance || nextPartInstance.orphaned === 'deleted') { - // Don't have a nextPart or it has been deleted, so autoselect something - const newNextPart = selectNextPart( - context, - playlist, - currentPartInstance ?? null, - nextPartInstance ?? null, - allPartsAndSegments - ) - await setNextPart(context, cache, newNextPart ?? null, false) - - await updateTimeline(context, cache) - } + span?.end() + return true } span?.end() + return false } From bf48f171719452eb6e309964b3779bb8ad2992e9 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 7 Aug 2024 13:59:03 +0100 Subject: [PATCH 404/479] fix: use same piece timing as timeline in LSG SOFIE-3305 (#1219) --- .../src/collections/pieceInstancesHandler.ts | 76 ++++++++++--------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts index 732bf79a67..12913f6bd7 100644 --- a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts +++ b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts @@ -10,7 +10,10 @@ import throttleToNextTick from '@sofie-automation/shared-lib/dist/lib/throttleTo import _ = require('underscore') import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { PartInstanceId, PieceInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { processAndPrunePieceInstanceTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' +import { + processAndPrunePieceInstanceTimings, + resolvePrunedPieceInstance, +} from '@sofie-automation/corelib/dist/playout/processAndPrune' import { ShowStyleBaseExt, ShowStyleBaseHandler } from './showStyleBaseHandler' import { PlaylistHandler } from './playlistHandler' import { SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' @@ -44,8 +47,7 @@ export class PieceInstancesHandler private _partInstances: SelectedPartInstances | undefined private _throttledUpdateAndNotify = throttleToNextTick(() => { - this.updateCollectionData() - this.notify(this._collectionData).catch(this._logger.error) + this.updateAndNotify().catch(this._logger.error) }) constructor(logger: Logger, coreHandler: CoreHandler) { @@ -72,13 +74,33 @@ export class PieceInstancesHandler private processAndPrunePieceInstanceTimings( partInstance: DBPartInstance | undefined, - pieceInstances: PieceInstance[] + pieceInstances: PieceInstance[], + filterActive: boolean ): ReadonlyDeep[] { // Approximate when 'now' is in the PartInstance, so that any adlibbed Pieces will be timed roughly correctly const partStarted = partInstance?.timings?.plannedStartedPlayback const nowInPart = partStarted === undefined ? 0 : Date.now() - partStarted - return processAndPrunePieceInstanceTimings(this._sourceLayers, pieceInstances, nowInPart, false, false) + const prunedPieceInstances = processAndPrunePieceInstanceTimings( + this._sourceLayers, + pieceInstances, + nowInPart, + false, + false + ) + if (!filterActive) return prunedPieceInstances + + return prunedPieceInstances.filter((pieceInstance) => { + const resolvedPieceInstance = resolvePrunedPieceInstance(nowInPart, pieceInstance) + + return ( + resolvedPieceInstance.resolvedStart <= nowInPart && + (resolvedPieceInstance.resolvedDuration == null || + resolvedPieceInstance.resolvedStart + resolvedPieceInstance.resolvedDuration > nowInPart) && + pieceInstance.piece.virtual !== true && + pieceInstance.disabled !== true + ) + }) } private updateCollectionData(): boolean { @@ -89,36 +111,37 @@ export class PieceInstancesHandler const inPreviousPartInstance = this._currentPlaylist?.previousPartInfo?.partInstanceId ? this.processAndPrunePieceInstanceTimings( this._partInstances?.previous, - collection.find({ partInstanceId: this._currentPlaylist.previousPartInfo.partInstanceId }) + collection.find({ partInstanceId: this._currentPlaylist.previousPartInfo.partInstanceId }), + true ) : [] const inCurrentPartInstance = this._currentPlaylist?.currentPartInfo?.partInstanceId ? this.processAndPrunePieceInstanceTimings( this._partInstances?.current, - collection.find({ partInstanceId: this._currentPlaylist.currentPartInfo.partInstanceId }) + collection.find({ partInstanceId: this._currentPlaylist.currentPartInfo.partInstanceId }), + true ) : [] const inNextPartInstance = this._currentPlaylist?.nextPartInfo?.partInstanceId ? this.processAndPrunePieceInstanceTimings( undefined, - collection.find({ partInstanceId: this._currentPlaylist.nextPartInfo.partInstanceId }) + collection.find({ partInstanceId: this._currentPlaylist.nextPartInfo.partInstanceId }), + false ) : [] - const active = [...inPreviousPartInstance, ...inCurrentPartInstance].filter((pieceInstance) => - this.isPieceInstanceActive(pieceInstance) - ) + const active = [...inPreviousPartInstance, ...inCurrentPartInstance] let hasAnythingChanged = false - if (!areElementsShallowEqual(this._collectionData.active, active)) { + if (!_.isEqual(this._collectionData.active, active)) { this._collectionData.active = active hasAnythingChanged = true } if ( - !areElementsShallowEqual(this._collectionData.currentPartInstance, inCurrentPartInstance) && + !_.isEqual(this._collectionData.currentPartInstance, inCurrentPartInstance) && (this._collectionData.currentPartInstance.length !== inCurrentPartInstance.length || this._collectionData.currentPartInstance.some((pieceInstance, index) => { - return !arePropertiesShallowEqual>( + return !arePropertiesDeepEqual>( inCurrentPartInstance[index], pieceInstance, ['reportedStartedPlayback', 'reportedStoppedPlayback'] @@ -128,7 +151,7 @@ export class PieceInstancesHandler this._collectionData.currentPartInstance = inCurrentPartInstance hasAnythingChanged = true } - if (!areElementsShallowEqual(this._collectionData.nextPartInstance, inNextPartInstance)) { + if (!_.isEqual(this._collectionData.nextPartInstance, inNextPartInstance)) { this._collectionData.nextPartInstance = inNextPartInstance hasAnythingChanged = true } @@ -235,28 +258,9 @@ export class PieceInstancesHandler await this.notify(this._collectionData) } } - - private isPieceInstanceActive(pieceInstance: ReadonlyDeep) { - return ( - pieceInstance.reportedStoppedPlayback == null && - pieceInstance.piece.virtual !== true && - pieceInstance.disabled !== true && - (pieceInstance.partInstanceId === this._currentPlaylist?.previousPartInfo?.partInstanceId || // a piece from previous part instance may be active during transition - pieceInstance.partInstanceId === this._currentPlaylist?.currentPartInfo?.partInstanceId) && - (pieceInstance.reportedStartedPlayback != null || // has been reported to have started by the Playout Gateway - pieceInstance.plannedStartedPlayback != null || // a time to start playing has been set by Core - (pieceInstance.partInstanceId === this._currentPlaylist?.currentPartInfo?.partInstanceId && - pieceInstance.piece.enable.start === 0) || // this is to speed things up immediately after a part instance is taken when not yet reported by the Playout Gateway - pieceInstance.infinite?.fromPreviousPart) // infinites from previous part also are on air from the start of the current part - ) - } } -export function arePropertiesShallowEqual>( - a: T, - b: T, - omitProperties: Array -): boolean { +function arePropertiesDeepEqual>(a: T, b: T, omitProperties: Array): boolean { if (typeof a !== 'object' || a == null || typeof b !== 'object' || b == null) { return false } @@ -267,7 +271,7 @@ export function arePropertiesShallowEqual>( if (keysA.length !== keysB.length) return false for (const key of keysA) { - if (!keysB.includes(key) || a[key] !== b[key]) { + if (!keysB.includes(key) || !_.isEqual(a[key], b[key])) { return false } } From 966c007463cc1711cd86908ed114637fdd443fdf Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 8 Aug 2024 15:22:15 +0100 Subject: [PATCH 405/479] chore(release): 1.50.5 --- meteor/CHANGELOG.md | 9 +++++ meteor/package.json | 2 +- meteor/yarn.lock | 12 +++--- packages/blueprints-integration/CHANGELOG.md | 8 ++++ packages/blueprints-integration/package.json | 4 +- packages/corelib/package.json | 6 +-- packages/documentation/package.json | 2 +- packages/job-worker/package.json | 8 ++-- packages/lerna.json | 2 +- packages/live-status-gateway/package.json | 10 ++--- packages/mos-gateway/CHANGELOG.md | 8 ++++ packages/mos-gateway/package.json | 6 +-- packages/openapi/package.json | 2 +- packages/playout-gateway/CHANGELOG.md | 8 ++++ packages/playout-gateway/package.json | 6 +-- packages/server-core-integration/CHANGELOG.md | 8 ++++ packages/server-core-integration/package.json | 4 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 38 +++++++++---------- 19 files changed, 93 insertions(+), 52 deletions(-) diff --git a/meteor/CHANGELOG.md b/meteor/CHANGELOG.md index ab5960933e..8c2e271181 100644 --- a/meteor/CHANGELOG.md +++ b/meteor/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [1.50.5](///compare/v1.50.4-LSG-updates...v1.50.5) (2024-08-08) + + +### Bug Fixes + +* compensate for piece preroll for adlibbed pieces SOFIE-3369 ([#1236](undefined/undefined/undefined/issues/1236)) 195a7d9 +* rundown timing drifting when playing parts with preroll SOFIE-3291 ([#1234](undefined/undefined/undefined/issues/1234)) beee11a +* unexpected timeline updates while playing final part in rundown SOFIE-3371 ([#1237](undefined/undefined/undefined/issues/1237)) 0fe74b4 + ### [1.50.4](https://github.com/nrkno/sofie-core/compare/v1.50.3-LSG-updates...v1.50.4) (2024-07-30) diff --git a/meteor/package.json b/meteor/package.json index db512d753d..52af2a231b 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -1,6 +1,6 @@ { "name": "automation-core", - "version": "1.50.4", + "version": "1.50.5", "private": true, "engines": { "node": ">=14.19.1" diff --git a/meteor/yarn.lock b/meteor/yarn.lock index b959fdb83c..b139485375 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -2082,7 +2082,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/blueprints-integration@portal:../packages/blueprints-integration::locator=automation-core%40workspace%3A." dependencies: - "@sofie-automation/shared-lib": 1.50.4 + "@sofie-automation/shared-lib": 1.50.5 tslib: ^2.4.0 type-fest: ^2.19.0 languageName: node @@ -2123,8 +2123,8 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/corelib@portal:../packages/corelib::locator=automation-core%40workspace%3A." dependencies: - "@sofie-automation/blueprints-integration": 1.50.4 - "@sofie-automation/shared-lib": 1.50.4 + "@sofie-automation/blueprints-integration": 1.50.5 + "@sofie-automation/shared-lib": 1.50.5 fast-clone: ^1.5.13 i18next: ^21.9.1 influx: ^5.9.3 @@ -2155,9 +2155,9 @@ __metadata: resolution: "@sofie-automation/job-worker@portal:../packages/job-worker::locator=automation-core%40workspace%3A." dependencies: "@slack/webhook": ^6.1.0 - "@sofie-automation/blueprints-integration": 1.50.4 - "@sofie-automation/corelib": 1.50.4 - "@sofie-automation/shared-lib": 1.50.4 + "@sofie-automation/blueprints-integration": 1.50.5 + "@sofie-automation/corelib": 1.50.5 + "@sofie-automation/shared-lib": 1.50.5 amqplib: ^0.10.3 deepmerge: ^4.3.1 elastic-apm-node: ^3.43.0 diff --git a/packages/blueprints-integration/CHANGELOG.md b/packages/blueprints-integration/CHANGELOG.md index ca99a53bf2..d29fa50f13 100644 --- a/packages/blueprints-integration/CHANGELOG.md +++ b/packages/blueprints-integration/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.50.5](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.4-LSG-updates...v1.50.5) (2024-08-08) + +**Note:** Version bump only for package @sofie-automation/blueprints-integration + + + + + ## [1.50.4](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.3-LSG-updates...v1.50.4) (2024-07-30) **Note:** Version bump only for package @sofie-automation/blueprints-integration diff --git a/packages/blueprints-integration/package.json b/packages/blueprints-integration/package.json index fde026582b..f71416a2b5 100644 --- a/packages/blueprints-integration/package.json +++ b/packages/blueprints-integration/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/blueprints-integration", - "version": "1.50.4", + "version": "1.50.5", "description": "Library to define the interaction between core and the blueprints.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -38,7 +38,7 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/shared-lib": "1.50.4", + "@sofie-automation/shared-lib": "1.50.5", "tslib": "^2.4.0", "type-fest": "^2.19.0" }, diff --git a/packages/corelib/package.json b/packages/corelib/package.json index 0d26799e42..20e5189bfe 100644 --- a/packages/corelib/package.json +++ b/packages/corelib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/corelib", - "version": "1.50.4", + "version": "1.50.5", "private": true, "description": "Internal library for some types shared by core and workers", "main": "dist/index.js", @@ -39,8 +39,8 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/blueprints-integration": "1.50.4", - "@sofie-automation/shared-lib": "1.50.4", + "@sofie-automation/blueprints-integration": "1.50.5", + "@sofie-automation/shared-lib": "1.50.5", "fast-clone": "^1.5.13", "i18next": "^21.9.1", "influx": "^5.9.3", diff --git a/packages/documentation/package.json b/packages/documentation/package.json index d95532143e..24d5f59cd3 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -1,6 +1,6 @@ { "name": "sofie-documentation", - "version": "1.50.4", + "version": "1.50.5", "private": true, "scripts": { "docusaurus": "docusaurus", diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index 5ce5041061..cdaa6086bf 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/job-worker", - "version": "1.50.4", + "version": "1.50.5", "description": "Worker for things", "main": "dist/index.js", "license": "MIT", @@ -41,9 +41,9 @@ ], "dependencies": { "@slack/webhook": "^6.1.0", - "@sofie-automation/blueprints-integration": "1.50.4", - "@sofie-automation/corelib": "1.50.4", - "@sofie-automation/shared-lib": "1.50.4", + "@sofie-automation/blueprints-integration": "1.50.5", + "@sofie-automation/corelib": "1.50.5", + "@sofie-automation/shared-lib": "1.50.5", "amqplib": "^0.10.3", "deepmerge": "^4.3.1", "elastic-apm-node": "^3.43.0", diff --git a/packages/lerna.json b/packages/lerna.json index d9f6ff6141..699fd6b6bd 100644 --- a/packages/lerna.json +++ b/packages/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.50.4", + "version": "1.50.5", "npmClient": "yarn", "useWorkspaces": true } diff --git a/packages/live-status-gateway/package.json b/packages/live-status-gateway/package.json index e0cf67276e..1d51e72087 100644 --- a/packages/live-status-gateway/package.json +++ b/packages/live-status-gateway/package.json @@ -1,6 +1,6 @@ { "name": "live-status-gateway", - "version": "1.50.4", + "version": "1.50.5", "private": true, "description": "Provides state from Sofie over sockets", "license": "MIT", @@ -53,10 +53,10 @@ "production" ], "dependencies": { - "@sofie-automation/blueprints-integration": "1.50.4", - "@sofie-automation/corelib": "1.50.4", - "@sofie-automation/server-core-integration": "1.50.4", - "@sofie-automation/shared-lib": "1.50.4", + "@sofie-automation/blueprints-integration": "1.50.5", + "@sofie-automation/corelib": "1.50.5", + "@sofie-automation/server-core-integration": "1.50.5", + "@sofie-automation/shared-lib": "1.50.5", "debug": "^4.3.2", "fast-clone": "^1.5.13", "influx": "^5.9.2", diff --git a/packages/mos-gateway/CHANGELOG.md b/packages/mos-gateway/CHANGELOG.md index a0ba68997e..e116fd0ad5 100644 --- a/packages/mos-gateway/CHANGELOG.md +++ b/packages/mos-gateway/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.50.5](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.4-LSG-updates...v1.50.5) (2024-08-08) + +**Note:** Version bump only for package mos-gateway + + + + + ## [1.50.4](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.3-LSG-updates...v1.50.4) (2024-07-30) diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index 76c9eadd35..2929a8608b 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -1,6 +1,6 @@ { "name": "mos-gateway", - "version": "1.50.4", + "version": "1.50.5", "private": true, "description": "MOS-Gateway for the Sofie project", "license": "MIT", @@ -66,8 +66,8 @@ ], "dependencies": { "@mos-connection/connector": "^3.0.4", - "@sofie-automation/server-core-integration": "1.50.4", - "@sofie-automation/shared-lib": "1.50.4", + "@sofie-automation/server-core-integration": "1.50.5", + "@sofie-automation/shared-lib": "1.50.5", "tslib": "^2.4.0", "type-fest": "^2.19.0", "underscore": "^1.13.4", diff --git a/packages/openapi/package.json b/packages/openapi/package.json index ca2649a852..0b4e3187a5 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/openapi", - "version": "1.50.4", + "version": "1.50.5", "private": true, "license": "MIT", "repository": { diff --git a/packages/playout-gateway/CHANGELOG.md b/packages/playout-gateway/CHANGELOG.md index 51ce2aa8e7..7df7797666 100644 --- a/packages/playout-gateway/CHANGELOG.md +++ b/packages/playout-gateway/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.50.5](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.4-LSG-updates...v1.50.5) (2024-08-08) + +**Note:** Version bump only for package playout-gateway + + + + + ## [1.50.4](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.3-LSG-updates...v1.50.4) (2024-07-30) diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index 8183a95b88..1a95087ae5 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -1,6 +1,6 @@ { "name": "playout-gateway", - "version": "1.50.4", + "version": "1.50.5", "private": true, "description": "Connect to Core, play stuff", "license": "MIT", @@ -56,8 +56,8 @@ "production" ], "dependencies": { - "@sofie-automation/server-core-integration": "1.50.4", - "@sofie-automation/shared-lib": "1.50.4", + "@sofie-automation/server-core-integration": "1.50.5", + "@sofie-automation/shared-lib": "1.50.5", "debug": "^4.3.3", "influx": "^5.9.3", "timeline-state-resolver": "9.0.1", diff --git a/packages/server-core-integration/CHANGELOG.md b/packages/server-core-integration/CHANGELOG.md index 85a829f08a..2d019af56f 100644 --- a/packages/server-core-integration/CHANGELOG.md +++ b/packages/server-core-integration/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.50.5](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.4-LSG-updates...v1.50.5) (2024-08-08) + +**Note:** Version bump only for package @sofie-automation/server-core-integration + + + + + ## [1.50.4](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.3-LSG-updates...v1.50.4) (2024-07-30) **Note:** Version bump only for package @sofie-automation/server-core-integration diff --git a/packages/server-core-integration/package.json b/packages/server-core-integration/package.json index ce03404bc0..07f4a3b2a3 100644 --- a/packages/server-core-integration/package.json +++ b/packages/server-core-integration/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/server-core-integration", - "version": "1.50.4", + "version": "1.50.5", "description": "Library for connecting to Core", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -70,7 +70,7 @@ "production" ], "dependencies": { - "@sofie-automation/shared-lib": "1.50.4", + "@sofie-automation/shared-lib": "1.50.5", "ejson": "^2.2.3", "eventemitter3": "^4.0.7", "faye-websocket": "^0.11.4", diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 13bcbdd014..1246edc4d8 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/shared-lib", - "version": "1.50.4", + "version": "1.50.5", "description": "Library for types & values shared by core, workers and gateways", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/packages/yarn.lock b/packages/yarn.lock index 47a05b8d8b..c861ba1446 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -5030,11 +5030,11 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/blueprints-integration@1.50.4, @sofie-automation/blueprints-integration@workspace:blueprints-integration": +"@sofie-automation/blueprints-integration@1.50.5, @sofie-automation/blueprints-integration@workspace:blueprints-integration": version: 0.0.0-use.local resolution: "@sofie-automation/blueprints-integration@workspace:blueprints-integration" dependencies: - "@sofie-automation/shared-lib": 1.50.4 + "@sofie-automation/shared-lib": 1.50.5 tslib: ^2.4.0 type-fest: ^2.19.0 languageName: unknown @@ -5071,12 +5071,12 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/corelib@1.50.4, @sofie-automation/corelib@workspace:corelib": +"@sofie-automation/corelib@1.50.5, @sofie-automation/corelib@workspace:corelib": version: 0.0.0-use.local resolution: "@sofie-automation/corelib@workspace:corelib" dependencies: - "@sofie-automation/blueprints-integration": 1.50.4 - "@sofie-automation/shared-lib": 1.50.4 + "@sofie-automation/blueprints-integration": 1.50.5 + "@sofie-automation/shared-lib": 1.50.5 fast-clone: ^1.5.13 i18next: ^21.9.1 influx: ^5.9.3 @@ -5107,9 +5107,9 @@ __metadata: resolution: "@sofie-automation/job-worker@workspace:job-worker" dependencies: "@slack/webhook": ^6.1.0 - "@sofie-automation/blueprints-integration": 1.50.4 - "@sofie-automation/corelib": 1.50.4 - "@sofie-automation/shared-lib": 1.50.4 + "@sofie-automation/blueprints-integration": 1.50.5 + "@sofie-automation/corelib": 1.50.5 + "@sofie-automation/shared-lib": 1.50.5 amqplib: ^0.10.3 deepmerge: ^4.3.1 elastic-apm-node: ^3.43.0 @@ -5139,11 +5139,11 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/server-core-integration@1.50.4, @sofie-automation/server-core-integration@workspace:server-core-integration": +"@sofie-automation/server-core-integration@1.50.5, @sofie-automation/server-core-integration@workspace:server-core-integration": version: 0.0.0-use.local resolution: "@sofie-automation/server-core-integration@workspace:server-core-integration" dependencies: - "@sofie-automation/shared-lib": 1.50.4 + "@sofie-automation/shared-lib": 1.50.5 ejson: ^2.2.3 eventemitter3: ^4.0.7 faye-websocket: ^0.11.4 @@ -5153,7 +5153,7 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/shared-lib@1.50.4, @sofie-automation/shared-lib@workspace:shared-lib": +"@sofie-automation/shared-lib@1.50.5, @sofie-automation/shared-lib@workspace:shared-lib": version: 0.0.0-use.local resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: @@ -14992,10 +14992,10 @@ asn1@evs-broadcast/node-asn1: "@asyncapi/generator": 1.9.13 "@asyncapi/html-template": 0.26.0 "@asyncapi/nodejs-ws-template": 0.9.25 - "@sofie-automation/blueprints-integration": 1.50.4 - "@sofie-automation/corelib": 1.50.4 - "@sofie-automation/server-core-integration": 1.50.4 - "@sofie-automation/shared-lib": 1.50.4 + "@sofie-automation/blueprints-integration": 1.50.5 + "@sofie-automation/corelib": 1.50.5 + "@sofie-automation/server-core-integration": 1.50.5 + "@sofie-automation/shared-lib": 1.50.5 debug: ^4.3.2 fast-clone: ^1.5.13 influx: ^5.9.2 @@ -16076,8 +16076,8 @@ asn1@evs-broadcast/node-asn1: resolution: "mos-gateway@workspace:mos-gateway" dependencies: "@mos-connection/connector": ^3.0.4 - "@sofie-automation/server-core-integration": 1.50.4 - "@sofie-automation/shared-lib": 1.50.4 + "@sofie-automation/server-core-integration": 1.50.5 + "@sofie-automation/shared-lib": 1.50.5 tslib: ^2.4.0 type-fest: ^2.19.0 underscore: ^1.13.4 @@ -18053,8 +18053,8 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "playout-gateway@workspace:playout-gateway" dependencies: - "@sofie-automation/server-core-integration": 1.50.4 - "@sofie-automation/shared-lib": 1.50.4 + "@sofie-automation/server-core-integration": 1.50.5 + "@sofie-automation/shared-lib": 1.50.5 debug: ^4.3.3 influx: ^5.9.3 timeline-state-resolver: 9.0.1 From d4cc0f7d508c0fcbe516be5babcbdf17cca05c9d Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Mon, 12 Aug 2024 13:35:31 +0200 Subject: [PATCH 406/479] fix: update sorensen dep --- meteor/package.json | 2 +- meteor/yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/meteor/package.json b/meteor/package.json index 34cabd8ce9..f0687a5c05 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -56,7 +56,7 @@ "@sofie-automation/corelib": "portal:../packages/corelib", "@sofie-automation/job-worker": "portal:../packages/job-worker", "@sofie-automation/shared-lib": "portal:../packages/shared-lib", - "@sofie-automation/sorensen": "^1.4.2", + "@sofie-automation/sorensen": "^1.4.3", "app-root-path": "^3.1.0", "bcrypt": "^5.1.1", "body-parser": "^1.20.2", diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 4216c09816..42bf567538 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1424,10 +1424,10 @@ __metadata: languageName: node linkType: soft -"@sofie-automation/sorensen@npm:^1.4.2": - version: 1.4.2 - resolution: "@sofie-automation/sorensen@npm:1.4.2" - checksum: 0fb9d945af37a0dc278be05ed16e77ed8a0c69a5dee891ab0430fff7f4aace036baf96c9fe8f7c80e1addaea634e9bf9547f97818f5ba1279c77c32387079844 +"@sofie-automation/sorensen@npm:^1.4.3": + version: 1.4.3 + resolution: "@sofie-automation/sorensen@npm:1.4.3" + checksum: 2ec7bd38fd6cf91456303cef103265c0dfb031074f601278d4fa55a355d0a1c043763a5f323bb12147385426dcb1f91869f01f788994bd3c51fa3cd8aaf431a8 languageName: node linkType: hard @@ -2700,7 +2700,7 @@ __metadata: "@sofie-automation/eslint-plugin": ^0.1.1 "@sofie-automation/job-worker": "portal:../packages/job-worker" "@sofie-automation/shared-lib": "portal:../packages/shared-lib" - "@sofie-automation/sorensen": ^1.4.2 + "@sofie-automation/sorensen": ^1.4.3 "@types/app-root-path": ^1.2.8 "@types/body-parser": ^1.19.5 "@types/classnames": ^2.3.1 From d5cafe8db5e453f87f8d46262f23e118b580d4d5 Mon Sep 17 00:00:00 2001 From: Simon Rogers Date: Wed, 14 Aug 2024 11:50:06 +0100 Subject: [PATCH 407/479] feat: Ensure peripheralDevice subdevice removal when requested (#1227) Co-authored-by: Simon Rogers --- packages/mos-gateway/src/CoreMosDeviceHandler.ts | 3 ++- packages/mos-gateway/src/coreHandler.ts | 2 +- packages/playout-gateway/src/coreHandler.ts | 3 ++- packages/playout-gateway/src/tsrHandler.ts | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/mos-gateway/src/CoreMosDeviceHandler.ts b/packages/mos-gateway/src/CoreMosDeviceHandler.ts index 6063571da5..fa9606a8ef 100644 --- a/packages/mos-gateway/src/CoreMosDeviceHandler.ts +++ b/packages/mos-gateway/src/CoreMosDeviceHandler.ts @@ -427,7 +427,7 @@ export class CoreMosDeviceHandler { }, 2000) }) } - async dispose(): Promise { + async dispose(subdevice: 'keepSubDevice' | 'removeSubDevice' = 'keepSubDevice'): Promise { this._observers.forEach((obs) => obs.stop()) await this.core.setStatus({ @@ -435,6 +435,7 @@ export class CoreMosDeviceHandler { messages: ['Uninitialized'], }) + if (subdevice === 'removeSubDevice') await this.core.unInitialize() await this.core.destroy() } killProcess(): void { diff --git a/packages/mos-gateway/src/coreHandler.ts b/packages/mos-gateway/src/coreHandler.ts index dee6f39bf6..54871df2e8 100644 --- a/packages/mos-gateway/src/coreHandler.ts +++ b/packages/mos-gateway/src/coreHandler.ts @@ -167,7 +167,7 @@ export class CoreHandler { const coreMosHandler = this._coreMosHandlers[foundI] if (coreMosHandler) { this._coreMosHandlers.splice(foundI, 1) - await coreMosHandler.dispose() + await coreMosHandler.dispose('removeSubDevice') } } onConnectionRestored(): void { diff --git a/packages/playout-gateway/src/coreHandler.ts b/packages/playout-gateway/src/coreHandler.ts index 8262cdd4e1..dd31642400 100644 --- a/packages/playout-gateway/src/coreHandler.ts +++ b/packages/playout-gateway/src/coreHandler.ts @@ -542,7 +542,7 @@ export class CoreTSRDeviceHandler { ) } - async dispose(): Promise { + async dispose(subdevice: 'keepSubDevice' | 'removeSubDevice' = 'keepSubDevice'): Promise { this._observers.forEach((obs) => obs.stop()) await this._tsrHandler.tsr.removeDevice(this._deviceId) @@ -551,6 +551,7 @@ export class CoreTSRDeviceHandler { messages: ['Uninitialized'], }) + if (subdevice === 'removeSubDevice') await this.core.unInitialize() await this.core.destroy() } killProcess(): void { diff --git a/packages/playout-gateway/src/tsrHandler.ts b/packages/playout-gateway/src/tsrHandler.ts index 9ba3ba178e..b426f82de8 100644 --- a/packages/playout-gateway/src/tsrHandler.ts +++ b/packages/playout-gateway/src/tsrHandler.ts @@ -1002,7 +1002,7 @@ export class TSRHandler { let success = false if (this._coreTsrHandlers[deviceId]) { try { - await this._coreTsrHandlers[deviceId].dispose() + await this._coreTsrHandlers[deviceId].dispose('removeSubDevice') this.logger.debug('Disposed device ' + deviceId) success = true } catch (error) { From d541ca4ea790869b81eb85cca939efc83b5868e8 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 15 Aug 2024 09:52:35 +0100 Subject: [PATCH 408/479] chore: update tsr to 9.0.2 --- meteor/yarn.lock | 10 +++++----- packages/playout-gateway/package.json | 2 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 22 +++++++++++----------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/meteor/yarn.lock b/meteor/yarn.lock index b139485375..10513c8e79 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -2180,7 +2180,7 @@ __metadata: resolution: "@sofie-automation/shared-lib@portal:../packages/shared-lib::locator=automation-core%40workspace%3A." dependencies: "@mos-connection/model": ^3.0.4 - timeline-state-resolver-types: 9.0.1 + timeline-state-resolver-types: 9.0.2 tslib: ^2.4.0 type-fest: ^2.19.0 languageName: node @@ -12455,12 +12455,12 @@ __metadata: languageName: node linkType: hard -"timeline-state-resolver-types@npm:9.0.1": - version: 9.0.1 - resolution: "timeline-state-resolver-types@npm:9.0.1" +"timeline-state-resolver-types@npm:9.0.2": + version: 9.0.2 + resolution: "timeline-state-resolver-types@npm:9.0.2" dependencies: tslib: ^2.5.1 - checksum: b814088e1118f0c5f80459958c5008e9bc9cf4484f5d0f3858e4bcb5146685f42a8328ef4e4764c1f95e1670b33109a8dd74c4582d6204a3f61b57efb216e013 + checksum: be24c805246cea986f19c051c3fa71d9730bbc5baa17dba83d910e0aa139a758571ece15ac96da833a815186705a146bf051a2985dc5abaf034bd9fed2a067e0 languageName: node linkType: hard diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index 1a95087ae5..857045fbc5 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -60,7 +60,7 @@ "@sofie-automation/shared-lib": "1.50.5", "debug": "^4.3.3", "influx": "^5.9.3", - "timeline-state-resolver": "9.0.1", + "timeline-state-resolver": "9.0.2", "tslib": "^2.4.0", "underscore": "^1.13.4", "winston": "^3.8.2" diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 1246edc4d8..48f4a718c2 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -39,7 +39,7 @@ ], "dependencies": { "@mos-connection/model": "^3.0.4", - "timeline-state-resolver-types": "9.0.1", + "timeline-state-resolver-types": "9.0.2", "tslib": "^2.4.0", "type-fest": "^2.19.0" }, diff --git a/packages/yarn.lock b/packages/yarn.lock index c861ba1446..930d6350f2 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -5158,7 +5158,7 @@ __metadata: resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: "@mos-connection/model": ^3.0.4 - timeline-state-resolver-types: 9.0.1 + timeline-state-resolver-types: 9.0.2 tslib: ^2.4.0 type-fest: ^2.19.0 languageName: unknown @@ -18057,7 +18057,7 @@ asn1@evs-broadcast/node-asn1: "@sofie-automation/shared-lib": 1.50.5 debug: ^4.3.3 influx: ^5.9.3 - timeline-state-resolver: 9.0.1 + timeline-state-resolver: 9.0.2 tslib: ^2.4.0 underscore: ^1.13.4 winston: ^3.8.2 @@ -21687,18 +21687,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"timeline-state-resolver-types@npm:9.0.1": - version: 9.0.1 - resolution: "timeline-state-resolver-types@npm:9.0.1" +"timeline-state-resolver-types@npm:9.0.2": + version: 9.0.2 + resolution: "timeline-state-resolver-types@npm:9.0.2" dependencies: tslib: ^2.5.1 - checksum: b814088e1118f0c5f80459958c5008e9bc9cf4484f5d0f3858e4bcb5146685f42a8328ef4e4764c1f95e1670b33109a8dd74c4582d6204a3f61b57efb216e013 + checksum: be24c805246cea986f19c051c3fa71d9730bbc5baa17dba83d910e0aa139a758571ece15ac96da833a815186705a146bf051a2985dc5abaf034bd9fed2a067e0 languageName: node linkType: hard -"timeline-state-resolver@npm:9.0.1": - version: 9.0.1 - resolution: "timeline-state-resolver@npm:9.0.1" +"timeline-state-resolver@npm:9.0.2": + version: 9.0.2 + resolution: "timeline-state-resolver@npm:9.0.2" dependencies: "@tv2media/v-connection": ^7.3.2 atem-connection: 2.5.0 @@ -21720,7 +21720,7 @@ asn1@evs-broadcast/node-asn1: sprintf-js: ^1.1.2 superfly-timeline: ^8.3.1 threadedclass: ^1.2.1 - timeline-state-resolver-types: 9.0.1 + timeline-state-resolver-types: 9.0.2 tslib: ^2.5.1 tv-automation-quantel-gateway-client: ^3.1.7 underscore: ^1.13.6 @@ -21728,7 +21728,7 @@ asn1@evs-broadcast/node-asn1: utf-8-validate: ^5.0.10 ws: ^7.5.9 xml-js: ^1.6.11 - checksum: 3769f084c29d0bee4bf6f80e48a395e838956d8ec9d0f57539fd8d90c9e71f9abab24167edfcf8d103e86e31e965bcf77acd85fab8f111e47208aa908b0ac8bc + checksum: 3d3d5ecbdf40358499acd17d2c6ff219a2fb5b1a9b878915a31a3f9136db544ce57dce84cdec0f44b8681f334f7ba4988be78acbccbabca1aa3e654856bb6887 languageName: node linkType: hard From ce237c58a6c5f13c6dd38b13bd23ccb18d3db55b Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Mon, 19 Aug 2024 08:13:25 +0200 Subject: [PATCH 409/479] fix: Only log full job payloads on silly-level --- packages/job-worker/src/playout/adlibAction.ts | 17 ++++++++--------- packages/job-worker/src/workers/parent-base.ts | 6 +++++- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/job-worker/src/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index c7943fba18..14289690c2 100644 --- a/packages/job-worker/src/playout/adlibAction.ts +++ b/packages/job-worker/src/playout/adlibAction.ts @@ -150,10 +150,11 @@ export async function executeActionInner( // If any action cannot be done due to timings, that needs to be rejected by the context if (!blueprint.blueprint.executeAction) throw UserError.create(UserErrorMessage.ActionsNotSupported) - logger.info( - `Executing AdlibAction "${actionParameters.actionId}": ${JSON.stringify(actionParameters.userData)} (${ - actionParameters.triggerMode - })` + logger.info(`Executing AdlibAction "${actionParameters.actionId}"`) + logger.silly( + `Executing AdlibAction Payload "${actionParameters.actionId}" Payload: ${JSON.stringify( + actionParameters.userData + )} (${actionParameters.triggerMode})` ) try { @@ -232,11 +233,9 @@ async function executeDataStoreAction( showStyle, watchedPackages ) - - logger.info( - `Executing Datastore AdlibAction "${actionParameters.actionId}": ${JSON.stringify( - actionParameters.userData - )}` + logger.info(`Executing Datastore AdlibAction "${actionParameters.actionId}"`) + logger.silly( + `Datastore AdlibAction "${actionParameters.actionId}" Payload: ${JSON.stringify(actionParameters.userData)}` ) try { diff --git a/packages/job-worker/src/workers/parent-base.ts b/packages/job-worker/src/workers/parent-base.ts index 23f074b6dd..1e5997e643 100644 --- a/packages/job-worker/src/workers/parent-base.ts +++ b/packages/job-worker/src/workers/parent-base.ts @@ -278,7 +278,11 @@ export abstract class WorkerParentBase { try { logger.verbose(`Starting work ${job.id}: "${job.name}"`) - logger.debug(`Payload ${job.id}: ${JSON.stringify(job.data)}`) + logger.silly( + `Starting work ${job.id}: "${job.name}", payload ${job.id}: ${JSON.stringify( + job.data + )}` + ) // Future - extend the job lock on an interval let result: WorkerJobResult From 6f44b3f134b5ab981c5f96c5fce8045c1ebdb04e Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Mon, 19 Aug 2024 08:16:20 +0200 Subject: [PATCH 410/479] chore: refactor: move out function definitions from getLookeaheadObjects --- .../job-worker/src/playout/lookahead/index.ts | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/job-worker/src/playout/lookahead/index.ts b/packages/job-worker/src/playout/lookahead/index.ts index c4b5e18bde..5535f5277b 100644 --- a/packages/job-worker/src/playout/lookahead/index.ts +++ b/packages/job-worker/src/playout/lookahead/index.ts @@ -42,6 +42,22 @@ function findLargestLookaheadDistance(mappings: Array<[string, MappingExt]>): nu type ValidLookaheadMode = LookaheadMode.PRELOAD | LookaheadMode.WHEN_CLEAR +function getPrunedEndedPieceInstances(info: SelectedPartInstanceTimelineInfo) { + if (!info.partInstance.timings?.plannedStartedPlayback) { + return info.pieceInstances + } else { + return info.pieceInstances.filter((p) => !hasPieceInstanceDefinitelyEnded(p, info.nowInPart)) + } +} +function removeInfiniteContinuations(info: PartInstanceAndPieceInstances): PartInstanceAndPieceInstances { + const partId = info.part.part._id + return { + ...info, + // Ignore PieceInstances that continue from the previous part, as they will not need lookahead + allPieces: info.allPieces.filter((inst) => !inst.infinite || inst.piece.startPartId === partId), + } +} + export async function getLookeaheadObjects( context: JobContext, cache: CacheForPlayout, @@ -74,22 +90,6 @@ export async function getLookeaheadObjects( }, }) - function removeInfiniteContinuations(info: PartInstanceAndPieceInstances): PartInstanceAndPieceInstances { - const partId = info.part.part._id - return { - ...info, - // Ignore PieceInstances that continue from the previous part, as they will not need lookahead - allPieces: info.allPieces.filter((inst) => !inst.infinite || inst.piece.startPartId === partId), - } - } - - function getPrunedEndedPieceInstances(info: SelectedPartInstanceTimelineInfo) { - if (!info.partInstance.timings?.plannedStartedPlayback) { - return info.pieceInstances - } else { - return info.pieceInstances.filter((p) => !hasPieceInstanceDefinitelyEnded(p, info.nowInPart)) - } - } const partInstancesInfo: PartInstanceAndPieceInstances[] = _.compact([ partInstancesInfo0.current ? removeInfiniteContinuations({ From c8131534ba2315f0966be599dcbc8d63f314f25b Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Mon, 19 Aug 2024 10:40:12 +0200 Subject: [PATCH 411/479] chore: tsr dep --- meteor/yarn.lock | 10 +- packages/playout-gateway/package.json | 2 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 131 ++++++++++++++++++++------ 4 files changed, 110 insertions(+), 35 deletions(-) diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 42bf567538..c890b1bd40 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1418,7 +1418,7 @@ __metadata: resolution: "@sofie-automation/shared-lib@portal:../packages/shared-lib::locator=automation-core%40workspace%3A." dependencies: "@mos-connection/model": ^4.1.1-nightly-master-20240430-072032-ffb8bf6.0 - timeline-state-resolver-types: 9.1.0-nightly-release51-20240228-132329-b7ceb6950.0 + timeline-state-resolver-types: 9.1.0 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: node @@ -11748,12 +11748,12 @@ __metadata: languageName: node linkType: hard -"timeline-state-resolver-types@npm:9.1.0-nightly-release51-20240228-132329-b7ceb6950.0": - version: 9.1.0-nightly-release51-20240228-132329-b7ceb6950.0 - resolution: "timeline-state-resolver-types@npm:9.1.0-nightly-release51-20240228-132329-b7ceb6950.0" +"timeline-state-resolver-types@npm:9.1.0": + version: 9.1.0 + resolution: "timeline-state-resolver-types@npm:9.1.0" dependencies: tslib: ^2.6.2 - checksum: da20544cc1ce9118c318ceccec240cc2f05c7eb415da9365625bdeffafb508b358d4f51842ca7ec9f10f44f25e6c9956cda2d74c22bf138bb869bcceacbbc2f2 + checksum: dab48013908d588a4a9357aeb824117316ce1666d972575c4fc89a0f70647bee4aacdfddb73bce5db7f7bdc5920f9e31b3669b47d6e97cf627e41f9f205e3f03 languageName: node linkType: hard diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index 956c20e1ef..d687794f80 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -60,7 +60,7 @@ "@sofie-automation/shared-lib": "1.51.0-in-development", "debug": "^4.3.4", "influx": "^5.9.3", - "timeline-state-resolver": "9.1.0-nightly-release51-20240228-132329-b7ceb6950.0", + "timeline-state-resolver": "9.1.0", "tslib": "^2.6.2", "underscore": "^1.13.6", "winston": "^3.11.0" diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 91e6ba87b5..ea68d9893e 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -39,7 +39,7 @@ ], "dependencies": { "@mos-connection/model": "^4.1.1-nightly-master-20240430-072032-ffb8bf6.0", - "timeline-state-resolver-types": "9.1.0-nightly-release51-20240228-132329-b7ceb6950.0", + "timeline-state-resolver-types": "9.1.0", "tslib": "^2.6.2", "type-fest": "^3.13.1" }, diff --git a/packages/yarn.lock b/packages/yarn.lock index 274d7fb2f3..6e416d472e 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -2684,6 +2684,29 @@ __metadata: languageName: node linkType: hard +"@hapi/boom@npm:^10.0.1": + version: 10.0.1 + resolution: "@hapi/boom@npm:10.0.1" + dependencies: + "@hapi/hoek": ^11.0.2 + checksum: c80f76e86386c65fb5e3f2aae489e82c318615b52c1462af913b1db3d05ebf1564336e2cd88f1ee79d66a8b6f48fa075089c617bfe93ad88e6e1a695d0cde499 + languageName: node + linkType: hard + +"@hapi/bourne@npm:^3.0.0": + version: 3.0.0 + resolution: "@hapi/bourne@npm:3.0.0" + checksum: 7174cab6c33191918fcdb1953fe3169a1106e6ac79a67ef5fd08b351f0813f8f608170f2239786cbe5519e03cdfe5ab748ea1635caa06dcd5802410295514ef8 + languageName: node + linkType: hard + +"@hapi/hoek@npm:^11.0.2, @hapi/hoek@npm:^11.0.4": + version: 11.0.4 + resolution: "@hapi/hoek@npm:11.0.4" + checksum: 67dc9782a8235e066e3885352e40faa3c239621d7de29bfc88a72aba60eeb8df76b5c05d77962c734c8b0ef3fc7f818cc692689003d270a1b73ea98ba1508c5b + languageName: node + linkType: hard + "@hapi/hoek@npm:^9.0.0, @hapi/hoek@npm:^9.3.0": version: 9.3.0 resolution: "@hapi/hoek@npm:9.3.0" @@ -2700,6 +2723,17 @@ __metadata: languageName: node linkType: hard +"@hapi/wreck@npm:^18.0.0": + version: 18.1.0 + resolution: "@hapi/wreck@npm:18.1.0" + dependencies: + "@hapi/boom": ^10.0.1 + "@hapi/bourne": ^3.0.0 + "@hapi/hoek": ^11.0.2 + checksum: 08e20e829a46fcdd1595b7181d3c0f94bc1cd2966aaf272138a6b80fa5266852295189295169903be20ef53bb35ba7b9a9080ce7e1147116ffce4db492b99bec + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.11.11": version: 0.11.11 resolution: "@humanwhocodes/config-array@npm:0.11.11" @@ -4677,7 +4711,7 @@ __metadata: resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: "@mos-connection/model": ^4.1.1-nightly-master-20240430-072032-ffb8bf6.0 - timeline-state-resolver-types: 9.1.0-nightly-release51-20240228-132329-b7ceb6950.0 + timeline-state-resolver-types: 9.1.0 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: unknown @@ -7079,16 +7113,16 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"atem-state@npm:1.2.0-nightly-master-20240202-124617-5b52568.0": - version: 1.2.0-nightly-master-20240202-124617-5b52568.0 - resolution: "atem-state@npm:1.2.0-nightly-master-20240202-124617-5b52568.0" +"atem-state@npm:1.2.0": + version: 1.2.0 + resolution: "atem-state@npm:1.2.0" dependencies: deepmerge: ^4.3.1 tslib: ^2.6.2 type-fest: ^3.13.1 peerDependencies: atem-connection: 3.4 - checksum: 473c305838b6e5540beba5433dd8c1bd9212734b578e130b6eca6aee274f70ef3d469412bb34ea069e89921f15bf7fa320adfb26658698fc99d82040c34d7a84 + checksum: 7dd676d29091d0c769e3f40cce18c54e1e773e1ff3237803275147a4e3abfca0751af29349fdf8df2508b13e6834249690f6546258a25314d07986d33b9e3ab8 languageName: node linkType: hard @@ -7885,14 +7919,14 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"casparcg-connection@npm:6.2.1": - version: 6.2.1 - resolution: "casparcg-connection@npm:6.2.1" +"casparcg-connection@npm:6.3.0": + version: 6.3.0 + resolution: "casparcg-connection@npm:6.3.0" dependencies: eventemitter3: ^5.0.1 tslib: ^2.5.0 xml2js: ^0.6.2 - checksum: a086810faf0dbf91fc2987c1f4062696d5396fcbfeee16bc8b969a1a5edaac30657179c25164b1b62d9f79789ceee2e842822fe30dece420893e6fab50a467dd + checksum: 2aa7e2160078f0de08b3b0491b2dde180baaca7fb96fdf4b74522b90f053efc265b42ea48cb7ae0ede2dab697be61d09d6ee18ce72826df0091156aaceeb8e06 languageName: node linkType: hard @@ -13121,13 +13155,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"hyperdeck-connection@npm:2.0.0": - version: 2.0.0 - resolution: "hyperdeck-connection@npm:2.0.0" +"hyperdeck-connection@npm:2.0.1": + version: 2.0.1 + resolution: "hyperdeck-connection@npm:2.0.1" dependencies: eventemitter3: ^4.0.7 tslib: ^2.6.2 - checksum: 86fd42d64771b9ce58eb4c542cd7f12ee3ab932ff97bc114840c52c09c3052a431f896b8b5900be2c2f2419182e50ab080bcb60a141285d9de2ba52fce4c8d61 + checksum: 028f60498fa48c98a2cc0297c0f8af54e2f67c916f6796c64cb59c667a73e2bc314e1342b88ee234289e6085e10ac2886e81b0e5d2334a7e67d6c0d373fbcc88 languageName: node linkType: hard @@ -14652,6 +14686,19 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"joi@npm:^17.6.4": + version: 17.13.3 + resolution: "joi@npm:17.13.3" + dependencies: + "@hapi/hoek": ^9.3.0 + "@hapi/topo": ^5.1.0 + "@sideway/address": ^4.1.5 + "@sideway/formula": ^3.0.1 + "@sideway/pinpoint": ^2.0.0 + checksum: 66ed454fee3d8e8da1ce21657fd2c7d565d98f3e539d2c5c028767e5f38cbd6297ce54df8312d1d094e62eb38f9452ebb43da4ce87321df66cf5e3f128cbc400 + languageName: node + linkType: hard + "joi@npm:^17.9.2": version: 17.12.3 resolution: "joi@npm:17.12.3" @@ -19372,7 +19419,7 @@ asn1@evs-broadcast/node-asn1: "@sofie-automation/shared-lib": 1.51.0-in-development debug: ^4.3.4 influx: ^5.9.3 - timeline-state-resolver: 9.1.0-nightly-release51-20240228-132329-b7ceb6950.0 + timeline-state-resolver: 9.1.0 tslib: ^2.6.2 underscore: ^1.13.6 winston: ^3.11.0 @@ -21826,6 +21873,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"simple-oauth2@npm:^5.0.0": + version: 5.1.0 + resolution: "simple-oauth2@npm:5.1.0" + dependencies: + "@hapi/hoek": ^11.0.4 + "@hapi/wreck": ^18.0.0 + debug: ^4.3.4 + joi: ^17.6.4 + checksum: 65e07a92ffdfbc38fdd7db5480349b09c4c896559b6b3786a256546fe3aa6ac80086fa4f2c30d1d68ff551668147343c0a0dcc53c33fe7e5d1a7b42b4772eedc + languageName: node + linkType: hard + "simple-swizzle@npm:^0.2.2": version: 0.2.2 resolution: "simple-swizzle@npm:0.2.2" @@ -22956,24 +23015,24 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"timeline-state-resolver-types@npm:9.1.0-nightly-release51-20240228-132329-b7ceb6950.0": - version: 9.1.0-nightly-release51-20240228-132329-b7ceb6950.0 - resolution: "timeline-state-resolver-types@npm:9.1.0-nightly-release51-20240228-132329-b7ceb6950.0" +"timeline-state-resolver-types@npm:9.1.0": + version: 9.1.0 + resolution: "timeline-state-resolver-types@npm:9.1.0" dependencies: tslib: ^2.6.2 - checksum: da20544cc1ce9118c318ceccec240cc2f05c7eb415da9365625bdeffafb508b358d4f51842ca7ec9f10f44f25e6c9956cda2d74c22bf138bb869bcceacbbc2f2 + checksum: dab48013908d588a4a9357aeb824117316ce1666d972575c4fc89a0f70647bee4aacdfddb73bce5db7f7bdc5920f9e31b3669b47d6e97cf627e41f9f205e3f03 languageName: node linkType: hard -"timeline-state-resolver@npm:9.1.0-nightly-release51-20240228-132329-b7ceb6950.0": - version: 9.1.0-nightly-release51-20240228-132329-b7ceb6950.0 - resolution: "timeline-state-resolver@npm:9.1.0-nightly-release51-20240228-132329-b7ceb6950.0" +"timeline-state-resolver@npm:9.1.0": + version: 9.1.0 + resolution: "timeline-state-resolver@npm:9.1.0" dependencies: "@tv2media/v-connection": ^7.3.2 atem-connection: 3.5.0 - atem-state: 1.2.0-nightly-master-20240202-124617-5b52568.0 + atem-state: 1.2.0 cacheable-lookup: ^5.0.4 - casparcg-connection: 6.2.1 + casparcg-connection: 6.3.0 casparcg-state: 3.0.3 debug: ^4.3.4 deepmerge: ^4.3.1 @@ -22981,25 +23040,26 @@ asn1@evs-broadcast/node-asn1: eventemitter3: ^4.0.7 got: ^11.8.6 hpagent: ^1.2.0 - hyperdeck-connection: 2.0.0 + hyperdeck-connection: 2.0.1 klona: ^2.0.6 obs-websocket-js: ^5.0.4 osc: ^2.4.4 p-all: ^3.0.0 p-queue: ^6.6.2 p-timeout: ^3.2.0 + simple-oauth2: ^5.0.0 sprintf-js: ^1.1.3 superfly-timeline: ^9.0.0 threadedclass: ^1.2.1 - timeline-state-resolver-types: 9.1.0-nightly-release51-20240228-132329-b7ceb6950.0 + timeline-state-resolver-types: 9.1.0 tslib: ^2.6.2 tv-automation-quantel-gateway-client: ^3.1.7 type-fest: ^3.13.1 underscore: ^1.13.6 utf-8-validate: ^5.0.10 - ws: ^7.5.9 + ws: ^7.5.10 xml-js: ^1.6.11 - checksum: 56df8dbe5f0303795ebc7d8d645bd5d2eb59e1e00f01f267b33ccf66268d21412276a46cfba8631e97bde15acc9aff68a52b7378fd8f9d192890e04cfebfaee6 + checksum: 015f0c0b120958aea181d16cf26a60504942fd56eee8328c754a74e553f90c5c18d793e57224cec557b9f1c4f3d4a0d93672520e90eda98f97aa3b00f5f01c5f languageName: node linkType: hard @@ -24907,7 +24967,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"ws@npm:^7.3.1, ws@npm:^7.4.6, ws@npm:^7.5.9": +"ws@npm:^7.3.1, ws@npm:^7.4.6": version: 7.5.9 resolution: "ws@npm:7.5.9" peerDependencies: @@ -24922,6 +24982,21 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"ws@npm:^7.5.10": + version: 7.5.10 + resolution: "ws@npm:7.5.10" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: f9bb062abf54cc8f02d94ca86dcd349c3945d63851f5d07a3a61c2fcb755b15a88e943a63cf580cbdb5b74436d67ef6b67f745b8f7c0814e411379138e1863cb + languageName: node + linkType: hard + "ws@npm:^8.13.0, ws@npm:^8.16.0": version: 8.16.0 resolution: "ws@npm:8.16.0" From 1b80e69d560c071812e8be096e673a99c314f189 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Mon, 19 Aug 2024 10:40:49 +0200 Subject: [PATCH 412/479] chore: prepare Release 51 for release testing --- meteor/server/migration/1_40_0.ts | 2 - meteor/server/migration/1_42_0.ts | 4 +- meteor/server/migration/1_44_0.ts | 2 + meteor/server/migration/1_46_0.ts | 2 +- meteor/server/migration/1_47_0.ts | 12 +-- meteor/server/migration/1_48_0.ts | 11 +-- meteor/server/migration/1_49_0.ts | 10 +- meteor/server/migration/1_50_0.ts | 10 +- meteor/server/migration/1_51_0.ts | 95 +++++++++++++++++++ meteor/server/migration/X_X_X.ts | 89 +---------------- .../server/migration/currentSystemVersion.ts | 4 +- meteor/server/migration/migrations.ts | 3 + 12 files changed, 111 insertions(+), 133 deletions(-) create mode 100644 meteor/server/migration/1_51_0.ts diff --git a/meteor/server/migration/1_40_0.ts b/meteor/server/migration/1_40_0.ts index 6e08378cd6..196ca9af78 100644 --- a/meteor/server/migration/1_40_0.ts +++ b/meteor/server/migration/1_40_0.ts @@ -42,8 +42,6 @@ const OldSettings = Settings as Partial const oldFrameRate = OldSettings.frameRate ?? 25 export const addSteps = addMigrationSteps('1.40.0', [ - // Add some migrations! - { id: `Studio.settings.frameRate`, canBeRunAutomatically: true, diff --git a/meteor/server/migration/1_42_0.ts b/meteor/server/migration/1_42_0.ts index e5f454d5bc..0d6278e580 100644 --- a/meteor/server/migration/1_42_0.ts +++ b/meteor/server/migration/1_42_0.ts @@ -2,9 +2,9 @@ import { addMigrationSteps } from './databaseMigration' import { StudioRouteSet, StudioRouteType } from '@sofie-automation/corelib/dist/dataModel/Studio' import { Studios } from '../collections' -export const addSteps = addMigrationSteps('1.42.0', [ - // Add some migrations! +// Release 42 +export const addSteps = addMigrationSteps('1.42.0', [ { id: 'Add new routeType property to routeSets where missing', canBeRunAutomatically: true, diff --git a/meteor/server/migration/1_44_0.ts b/meteor/server/migration/1_44_0.ts index ed2b35e53d..b6d0d5c9a9 100644 --- a/meteor/server/migration/1_44_0.ts +++ b/meteor/server/migration/1_44_0.ts @@ -1,6 +1,8 @@ import { addMigrationSteps } from './databaseMigration' import { RundownPlaylists, Rundowns } from '../collections' +// Release 44 + export const addSteps = addMigrationSteps('1.44.0', [ { id: 'Add new rundownIdsInOrder property to playlists where missing', diff --git a/meteor/server/migration/1_46_0.ts b/meteor/server/migration/1_46_0.ts index 30c5bf7ee2..d0c59be8ce 100644 --- a/meteor/server/migration/1_46_0.ts +++ b/meteor/server/migration/1_46_0.ts @@ -2,5 +2,5 @@ import { addMigrationSteps } from './databaseMigration' // Release 46 export const addSteps = addMigrationSteps('1.46.0', [ - // Add some migrations! + // >> No migration steps in this release << ]) diff --git a/meteor/server/migration/1_47_0.ts b/meteor/server/migration/1_47_0.ts index b001e89e37..4a2b2bf4b6 100644 --- a/meteor/server/migration/1_47_0.ts +++ b/meteor/server/migration/1_47_0.ts @@ -8,6 +8,7 @@ import { getRandomString, normalizeArray } from '@sofie-automation/corelib/dist/ import { IBlueprintConfig, IOutputLayer, ISourceLayer, SomeAction } from '@sofie-automation/blueprints-integration' import { ShowStyleBases, ShowStyleVariants, Studios, TriggeredActions } from '../collections' +// Release 47 interface StudioOld { mappings: MappingsExt blueprintConfig: IBlueprintConfig @@ -33,18 +34,7 @@ function normalizeArrayRandomId(array: Array): { [indexKey: string]: T } { return normalizedObject as { [key: string]: T } } -/* - * ************************************************************************************** - * - * These migrations are destined for the next release - * - * (This file is to be renamed to the correct version number when doing the release) - * - * ************************************************************************************** - */ - export const addSteps = addMigrationSteps('1.47.0', [ - // Add some migrations! { id: `Studios generate *withOverrides`, canBeRunAutomatically: true, diff --git a/meteor/server/migration/1_48_0.ts b/meteor/server/migration/1_48_0.ts index 492860f498..f663d9cffa 100644 --- a/meteor/server/migration/1_48_0.ts +++ b/meteor/server/migration/1_48_0.ts @@ -5,18 +5,9 @@ import { BlueprintId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { TranslationsBundles } from '../collections' -/* - * ************************************************************************************** - * - * These migrations are destined for the next release - * - * (This file is to be renamed to the correct version number when doing the release) - * - * ************************************************************************************** - */ +// Release 48 export const addSteps = addMigrationSteps('1.48.0', [ - // Add some migrations! { id: `TranslationBundles originId`, canBeRunAutomatically: true, diff --git a/meteor/server/migration/1_49_0.ts b/meteor/server/migration/1_49_0.ts index a5c022f78b..d2016e2210 100644 --- a/meteor/server/migration/1_49_0.ts +++ b/meteor/server/migration/1_49_0.ts @@ -2,15 +2,7 @@ import { addMigrationSteps } from './databaseMigration' import { Blueprints, ShowStyleVariants } from '../collections' import { getRandomId } from '@sofie-automation/corelib/dist/lib' -/* - * ************************************************************************************** - * - * These migrations are destined for the next release - * - * (This file is to be renamed to the correct version number when doing the release) - * - * ************************************************************************************** - */ +// Release 49 export const addSteps = addMigrationSteps('1.49.0', [ { diff --git a/meteor/server/migration/1_50_0.ts b/meteor/server/migration/1_50_0.ts index dfb56eb5d3..a804a72ad7 100644 --- a/meteor/server/migration/1_50_0.ts +++ b/meteor/server/migration/1_50_0.ts @@ -39,15 +39,7 @@ import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' -/* - * ************************************************************************************** - * - * These migrations are destined for the next release - * - * (This file is to be renamed to the correct version number when doing the release) - * - * ************************************************************************************** - */ +// Release 50 const mappingBaseOptions: Array = [ '_id' as any, diff --git a/meteor/server/migration/1_51_0.ts b/meteor/server/migration/1_51_0.ts new file mode 100644 index 0000000000..7b49f69518 --- /dev/null +++ b/meteor/server/migration/1_51_0.ts @@ -0,0 +1,95 @@ +import { addMigrationSteps } from './databaseMigration' + +import { Rundowns } from '../collections' +import { RundownOrphanedReason, RundownSource } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { PeripheralDeviceId, RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' + +// Release 51 + +interface RemovedRundownProps { + /** The peripheral device the rundown originates from */ + peripheralDeviceId?: PeripheralDeviceId + restoredFromSnapshotId?: RundownId + externalNRCSName: string +} + +export const addSteps = addMigrationSteps('1.51.0', [ + { + id: `Rundowns without source`, + canBeRunAutomatically: true, + validate: async () => { + const objects = await Rundowns.findFetchAsync({ + source: { $exists: false }, + }) + + if (objects.length > 0) { + return `object needs to be updated` + } + return false + }, + migrate: async () => { + const objects = await Rundowns.findFetchAsync({ + source: { $exists: false }, + }) + for (const obj of objects) { + const oldPartialObj = obj as any as RemovedRundownProps + + let newSource: RundownSource = { + type: 'http', // Fallback + } + if (oldPartialObj.peripheralDeviceId) { + newSource = { + type: 'nrcs', + peripheralDeviceId: oldPartialObj.peripheralDeviceId, + nrcsName: oldPartialObj.externalNRCSName, + } + } else if (oldPartialObj.restoredFromSnapshotId) { + newSource = { + type: 'snapshot', + rundownId: oldPartialObj.restoredFromSnapshotId, + } + } + + await Rundowns.mutableCollection.updateAsync(obj._id, { + $set: { + source: newSource, + }, + $unset: { + peripheralDeviceId: 1, + externalNrcsName: 1, + restoredFromSnapshotId: 1, + }, + }) + } + }, + }, + { + id: `Rundowns remove orphaned FROM_SNAPSHOT`, + canBeRunAutomatically: true, + validate: async () => { + const objects = await Rundowns.findFetchAsync({ + orphaned: 'from-snapshot' as any, + }) + + if (objects.length > 0) { + return `object needs to be updated` + } + return false + }, + migrate: async () => { + await Rundowns.mutableCollection.updateAsync( + { + orphaned: 'from-snapshot' as any, + }, + { + $set: { + orphaned: RundownOrphanedReason.DELETED, + }, + }, + { + multi: true, + } + ) + }, + }, +]) diff --git a/meteor/server/migration/X_X_X.ts b/meteor/server/migration/X_X_X.ts index cb88eb6813..ea8bc303e5 100644 --- a/meteor/server/migration/X_X_X.ts +++ b/meteor/server/migration/X_X_X.ts @@ -1,8 +1,5 @@ import { addMigrationSteps } from './databaseMigration' import { CURRENT_SYSTEM_VERSION } from './currentSystemVersion' -import { Rundowns } from '../collections' -import { RundownOrphanedReason, RundownSource } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { PeripheralDeviceId, RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' /* * ************************************************************************************** @@ -14,90 +11,6 @@ import { PeripheralDeviceId, RundownId } from '@sofie-automation/corelib/dist/da * ************************************************************************************** */ -interface RemovedRundownProps { - /** The peripheral device the rundown originates from */ - peripheralDeviceId?: PeripheralDeviceId - restoredFromSnapshotId?: RundownId - externalNRCSName: string -} - export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [ - { - id: `Rundowns without source`, - canBeRunAutomatically: true, - validate: async () => { - const objects = await Rundowns.findFetchAsync({ - source: { $exists: false }, - }) - - if (objects.length > 0) { - return `object needs to be updated` - } - return false - }, - migrate: async () => { - const objects = await Rundowns.findFetchAsync({ - source: { $exists: false }, - }) - for (const obj of objects) { - const oldPartialObj = obj as any as RemovedRundownProps - - let newSource: RundownSource = { - type: 'http', // Fallback - } - if (oldPartialObj.peripheralDeviceId) { - newSource = { - type: 'nrcs', - peripheralDeviceId: oldPartialObj.peripheralDeviceId, - nrcsName: oldPartialObj.externalNRCSName, - } - } else if (oldPartialObj.restoredFromSnapshotId) { - newSource = { - type: 'snapshot', - rundownId: oldPartialObj.restoredFromSnapshotId, - } - } - - await Rundowns.mutableCollection.updateAsync(obj._id, { - $set: { - source: newSource, - }, - $unset: { - peripheralDeviceId: 1, - externalNrcsName: 1, - restoredFromSnapshotId: 1, - }, - }) - } - }, - }, - { - id: `Rundowns remove orphaned FROM_SNAPSHOT`, - canBeRunAutomatically: true, - validate: async () => { - const objects = await Rundowns.findFetchAsync({ - orphaned: 'from-snapshot' as any, - }) - - if (objects.length > 0) { - return `object needs to be updated` - } - return false - }, - migrate: async () => { - await Rundowns.mutableCollection.updateAsync( - { - orphaned: 'from-snapshot' as any, - }, - { - $set: { - orphaned: RundownOrphanedReason.DELETED, - }, - }, - { - multi: true, - } - ) - }, - }, + // Add some migrations here: ]) diff --git a/meteor/server/migration/currentSystemVersion.ts b/meteor/server/migration/currentSystemVersion.ts index 9486960d8b..6a2a3c3b0c 100644 --- a/meteor/server/migration/currentSystemVersion.ts +++ b/meteor/server/migration/currentSystemVersion.ts @@ -49,7 +49,9 @@ * 1.47.0: Release 47 (?) * 1.49.0: Release 49 (?) * 1.50.0: Release 50 (2024-02-23) + * 1.51.0: Release 51 (TBD) + * 1.52.0: Release 52 (TBD) */ // Note: Only set this to release versions, (ie X.Y.Z), not pre-releases (ie X.Y.Z-0-pre-release) -export const CURRENT_SYSTEM_VERSION = '1.51.0' +export const CURRENT_SYSTEM_VERSION = '1.52.0' diff --git a/meteor/server/migration/migrations.ts b/meteor/server/migration/migrations.ts index 0a0b004913..578fd6b955 100644 --- a/meteor/server/migration/migrations.ts +++ b/meteor/server/migration/migrations.ts @@ -35,6 +35,9 @@ addSteps1_49_0() import { addSteps as addSteps1_50_0 } from './1_50_0' addSteps1_50_0() +import { addSteps as addSteps1_51_0 } from './1_51_0' +addSteps1_51_0() + // Migrations for the in-development release: import { addSteps as addStepsX_X_X } from './X_X_X' addStepsX_X_X() From 685594076cc0687baa97430220899c4a9af55b27 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Mon, 19 Aug 2024 10:50:35 +0200 Subject: [PATCH 413/479] chore: 1.51.0-in-testing.0 --- meteor/CHANGELOG.md | 255 ++++++++++++++++++ meteor/package.json | 2 +- meteor/yarn.lock | 12 +- packages/blueprints-integration/CHANGELOG.md | 163 +++++------ packages/blueprints-integration/package.json | 4 +- packages/corelib/package.json | 6 +- packages/documentation/package.json | 2 +- packages/job-worker/package.json | 8 +- packages/lerna.json | 6 +- packages/live-status-gateway/package.json | 10 +- packages/mos-gateway/CHANGELOG.md | 104 ++++--- packages/mos-gateway/package.json | 6 +- packages/openapi/package.json | 2 +- packages/playout-gateway/CHANGELOG.md | 68 +++++ packages/playout-gateway/package.json | 6 +- packages/server-core-integration/CHANGELOG.md | 105 +++----- packages/server-core-integration/package.json | 4 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 38 +-- 19 files changed, 547 insertions(+), 256 deletions(-) diff --git a/meteor/CHANGELOG.md b/meteor/CHANGELOG.md index 8c2e271181..df0291d5b7 100644 --- a/meteor/CHANGELOG.md +++ b/meteor/CHANGELOG.md @@ -2,6 +2,261 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.51.0-in-testing.0](https://github.com/nrkno/sofie-core/compare/v1.50.4...v1.51.0-in-testing.0) (2024-08-19) + + +### Features + +* Ensure peripheralDevice subdevice removal when requested ([#1227](https://github.com/nrkno/sofie-core/issues/1227)) ([d5cafe8](https://github.com/nrkno/sofie-core/commit/d5cafe8db5e453f87f8d46262f23e118b580d4d5)) + + +### Bug Fixes + +* update sorensen dep ([d4cc0f7](https://github.com/nrkno/sofie-core/commit/d4cc0f7d508c0fcbe516be5babcbdf17cca05c9d)) + +### [1.50.5-LSG-updates](https://github.com/nrkno/sofie-core/compare/v1.50.4-LSG-updates...v1.50.5-LSG-updates) (2024-08-08) + + +### Bug Fixes + +* compensate for piece preroll for adlibbed pieces SOFIE-3369 ([#1236](https://github.com/nrkno/sofie-core/issues/1236)) ([195a7d9](https://github.com/nrkno/sofie-core/commit/195a7d98242d6377dac9f1efc69f861fc3b554e5)) +* refactor Rundown orphaned property SOFIE-2963 [#1210](https://github.com/nrkno/sofie-core/issues/1210) ([#1217](https://github.com/nrkno/sofie-core/issues/1217)) ([cf9fa15](https://github.com/nrkno/sofie-core/commit/cf9fa159b7385614a879ee215b3093a7e708bb7d)) +* rundown timing drifting when playing parts with preroll SOFIE-3291 ([#1234](https://github.com/nrkno/sofie-core/issues/1234)) ([beee11a](https://github.com/nrkno/sofie-core/commit/beee11a21a44a3e3e75c8189061b74ae45f9068a)) +* unexpected timeline updates while playing final part in rundown SOFIE-3371 ([#1237](https://github.com/nrkno/sofie-core/issues/1237)) ([0fe74b4](https://github.com/nrkno/sofie-core/commit/0fe74b45d2c09e2e08e942c49dae55ceb1ef698c)) +* use same piece timing as timeline in LSG SOFIE-3305 ([#1219](https://github.com/nrkno/sofie-core/issues/1219)) ([bf48f17](https://github.com/nrkno/sofie-core/commit/bf48f171719452eb6e309964b3779bb8ad2992e9)) + +### [1.50.4-LSG-updates](https://github.com/nrkno/sofie-core/compare/v1.50.3-LSG-updates...v1.50.4-LSG-updates) (2024-07-30) + + +### Features + +* create adlib testing rundowns SOFIE-2963 ([#1211](https://github.com/nrkno/sofie-core/issues/1211)) ([a831989](https://github.com/nrkno/sofie-core/commit/a8319898c73d7233a2a665fd0bf5b9d6da83d713)) +* move `contentStatus` to separate property ([02d49bd](https://github.com/nrkno/sofie-core/commit/02d49bd30b7b2ae05ffd143986b119470c75249b)) +* Rundown `source` property SOFIE-2963 ([#1210](https://github.com/nrkno/sofie-core/issues/1210)) ([de6ee9b](https://github.com/nrkno/sofie-core/commit/de6ee9b3590cbdb6270c291cc41ad15c23652726)) + + +### Bug Fixes + +* `Cannot replace infinite PieceInstance` during normal operation SOFIE-3305 ([13264bb](https://github.com/nrkno/sofie-core/commit/13264bb5a48056d895f9bd206437554b3b2499fd)) +* further improve stringifyError ([0233073](https://github.com/nrkno/sofie-core/commit/02330735fd4f3b03f9bff84e4c41fb9199a4623e)) +* improve error logging: use stringifyError() ([8da63de](https://github.com/nrkno/sofie-core/commit/8da63dec44915439ea436eee9697f3774241537b)) +* **LSG:** fix async-api generation ([faaf202](https://github.com/nrkno/sofie-core/commit/faaf202703fe6c7829cd2216882ed96c6d2a1e26)) +* **LSG:** make AsyncAPI stuff build again ([53bea2a](https://github.com/nrkno/sofie-core/commit/53bea2a75f366f0c2ef487da13195fb6f7f70a36)) +* **LSG:** Token "examples" does not exist when running `yarn gendocs` ([87065d7](https://github.com/nrkno/sofie-core/commit/87065d7af229cd2e3bf1db1f9217d6d8b8945fee)) +* **LSG:** Token "examples" does not exist when running `yarn gendocs` ([a0ea9b4](https://github.com/nrkno/sofie-core/commit/a0ea9b48997fe39e8db5ababef744c103a68509f)) +* make stringifyError handle UserError better ([e2ecc7e](https://github.com/nrkno/sofie-core/commit/e2ecc7eb48b9ad6c0d95b299d954abac577e70a7)) +* move `UIDeviceTriggerPreview` to remove cross-boundary access ([1de77fc](https://github.com/nrkno/sofie-core/commit/1de77fc7544559c8425bc03bca4c98f22eb8b99a)) +* refactor VirtualElement to be a FC ([0dd6a1f](https://github.com/nrkno/sofie-core/commit/0dd6a1f61be57f0177b0b1953c489ee9c28f9215)) +* remove remaining usages of `withMediaObjectStatus` ([598b932](https://github.com/nrkno/sofie-core/commit/598b93201c09ef0a8860ebf2b0886e0f3c0d3519)) + +### [1.50.3-LSG-updates](https://github.com/nrkno/sofie-core/compare/v1.50.3...v1.50.3-LSG-updates) (2024-06-24) + + +### ⚠ BREAKING CHANGES + +* remove `metaData` from various interfaces +* Remove unused config manifest types + +### Features + +* A/B player Ids as strings ([#1054](https://github.com/nrkno/sofie-core/issues/1054)) ([d9dee47](https://github.com/nrkno/sofie-core/commit/d9dee473068c4d96d15a6ef799ace830cf1fe1c0)) +* a11ly WIP ([3df556c](https://github.com/nrkno/sofie-core/commit/3df556c35ece15d1f3b7c192bda5c73422af3952)) +* **a11ly:** add :focus-visible outline ([3adcd49](https://github.com/nrkno/sofie-core/commit/3adcd49cbb3e42c3371e41abb8cdbd59bf43a9fd)) +* **a11ly:** replace systemActionKeys with stopPropagation on action listeners ([0fe2d24](https://github.com/nrkno/sofie-core/commit/0fe2d24152236b79e7b99e8e95502ff4d7a444d8)) +* **a11y:** add role properties to the RundownList components ([91113fc](https://github.com/nrkno/sofie-core/commit/91113fc6ee3706c583efc4b9641485c0fd03f4aa)) +* **a11y:** make buttons into buttons, add keyboard handler for shelf Handle ([44bfca2](https://github.com/nrkno/sofie-core/commit/44bfca293fec139fb0771261283a51db803968ca)) +* action triggers styleClassNames SOFIE-3138 ([#1192](https://github.com/nrkno/sofie-core/issues/1192)) ([fa306dc](https://github.com/nrkno/sofie-core/commit/fa306dc16d5f6ee31418e282180d4c704f5d9feb)) +* action triggers styleClassNames SOFIE-3138 ([#1192](https://github.com/nrkno/sofie-core/issues/1192)) ([49274d6](https://github.com/nrkno/sofie-core/commit/49274d64bdc0532f294be34b0a4413d848321e40)) +* add "processing" warning notification ([d123037](https://github.com/nrkno/sofie-core/commit/d123037ec455743d3e5fbe54a886a5cc7d287dd1)) +* add import/export support to config object tables SOFIE-3138 ([#1194](https://github.com/nrkno/sofie-core/issues/1194)) ([306d31e](https://github.com/nrkno/sofie-core/commit/306d31e03a63539035fce9d8f3f303122c13cbb3)) +* add import/export support to config object tables SOFIE-3138 ([#1194](https://github.com/nrkno/sofie-core/issues/1194)) ([3daaff0](https://github.com/nrkno/sofie-core/commit/3daaff052db261abdccdb30e170dbe2f91ecf2d8)) +* add onTake and onSetAsNext blueprint callbacks SOFIE-2897 SOFIE-2808 ([#1117](https://github.com/nrkno/sofie-core/issues/1117)) ([0bd621c](https://github.com/nrkno/sofie-core/commit/0bd621c13b08cb20c923ea573baca9db003c7df1)) +* add SourceLayer.STUDIO_SCREEN ([158e660](https://github.com/nrkno/sofie-core/commit/158e6603eda370e97e4b2fdaf23b5df229e8ff4a)) +* allow executing bucket adlibs by `externalId` ([af4e5e2](https://github.com/nrkno/sofie-core/commit/af4e5e2fe83d01afb3fb28c5b841e571a345bc0b)) +* backport of release51 live-status-gateway onto release50 ([0a87a95](https://github.com/nrkno/sofie-core/commit/0a87a9519ca1f344429e9b4d47a44c1a9acddff2)) +* base64 image input control SOFIE-3138 ([#1191](https://github.com/nrkno/sofie-core/issues/1191)) ([1a6f2ff](https://github.com/nrkno/sofie-core/commit/1a6f2ff51c98a80a6049959a68a15f5a4bbf7739)) +* base64 image input control SOFIE-3138 ([#1191](https://github.com/nrkno/sofie-core/issues/1191)) ([3a8ef45](https://github.com/nrkno/sofie-core/commit/3a8ef454b3675a56b1f05ce08c19d48eb3eacce1)) +* blueprint config fixup step SOFIE-2258 ([#1050](https://github.com/nrkno/sofie-core/issues/1050)) ([ea6c343](https://github.com/nrkno/sofie-core/commit/ea6c3435079172c682dd1e23d7813ae33fcdee78)) +* break out of maintain focus on wheel event ([bfcf08e](https://github.com/nrkno/sofie-core/commit/bfcf08e2fe2629b9c25a6c8a1fce256858d86e00)) +* Buckets in the HTTP API (SOFIE-2795) ([#1070](https://github.com/nrkno/sofie-core/issues/1070)) ([5d94d8a](https://github.com/nrkno/sofie-core/commit/5d94d8a0f3d0ef09894ccb03e1987638d1ad3573)) +* change sort icons ([6ee9282](https://github.com/nrkno/sofie-core/commit/6ee928230463eedfe496cc5f6c3951e72e1ef744)) +* check if segment should be immediately regenerated during ingest SOFIE-2887 ([64fb1da](https://github.com/nrkno/sofie-core/commit/64fb1da27e041e91a9faf70a30d602f017131764)) +* cleanup deprecations ([#996](https://github.com/nrkno/sofie-core/issues/996)) ([f04f18f](https://github.com/nrkno/sofie-core/commit/f04f18f8e94b706dd1a3da1525feb9787f6eb85f)) +* **Device Triggers:** add support for setting shift register operations ([aa19fcc](https://github.com/nrkno/sofie-core/commit/aa19fcc3b5bfad05158677701b838673578a4dcb)) +* **Device Triggers:** add support for setting shift register operations SOFIE-3136 ([c5a6292](https://github.com/nrkno/sofie-core/commit/c5a6292a85821d67653eebef75bbd929ff0cd44d)) +* document publications and remove mongo queries from parameters SOFIE-1183 ([#1062](https://github.com/nrkno/sofie-core/issues/1062)) ([17bb2e3](https://github.com/nrkno/sofie-core/commit/17bb2e3225db22505244b98a0c9c95fe54bef083)) +* Dynamic message in Evaluation form SOFIE-2766 ([#1101](https://github.com/nrkno/sofie-core/issues/1101)) ([2dc6b42](https://github.com/nrkno/sofie-core/commit/2dc6b4245fc416d87147b7b49d7dd5e52908af6c)) +* **EAV-31:** add blueprintsData on adlib actions ([114c064](https://github.com/nrkno/sofie-core/commit/114c06466ff1f35660454b66d1e35b3d339b9a80)) +* Editable fields as JSON ([ddfb66a](https://github.com/nrkno/sofie-core/commit/ddfb66a1ef8afbd507535037d12752742c7dfeda)) +* have both part and partInstance timings in rundownTiming context ([91c07c8](https://github.com/nrkno/sofie-core/commit/91c07c8410039c0527f3b09f11a5b9e64cc7737f)) +* have withResolvedSegment return Parts in correct order ([7362b52](https://github.com/nrkno/sofie-core/commit/7362b5216d89b8ed6061448ac73b30393c8caa57)) +* implement `loop` content property for Graphics ([2cf38ba](https://github.com/nrkno/sofie-core/commit/2cf38baadd5e7ba16d624b9ad75cfff9d48297a6)) +* implement partInstance-based timing calculator input ([6d77853](https://github.com/nrkno/sofie-core/commit/6d77853170b9da0db69c54ce0d63f4a6a874df95)) +* introduce `privateData` and `publicData` ([d94f1aa](https://github.com/nrkno/sofie-core/commit/d94f1aa82cec779c7eebddae42e1c4fab91ac494)) +* **live-status-gateway:** add partId and segmentId to adLibs ([a51e9f7](https://github.com/nrkno/sofie-core/commit/a51e9f7e6d0a3eb9b12b7d0c2073fba2892fd90e)) +* **live-status-gw:** expose publicData ([a7ab6e4](https://github.com/nrkno/sofie-core/commit/a7ab6e428b05e678d3def3df124b76d35040942b)) +* **LSG:** sort adlibs to match GUI ([167a625](https://github.com/nrkno/sofie-core/commit/167a6252fa7036ae5c1239f8af35292c8147e7de)) +* Media Status WIP ([fa3747e](https://github.com/nrkno/sofie-core/commit/fa3747ec1cc7965ecc12670022844513fea212e2)) +* Media Status WIP ([65f543a](https://github.com/nrkno/sofie-core/commit/65f543ad59d218dd5336bb293fa6621c3703f8a4)) +* MediaStatus WIP ([4468458](https://github.com/nrkno/sofie-core/commit/4468458be41fe975c687ef44cad1a6e12f562b6b)) +* **MediaStatus:** filter on part and segment identifiers, but require to start from filter string ([d407a9c](https://github.com/nrkno/sofie-core/commit/d407a9c0c64effbb45ae19503666bf911a11db50)) +* **MediaStatusList:** finish CSS ([4d081ab](https://github.com/nrkno/sofie-core/commit/4d081ab8c3252067d87a6e5f40b4309076edd99a)) +* **MediaStatusList:** search box clear button ([7e4edfd](https://github.com/nrkno/sofie-core/commit/7e4edfd6963f63916d2c4fe0d766896919c9a654)) +* **MediaStatusPopUp/MediaStatus:** add filter debounce for a better typing experience ([c312b43](https://github.com/nrkno/sofie-core/commit/c312b43a88b9b451402919fef0ff0d488f9d252b)) +* **MediaStatusPopUp:** finish styling, icons, etc. ([5b5c1c2](https://github.com/nrkno/sofie-core/commit/5b5c1c22f2998b05ec0aa9ff9ea171ae253145fa)) +* **MediaStatusPopUp:** WIP ([fbb0bc7](https://github.com/nrkno/sofie-core/commit/fbb0bc7e622afd6309b020fcc48c9f4469f37da9)) +* **MediaStatusPopUp:** WIP ([8e2ce74](https://github.com/nrkno/sofie-core/commit/8e2ce7430f060fefb0937ee11e67658765bef918)) +* **MediaStatus:** Rundown panel WIP ([e574bbb](https://github.com/nrkno/sofie-core/commit/e574bbb09e62e34ddacea80d326e3087e539edec)) +* **MediaStatus:** WIP ([e8acba2](https://github.com/nrkno/sofie-core/commit/e8acba2d43f7da396fb2d15fdb90e19fe9872fb6)) +* new style for step GFX pill ([869d301](https://github.com/nrkno/sofie-core/commit/869d3012bd3bbf891e9c701f23f42e31cb3e7ff0)) +* publication for blueprint upgrade status SOFIE-2258 ([#1049](https://github.com/nrkno/sofie-core/issues/1049)) ([48dbda2](https://github.com/nrkno/sofie-core/commit/48dbda2573bc9b48f252d3220c8d154c352cc255)) +* refactor server-core-integration subscription handling to reduce duplication ([8eaedd2](https://github.com/nrkno/sofie-core/commit/8eaedd22e8efb9750f00ff472301c6b3f2d0f0af)) +* remove some usages of `DbCacheWriteCollection` SOFIE-2672 ([#1106](https://github.com/nrkno/sofie-core/issues/1106)) ([ff1f730](https://github.com/nrkno/sofie-core/commit/ff1f730dacf82154e809e01766974e6955c29cf7)) +* remove supertimeline from Parts and Pieces resolving SOFIE-2373 ([#983](https://github.com/nrkno/sofie-core/issues/983)) ([55e02cb](https://github.com/nrkno/sofie-core/commit/55e02cb1a0b6d237ae9bcd02ebf54e06cf08fb26)) +* rename internal 'scratchpad' naming to 'adlib testing' SOFIE-3015 ([afb2f43](https://github.com/nrkno/sofie-core/commit/afb2f43c8f199ff5da4ea6c3ccc3bd1e97a80f4f)) +* reorder elements in Media Status lists ([4e77241](https://github.com/nrkno/sofie-core/commit/4e77241080b7e20320607ea74e4566ca4a76970b)) +* replace user facing 'scratchpad' references to 'rehearsal mode' and 'adlib testing' SOFIE-3015 ([251b1cc](https://github.com/nrkno/sofie-core/commit/251b1ccd83c0f3686fa3dc698002e758c00488bd)) +* Rework `CacheForIngest` to `IngestModel` SOFIE-2672 ([#1108](https://github.com/nrkno/sofie-core/issues/1108)) ([825b523](https://github.com/nrkno/sofie-core/commit/825b5231d7afe6fd13393b44e4f39a41e6093adb)) +* rework CacheForPlayout as structured objects SOFIE-2513 ([d28511a](https://github.com/nrkno/sofie-core/commit/d28511a0501254d9c432cf0a751b8b43e86f3661)) +* rework CacheForPlayout as structured objects SOFIE-2513 ([2b032f3](https://github.com/nrkno/sofie-core/commit/2b032f3cb695fb5fb361a016cdb542a309097c1f)) +* rework CacheForPlayout as structured objects SOFIE-2513 ([f2281bd](https://github.com/nrkno/sofie-core/commit/f2281bda6e61808e4417edeb1a7cba834e752cbd)) +* rework CacheForPlayout merge conflicts SOFIE-2513 ([dff6d2c](https://github.com/nrkno/sofie-core/commit/dff6d2ce87da66adf43662aabdd5a0f007058181)) +* rundown scratchpad SOFIE-2432 ([5e16632](https://github.com/nrkno/sofie-core/commit/5e16632908b8dd62e4470a9ba6089a56aa571b9f)) +* **RundownView:** add an h1 element for the playlist name ([b9292e6](https://github.com/nrkno/sofie-core/commit/b9292e6fbb09fe54b52a151549fb25fb29e658a6)) +* support packageinfo update flow for buckets and studio baseline SOFIE-2655 ([#1051](https://github.com/nrkno/sofie-core/issues/1051)) ([df7ed0c](https://github.com/nrkno/sofie-core/commit/df7ed0c653b897774f01a33c9e60a5e22fac99e4)) +* typed publications in gateways/peripheraldevices SOFIE-1183 ([#1056](https://github.com/nrkno/sofie-core/issues/1056)) ([0c3c1bf](https://github.com/nrkno/sofie-core/commit/0c3c1bfd2bb779034976dc34e49aa6e664ea874b)) +* update meteor to 2.12 SOFIE-2368 ([#931](https://github.com/nrkno/sofie-core/issues/931)) ([d7dfb71](https://github.com/nrkno/sofie-core/commit/d7dfb71d19405267cab5e2abc39794a80acb30b1)) +* upgrades docusaurus to 3.2.1 ([b025be4](https://github.com/nrkno/sofie-core/commit/b025be4c43a7742745427d6da4306086e053f657)) + + +### Bug Fixes + +* `DataCloneError: [object Object] could not be cloned` when propogating some errors from job-worker SOFIE-2903 ([7f618c7](https://github.com/nrkno/sofie-core/commit/7f618c75bb6a94e28e4a9c00db4688a77a1bc184)) +* `DataCloneError: [object Object] could not be cloned` when propogating some errors from job-worker SOFIE-2903 ([c1bb47f](https://github.com/nrkno/sofie-core/commit/c1bb47fe3958c245b708f9698c458b3d94055a3b)) +* `UIBlueprintUpgradeStatuses` crashing if no playlists in the system ([3491ec5](https://github.com/nrkno/sofie-core/commit/3491ec5c509adf6b83362973821d93a66595e828)) +* **a11y/ModalDialog:** reduce behavioral changes after refactoring ([02ecaad](https://github.com/nrkno/sofie-core/commit/02ecaad0a210e1f7f8b7c932d38cbe7ba097f7b1)) +* **a11y:** some button layout/tabbing order ([3bd2085](https://github.com/nrkno/sofie-core/commit/3bd2085417b9a32bf148cb30640a8eb9d965ebea)) +* **a11y:** trap focus within ModalDialog ([9edd110](https://github.com/nrkno/sofie-core/commit/9edd11075111471b5a72b64dc6b3210183ef6911)) +* action triggers disappearing when adlibbing and immediately taking a part [#1197](https://github.com/nrkno/sofie-core/issues/1197) SOFIE-3182 ([d01cd99](https://github.com/nrkno/sofie-core/commit/d01cd991865485d97435ffb1065c574653bf508f)) +* action triggers disappearing when adlibbing and immediately taking a part [#1197](https://github.com/nrkno/sofie-core/issues/1197) SOFIE-3182 ([1a44ff8](https://github.com/nrkno/sofie-core/commit/1a44ff8bea971922fc55fbb03686467e9bd3b0c9)) +* activate scratchpad segment failing ([3cd426f](https://github.com/nrkno/sofie-core/commit/3cd426ff7e6d7b28aef65bfb31f4874c34bae509)) +* add `count` method to job-worker `ICollection` ([9f6be1d](https://github.com/nrkno/sofie-core/commit/9f6be1d51fa46d7853f2c95c48fcb8298306db98)) +* add `RundownOrphanedReason` enum ([b4648d2](https://github.com/nrkno/sofie-core/commit/b4648d2e03cce09eb1fff8e3b9292dc1c8739801)) +* add a way to take a heap snapshot. ([ef918c2](https://github.com/nrkno/sofie-core/commit/ef918c2390c8af46caa1cd0ff08bab45af2f8380)) +* Add missing metadata types ([71eb739](https://github.com/nrkno/sofie-core/commit/71eb739bc87f64db50f06ab4647cb6972e58c7fa)) +* Add missing piece metadata types ([#1133](https://github.com/nrkno/sofie-core/issues/1133)) ([70c5739](https://github.com/nrkno/sofie-core/commit/70c5739c5e9848e57c21bde78ee16d4c78da8e1a)) +* add missing SourceLayerType cases ([45eefce](https://github.com/nrkno/sofie-core/commit/45eefcef737e8e8a5a7e425f9fdd713487ab4a59)) +* add missing SourceLayerType cases ([1c4bd0a](https://github.com/nrkno/sofie-core/commit/1c4bd0a36885cb582af88c2750b155de9b23fa2a)) +* add PromiseButton component ([4fa9f72](https://github.com/nrkno/sofie-core/commit/4fa9f72fe277c55449b28384abd77de538a99ea2)) +* **AfterBroadcastForm:** refactor the form to have a semantically-correct layout ([9e4f2b6](https://github.com/nrkno/sofie-core/commit/9e4f2b65fd2e30d04bae1cd37304a33ab744fc11)) +* allow blueprint assets to be gif SOFIE-3183 ([1d8bed7](https://github.com/nrkno/sofie-core/commit/1d8bed77e4262a12bba46eecf0631cbcae96fb34)) +* allow blueprint assets to be gif SOFIE-3183 ([afc20d8](https://github.com/nrkno/sofie-core/commit/afc20d87f62cf0098510f6ca0ce2c663d8154778)) +* allways show trigger popups ([68172c5](https://github.com/nrkno/sofie-core/commit/68172c565af3aaea7a2517abc9623b2eefd77fba)) +* also sort on "own" rank ([4224b02](https://github.com/nrkno/sofie-core/commit/4224b02be70851ca2058d85c73571d9085cbf97f)) +* AMQP connection issues logging SOFIE-2597 ([#1066](https://github.com/nrkno/sofie-core/issues/1066)) ([de04512](https://github.com/nrkno/sofie-core/commit/de045127a0b64e67d31bba7dbe42f4e0f5caadaf)) +* asyncapi generation ([e4d45b6](https://github.com/nrkno/sofie-core/commit/e4d45b6e5142e8ae71a75edf0fd5dcbeeec86f45)) +* avoid adding `originalId` to timeline-objects during lookahead ([282cbec](https://github.com/nrkno/sofie-core/commit/282cbec6e9de1948bbb6d82ccd3f0f56efa7fc64)) +* blueprint-integration missed typing in [#1077](https://github.com/nrkno/sofie-core/issues/1077) ([bd5e36f](https://github.com/nrkno/sofie-core/commit/bd5e36f9c0b390999ef465a5e1554b8faa2c456f)) +* broken link ([08e5130](https://github.com/nrkno/sofie-core/commit/08e513048d9b4660dd89ae37c1ae29a20ab48880)) +* bucket selectors and unhandled promises ([26ae99e](https://github.com/nrkno/sofie-core/commit/26ae99ed00b5f2310439697a1a3b59d44be42f46)) +* build errors ([020d3cf](https://github.com/nrkno/sofie-core/commit/020d3cfd2c112b57667d6612d19472570f8455b9)) +* build server-core image on alpine 3.19 ([#1160](https://github.com/nrkno/sofie-core/issues/1160)) ([0f73bed](https://github.com/nrkno/sofie-core/commit/0f73bedbeb0490417c0643e521465a8112c2e444)) +* **CameraView:** broken countdowns after RundownTimingCalculator modifications ([d5ed171](https://github.com/nrkno/sofie-core/commit/d5ed171f8ab99ddea16707c7a34e51dae0f71ea8)) +* clear Scratchpad when activating Rundown ([b629fe9](https://github.com/nrkno/sofie-core/commit/b629fe95ccac283c0229d7e0753d330455e795d7)) +* **Core System Settings:** Cron Job Settings are duplicated ([1b52f5e](https://github.com/nrkno/sofie-core/commit/1b52f5ecf1a7397182b476c6b76a1991697e1185)) +* core-integration: handle error when subscribing ([3ad0c73](https://github.com/nrkno/sofie-core/commit/3ad0c73dd7fa4d9572cc00a7ffa9987fed0df4e3)) +* create new mosTimes correctly ([e8cec88](https://github.com/nrkno/sofie-core/commit/e8cec88f8b9d8c298f1b2ddec8d45f6f80fea9bf)) +* **DashboardPieceButton:** hover previews are not positioned correctly ([e5a022a](https://github.com/nrkno/sofie-core/commit/e5a022a7f2b8b0e9abeb792d48bab741beeac567)) +* **DashboardPieceButton:** hover previews are not positioned correctly ([8f8d147](https://github.com/nrkno/sofie-core/commit/8f8d147a42b3e80f87f9d9f575f81a048521d989)) +* **DeviceTriggers:** improve code clarity ([a1dddb9](https://github.com/nrkno/sofie-core/commit/a1dddb9d817b644ff66f2d1ef0061daf769b4f13)) +* display adjustments ([bcfeb99](https://github.com/nrkno/sofie-core/commit/bcfeb99bc82c5dd9d849efdb1ea85d3a9ce20501)) +* displayDurationGroup calculations ([0c07924](https://github.com/nrkno/sofie-core/commit/0c0792460ed1a73beaf4bd754e1ea1adf46167b6)) +* don't log UserError messages at error level from the job-worker SOFIE-2592 ([#1026](https://github.com/nrkno/sofie-core/issues/1026)) ([a146d65](https://github.com/nrkno/sofie-core/commit/a146d65ea9adf14dea92c62923fdadce85306fc5)) +* **EAV-95:** interface inconsistencies ([cc3044b](https://github.com/nrkno/sofie-core/commit/cc3044b15733df599f6e3cc80133c73f8c0b3133)) +* emit ddp publciation remove messages before add SOFIE-3172 ([1b17a1d](https://github.com/nrkno/sofie-core/commit/1b17a1d946fb0b23b720b110e1f714324901dde5)) +* emit ddp publciation remove messages before add SOFIE-3172 ([348a6e6](https://github.com/nrkno/sofie-core/commit/348a6e60b23e7ef6ed8117740915f32654ce8c1e)) +* ensure that Shift register Ids are only positive integers ([16895bc](https://github.com/nrkno/sofie-core/commit/16895bc41e8b8571109a915f48846f6bd52d522e)) +* expose metaData as publicData as a temporary solution for R50 ([aa79096](https://github.com/nrkno/sofie-core/commit/aa790960e86f850baec1842c80e136fad2ed9e67)) +* expose metaData propery to liveStatusGateway ([596ab8d](https://github.com/nrkno/sofie-core/commit/596ab8ddbde3d590b62558c80ef675f24d1ad28c)) +* extract `PlayoutPieceInstanceModel` from `PlayoutPartInstanceModel` ([9e07ed9](https://github.com/nrkno/sofie-core/commit/9e07ed9bb8851f84364753173a7053102cc0dac5)) +* filter out virtual pieces in isPieceInstanceActive ([188c985](https://github.com/nrkno/sofie-core/commit/188c9855ce4398076960daa8a14f0f4009b145eb)) +* group sends together, for increased performance ([fb0d2c8](https://github.com/nrkno/sofie-core/commit/fb0d2c8ef2e316bfe53cfa7160711101ede29ac4)) +* **HelpPanel:** invalid child React error ([5682379](https://github.com/nrkno/sofie-core/commit/5682379d85df92b1b1a6d2f08e1a485f701387e1)) +* hide create playlist drop box when dragging a rundown not in a multi-rundown playlist SOFIE-2462 ([#1181](https://github.com/nrkno/sofie-core/issues/1181)) ([79ae7f4](https://github.com/nrkno/sofie-core/commit/79ae7f4207459310a84778b8fe1474c05c12ce89)) +* improve UX of object table, to indicate which rows have overrides and are custom ([68b1374](https://github.com/nrkno/sofie-core/commit/68b137489b1f3cfb6af290fe7ae542ac8448f50c)) +* improve UX of object table, to indicate which rows have overrides and are custom ([c39311d](https://github.com/nrkno/sofie-core/commit/c39311d4a8b194cde683a112b55ff10a76b1b8a3)) +* include timestamp when logging to file ([#1207](https://github.com/nrkno/sofie-core/issues/1207)) ([2dcca21](https://github.com/nrkno/sofie-core/commit/2dcca215403cc0256c78f5f8f5f6befa2a1ca683)) +* IngestDataCache not being populated correctly SOFIE-2672 [#1106](https://github.com/nrkno/sofie-core/issues/1106) ([33394a7](https://github.com/nrkno/sofie-core/commit/33394a7e494e1e2a32d8f8cc55b34df6e52747fb)) +* introduce MeteorApply as alternative to Meteor.apply, to log when a method is sent late, due to other blocking methods on the client. ([0425d8f](https://github.com/nrkno/sofie-core/commit/0425d8f5a8b289922d146d0fe5241e08ade06935)) +* invalidate deviceTriggerPreviews when the filterChain changes SOFIE-3172 ([c4e2ad0](https://github.com/nrkno/sofie-core/commit/c4e2ad0282a7b683f3d19b8b9b5b541bf6518cbe)) +* invalidate deviceTriggerPreviews when the filterChain changes SOFIE-3172 ([082f334](https://github.com/nrkno/sofie-core/commit/082f3344fe19f080489dbde8ff155513a1c203b8)) +* invalidate deviceTriggers when changing triggermode SOFIE-3253 ([50e8cd2](https://github.com/nrkno/sofie-core/commit/50e8cd26b8da3394eaa9f5fd9909d3f5d8dfe73c)) +* live status gateway including pieces which have been pruned and will never be played SOFIE-3301 ([#1206](https://github.com/nrkno/sofie-core/issues/1206)) ([65d0d35](https://github.com/nrkno/sofie-core/commit/65d0d35c586e080a08f5837e18e4583ef940770c)) +* live status gateway including pieces which have been pruned and will never be played SOFIE-3301 ([#1206](https://github.com/nrkno/sofie-core/issues/1206)) ([8682eca](https://github.com/nrkno/sofie-core/commit/8682eca8004aedb092a1dca7f7c856c0814eaf66)) +* live-status-gateway ignored settings from Core, so logLevel ended up always being the default ([58261e5](https://github.com/nrkno/sofie-core/commit/58261e54ffe87e448b0de80b007ddf7a06dd924b)) +* **live-status-gw:** active piece conditions ([89f155c](https://github.com/nrkno/sofie-core/commit/89f155c4ae903920af91cbc586c34eb222946623)) +* **live-status-gw:** adLibs having invalid layers before the first take ([4dd5231](https://github.com/nrkno/sofie-core/commit/4dd5231055cc907cee37453ef723906258d54044)) +* **live-status-gw:** adLibs having invalid layers before the first take ([566e0b8](https://github.com/nrkno/sofie-core/commit/566e0b89952afbd8a2c95703cea4e53b270810e4)) +* **live-status-gw:** missing handlers ([0eb19ac](https://github.com/nrkno/sofie-core/commit/0eb19ac3d8c05e791e7ef910db717731e3af8bde)) +* **live-status-gw:** not sending updates when relevant data has actually changed ([748b7a0](https://github.com/nrkno/sofie-core/commit/748b7a0aca8ef290d58ebb6f56bc5a323b9566a4)) +* **live-status-gw:** prevent incomplete data to arriving at topics ([a90688c](https://github.com/nrkno/sofie-core/commit/a90688ca24175dbeadc0ccd1b0c5a918c97fee6e)) +* **live-status-gw:** wrong pieces in nextPart ([56059f4](https://github.com/nrkno/sofie-core/commit/56059f401a2f3cdfaf62901cae8bd5734e408fb1)) +* looks for the correct property itemSlug on ncsItems ([45d233f](https://github.com/nrkno/sofie-core/commit/45d233f3b71cde6b8c0d09ab53bb1aee3ceca133)) +* **LSG:** further optimize sending of messages ([ae1adeb](https://github.com/nrkno/sofie-core/commit/ae1adeba0c9f992797a59a7a3ec1697897b5bd45)) +* **LSG:** sorting, make getRank return undefined if rank could not be established ([5667c0f](https://github.com/nrkno/sofie-core/commit/5667c0ffc4f28a92f477d4c2927875bec04a4301)) +* make content sorting consistent with Core ([67d5916](https://github.com/nrkno/sofie-core/commit/67d5916f64f0a543ebf8e93caee3243917458bd7)) +* Media Status PopUp panel filtering doesn't work ([471ad59](https://github.com/nrkno/sofie-core/commit/471ad593af5d2679eb1750f864c3984b40b6e82a)) +* **MediaStatus PopUp:** improve scroll to Part ([cc9401a](https://github.com/nrkno/sofie-core/commit/cc9401a9f5abf7ee387c654a729a5e6fc254bd3c)) +* **MediaStatus:** letter spacing ([a6494bd](https://github.com/nrkno/sofie-core/commit/a6494bdc1fe46ab402203a63e3cfbf93bcc11a1e)) +* **MediaStatus:** use lobby playlist sorting in Media Status view ([e8f3734](https://github.com/nrkno/sofie-core/commit/e8f3734ddac305c1b4d41823cb34a9ddb254a2fb)) +* Mounted AdLibs on DeviceTriggers don't refresh correctly after a Rundown Reset operation ([dbfc4a1](https://github.com/nrkno/sofie-core/commit/dbfc4a190019712f248e141074ada74226ef5897)) +* only load packages during ingest which have `listenToPackageInfoUpdates: true` ([fdf189d](https://github.com/nrkno/sofie-core/commit/fdf189dd9a059c72298afcdd8865ddb636e26e20)) +* **OpenAPI:** `showStyleVariant` schema has non-existent property `blueprintId` listed as required ([dc22c58](https://github.com/nrkno/sofie-core/commit/dc22c58e771afff6f591b0ffbadd437627299ecd)) +* **openapi:** buckets definitions and tests ([e502ed1](https://github.com/nrkno/sofie-core/commit/e502ed194ba6a2fa829c10c172fee349157c4d84)) +* **openapi:** incorrect return type ([65539b9](https://github.com/nrkno/sofie-core/commit/65539b93a63d0dccd44c8cd118a4eac1c9496a55)) +* **openapi:** missing and incorrect parameter definition ([abed9ec](https://github.com/nrkno/sofie-core/commit/abed9ecdf7fb4e9a9390cc20f9919faa4a32e198)) +* **openapi:** url parameter name ([4bd22b1](https://github.com/nrkno/sofie-core/commit/4bd22b12ccdca25f837374fd0bdd903ec1bedb8a)) +* Playout ignores 'Play from here' offset SOFIE-2356 ([#1063](https://github.com/nrkno/sofie-core/issues/1063)) ([479154a](https://github.com/nrkno/sofie-core/commit/479154a8029a076f0e76c79efd89922cd286f9e5)) +* **playout-gateway:** improve handling and typing of TSR events (fixes some logging issues) ([26a8613](https://github.com/nrkno/sofie-core/commit/26a8613cd64d7e077dfd7ff2092555c1b7805f4d)) +* PromiseButton: use Spinner ([8098aa2](https://github.com/nrkno/sofie-core/commit/8098aa26aeba35fb50d9ccaeb4faeac4291c31dc)) +* refactor ingest.test.ts to remove dependencies between tests ([#1172](https://github.com/nrkno/sofie-core/issues/1172)) ([2dbe0dd](https://github.com/nrkno/sofie-core/commit/2dbe0dd3e7cc7fc43d06690d608691c3c08c3b4b)) +* refactor part generation, to remove some 'owned' properties from the `replacePart` method signature ([4e3a359](https://github.com/nrkno/sofie-core/commit/4e3a359c4e0216b4b360d2a9d3e9ad439a8bc1da)) +* refactor VirtualElement to be a FC ([bf81baf](https://github.com/nrkno/sofie-core/commit/bf81baf9ff520ad6a9fc9b6378ce6ceeac320645)) +* remove adlib action's publicData from userLog SOFIE-2939 ([#1137](https://github.com/nrkno/sofie-core/issues/1137)) ([d92bf8d](https://github.com/nrkno/sofie-core/commit/d92bf8d030dbfd160cfced407f671fe7e1a58a8e)) +* remove duplicated migration ([c10d27d](https://github.com/nrkno/sofie-core/commit/c10d27d1c57d89d996a9f4addbb260318e7b763b)) +* Remove inspector for editable fields ([66561b8](https://github.com/nrkno/sofie-core/commit/66561b81accf03e5df8267311c171966e95b3a65)) +* rename 'onPresenterScreen' description in GUI ([43dfa10](https://github.com/nrkno/sofie-core/commit/43dfa10874c42a1d0cb2925e06cae52f0d0eec04)) +* replace `vm2` with builtin `vm` SOFIE-2535 ([#1136](https://github.com/nrkno/sofie-core/issues/1136)) ([64bf4d5](https://github.com/nrkno/sofie-core/commit/64bf4d55abd3d16a56c2a43392929ac927585e5a)) +* REST API UserAction log entries are incorrect and in ([813ae80](https://github.com/nrkno/sofie-core/commit/813ae80e2a0d21b7bee07a17365f5f90dfa9753e)) +* REST API UserAction log entries are incorrect and in ([65102f6](https://github.com/nrkno/sofie-core/commit/65102f67cd941c38c268d6edbed502b3f9c02524)) +* restore ALL data from snapshot, for debugging ([2464744](https://github.com/nrkno/sofie-core/commit/24647442de293cb72563635a6ee4f4f029218ee7)) +* **RundownView:** preserve scroll position is not preserved when Segments are moved around before the live Segment ([1b79d82](https://github.com/nrkno/sofie-core/commit/1b79d8211bf176946eb27b58db2fb8ffe29d8a67)) +* **RundownView:** split subscriptions into required and not-required ([8dcd844](https://github.com/nrkno/sofie-core/commit/8dcd844c7e6352d02288e70adaf8ad953f8c7975)) +* **RundownView:** subscriptions are considered ready too soon ([5bfda8f](https://github.com/nrkno/sofie-core/commit/5bfda8ff85ad343ed9728be2f726ff0b6a8bf063)) +* save to database when starting a piece ([6c2a1bf](https://github.com/nrkno/sofie-core/commit/6c2a1bf3c010fe882f0ef91f829ede183d43375a)) +* **SegmentContextMenu:** change the collection of mouse position from mouseUp to mouseDown ([d008bcf](https://github.com/nrkno/sofie-core/commit/d008bcfbe87afdd8ad1f2fe1d0e4654f55445247)) +* **SegmentContextMenu:** don't block play from here on current Part ([6967e9c](https://github.com/nrkno/sofie-core/commit/6967e9c2cd5ef168e67b0375c52f4773c6a8e8c8)) +* **SegmentTimeline:** prevent accidentally opening browser context menu ([4227ba2](https://github.com/nrkno/sofie-core/commit/4227ba20f73d7bcff34771269bdcf4c289147c8f)) +* **ShowStyleVariants:** the additionalProperties and required properties were mis-indented ([4403da5](https://github.com/nrkno/sofie-core/commit/4403da5a26adff9674bdbb699d2ef9a82fd4a1af)) +* simplify splits for DoS-safety ([8b4bb5d](https://github.com/nrkno/sofie-core/commit/8b4bb5d47ab3d0a945a322ec563bb704e57667dd)) +* simplify splits for DoS-safety ([c5d8c7e](https://github.com/nrkno/sofie-core/commit/c5d8c7e7a78c59d316950dcac648fb312284a04f)) +* **SupportPopUp:** invalid child React error ([230cb9a](https://github.com/nrkno/sofie-core/commit/230cb9afcd64aa76d7b1283abbaf80e6f3214640)) +* test ([43763fe](https://github.com/nrkno/sofie-core/commit/43763fe1bdb3228c542de7412291f173b4e1b525)) +* tests ([4f60881](https://github.com/nrkno/sofie-core/commit/4f60881e04392b2c2584c144bc32087dade8d20e)) +* transition piece hover preview SOFIE-3183 ([63939cd](https://github.com/nrkno/sofie-core/commit/63939cd9eea06ffb2d018b918b57dc648a679593)) +* transition piece hover preview SOFIE-3183 ([0fc20d9](https://github.com/nrkno/sofie-core/commit/0fc20d9d0234925498f4939924351907f5f4f347)) +* type fix ([f448092](https://github.com/nrkno/sofie-core/commit/f448092459ce5d5ae26e952a8aa0df966a230c1a)) +* update timeline to v9 ([15ff3ee](https://github.com/nrkno/sofie-core/commit/15ff3eef1760066f6f0a648f441fdd36f37e418b)) +* use helper function to keep the scope of 'unsafe' code small ([c58a6de](https://github.com/nrkno/sofie-core/commit/c58a6de80597373e158316d33661ac0e8a07956a)) +* Use PromiseButton for Take a Snapshot button ([e329935](https://github.com/nrkno/sofie-core/commit/e329935697b1969f5b8a79e2384282f7dfdb428d)) +* userEvent for REST API calls does not look sensible ([cfc178a](https://github.com/nrkno/sofie-core/commit/cfc178a36f21f1ccc98c8bcd208a02ea04d13f3a)) +* userEvent for REST API calls does not look sensible ([70ddd11](https://github.com/nrkno/sofie-core/commit/70ddd11431771b4cb71d74851c3d56161a8a1490)) +* validating studio blueprint config fails with DataCloneError ([d5f9221](https://github.com/nrkno/sofie-core/commit/d5f92213bee771d6cf78e4d51007f27e05b9fa98)) + + +* remove `metaData` from various interfaces ([2a9e416](https://github.com/nrkno/sofie-core/commit/2a9e416b8836857bc645252180098823d4252933)) +* Remove unused config manifest types ([1ec0ea7](https://github.com/nrkno/sofie-core/commit/1ec0ea7773e91fa6fc8c85c96de25a150b093ba0)) + ### [1.50.5](///compare/v1.50.4-LSG-updates...v1.50.5) (2024-08-08) diff --git a/meteor/package.json b/meteor/package.json index f0687a5c05..40309761ff 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -1,6 +1,6 @@ { "name": "automation-core", - "version": "1.51.0-in-development", + "version": "1.51.0-in-testing.0", "private": true, "engines": { "node": ">=14.19.1" diff --git a/meteor/yarn.lock b/meteor/yarn.lock index c890b1bd40..8e58fadf3d 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1321,7 +1321,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/blueprints-integration@portal:../packages/blueprints-integration::locator=automation-core%40workspace%3A." dependencies: - "@sofie-automation/shared-lib": 1.51.0-in-development + "@sofie-automation/shared-lib": 1.51.0-in-testing.0 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: node @@ -1362,8 +1362,8 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/corelib@portal:../packages/corelib::locator=automation-core%40workspace%3A." dependencies: - "@sofie-automation/blueprints-integration": 1.51.0-in-development - "@sofie-automation/shared-lib": 1.51.0-in-development + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.0 + "@sofie-automation/shared-lib": 1.51.0-in-testing.0 fast-clone: ^1.5.13 i18next: ^21.10.0 influx: ^5.9.3 @@ -1394,9 +1394,9 @@ __metadata: resolution: "@sofie-automation/job-worker@portal:../packages/job-worker::locator=automation-core%40workspace%3A." dependencies: "@slack/webhook": ^6.1.0 - "@sofie-automation/blueprints-integration": 1.51.0-in-development - "@sofie-automation/corelib": 1.51.0-in-development - "@sofie-automation/shared-lib": 1.51.0-in-development + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.0 + "@sofie-automation/corelib": 1.51.0-in-testing.0 + "@sofie-automation/shared-lib": 1.51.0-in-testing.0 amqplib: ^0.10.3 deepmerge: ^4.3.1 elastic-apm-node: ^3.51.0 diff --git a/packages/blueprints-integration/CHANGELOG.md b/packages/blueprints-integration/CHANGELOG.md index d29fa50f13..e9fe08dc42 100644 --- a/packages/blueprints-integration/CHANGELOG.md +++ b/packages/blueprints-integration/CHANGELOG.md @@ -3,141 +3,157 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. -## [1.50.5](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.4-LSG-updates...v1.50.5) (2024-08-08) +# [1.51.0-in-testing.0](https://github.com/nrkno/sofie-core/compare/v1.50.4...v1.51.0-in-testing.0) (2024-08-19) -**Note:** Version bump only for package @sofie-automation/blueprints-integration +## [1.50.5-LSG-updates](https://github.com/nrkno/sofie-core/compare/v1.50.4-LSG-updates...v1.50.5-LSG-updates) (2024-08-08) +## [1.50.4-LSG-updates](https://github.com/nrkno/sofie-core/compare/v1.50.3-LSG-updates...v1.50.4-LSG-updates) (2024-07-30) +### Bug Fixes +- Add missing metadata types ([71eb739](https://github.com/nrkno/sofie-core/commit/71eb739bc87f64db50f06ab4647cb6972e58c7fa)) +- Add missing piece metadata types ([#1133](https://github.com/nrkno/sofie-core/issues/1133)) ([70c5739](https://github.com/nrkno/sofie-core/commit/70c5739c5e9848e57c21bde78ee16d4c78da8e1a)) +- blueprint-integration missed typing in [#1077](https://github.com/nrkno/sofie-core/issues/1077) ([bd5e36f](https://github.com/nrkno/sofie-core/commit/bd5e36f9c0b390999ef465a5e1554b8faa2c456f)) +### Features -## [1.50.4](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.3-LSG-updates...v1.50.4) (2024-07-30) +- A/B player Ids as strings ([#1054](https://github.com/nrkno/sofie-core/issues/1054)) ([d9dee47](https://github.com/nrkno/sofie-core/commit/d9dee473068c4d96d15a6ef799ace830cf1fe1c0)) +- action triggers styleClassNames SOFIE-3138 ([#1192](https://github.com/nrkno/sofie-core/issues/1192)) ([fa306dc](https://github.com/nrkno/sofie-core/commit/fa306dc16d5f6ee31418e282180d4c704f5d9feb)) +- action triggers styleClassNames SOFIE-3138 ([#1192](https://github.com/nrkno/sofie-core/issues/1192)) ([49274d6](https://github.com/nrkno/sofie-core/commit/49274d64bdc0532f294be34b0a4413d848321e40)) +- add onTake and onSetAsNext blueprint callbacks SOFIE-2897 SOFIE-2808 ([#1117](https://github.com/nrkno/sofie-core/issues/1117)) ([0bd621c](https://github.com/nrkno/sofie-core/commit/0bd621c13b08cb20c923ea573baca9db003c7df1)) +- blueprint config fixup step SOFIE-2258 ([#1050](https://github.com/nrkno/sofie-core/issues/1050)) ([ea6c343](https://github.com/nrkno/sofie-core/commit/ea6c3435079172c682dd1e23d7813ae33fcdee78)) +- cleanup deprecations ([#996](https://github.com/nrkno/sofie-core/issues/996)) ([f04f18f](https://github.com/nrkno/sofie-core/commit/f04f18f8e94b706dd1a3da1525feb9787f6eb85f)) +- create adlib testing rundowns SOFIE-2963 ([#1211](https://github.com/nrkno/sofie-core/issues/1211)) ([a831989](https://github.com/nrkno/sofie-core/commit/a8319898c73d7233a2a665fd0bf5b9d6da83d713)) +- **Device Triggers:** add support for setting shift register operations ([aa19fcc](https://github.com/nrkno/sofie-core/commit/aa19fcc3b5bfad05158677701b838673578a4dcb)) +- **Device Triggers:** add support for setting shift register operations SOFIE-3136 ([c5a6292](https://github.com/nrkno/sofie-core/commit/c5a6292a85821d67653eebef75bbd929ff0cd44d)) +- **EAV-31:** add blueprintsData on adlib actions ([114c064](https://github.com/nrkno/sofie-core/commit/114c06466ff1f35660454b66d1e35b3d339b9a80)) +- Editable fields as JSON ([ddfb66a](https://github.com/nrkno/sofie-core/commit/ddfb66a1ef8afbd507535037d12752742c7dfeda)) +- implement `loop` content property for Graphics ([2cf38ba](https://github.com/nrkno/sofie-core/commit/2cf38baadd5e7ba16d624b9ad75cfff9d48297a6)) +- introduce `privateData` and `publicData` ([d94f1aa](https://github.com/nrkno/sofie-core/commit/d94f1aa82cec779c7eebddae42e1c4fab91ac494)) +- remove supertimeline from Parts and Pieces resolving SOFIE-2373 ([#983](https://github.com/nrkno/sofie-core/issues/983)) ([55e02cb](https://github.com/nrkno/sofie-core/commit/55e02cb1a0b6d237ae9bcd02ebf54e06cf08fb26)) +- rename internal 'scratchpad' naming to 'adlib testing' SOFIE-3015 ([afb2f43](https://github.com/nrkno/sofie-core/commit/afb2f43c8f199ff5da4ea6c3ccc3bd1e97a80f4f)) +- rundown scratchpad SOFIE-2432 ([5e16632](https://github.com/nrkno/sofie-core/commit/5e16632908b8dd62e4470a9ba6089a56aa571b9f)) +- support packageinfo update flow for buckets and studio baseline SOFIE-2655 ([#1051](https://github.com/nrkno/sofie-core/issues/1051)) ([df7ed0c](https://github.com/nrkno/sofie-core/commit/df7ed0c653b897774f01a33c9e60a5e22fac99e4)) +- typed publications in gateways/peripheraldevices SOFIE-1183 ([#1056](https://github.com/nrkno/sofie-core/issues/1056)) ([0c3c1bf](https://github.com/nrkno/sofie-core/commit/0c3c1bfd2bb779034976dc34e49aa6e664ea874b)) +- update meteor to 2.12 SOFIE-2368 ([#931](https://github.com/nrkno/sofie-core/issues/931)) ([d7dfb71](https://github.com/nrkno/sofie-core/commit/d7dfb71d19405267cab5e2abc39794a80acb30b1)) -**Note:** Version bump only for package @sofie-automation/blueprints-integration +# [1.51.0-in-testing.0](https://github.com/nrkno/sofie-core/compare/v1.50.4...v1.51.0-in-testing.0) (2024-08-19) +## [1.50.5-LSG-updates](https://github.com/nrkno/sofie-core/compare/v1.50.4-LSG-updates...v1.50.5-LSG-updates) (2024-08-08) +## [1.50.4-LSG-updates](https://github.com/nrkno/sofie-core/compare/v1.50.3-LSG-updates...v1.50.4-LSG-updates) (2024-07-30) +### Bug Fixes +- Add missing metadata types ([71eb739](https://github.com/nrkno/sofie-core/commit/71eb739bc87f64db50f06ab4647cb6972e58c7fa)) +- Add missing piece metadata types ([#1133](https://github.com/nrkno/sofie-core/issues/1133)) ([70c5739](https://github.com/nrkno/sofie-core/commit/70c5739c5e9848e57c21bde78ee16d4c78da8e1a)) +- blueprint-integration missed typing in [#1077](https://github.com/nrkno/sofie-core/issues/1077) ([bd5e36f](https://github.com/nrkno/sofie-core/commit/bd5e36f9c0b390999ef465a5e1554b8faa2c456f)) -## [1.50.3](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.2...v1.50.3) (2024-06-24) +### Features -**Note:** Version bump only for package @sofie-automation/blueprints-integration +- A/B player Ids as strings ([#1054](https://github.com/nrkno/sofie-core/issues/1054)) ([d9dee47](https://github.com/nrkno/sofie-core/commit/d9dee473068c4d96d15a6ef799ace830cf1fe1c0)) +- action triggers styleClassNames SOFIE-3138 ([#1192](https://github.com/nrkno/sofie-core/issues/1192)) ([fa306dc](https://github.com/nrkno/sofie-core/commit/fa306dc16d5f6ee31418e282180d4c704f5d9feb)) +- action triggers styleClassNames SOFIE-3138 ([#1192](https://github.com/nrkno/sofie-core/issues/1192)) ([49274d6](https://github.com/nrkno/sofie-core/commit/49274d64bdc0532f294be34b0a4413d848321e40)) +- add onTake and onSetAsNext blueprint callbacks SOFIE-2897 SOFIE-2808 ([#1117](https://github.com/nrkno/sofie-core/issues/1117)) ([0bd621c](https://github.com/nrkno/sofie-core/commit/0bd621c13b08cb20c923ea573baca9db003c7df1)) +- blueprint config fixup step SOFIE-2258 ([#1050](https://github.com/nrkno/sofie-core/issues/1050)) ([ea6c343](https://github.com/nrkno/sofie-core/commit/ea6c3435079172c682dd1e23d7813ae33fcdee78)) +- cleanup deprecations ([#996](https://github.com/nrkno/sofie-core/issues/996)) ([f04f18f](https://github.com/nrkno/sofie-core/commit/f04f18f8e94b706dd1a3da1525feb9787f6eb85f)) +- create adlib testing rundowns SOFIE-2963 ([#1211](https://github.com/nrkno/sofie-core/issues/1211)) ([a831989](https://github.com/nrkno/sofie-core/commit/a8319898c73d7233a2a665fd0bf5b9d6da83d713)) +- **Device Triggers:** add support for setting shift register operations ([aa19fcc](https://github.com/nrkno/sofie-core/commit/aa19fcc3b5bfad05158677701b838673578a4dcb)) +- **Device Triggers:** add support for setting shift register operations SOFIE-3136 ([c5a6292](https://github.com/nrkno/sofie-core/commit/c5a6292a85821d67653eebef75bbd929ff0cd44d)) +- **EAV-31:** add blueprintsData on adlib actions ([114c064](https://github.com/nrkno/sofie-core/commit/114c06466ff1f35660454b66d1e35b3d339b9a80)) +- Editable fields as JSON ([ddfb66a](https://github.com/nrkno/sofie-core/commit/ddfb66a1ef8afbd507535037d12752742c7dfeda)) +- implement `loop` content property for Graphics ([2cf38ba](https://github.com/nrkno/sofie-core/commit/2cf38baadd5e7ba16d624b9ad75cfff9d48297a6)) +- introduce `privateData` and `publicData` ([d94f1aa](https://github.com/nrkno/sofie-core/commit/d94f1aa82cec779c7eebddae42e1c4fab91ac494)) +- remove supertimeline from Parts and Pieces resolving SOFIE-2373 ([#983](https://github.com/nrkno/sofie-core/issues/983)) ([55e02cb](https://github.com/nrkno/sofie-core/commit/55e02cb1a0b6d237ae9bcd02ebf54e06cf08fb26)) +- rename internal 'scratchpad' naming to 'adlib testing' SOFIE-3015 ([afb2f43](https://github.com/nrkno/sofie-core/commit/afb2f43c8f199ff5da4ea6c3ccc3bd1e97a80f4f)) +- rundown scratchpad SOFIE-2432 ([5e16632](https://github.com/nrkno/sofie-core/commit/5e16632908b8dd62e4470a9ba6089a56aa571b9f)) +- support packageinfo update flow for buckets and studio baseline SOFIE-2655 ([#1051](https://github.com/nrkno/sofie-core/issues/1051)) ([df7ed0c](https://github.com/nrkno/sofie-core/commit/df7ed0c653b897774f01a33c9e60a5e22fac99e4)) +- typed publications in gateways/peripheraldevices SOFIE-1183 ([#1056](https://github.com/nrkno/sofie-core/issues/1056)) ([0c3c1bf](https://github.com/nrkno/sofie-core/commit/0c3c1bfd2bb779034976dc34e49aa6e664ea874b)) +- update meteor to 2.12 SOFIE-2368 ([#931](https://github.com/nrkno/sofie-core/issues/931)) ([d7dfb71](https://github.com/nrkno/sofie-core/commit/d7dfb71d19405267cab5e2abc39794a80acb30b1)) +## [1.50.5](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.4-LSG-updates...v1.50.5) (2024-08-08) +**Note:** Version bump only for package @sofie-automation/blueprints-integration +## [1.50.4](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.3-LSG-updates...v1.50.4) (2024-07-30) +**Note:** Version bump only for package @sofie-automation/blueprints-integration -## [1.50.2](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.6...v1.50.2) (2024-05-15) +## [1.50.3](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.2...v1.50.3) (2024-06-24) +**Note:** Version bump only for package @sofie-automation/blueprints-integration +## [1.50.2](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.6...v1.50.2) (2024-05-15) ## [1.50.1](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.5-0...v1.50.1) (2024-03-11) - - ## [1.50.1-0](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.0...v1.50.1-0) (2024-03-05) - - # [1.50.0](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.4...v1.50.0) (2024-02-23) - ### Features -* allow blueprints to specify a custom resolution for NORA hover preview SOFIE-2840 ([#1123](https://github.com/nrkno/tv-automation-server-core/issues/1123)) ([d2f3cef](https://github.com/nrkno/tv-automation-server-core/commit/d2f3cef03259f594feba8814046614d1ed16cf62)) - - +- allow blueprints to specify a custom resolution for NORA hover preview SOFIE-2840 ([#1123](https://github.com/nrkno/tv-automation-server-core/issues/1123)) ([d2f3cef](https://github.com/nrkno/tv-automation-server-core/commit/d2f3cef03259f594feba8814046614d1ed16cf62)) # [1.50.0-in-testing.5](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.0-in-testing.4...v1.50.0-in-testing.5) (2023-08-25) - - # [1.50.0-in-testing.4](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.0-in-testing.7...v1.50.0-in-testing.4) (2023-08-23) - - # [1.50.0-in-testing.2](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.0-in-testing.1...v1.50.0-in-testing.2) (2023-07-17) - ### Bug Fixes -* Allow JSON objects in config presets ([#949](https://github.com/nrkno/tv-automation-server-core/issues/949)) ([4e5a797](https://github.com/nrkno/tv-automation-server-core/commit/4e5a79752d6c06f91ce070b1d67e7bed97a4f0f2)) -* expose `deviceType` in `listPlayoutDevices` to blueprints ([2b32c72](https://github.com/nrkno/tv-automation-server-core/commit/2b32c7256762716071d467193f344646af484f18)) -* require blueprint baseline objects to be of type `TimelineObjectCoreExt` instead of `TSR.TSRTimelineObj` [#961](https://github.com/nrkno/tv-automation-server-core/issues/961) ([b8d0cf3](https://github.com/nrkno/tv-automation-server-core/commit/b8d0cf313c88e29fd08f6405b86a55d3579e7692)) - +- Allow JSON objects in config presets ([#949](https://github.com/nrkno/tv-automation-server-core/issues/949)) ([4e5a797](https://github.com/nrkno/tv-automation-server-core/commit/4e5a79752d6c06f91ce070b1d67e7bed97a4f0f2)) +- expose `deviceType` in `listPlayoutDevices` to blueprints ([2b32c72](https://github.com/nrkno/tv-automation-server-core/commit/2b32c7256762716071d467193f344646af484f18)) +- require blueprint baseline objects to be of type `TimelineObjectCoreExt` instead of `TSR.TSRTimelineObj` [#961](https://github.com/nrkno/tv-automation-server-core/issues/961) ([b8d0cf3](https://github.com/nrkno/tv-automation-server-core/commit/b8d0cf313c88e29fd08f6405b86a55d3579e7692)) ### Features -* add `prompterTitle` property SOFIE-2404 ([#975](https://github.com/nrkno/tv-automation-server-core/issues/975)) ([6f0567d](https://github.com/nrkno/tv-automation-server-core/commit/6f0567de958cede745e918529bae660305ceb66e)) -* add milestone timing ([5950fd5](https://github.com/nrkno/tv-automation-server-core/commit/5950fd5e202cfab5e7f5b36942ffb7525c713e0a)) -* allow getting the Source and Output layers from the ShowStyle context ([#979](https://github.com/nrkno/tv-automation-server-core/issues/979)) ([ab57fb4](https://github.com/nrkno/tv-automation-server-core/commit/ab57fb4871bf23e37b99e73f5f809738fd8fdd1b)) -* blueprint settings overhaul ([#878](https://github.com/nrkno/tv-automation-server-core/issues/878)) ([ee3307c](https://github.com/nrkno/tv-automation-server-core/commit/ee3307cae10076e2d91d624f783e40c54c297b91)) -* fallback to cumetime if no backtime ([2cafbab](https://github.com/nrkno/tv-automation-server-core/commit/2cafbab2f03d49b92c121a7206ddfed62bf770fa)) -* move blueprint ab logic into sofie SOFIE-2403 ([#946](https://github.com/nrkno/tv-automation-server-core/issues/946)) ([cc37b75](https://github.com/nrkno/tv-automation-server-core/commit/cc37b751d674be7f8631d0315e52f8fa29a8d3c4)) -* move gateway settings onto Studio SOFIE-1330 ([#907](https://github.com/nrkno/tv-automation-server-core/issues/907)) ([523c061](https://github.com/nrkno/tv-automation-server-core/commit/523c061c51296e21814deeeabbe9aafca21cd5a6)) -* trs actions in blueprints rundown hooks ([bb5e442](https://github.com/nrkno/tv-automation-server-core/commit/bb5e4422fa32e1af1e520e0c288360b304da4289)) - - - - +- add `prompterTitle` property SOFIE-2404 ([#975](https://github.com/nrkno/tv-automation-server-core/issues/975)) ([6f0567d](https://github.com/nrkno/tv-automation-server-core/commit/6f0567de958cede745e918529bae660305ceb66e)) +- add milestone timing ([5950fd5](https://github.com/nrkno/tv-automation-server-core/commit/5950fd5e202cfab5e7f5b36942ffb7525c713e0a)) +- allow getting the Source and Output layers from the ShowStyle context ([#979](https://github.com/nrkno/tv-automation-server-core/issues/979)) ([ab57fb4](https://github.com/nrkno/tv-automation-server-core/commit/ab57fb4871bf23e37b99e73f5f809738fd8fdd1b)) +- blueprint settings overhaul ([#878](https://github.com/nrkno/tv-automation-server-core/issues/878)) ([ee3307c](https://github.com/nrkno/tv-automation-server-core/commit/ee3307cae10076e2d91d624f783e40c54c297b91)) +- fallback to cumetime if no backtime ([2cafbab](https://github.com/nrkno/tv-automation-server-core/commit/2cafbab2f03d49b92c121a7206ddfed62bf770fa)) +- move blueprint ab logic into sofie SOFIE-2403 ([#946](https://github.com/nrkno/tv-automation-server-core/issues/946)) ([cc37b75](https://github.com/nrkno/tv-automation-server-core/commit/cc37b751d674be7f8631d0315e52f8fa29a8d3c4)) +- move gateway settings onto Studio SOFIE-1330 ([#907](https://github.com/nrkno/tv-automation-server-core/issues/907)) ([523c061](https://github.com/nrkno/tv-automation-server-core/commit/523c061c51296e21814deeeabbe9aafca21cd5a6)) +- trs actions in blueprints rundown hooks ([bb5e442](https://github.com/nrkno/tv-automation-server-core/commit/bb5e4422fa32e1af1e520e0c288360b304da4289)) ## [1.50.1](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.1-0...v1.50.1) (2024-03-11) **Note:** Version bump only for package @sofie-automation/blueprints-integration - - - - ## [1.50.1-0](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.0...v1.50.1-0) (2024-03-05) **Note:** Version bump only for package @sofie-automation/blueprints-integration - - - - # [1.50.0](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.4...v1.50.0) (2024-02-23) - ### Features -* allow blueprints to specify a custom resolution for NORA hover preview SOFIE-2840 ([#1123](https://github.com/nrkno/tv-automation-server-core/issues/1123)) ([d2f3cef](https://github.com/nrkno/tv-automation-server-core/commit/d2f3cef03259f594feba8814046614d1ed16cf62)) - - +- allow blueprints to specify a custom resolution for NORA hover preview SOFIE-2840 ([#1123](https://github.com/nrkno/tv-automation-server-core/issues/1123)) ([d2f3cef](https://github.com/nrkno/tv-automation-server-core/commit/d2f3cef03259f594feba8814046614d1ed16cf62)) # [1.50.0-in-testing.5](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.0-in-testing.4...v1.50.0-in-testing.5) (2023-08-25) - - # [1.50.0-in-testing.4](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.0-in-testing.7...v1.50.0-in-testing.4) (2023-08-23) - - # [1.50.0-in-testing.2](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.0-in-testing.1...v1.50.0-in-testing.2) (2023-07-17) - ### Bug Fixes -* Allow JSON objects in config presets ([#949](https://github.com/nrkno/tv-automation-server-core/issues/949)) ([4e5a797](https://github.com/nrkno/tv-automation-server-core/commit/4e5a79752d6c06f91ce070b1d67e7bed97a4f0f2)) -* expose `deviceType` in `listPlayoutDevices` to blueprints ([2b32c72](https://github.com/nrkno/tv-automation-server-core/commit/2b32c7256762716071d467193f344646af484f18)) -* require blueprint baseline objects to be of type `TimelineObjectCoreExt` instead of `TSR.TSRTimelineObj` [#961](https://github.com/nrkno/tv-automation-server-core/issues/961) ([b8d0cf3](https://github.com/nrkno/tv-automation-server-core/commit/b8d0cf313c88e29fd08f6405b86a55d3579e7692)) - +- Allow JSON objects in config presets ([#949](https://github.com/nrkno/tv-automation-server-core/issues/949)) ([4e5a797](https://github.com/nrkno/tv-automation-server-core/commit/4e5a79752d6c06f91ce070b1d67e7bed97a4f0f2)) +- expose `deviceType` in `listPlayoutDevices` to blueprints ([2b32c72](https://github.com/nrkno/tv-automation-server-core/commit/2b32c7256762716071d467193f344646af484f18)) +- require blueprint baseline objects to be of type `TimelineObjectCoreExt` instead of `TSR.TSRTimelineObj` [#961](https://github.com/nrkno/tv-automation-server-core/issues/961) ([b8d0cf3](https://github.com/nrkno/tv-automation-server-core/commit/b8d0cf313c88e29fd08f6405b86a55d3579e7692)) ### Features -* add `prompterTitle` property SOFIE-2404 ([#975](https://github.com/nrkno/tv-automation-server-core/issues/975)) ([6f0567d](https://github.com/nrkno/tv-automation-server-core/commit/6f0567de958cede745e918529bae660305ceb66e)) -* add milestone timing ([5950fd5](https://github.com/nrkno/tv-automation-server-core/commit/5950fd5e202cfab5e7f5b36942ffb7525c713e0a)) -* allow getting the Source and Output layers from the ShowStyle context ([#979](https://github.com/nrkno/tv-automation-server-core/issues/979)) ([ab57fb4](https://github.com/nrkno/tv-automation-server-core/commit/ab57fb4871bf23e37b99e73f5f809738fd8fdd1b)) -* blueprint settings overhaul ([#878](https://github.com/nrkno/tv-automation-server-core/issues/878)) ([ee3307c](https://github.com/nrkno/tv-automation-server-core/commit/ee3307cae10076e2d91d624f783e40c54c297b91)) -* fallback to cumetime if no backtime ([2cafbab](https://github.com/nrkno/tv-automation-server-core/commit/2cafbab2f03d49b92c121a7206ddfed62bf770fa)) -* move blueprint ab logic into sofie SOFIE-2403 ([#946](https://github.com/nrkno/tv-automation-server-core/issues/946)) ([cc37b75](https://github.com/nrkno/tv-automation-server-core/commit/cc37b751d674be7f8631d0315e52f8fa29a8d3c4)) -* move gateway settings onto Studio SOFIE-1330 ([#907](https://github.com/nrkno/tv-automation-server-core/issues/907)) ([523c061](https://github.com/nrkno/tv-automation-server-core/commit/523c061c51296e21814deeeabbe9aafca21cd5a6)) -* trs actions in blueprints rundown hooks ([bb5e442](https://github.com/nrkno/tv-automation-server-core/commit/bb5e4422fa32e1af1e520e0c288360b304da4289)) - - - - +- add `prompterTitle` property SOFIE-2404 ([#975](https://github.com/nrkno/tv-automation-server-core/issues/975)) ([6f0567d](https://github.com/nrkno/tv-automation-server-core/commit/6f0567de958cede745e918529bae660305ceb66e)) +- add milestone timing ([5950fd5](https://github.com/nrkno/tv-automation-server-core/commit/5950fd5e202cfab5e7f5b36942ffb7525c713e0a)) +- allow getting the Source and Output layers from the ShowStyle context ([#979](https://github.com/nrkno/tv-automation-server-core/issues/979)) ([ab57fb4](https://github.com/nrkno/tv-automation-server-core/commit/ab57fb4871bf23e37b99e73f5f809738fd8fdd1b)) +- blueprint settings overhaul ([#878](https://github.com/nrkno/tv-automation-server-core/issues/878)) ([ee3307c](https://github.com/nrkno/tv-automation-server-core/commit/ee3307cae10076e2d91d624f783e40c54c297b91)) +- fallback to cumetime if no backtime ([2cafbab](https://github.com/nrkno/tv-automation-server-core/commit/2cafbab2f03d49b92c121a7206ddfed62bf770fa)) +- move blueprint ab logic into sofie SOFIE-2403 ([#946](https://github.com/nrkno/tv-automation-server-core/issues/946)) ([cc37b75](https://github.com/nrkno/tv-automation-server-core/commit/cc37b751d674be7f8631d0315e52f8fa29a8d3c4)) +- move gateway settings onto Studio SOFIE-1330 ([#907](https://github.com/nrkno/tv-automation-server-core/issues/907)) ([523c061](https://github.com/nrkno/tv-automation-server-core/commit/523c061c51296e21814deeeabbe9aafca21cd5a6)) +- trs actions in blueprints rundown hooks ([bb5e442](https://github.com/nrkno/tv-automation-server-core/commit/bb5e4422fa32e1af1e520e0c288360b304da4289)) # [1.50.0-in-testing.5](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.0-in-testing.4...v1.50.0-in-testing.5) (2023-08-25) @@ -184,22 +200,15 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline - move blueprint ab logic into sofie SOFIE-2403 ([#946](https://github.com/nrkno/tv-automation-server-core/issues/946)) ([cc37b75](https://github.com/nrkno/tv-automation-server-core/commit/cc37b751d674be7f8631d0315e52f8fa29a8d3c4)) - move gateway settings onto Studio SOFIE-1330 ([#907](https://github.com/nrkno/tv-automation-server-core/issues/907)) ([523c061](https://github.com/nrkno/tv-automation-server-core/commit/523c061c51296e21814deeeabbe9aafca21cd5a6)) - trs actions in blueprints rundown hooks ([bb5e442](https://github.com/nrkno/tv-automation-server-core/commit/bb5e4422fa32e1af1e520e0c288360b304da4289)) + ## [1.49.3](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.2...v1.49.3) (2023-12-21) **Note:** Version bump only for package @sofie-automation/blueprints-integration - - - - ## [1.49.2](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.1...v1.49.2) (2023-11-29) **Note:** Version bump only for package @sofie-automation/blueprints-integration - - - - ## [1.49.1](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.0...v1.49.1) (2023-10-20) **Note:** Version bump only for package @sofie-automation/blueprints-integration diff --git a/packages/blueprints-integration/package.json b/packages/blueprints-integration/package.json index 2567e0d707..7135a1abce 100644 --- a/packages/blueprints-integration/package.json +++ b/packages/blueprints-integration/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/blueprints-integration", - "version": "1.51.0-in-development", + "version": "1.51.0-in-testing.0", "description": "Library to define the interaction between core and the blueprints.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -38,7 +38,7 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/shared-lib": "1.51.0-in-development", + "@sofie-automation/shared-lib": "1.51.0-in-testing.0", "tslib": "^2.6.2", "type-fest": "^3.13.1" }, diff --git a/packages/corelib/package.json b/packages/corelib/package.json index 6f4cb0a28c..086bc24b7b 100644 --- a/packages/corelib/package.json +++ b/packages/corelib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/corelib", - "version": "1.51.0-in-development", + "version": "1.51.0-in-testing.0", "private": true, "description": "Internal library for some types shared by core and workers", "main": "dist/index.js", @@ -39,8 +39,8 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/blueprints-integration": "1.51.0-in-development", - "@sofie-automation/shared-lib": "1.51.0-in-development", + "@sofie-automation/blueprints-integration": "1.51.0-in-testing.0", + "@sofie-automation/shared-lib": "1.51.0-in-testing.0", "fast-clone": "^1.5.13", "i18next": "^21.10.0", "influx": "^5.9.3", diff --git a/packages/documentation/package.json b/packages/documentation/package.json index 2db1628111..ae06aefb4c 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -1,6 +1,6 @@ { "name": "sofie-documentation", - "version": "1.51.0-in-development", + "version": "1.51.0-in-testing.0", "private": true, "scripts": { "docusaurus": "docusaurus", diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index 5582c93c58..f9779400b8 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/job-worker", - "version": "1.51.0-in-development", + "version": "1.51.0-in-testing.0", "description": "Worker for things", "main": "dist/index.js", "license": "MIT", @@ -41,9 +41,9 @@ ], "dependencies": { "@slack/webhook": "^6.1.0", - "@sofie-automation/blueprints-integration": "1.51.0-in-development", - "@sofie-automation/corelib": "1.51.0-in-development", - "@sofie-automation/shared-lib": "1.51.0-in-development", + "@sofie-automation/blueprints-integration": "1.51.0-in-testing.0", + "@sofie-automation/corelib": "1.51.0-in-testing.0", + "@sofie-automation/shared-lib": "1.51.0-in-testing.0", "amqplib": "^0.10.3", "deepmerge": "^4.3.1", "elastic-apm-node": "^3.51.0", diff --git a/packages/lerna.json b/packages/lerna.json index 0fdc1ad853..aec3a9d3e7 100644 --- a/packages/lerna.json +++ b/packages/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.51.0-in-development", - "npmClient": "yarn", - "useWorkspaces": true + "version": "1.51.0-in-testing.0", + "npmClient": "yarn", + "useWorkspaces": true } diff --git a/packages/live-status-gateway/package.json b/packages/live-status-gateway/package.json index 0a87ab28a3..b2de220d7e 100644 --- a/packages/live-status-gateway/package.json +++ b/packages/live-status-gateway/package.json @@ -1,6 +1,6 @@ { "name": "live-status-gateway", - "version": "1.51.0-in-development", + "version": "1.51.0-in-testing.0", "private": true, "description": "Provides state from Sofie over sockets", "license": "MIT", @@ -53,10 +53,10 @@ "production" ], "dependencies": { - "@sofie-automation/blueprints-integration": "1.51.0-in-development", - "@sofie-automation/corelib": "1.51.0-in-development", - "@sofie-automation/server-core-integration": "1.51.0-in-development", - "@sofie-automation/shared-lib": "1.51.0-in-development", + "@sofie-automation/blueprints-integration": "1.51.0-in-testing.0", + "@sofie-automation/corelib": "1.51.0-in-testing.0", + "@sofie-automation/server-core-integration": "1.51.0-in-testing.0", + "@sofie-automation/shared-lib": "1.51.0-in-testing.0", "debug": "^4.3.4", "fast-clone": "^1.5.13", "influx": "^5.9.3", diff --git a/packages/mos-gateway/CHANGELOG.md b/packages/mos-gateway/CHANGELOG.md index e116fd0ad5..a3ae51a67f 100644 --- a/packages/mos-gateway/CHANGELOG.md +++ b/packages/mos-gateway/CHANGELOG.md @@ -3,118 +3,111 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. -## [1.50.5](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.4-LSG-updates...v1.50.5) (2024-08-08) - -**Note:** Version bump only for package mos-gateway - - +# [1.51.0-in-testing.0](https://github.com/nrkno/sofie-core/compare/v1.50.4...v1.51.0-in-testing.0) (2024-08-19) +### Features +- Ensure peripheralDevice subdevice removal when requested ([#1227](https://github.com/nrkno/sofie-core/issues/1227)) ([d5cafe8](https://github.com/nrkno/sofie-core/commit/d5cafe8db5e453f87f8d46262f23e118b580d4d5)) -## [1.50.4](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.3-LSG-updates...v1.50.4) (2024-07-30) +## [1.50.5-LSG-updates](https://github.com/nrkno/sofie-core/compare/v1.50.4-LSG-updates...v1.50.5-LSG-updates) (2024-08-08) +## [1.50.4-LSG-updates](https://github.com/nrkno/sofie-core/compare/v1.50.3-LSG-updates...v1.50.4-LSG-updates) (2024-07-30) ### Bug Fixes -* improve error logging: use stringifyError() ([8da63de](https://github.com/nrkno/tv-automation-server-core/commit/8da63dec44915439ea436eee9697f3774241537b)) - +- create new mosTimes correctly ([e8cec88](https://github.com/nrkno/sofie-core/commit/e8cec88f8b9d8c298f1b2ddec8d45f6f80fea9bf)) +- improve error logging: use stringifyError() ([8da63de](https://github.com/nrkno/sofie-core/commit/8da63dec44915439ea436eee9697f3774241537b)) +### Features +- backport of release51 live-status-gateway onto release50 ([0a87a95](https://github.com/nrkno/sofie-core/commit/0a87a9519ca1f344429e9b4d47a44c1a9acddff2)) +- refactor server-core-integration subscription handling to reduce duplication ([8eaedd2](https://github.com/nrkno/sofie-core/commit/8eaedd22e8efb9750f00ff472301c6b3f2d0f0af)) +- typed publications in gateways/peripheraldevices SOFIE-1183 ([#1056](https://github.com/nrkno/sofie-core/issues/1056)) ([0c3c1bf](https://github.com/nrkno/sofie-core/commit/0c3c1bfd2bb779034976dc34e49aa6e664ea874b)) +- update meteor to 2.12 SOFIE-2368 ([#931](https://github.com/nrkno/sofie-core/issues/931)) ([d7dfb71](https://github.com/nrkno/sofie-core/commit/d7dfb71d19405267cab5e2abc39794a80acb30b1)) +# [1.51.0-in-testing.0](https://github.com/nrkno/sofie-core/compare/v1.50.4...v1.51.0-in-testing.0) (2024-08-19) -## [1.50.3](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.2...v1.50.3) (2024-06-24) +### Features -**Note:** Version bump only for package mos-gateway +- Ensure peripheralDevice subdevice removal when requested ([#1227](https://github.com/nrkno/sofie-core/issues/1227)) ([d5cafe8](https://github.com/nrkno/sofie-core/commit/d5cafe8db5e453f87f8d46262f23e118b580d4d5)) +## [1.50.5-LSG-updates](https://github.com/nrkno/sofie-core/compare/v1.50.4-LSG-updates...v1.50.5-LSG-updates) (2024-08-08) +## [1.50.4-LSG-updates](https://github.com/nrkno/sofie-core/compare/v1.50.3-LSG-updates...v1.50.4-LSG-updates) (2024-07-30) +### Bug Fixes +- create new mosTimes correctly ([e8cec88](https://github.com/nrkno/sofie-core/commit/e8cec88f8b9d8c298f1b2ddec8d45f6f80fea9bf)) +- improve error logging: use stringifyError() ([8da63de](https://github.com/nrkno/sofie-core/commit/8da63dec44915439ea436eee9697f3774241537b)) -## [1.50.2](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.6...v1.50.2) (2024-05-15) +### Features +- backport of release51 live-status-gateway onto release50 ([0a87a95](https://github.com/nrkno/sofie-core/commit/0a87a9519ca1f344429e9b4d47a44c1a9acddff2)) +- refactor server-core-integration subscription handling to reduce duplication ([8eaedd2](https://github.com/nrkno/sofie-core/commit/8eaedd22e8efb9750f00ff472301c6b3f2d0f0af)) +- typed publications in gateways/peripheraldevices SOFIE-1183 ([#1056](https://github.com/nrkno/sofie-core/issues/1056)) ([0c3c1bf](https://github.com/nrkno/sofie-core/commit/0c3c1bfd2bb779034976dc34e49aa6e664ea874b)) +- update meteor to 2.12 SOFIE-2368 ([#931](https://github.com/nrkno/sofie-core/issues/931)) ([d7dfb71](https://github.com/nrkno/sofie-core/commit/d7dfb71d19405267cab5e2abc39794a80acb30b1)) +## [1.50.5](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.4-LSG-updates...v1.50.5) (2024-08-08) -## [1.50.1](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.5-0...v1.50.1) (2024-03-11) +**Note:** Version bump only for package mos-gateway +## [1.50.4](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.3-LSG-updates...v1.50.4) (2024-07-30) ### Bug Fixes -* strip unused node dependencies from docker images ([#1159](https://github.com/nrkno/tv-automation-server-core/issues/1159)) ([6d86132](https://github.com/nrkno/tv-automation-server-core/commit/6d86132c1bc36219f04f00d5360940dfcbd6df7c)) +- improve error logging: use stringifyError() ([8da63de](https://github.com/nrkno/tv-automation-server-core/commit/8da63dec44915439ea436eee9697f3774241537b)) +## [1.50.3](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.2...v1.50.3) (2024-06-24) +**Note:** Version bump only for package mos-gateway -## [1.50.1-0](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.0...v1.50.1-0) (2024-03-05) +## [1.50.2](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.6...v1.50.2) (2024-05-15) +## [1.50.1](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.5-0...v1.50.1) (2024-03-11) +### Bug Fixes -# [1.50.0](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.4...v1.50.0) (2024-02-23) +- strip unused node dependencies from docker images ([#1159](https://github.com/nrkno/tv-automation-server-core/issues/1159)) ([6d86132](https://github.com/nrkno/tv-automation-server-core/commit/6d86132c1bc36219f04f00d5360940dfcbd6df7c)) +## [1.50.1-0](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.0...v1.50.1-0) (2024-03-05) +# [1.50.0](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.4...v1.50.0) (2024-02-23) # [1.50.0-in-testing.5](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.0-in-testing.4...v1.50.0-in-testing.5) (2023-08-25) - - # [1.50.0-in-testing.4](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.0-in-testing.7...v1.50.0-in-testing.4) (2023-08-23) - - # [1.50.0-in-testing.2](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.0-in-testing.1...v1.50.0-in-testing.2) (2023-07-17) - ### Features -* expose MOS ports in mos-gateway settings ([#917](https://github.com/nrkno/tv-automation-server-core/issues/917)) ([4cb1649](https://github.com/nrkno/tv-automation-server-core/commit/4cb16493107c07be9c8a2a804f6bcb7e54072de2)) -* move gateway settings onto Studio SOFIE-1330 ([#907](https://github.com/nrkno/tv-automation-server-core/issues/907)) ([523c061](https://github.com/nrkno/tv-automation-server-core/commit/523c061c51296e21814deeeabbe9aafca21cd5a6)) -* peripheral device tidying ([#906](https://github.com/nrkno/tv-automation-server-core/issues/906)) ([0795ae5](https://github.com/nrkno/tv-automation-server-core/commit/0795ae5c0517114a34e350d5a5afbf7a731e9794)) - - - - +- expose MOS ports in mos-gateway settings ([#917](https://github.com/nrkno/tv-automation-server-core/issues/917)) ([4cb1649](https://github.com/nrkno/tv-automation-server-core/commit/4cb16493107c07be9c8a2a804f6bcb7e54072de2)) +- move gateway settings onto Studio SOFIE-1330 ([#907](https://github.com/nrkno/tv-automation-server-core/issues/907)) ([523c061](https://github.com/nrkno/tv-automation-server-core/commit/523c061c51296e21814deeeabbe9aafca21cd5a6)) +- peripheral device tidying ([#906](https://github.com/nrkno/tv-automation-server-core/issues/906)) ([0795ae5](https://github.com/nrkno/tv-automation-server-core/commit/0795ae5c0517114a34e350d5a5afbf7a731e9794)) ## [1.50.1](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.1-0...v1.50.1) (2024-03-11) - ### Bug Fixes -* strip unused node dependencies from docker images ([#1159](https://github.com/nrkno/tv-automation-server-core/issues/1159)) ([6d86132](https://github.com/nrkno/tv-automation-server-core/commit/6d86132c1bc36219f04f00d5360940dfcbd6df7c)) - - - - +- strip unused node dependencies from docker images ([#1159](https://github.com/nrkno/tv-automation-server-core/issues/1159)) ([6d86132](https://github.com/nrkno/tv-automation-server-core/commit/6d86132c1bc36219f04f00d5360940dfcbd6df7c)) ## [1.50.1-0](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.0...v1.50.1-0) (2024-03-05) **Note:** Version bump only for package mos-gateway - - - - # [1.50.0](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.4...v1.50.0) (2024-02-23) - - # [1.50.0-in-testing.5](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.0-in-testing.4...v1.50.0-in-testing.5) (2023-08-25) - - # [1.50.0-in-testing.4](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.0-in-testing.7...v1.50.0-in-testing.4) (2023-08-23) - - # [1.50.0-in-testing.2](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.0-in-testing.1...v1.50.0-in-testing.2) (2023-07-17) - ### Features -* expose MOS ports in mos-gateway settings ([#917](https://github.com/nrkno/tv-automation-server-core/issues/917)) ([4cb1649](https://github.com/nrkno/tv-automation-server-core/commit/4cb16493107c07be9c8a2a804f6bcb7e54072de2)) -* move gateway settings onto Studio SOFIE-1330 ([#907](https://github.com/nrkno/tv-automation-server-core/issues/907)) ([523c061](https://github.com/nrkno/tv-automation-server-core/commit/523c061c51296e21814deeeabbe9aafca21cd5a6)) -* peripheral device tidying ([#906](https://github.com/nrkno/tv-automation-server-core/issues/906)) ([0795ae5](https://github.com/nrkno/tv-automation-server-core/commit/0795ae5c0517114a34e350d5a5afbf7a731e9794)) - - - - +- expose MOS ports in mos-gateway settings ([#917](https://github.com/nrkno/tv-automation-server-core/issues/917)) ([4cb1649](https://github.com/nrkno/tv-automation-server-core/commit/4cb16493107c07be9c8a2a804f6bcb7e54072de2)) +- move gateway settings onto Studio SOFIE-1330 ([#907](https://github.com/nrkno/tv-automation-server-core/issues/907)) ([523c061](https://github.com/nrkno/tv-automation-server-core/commit/523c061c51296e21814deeeabbe9aafca21cd5a6)) +- peripheral device tidying ([#906](https://github.com/nrkno/tv-automation-server-core/issues/906)) ([0795ae5](https://github.com/nrkno/tv-automation-server-core/commit/0795ae5c0517114a34e350d5a5afbf7a731e9794)) # [1.50.0-in-testing.5](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.0-in-testing.4...v1.50.0-in-testing.5) (2023-08-25) @@ -141,22 +134,15 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline - peripheral device tidying ([#906](https://github.com/nrkno/tv-automation-server-core/issues/906)) ([0795ae5](https://github.com/nrkno/tv-automation-server-core/commit/0795ae5c0517114a34e350d5a5afbf7a731e9794)) # [1.50.0-in-testing.2](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.0-in-testing.1...v1.50.0-in-testing.2) (2023-07-17) + ## [1.49.3](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.2...v1.49.3) (2023-12-21) **Note:** Version bump only for package mos-gateway - - - - ## [1.49.2](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.1...v1.49.2) (2023-11-29) **Note:** Version bump only for package mos-gateway - - - - ## [1.49.1](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.0...v1.49.1) (2023-10-20) **Note:** Version bump only for package mos-gateway diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index edc4498e03..94aa2a5953 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -1,6 +1,6 @@ { "name": "mos-gateway", - "version": "1.51.0-in-development", + "version": "1.51.0-in-testing.0", "private": true, "description": "MOS-Gateway for the Sofie project", "license": "MIT", @@ -66,8 +66,8 @@ ], "dependencies": { "@mos-connection/connector": "4.1.1-nightly-master-20240430-072032-ffb8bf6.0", - "@sofie-automation/server-core-integration": "1.51.0-in-development", - "@sofie-automation/shared-lib": "1.51.0-in-development", + "@sofie-automation/server-core-integration": "1.51.0-in-testing.0", + "@sofie-automation/shared-lib": "1.51.0-in-testing.0", "tslib": "^2.6.2", "type-fest": "^3.13.1", "underscore": "^1.13.6", diff --git a/packages/openapi/package.json b/packages/openapi/package.json index 2324dd77c2..8550a44622 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/openapi", - "version": "1.51.0-in-development", + "version": "1.51.0-in-testing.0", "private": true, "license": "MIT", "repository": { diff --git a/packages/playout-gateway/CHANGELOG.md b/packages/playout-gateway/CHANGELOG.md index 7df7797666..8ebb3e627e 100644 --- a/packages/playout-gateway/CHANGELOG.md +++ b/packages/playout-gateway/CHANGELOG.md @@ -3,6 +3,74 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.51.0-in-testing.0](https://github.com/nrkno/sofie-core/compare/v1.50.4...v1.51.0-in-testing.0) (2024-08-19) + + +### Features + +* Ensure peripheralDevice subdevice removal when requested ([#1227](https://github.com/nrkno/sofie-core/issues/1227)) ([d5cafe8](https://github.com/nrkno/sofie-core/commit/d5cafe8db5e453f87f8d46262f23e118b580d4d5)) + + + +## [1.50.5-LSG-updates](https://github.com/nrkno/sofie-core/compare/v1.50.4-LSG-updates...v1.50.5-LSG-updates) (2024-08-08) + + + +## [1.50.4-LSG-updates](https://github.com/nrkno/sofie-core/compare/v1.50.3-LSG-updates...v1.50.4-LSG-updates) (2024-07-30) + + +### Bug Fixes + +* improve error logging: use stringifyError() ([8da63de](https://github.com/nrkno/sofie-core/commit/8da63dec44915439ea436eee9697f3774241537b)) +* include timestamp when logging to file ([#1207](https://github.com/nrkno/sofie-core/issues/1207)) ([2dcca21](https://github.com/nrkno/sofie-core/commit/2dcca215403cc0256c78f5f8f5f6befa2a1ca683)) +* **playout-gateway:** improve handling and typing of TSR events (fixes some logging issues) ([26a8613](https://github.com/nrkno/sofie-core/commit/26a8613cd64d7e077dfd7ff2092555c1b7805f4d)) +* use helper function to keep the scope of 'unsafe' code small ([c58a6de](https://github.com/nrkno/sofie-core/commit/c58a6de80597373e158316d33661ac0e8a07956a)) + + +### Features + +* backport of release51 live-status-gateway onto release50 ([0a87a95](https://github.com/nrkno/sofie-core/commit/0a87a9519ca1f344429e9b4d47a44c1a9acddff2)) +* refactor server-core-integration subscription handling to reduce duplication ([8eaedd2](https://github.com/nrkno/sofie-core/commit/8eaedd22e8efb9750f00ff472301c6b3f2d0f0af)) +* typed publications in gateways/peripheraldevices SOFIE-1183 ([#1056](https://github.com/nrkno/sofie-core/issues/1056)) ([0c3c1bf](https://github.com/nrkno/sofie-core/commit/0c3c1bfd2bb779034976dc34e49aa6e664ea874b)) + + + + + +# [1.51.0-in-testing.0](https://github.com/nrkno/sofie-core/compare/v1.50.4...v1.51.0-in-testing.0) (2024-08-19) + + +### Features + +* Ensure peripheralDevice subdevice removal when requested ([#1227](https://github.com/nrkno/sofie-core/issues/1227)) ([d5cafe8](https://github.com/nrkno/sofie-core/commit/d5cafe8db5e453f87f8d46262f23e118b580d4d5)) + + + +## [1.50.5-LSG-updates](https://github.com/nrkno/sofie-core/compare/v1.50.4-LSG-updates...v1.50.5-LSG-updates) (2024-08-08) + + + +## [1.50.4-LSG-updates](https://github.com/nrkno/sofie-core/compare/v1.50.3-LSG-updates...v1.50.4-LSG-updates) (2024-07-30) + + +### Bug Fixes + +* improve error logging: use stringifyError() ([8da63de](https://github.com/nrkno/sofie-core/commit/8da63dec44915439ea436eee9697f3774241537b)) +* include timestamp when logging to file ([#1207](https://github.com/nrkno/sofie-core/issues/1207)) ([2dcca21](https://github.com/nrkno/sofie-core/commit/2dcca215403cc0256c78f5f8f5f6befa2a1ca683)) +* **playout-gateway:** improve handling and typing of TSR events (fixes some logging issues) ([26a8613](https://github.com/nrkno/sofie-core/commit/26a8613cd64d7e077dfd7ff2092555c1b7805f4d)) +* use helper function to keep the scope of 'unsafe' code small ([c58a6de](https://github.com/nrkno/sofie-core/commit/c58a6de80597373e158316d33661ac0e8a07956a)) + + +### Features + +* backport of release51 live-status-gateway onto release50 ([0a87a95](https://github.com/nrkno/sofie-core/commit/0a87a9519ca1f344429e9b4d47a44c1a9acddff2)) +* refactor server-core-integration subscription handling to reduce duplication ([8eaedd2](https://github.com/nrkno/sofie-core/commit/8eaedd22e8efb9750f00ff472301c6b3f2d0f0af)) +* typed publications in gateways/peripheraldevices SOFIE-1183 ([#1056](https://github.com/nrkno/sofie-core/issues/1056)) ([0c3c1bf](https://github.com/nrkno/sofie-core/commit/0c3c1bfd2bb779034976dc34e49aa6e664ea874b)) + + + + + ## [1.50.5](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.4-LSG-updates...v1.50.5) (2024-08-08) **Note:** Version bump only for package playout-gateway diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index d687794f80..f526298e3d 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -1,6 +1,6 @@ { "name": "playout-gateway", - "version": "1.51.0-in-development", + "version": "1.51.0-in-testing.0", "private": true, "description": "Connect to Core, play stuff", "license": "MIT", @@ -56,8 +56,8 @@ "production" ], "dependencies": { - "@sofie-automation/server-core-integration": "1.51.0-in-development", - "@sofie-automation/shared-lib": "1.51.0-in-development", + "@sofie-automation/server-core-integration": "1.51.0-in-testing.0", + "@sofie-automation/shared-lib": "1.51.0-in-testing.0", "debug": "^4.3.4", "influx": "^5.9.3", "timeline-state-resolver": "9.1.0", diff --git a/packages/server-core-integration/CHANGELOG.md b/packages/server-core-integration/CHANGELOG.md index 2d019af56f..5173eccf92 100644 --- a/packages/server-core-integration/CHANGELOG.md +++ b/packages/server-core-integration/CHANGELOG.md @@ -3,133 +3,114 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. -## [1.50.5](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.4-LSG-updates...v1.50.5) (2024-08-08) +# [1.51.0-in-testing.0](https://github.com/nrkno/sofie-core/compare/v1.50.4...v1.51.0-in-testing.0) (2024-08-19) -**Note:** Version bump only for package @sofie-automation/server-core-integration +## [1.50.5-LSG-updates](https://github.com/nrkno/sofie-core/compare/v1.50.4-LSG-updates...v1.50.5-LSG-updates) (2024-08-08) +## [1.50.4-LSG-updates](https://github.com/nrkno/sofie-core/compare/v1.50.3-LSG-updates...v1.50.4-LSG-updates) (2024-07-30) +### Bug Fixes +- core-integration: handle error when subscribing ([3ad0c73](https://github.com/nrkno/sofie-core/commit/3ad0c73dd7fa4d9572cc00a7ffa9987fed0df4e3)) +### Features -## [1.50.4](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.3-LSG-updates...v1.50.4) (2024-07-30) +- backport of release51 live-status-gateway onto release50 ([0a87a95](https://github.com/nrkno/sofie-core/commit/0a87a9519ca1f344429e9b4d47a44c1a9acddff2)) +- **Device Triggers:** add support for setting shift register operations SOFIE-3136 ([c5a6292](https://github.com/nrkno/sofie-core/commit/c5a6292a85821d67653eebef75bbd929ff0cd44d)) +- refactor server-core-integration subscription handling to reduce duplication ([8eaedd2](https://github.com/nrkno/sofie-core/commit/8eaedd22e8efb9750f00ff472301c6b3f2d0f0af)) +- typed publications in gateways/peripheraldevices SOFIE-1183 ([#1056](https://github.com/nrkno/sofie-core/issues/1056)) ([0c3c1bf](https://github.com/nrkno/sofie-core/commit/0c3c1bfd2bb779034976dc34e49aa6e664ea874b)) -**Note:** Version bump only for package @sofie-automation/server-core-integration +# [1.51.0-in-testing.0](https://github.com/nrkno/sofie-core/compare/v1.50.4...v1.51.0-in-testing.0) (2024-08-19) +## [1.50.5-LSG-updates](https://github.com/nrkno/sofie-core/compare/v1.50.4-LSG-updates...v1.50.5-LSG-updates) (2024-08-08) +## [1.50.4-LSG-updates](https://github.com/nrkno/sofie-core/compare/v1.50.3-LSG-updates...v1.50.4-LSG-updates) (2024-07-30) +### Bug Fixes +- core-integration: handle error when subscribing ([3ad0c73](https://github.com/nrkno/sofie-core/commit/3ad0c73dd7fa4d9572cc00a7ffa9987fed0df4e3)) -## [1.50.3](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.2...v1.50.3) (2024-06-24) +### Features + +- backport of release51 live-status-gateway onto release50 ([0a87a95](https://github.com/nrkno/sofie-core/commit/0a87a9519ca1f344429e9b4d47a44c1a9acddff2)) +- **Device Triggers:** add support for setting shift register operations SOFIE-3136 ([c5a6292](https://github.com/nrkno/sofie-core/commit/c5a6292a85821d67653eebef75bbd929ff0cd44d)) +- refactor server-core-integration subscription handling to reduce duplication ([8eaedd2](https://github.com/nrkno/sofie-core/commit/8eaedd22e8efb9750f00ff472301c6b3f2d0f0af)) +- typed publications in gateways/peripheraldevices SOFIE-1183 ([#1056](https://github.com/nrkno/sofie-core/issues/1056)) ([0c3c1bf](https://github.com/nrkno/sofie-core/commit/0c3c1bfd2bb779034976dc34e49aa6e664ea874b)) + +## [1.50.5](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.4-LSG-updates...v1.50.5) (2024-08-08) **Note:** Version bump only for package @sofie-automation/server-core-integration +## [1.50.4](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.3-LSG-updates...v1.50.4) (2024-07-30) +**Note:** Version bump only for package @sofie-automation/server-core-integration +## [1.50.3](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.2...v1.50.3) (2024-06-24) +**Note:** Version bump only for package @sofie-automation/server-core-integration ## [1.50.2](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.6...v1.50.2) (2024-05-15) - ### Bug Fixes -* peripheraldevice child device pings not starting upon first connection SOFIE-3047 ([0379304](https://github.com/nrkno/tv-automation-server-core/commit/03793042547ea19200ef7629c53ea76a53ea7cd0)) - - +- peripheraldevice child device pings not starting upon first connection SOFIE-3047 ([0379304](https://github.com/nrkno/tv-automation-server-core/commit/03793042547ea19200ef7629c53ea76a53ea7cd0)) ## [1.50.1](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.5-0...v1.50.1) (2024-03-11) - - ## [1.50.1-0](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.0...v1.50.1-0) (2024-03-05) - - # [1.50.0](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.4...v1.50.0) (2024-02-23) - ### Bug Fixes -* don't rely on winston types in `server-core-integration`, have own logger interface ([b418530](https://github.com/nrkno/tv-automation-server-core/commit/b4185307c4d5dd3d2439e11005a858321832d02e)) -* improve disposal of core connection ([dc52fe5](https://github.com/nrkno/tv-automation-server-core/commit/dc52fe5b362e308f157e5009b0f8359ba6c63630)) - - +- don't rely on winston types in `server-core-integration`, have own logger interface ([b418530](https://github.com/nrkno/tv-automation-server-core/commit/b4185307c4d5dd3d2439e11005a858321832d02e)) +- improve disposal of core connection ([dc52fe5](https://github.com/nrkno/tv-automation-server-core/commit/dc52fe5b362e308f157e5009b0f8359ba6c63630)) # [1.50.0-in-testing.5](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.0-in-testing.4...v1.50.0-in-testing.5) (2023-08-25) - - # [1.50.0-in-testing.4](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.0-in-testing.7...v1.50.0-in-testing.4) (2023-08-23) - - # [1.50.0-in-testing.2](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.0-in-testing.1...v1.50.0-in-testing.2) (2023-07-17) - ### Bug Fixes -* **server-core-integration:** `autosubscribe` will resubscribe to collections using the same subscriptionId as when initially subscribed ([f0f3383](https://github.com/nrkno/tv-automation-server-core/commit/f0f33837b07b944abe320ae4a378641bba167a3c)) - +- **server-core-integration:** `autosubscribe` will resubscribe to collections using the same subscriptionId as when initially subscribed ([f0f3383](https://github.com/nrkno/tv-automation-server-core/commit/f0f33837b07b944abe320ae4a378641bba167a3c)) ### Features -* move gateway settings onto Studio SOFIE-1330 ([#907](https://github.com/nrkno/tv-automation-server-core/issues/907)) ([523c061](https://github.com/nrkno/tv-automation-server-core/commit/523c061c51296e21814deeeabbe9aafca21cd5a6)) -* peripheral device tidying ([#906](https://github.com/nrkno/tv-automation-server-core/issues/906)) ([0795ae5](https://github.com/nrkno/tv-automation-server-core/commit/0795ae5c0517114a34e350d5a5afbf7a731e9794)) - - - - +- move gateway settings onto Studio SOFIE-1330 ([#907](https://github.com/nrkno/tv-automation-server-core/issues/907)) ([523c061](https://github.com/nrkno/tv-automation-server-core/commit/523c061c51296e21814deeeabbe9aafca21cd5a6)) +- peripheral device tidying ([#906](https://github.com/nrkno/tv-automation-server-core/issues/906)) ([0795ae5](https://github.com/nrkno/tv-automation-server-core/commit/0795ae5c0517114a34e350d5a5afbf7a731e9794)) ## [1.50.1](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.1-0...v1.50.1) (2024-03-11) **Note:** Version bump only for package @sofie-automation/server-core-integration - - - - ## [1.50.1-0](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.0...v1.50.1-0) (2024-03-05) **Note:** Version bump only for package @sofie-automation/server-core-integration - - - - # [1.50.0](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.4...v1.50.0) (2024-02-23) - ### Bug Fixes -* core-integration: handle error when subscribing ([e9beb34](https://github.com/nrkno/tv-automation-server-core/commit/e9beb349b3e799f6a0110deb3c75374934a463a2)) -* don't rely on winston types in `server-core-integration`, have own logger interface ([b418530](https://github.com/nrkno/tv-automation-server-core/commit/b4185307c4d5dd3d2439e11005a858321832d02e)) -* improve disposal of core connection ([dc52fe5](https://github.com/nrkno/tv-automation-server-core/commit/dc52fe5b362e308f157e5009b0f8359ba6c63630)) - - +- core-integration: handle error when subscribing ([e9beb34](https://github.com/nrkno/tv-automation-server-core/commit/e9beb349b3e799f6a0110deb3c75374934a463a2)) +- don't rely on winston types in `server-core-integration`, have own logger interface ([b418530](https://github.com/nrkno/tv-automation-server-core/commit/b4185307c4d5dd3d2439e11005a858321832d02e)) +- improve disposal of core connection ([dc52fe5](https://github.com/nrkno/tv-automation-server-core/commit/dc52fe5b362e308f157e5009b0f8359ba6c63630)) # [1.50.0-in-testing.5](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.0-in-testing.4...v1.50.0-in-testing.5) (2023-08-25) - - # [1.50.0-in-testing.4](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.0-in-testing.7...v1.50.0-in-testing.4) (2023-08-23) - - # [1.50.0-in-testing.2](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.0-in-testing.1...v1.50.0-in-testing.2) (2023-07-17) - ### Bug Fixes -* **server-core-integration:** `autosubscribe` will resubscribe to collections using the same subscriptionId as when initially subscribed ([f0f3383](https://github.com/nrkno/tv-automation-server-core/commit/f0f33837b07b944abe320ae4a378641bba167a3c)) - +- **server-core-integration:** `autosubscribe` will resubscribe to collections using the same subscriptionId as when initially subscribed ([f0f3383](https://github.com/nrkno/tv-automation-server-core/commit/f0f33837b07b944abe320ae4a378641bba167a3c)) ### Features -* move gateway settings onto Studio SOFIE-1330 ([#907](https://github.com/nrkno/tv-automation-server-core/issues/907)) ([523c061](https://github.com/nrkno/tv-automation-server-core/commit/523c061c51296e21814deeeabbe9aafca21cd5a6)) -* peripheral device tidying ([#906](https://github.com/nrkno/tv-automation-server-core/issues/906)) ([0795ae5](https://github.com/nrkno/tv-automation-server-core/commit/0795ae5c0517114a34e350d5a5afbf7a731e9794)) - - - - +- move gateway settings onto Studio SOFIE-1330 ([#907](https://github.com/nrkno/tv-automation-server-core/issues/907)) ([523c061](https://github.com/nrkno/tv-automation-server-core/commit/523c061c51296e21814deeeabbe9aafca21cd5a6)) +- peripheral device tidying ([#906](https://github.com/nrkno/tv-automation-server-core/issues/906)) ([0795ae5](https://github.com/nrkno/tv-automation-server-core/commit/0795ae5c0517114a34e350d5a5afbf7a731e9794)) # [1.50.0-in-testing.5](https://github.com/nrkno/tv-automation-server-core/compare/v1.50.0-in-testing.4...v1.50.0-in-testing.5) (2023-08-25) @@ -184,18 +165,10 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline **Note:** Version bump only for package @sofie-automation/server-core-integration - - - - ## [1.49.2](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.1...v1.49.2) (2023-11-29) **Note:** Version bump only for package @sofie-automation/server-core-integration - - - - ## [1.49.1](https://github.com/nrkno/tv-automation-server-core/compare/v1.49.0...v1.49.1) (2023-10-20) **Note:** Version bump only for package @sofie-automation/server-core-integration diff --git a/packages/server-core-integration/package.json b/packages/server-core-integration/package.json index 4ce0860784..008cd6efc9 100644 --- a/packages/server-core-integration/package.json +++ b/packages/server-core-integration/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/server-core-integration", - "version": "1.51.0-in-development", + "version": "1.51.0-in-testing.0", "description": "Library for connecting to Core", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -70,7 +70,7 @@ "production" ], "dependencies": { - "@sofie-automation/shared-lib": "1.51.0-in-development", + "@sofie-automation/shared-lib": "1.51.0-in-testing.0", "ejson": "^2.2.3", "eventemitter3": "^4.0.7", "faye-websocket": "^0.11.4", diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index ea68d9893e..b88af543a6 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/shared-lib", - "version": "1.51.0-in-development", + "version": "1.51.0-in-testing.0", "description": "Library for types & values shared by core, workers and gateways", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/packages/yarn.lock b/packages/yarn.lock index 6e416d472e..4926001c7e 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -4583,11 +4583,11 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/blueprints-integration@1.51.0-in-development, @sofie-automation/blueprints-integration@workspace:blueprints-integration": +"@sofie-automation/blueprints-integration@1.51.0-in-testing.0, @sofie-automation/blueprints-integration@workspace:blueprints-integration": version: 0.0.0-use.local resolution: "@sofie-automation/blueprints-integration@workspace:blueprints-integration" dependencies: - "@sofie-automation/shared-lib": 1.51.0-in-development + "@sofie-automation/shared-lib": 1.51.0-in-testing.0 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: unknown @@ -4624,12 +4624,12 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/corelib@1.51.0-in-development, @sofie-automation/corelib@workspace:corelib": +"@sofie-automation/corelib@1.51.0-in-testing.0, @sofie-automation/corelib@workspace:corelib": version: 0.0.0-use.local resolution: "@sofie-automation/corelib@workspace:corelib" dependencies: - "@sofie-automation/blueprints-integration": 1.51.0-in-development - "@sofie-automation/shared-lib": 1.51.0-in-development + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.0 + "@sofie-automation/shared-lib": 1.51.0-in-testing.0 fast-clone: ^1.5.13 i18next: ^21.10.0 influx: ^5.9.3 @@ -4660,9 +4660,9 @@ __metadata: resolution: "@sofie-automation/job-worker@workspace:job-worker" dependencies: "@slack/webhook": ^6.1.0 - "@sofie-automation/blueprints-integration": 1.51.0-in-development - "@sofie-automation/corelib": 1.51.0-in-development - "@sofie-automation/shared-lib": 1.51.0-in-development + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.0 + "@sofie-automation/corelib": 1.51.0-in-testing.0 + "@sofie-automation/shared-lib": 1.51.0-in-testing.0 amqplib: ^0.10.3 deepmerge: ^4.3.1 elastic-apm-node: ^3.51.0 @@ -4692,11 +4692,11 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/server-core-integration@1.51.0-in-development, @sofie-automation/server-core-integration@workspace:server-core-integration": +"@sofie-automation/server-core-integration@1.51.0-in-testing.0, @sofie-automation/server-core-integration@workspace:server-core-integration": version: 0.0.0-use.local resolution: "@sofie-automation/server-core-integration@workspace:server-core-integration" dependencies: - "@sofie-automation/shared-lib": 1.51.0-in-development + "@sofie-automation/shared-lib": 1.51.0-in-testing.0 ejson: ^2.2.3 eventemitter3: ^4.0.7 faye-websocket: ^0.11.4 @@ -4706,7 +4706,7 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/shared-lib@1.51.0-in-development, @sofie-automation/shared-lib@workspace:shared-lib": +"@sofie-automation/shared-lib@1.51.0-in-testing.0, @sofie-automation/shared-lib@workspace:shared-lib": version: 0.0.0-use.local resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: @@ -15352,10 +15352,10 @@ asn1@evs-broadcast/node-asn1: "@asyncapi/generator": ^1.17.25 "@asyncapi/html-template": ^2.3.9 "@asyncapi/nodejs-ws-template": ^0.9.36 - "@sofie-automation/blueprints-integration": 1.51.0-in-development - "@sofie-automation/corelib": 1.51.0-in-development - "@sofie-automation/server-core-integration": 1.51.0-in-development - "@sofie-automation/shared-lib": 1.51.0-in-development + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.0 + "@sofie-automation/corelib": 1.51.0-in-testing.0 + "@sofie-automation/server-core-integration": 1.51.0-in-testing.0 + "@sofie-automation/shared-lib": 1.51.0-in-testing.0 debug: ^4.3.4 fast-clone: ^1.5.13 influx: ^5.9.3 @@ -17428,8 +17428,8 @@ asn1@evs-broadcast/node-asn1: resolution: "mos-gateway@workspace:mos-gateway" dependencies: "@mos-connection/connector": 4.1.1-nightly-master-20240430-072032-ffb8bf6.0 - "@sofie-automation/server-core-integration": 1.51.0-in-development - "@sofie-automation/shared-lib": 1.51.0-in-development + "@sofie-automation/server-core-integration": 1.51.0-in-testing.0 + "@sofie-automation/shared-lib": 1.51.0-in-testing.0 tslib: ^2.6.2 type-fest: ^3.13.1 underscore: ^1.13.6 @@ -19415,8 +19415,8 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "playout-gateway@workspace:playout-gateway" dependencies: - "@sofie-automation/server-core-integration": 1.51.0-in-development - "@sofie-automation/shared-lib": 1.51.0-in-development + "@sofie-automation/server-core-integration": 1.51.0-in-testing.0 + "@sofie-automation/shared-lib": 1.51.0-in-testing.0 debug: ^4.3.4 influx: ^5.9.3 timeline-state-resolver: 9.1.0 From 5c8b4e33fe5dbb4204b383547da04b722d8b6494 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Mon, 19 Aug 2024 10:58:44 +0200 Subject: [PATCH 414/479] chore: fix CURRENT_SYSTEM_VERSION --- meteor/server/migration/currentSystemVersion.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meteor/server/migration/currentSystemVersion.ts b/meteor/server/migration/currentSystemVersion.ts index 6a2a3c3b0c..eb20306081 100644 --- a/meteor/server/migration/currentSystemVersion.ts +++ b/meteor/server/migration/currentSystemVersion.ts @@ -54,4 +54,4 @@ */ // Note: Only set this to release versions, (ie X.Y.Z), not pre-releases (ie X.Y.Z-0-pre-release) -export const CURRENT_SYSTEM_VERSION = '1.52.0' +export const CURRENT_SYSTEM_VERSION = '1.51.0' From 7188a7af08a4fc4280c14e41a2c99976575fc257 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Mon, 19 Aug 2024 12:07:57 +0200 Subject: [PATCH 415/479] feat(Prompter): add Rundown name inter-titles --- meteor/client/styles/prompter.scss | 8 + meteor/client/ui/Prompter/PrompterView.tsx | 302 +++++++++++---------- meteor/client/ui/Prompter/prompter.ts | 79 ++++-- 3 files changed, 220 insertions(+), 169 deletions(-) diff --git a/meteor/client/styles/prompter.scss b/meteor/client/styles/prompter.scss index 2a6c061ce1..735428df22 100644 --- a/meteor/client/styles/prompter.scss +++ b/meteor/client/styles/prompter.scss @@ -70,6 +70,14 @@ body.prompter-scrollbar { transform: rotate(180deg); } + .prompter-rundown { + font-size: 75%; + text-align: center; + background: #fff; + color: #000; + font-weight: bold; + } + .prompter-segment { font-size: 75%; text-align: center; diff --git a/meteor/client/ui/Prompter/PrompterView.tsx b/meteor/client/ui/Prompter/PrompterView.tsx index 998e5194f4..01f673812a 100644 --- a/meteor/client/ui/Prompter/PrompterView.tsx +++ b/meteor/client/ui/Prompter/PrompterView.tsx @@ -1,38 +1,38 @@ import React, { PropsWithChildren } from 'react' import _ from 'underscore' // @ts-expect-error No types available -import Velocity from 'velocity-animate' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import ClassNames from 'classnames' import { Meteor } from 'meteor/meteor' +import { parse as queryStringParse } from 'query-string' import { Route } from 'react-router-dom' +import Velocity from 'velocity-animate' import { Translated, - useTracker, useGlobalDelayedTrackerUpdateState, useSubscription, useSubscriptions, + useTracker, } from '../../lib/ReactMeteorData/ReactMeteorData' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { parse as queryStringParse } from 'query-string' -import { Spinner } from '../../lib/Spinner' -import { firstIfArray, protectString } from '../../../lib/lib' -import { PrompterData, PrompterAPI, PrompterDataPart } from './prompter' -import { PrompterControlManager } from './controller/manager' -import { MeteorPubSub } from '../../../lib/api/pubsub' -import { documentTitle } from '../../lib/DocumentTitleProvider' -import { StudioScreenSaver } from '../StudioScreenSaver/StudioScreenSaver' -import { RundownTimingProvider } from '../RundownView/RundownTiming/RundownTimingProvider' -import { OverUnderTimer } from './OverUnderTimer' -import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { PartInstanceId, PieceId, RundownPlaylistId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { UIStudios } from '../Collections' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { withTranslation } from 'react-i18next' +import { MeteorPubSub } from '../../../lib/api/pubsub' import { UIStudio } from '../../../lib/api/studios' -import { RundownPlaylists, Rundowns } from '../../collections' import { RundownPlaylistCollectionUtil } from '../../../lib/collections/rundownPlaylistUtil' +import { firstIfArray, protectString } from '../../../lib/lib' import { logger } from '../../../lib/logging' -import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' -import { withTranslation } from 'react-i18next' +import { RundownPlaylists, Rundowns } from '../../collections' +import { documentTitle } from '../../lib/DocumentTitleProvider' +import { Spinner } from '../../lib/Spinner' +import { UIStudios } from '../Collections' +import { RundownTimingProvider } from '../RundownView/RundownTiming/RundownTimingProvider' +import { StudioScreenSaver } from '../StudioScreenSaver/StudioScreenSaver' +import { PrompterControlManager } from './controller/manager' +import { OverUnderTimer } from './OverUnderTimer' +import { PrompterAPI, PrompterData, PrompterDataPart } from './prompter' const DEFAULT_UPDATE_THROTTLE = 250 //ms const PIECE_MISSING_UPDATE_THROTTLE = 2000 //ms @@ -775,24 +775,28 @@ const PrompterContent = withTranslation()( return } + // TODO: In The 4 months this has been here (2024/09/19), not a single log line was recorded + // - does this still make sense? for (const scrollAnchor of scrollAnchors) { - for (const segment of this.props.prompterData.segments) { - if (scrollAnchor.anchorId === `segment_${segment.id}`) { - logger.error(`Read anchor troubleshooting: segment "${segment.id}" was found in prompterData!`) - return - } - - for (const part of segment.parts) { - if (scrollAnchor.anchorId === `partInstance_${part.id}`) { - logger.error(`Read anchor troubleshooting: part "${part.id}" was found in prompterData!`) + for (const rundown of this.props.prompterData.rundowns) { + for (const segment of rundown.segments) { + if (scrollAnchor.anchorId === `segment_${segment.id}`) { + logger.error(`Read anchor troubleshooting: segment "${segment.id}" was found in prompterData!`) return } - for (const piece of part.pieces) { - if (scrollAnchor.anchorId === `line_${piece.id}`) { - logger.error(`Read anchor troubleshooting: piece "${piece.id}" was found in prompterData!`) + for (const part of segment.parts) { + if (scrollAnchor.anchorId === `partInstance_${part.id}`) { + logger.error(`Read anchor troubleshooting: part "${part.id}" was found in prompterData!`) return } + + for (const piece of part.pieces) { + if (scrollAnchor.anchorId === `line_${piece.id}`) { + logger.error(`Read anchor troubleshooting: piece "${piece.id}" was found in prompterData!`) + return + } + } } } } @@ -804,18 +808,22 @@ const PrompterContent = withTranslation()( const { prompterData: nextPrompterData } = nextProps const currentPrompterPieces = _.flatten( - prompterData?.segments.map((segment) => - segment.parts.map((part) => - // collect all the PieceId's of all the non-empty pieces of script - _.compact(part.pieces.map((dataPiece) => (dataPiece.text !== '' ? dataPiece.id : null))) + prompterData?.rundowns.map((rundown) => + rundown.segments.map((segment) => + segment.parts.map((part) => + // collect all the PieceId's of all the non-empty pieces of script + _.compact(part.pieces.map((dataPiece) => (dataPiece.text !== '' ? dataPiece.id : null))) + ) ) ) ?? [] ) as PieceId[] const nextPrompterPieces = _.flatten( - nextPrompterData?.segments.map((segment) => - segment.parts.map((part) => - // collect all the PieceId's of all the non-empty pieces of script - _.compact(part.pieces.map((dataPiece) => (dataPiece.text !== '' ? dataPiece.id : null))) + nextPrompterData?.rundowns.map((rundown) => + rundown.segments.map((segment) => + segment.parts.map((part) => + // collect all the PieceId's of all the non-empty pieces of script + _.compact(part.pieces.map((dataPiece) => (dataPiece.text !== '' ? dataPiece.id : null))) + ) ) ) ?? [] ) as PieceId[] @@ -864,138 +872,154 @@ const PrompterContent = withTranslation()( } private renderPrompterData(prompterData: PrompterData) { + const { t } = this.props + const lines: React.ReactNode[] = [] + let hasInsertedScript = false - prompterData.segments.forEach((segment) => { - if (segment.parts.length === 0) { - return + for (const rundown of prompterData.rundowns) { + if (prompterData.rundowns.length > 1) { + lines.push( +
+ {rundown.title || 'N/A'} +
+ ) } - const firstPart = segment.parts[0] - const firstPartStatus = this.getPartStatus(prompterData, firstPart) + for (const segment of rundown.segments) { + if (segment.parts.length === 0) { + return + } - lines.push( -
- {segment.title || 'N/A'} -
- ) + const firstPart = segment.parts[0] + const firstPartStatus = this.getPartStatus(prompterData, firstPart) - segment.parts.forEach((part) => { lines.push(
- {part.title || 'N/A'} + {segment.title || 'N/A'}
) - part.pieces.forEach((line) => { + hasInsertedScript = true + + for (const part of segment.parts) { lines.push(
- {line.text || ''} + {part.title || 'N/A'}
) - }) - }) - }) + + for (const line of part.pieces) { + lines.push( +
+ {line.text || ''} +
+ ) + } + } + } + } + + if (hasInsertedScript) { + lines.push(
—{t('End of script')}—
) + } return lines } render(): JSX.Element { - const { t } = this.props - - if (this.props.prompterData) { + if (!this.props.prompterData) { return ( -
- {this.props.children} - -
-
+
+ +
+ ) + } -
12 ? `12vmin` : undefined, - }} - > -
-
-
-
+ return ( +
+ {this.props.children} + +
+
12 ? `12vmin` : undefined, }} > -
{this.props.prompterData.title}
- - {this.renderPrompterData(this.props.prompterData)} - - {this.props.prompterData.segments.length ? ( -
—{t('End of script')}—
- ) : null} +
+
- ) - } else { - return ( -
- + +
+
{this.props.prompterData.title}
+ + {this.renderPrompterData(this.props.prompterData)}
- ) - } +
+ ) } } ) diff --git a/meteor/client/ui/Prompter/prompter.ts b/meteor/client/ui/Prompter/prompter.ts index d7d0ae71b1..3b5b83515c 100644 --- a/meteor/client/ui/Prompter/prompter.ts +++ b/meteor/client/ui/Prompter/prompter.ts @@ -1,14 +1,4 @@ -import { check } from '../../../lib/check' -import * as _ from 'underscore' import { ScriptContent, SourceLayerType } from '@sofie-automation/blueprints-integration' -import { normalizeArrayToMap, protectString } from '../../../lib/lib' -import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' -import { getPieceInstancesForPartInstance, getSegmentsWithPartInstances } from '../../lib/RundownResolver' -import { FindOptions } from '../../../lib/collections/lib' -import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { processAndPrunePieceInstanceTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' -import { UIShowStyleBases } from '../Collections' import { PartId, PartInstanceId, @@ -18,10 +8,20 @@ import { SegmentId, ShowStyleBaseId, } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { RundownPlaylists, PieceInstances, Pieces, Segments } from '../../collections' +import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' +import { processAndPrunePieceInstanceTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' +import * as _ from 'underscore' +import { check } from '../../../lib/check' +import { FindOptions } from '../../../lib/collections/lib' import { RundownPlaylistCollectionUtil } from '../../../lib/collections/rundownPlaylistUtil' -import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import { normalizeArrayToMap, protectString } from '../../../lib/lib' +import { PieceInstances, Pieces, RundownPlaylists, Segments } from '../../collections' +import { getPieceInstancesForPartInstance, getSegmentsWithPartInstances } from '../../lib/RundownResolver' +import { UIShowStyleBases } from '../Collections' // export interface NewPrompterAPI { // getPrompterData (playlistId: RundownPlaylistId): Promise @@ -30,9 +30,15 @@ import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' // 'getPrompterData' = 'PrompterMethods.getPrompterData' // } +export interface PrompterDataRundown { + id: RundownId + title: string + segments: PrompterDataSegment[] +} + export interface PrompterDataSegment { id: SegmentId - title: string | undefined + title: string parts: PrompterDataPart[] } export interface PrompterDataPart { @@ -48,7 +54,7 @@ export interface PrompterData { title: string currentPartInstanceId: PartInstanceId | null nextPartInstanceId: PartInstanceId | null - segments: Array + rundowns: Array } export namespace PrompterAPI { @@ -113,7 +119,7 @@ export namespace PrompterAPI { title: playlist.name, currentPartInstanceId: currentPartInstance ? currentPartInstance._id : null, nextPartInstanceId: nextPartInstance ? nextPartInstance._id : null, - segments: [], + rundowns: [], } const piecesIncluded: PieceId[] = [] @@ -170,6 +176,8 @@ export namespace PrompterAPI { ).fetch() : undefined + const orderedRundowns = new Map() + groupedParts.forEach(({ segment, partInstances }, segmentIndex) => { const segmentId = segment._id const rundown = rundownMap.get(segment.rundownId) @@ -182,9 +190,19 @@ export namespace PrompterAPI { return } + let rundownObj = orderedRundowns.get(rundown._id) + if (!rundownObj) { + rundownObj = { + id: rundown._id, + title: rundown.name, + segments: [], + } + orderedRundowns.set(rundown._id, rundownObj) + } + const segmentData: PrompterDataSegment = { id: segmentId, - title: segment ? segment.name : undefined, + title: segment.name, parts: [], } @@ -230,19 +248,17 @@ export namespace PrompterAPI { const piece = pieceInstance.piece const sourceLayer = sourceLayers[piece.sourceLayerId] - if (piece.content && sourceLayer && sourceLayer.type === SourceLayerType.SCRIPT) { - const content = piece.content as ScriptContent - if (content.fullScript) { - if (piecesIncluded.indexOf(piece._id) >= 0) { - break // piece already included in prompter script - } - piecesIncluded.push(piece._id) - partData.pieces.push({ - id: piece._id, - text: content.fullScript, - }) - } - } + if (!piece.content || !sourceLayer || sourceLayer.type !== SourceLayerType.SCRIPT) break + + const content = piece.content as ScriptContent + if (!content.fullScript) break + if (piecesIncluded.indexOf(piece._id) >= 0) break // piece already included in prompter script + + piecesIncluded.push(piece._id) + partData.pieces.push({ + id: piece._id, + text: content.fullScript, + }) } } @@ -257,8 +273,11 @@ export namespace PrompterAPI { segmentData.parts.push(partData) } - data.segments.push(segmentData) + rundownObj.segments.push(segmentData) }) + + data.rundowns = Array.from(orderedRundowns.values()) + return data } } From 82f6fb2720992581f26ff18d161e787784195c95 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 19 Aug 2024 12:25:43 +0100 Subject: [PATCH 416/479] fix: playout-gateway not passing datastore to tsr at startup --- packages/playout-gateway/src/tsrHandler.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/playout-gateway/src/tsrHandler.ts b/packages/playout-gateway/src/tsrHandler.ts index b426f82de8..d3245bd383 100644 --- a/packages/playout-gateway/src/tsrHandler.ts +++ b/packages/playout-gateway/src/tsrHandler.ts @@ -17,6 +17,7 @@ import { SlowFulfilledCommandInfo, DeviceStatus, StatusCode, + Datastore, } from 'timeline-state-resolver' import { CoreHandler, CoreTSRDeviceHandler } from './coreHandler' import * as crypto from 'crypto' @@ -229,6 +230,7 @@ export class TSRHandler { this._triggerupdateTimelineAndMappings('TSRHandler.init(), later') this.onSettingsChanged() this._triggerUpdateDevices() + this._triggerUpdateDatastore() this.logger.debug('tsr init done') } @@ -1085,12 +1087,12 @@ export class TSRHandler { const datastoreObjs = datastoreCollection.find({ studioId: peripheralDevice.studioId, }) - const datastore: Record = {} + const datastore: Datastore = {} for (const { key, value, modified } of datastoreObjs) { datastore[key] = { value, modified } } - this.logger.debug(datastore) + this.logger.debug('datastore', datastore) this.tsr.setDatastore(datastore) } /** From 5b34251da686f211a98f4a0e9d62bac43b0d0e39 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Tue, 20 Aug 2024 09:46:02 +0200 Subject: [PATCH 417/479] chore: bump mos-connection dep --- meteor/package.json | 2 +- meteor/yarn.lock | 22 ++++++++++---------- packages/mos-gateway/package.json | 2 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 34 +++++++++++++++---------------- 5 files changed, 31 insertions(+), 31 deletions(-) diff --git a/meteor/package.json b/meteor/package.json index 40309761ff..be483a0def 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -48,7 +48,7 @@ "@jstarpl/react-contextmenu": "^2.15.0", "@koa/cors": "^5.0.0", "@koa/router": "^12.0.1", - "@mos-connection/helper": "^4.1.1-nightly-master-20240430-072032-ffb8bf6.0", + "@mos-connection/helper": "^4.1.1", "@nrk/core-icons": "^9.6.0", "@popperjs/core": "^2.11.8", "@slack/webhook": "^6.1.0", diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 8e58fadf3d..0ca5be7f22 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1020,23 +1020,23 @@ __metadata: languageName: node linkType: hard -"@mos-connection/helper@npm:^4.1.1-nightly-master-20240430-072032-ffb8bf6.0": - version: 4.1.1-nightly-master-20240430-072032-ffb8bf6.0 - resolution: "@mos-connection/helper@npm:4.1.1-nightly-master-20240430-072032-ffb8bf6.0" +"@mos-connection/helper@npm:^4.1.1": + version: 4.1.1 + resolution: "@mos-connection/helper@npm:4.1.1" dependencies: - "@mos-connection/model": 4.1.1-nightly-master-20240430-072032-ffb8bf6.0 + "@mos-connection/model": 4.1.1 iconv-lite: ^0.6.3 tslib: ^2.5.3 xml-js: ^1.6.11 xmlbuilder: ^15.1.1 - checksum: dcf7d5e800d08c007a4026b37ddcc6ebe77d020a311809bc379a5c7d15657c2d447b4149682aec17161d17217e2c6d5167c7ef81e954a4ed5a6283a7c75a7e0a + checksum: 3ff0982889d33e97bb573de101706aba59936b0413651425c900a433b3f2981668c7099898c3385a3a4c691813b9fb73d22b77647d09c6a4fd5e513fbb18b115 languageName: node linkType: hard -"@mos-connection/model@npm:4.1.1-nightly-master-20240430-072032-ffb8bf6.0, @mos-connection/model@npm:^4.1.1-nightly-master-20240430-072032-ffb8bf6.0": - version: 4.1.1-nightly-master-20240430-072032-ffb8bf6.0 - resolution: "@mos-connection/model@npm:4.1.1-nightly-master-20240430-072032-ffb8bf6.0" - checksum: 07bd3ffc94a8c140d994de445c34158bb9f5f8950f6cea34d59657fe4da1f0f6b5d0c2c7ef1b2ad902b43c846485ab10cb9f19d707233b875fe739d731151bdc +"@mos-connection/model@npm:4.1.1, @mos-connection/model@npm:^4.1.1": + version: 4.1.1 + resolution: "@mos-connection/model@npm:4.1.1" + checksum: 31332dcf977ca6735b6c39729bc1c31c750dc965e540d0dbbde34695c69651dfa1fdb97d8347f81ad6a66ad39062ff84bc2d93e8bcb142fe953e49490cf63d2d languageName: node linkType: hard @@ -1417,7 +1417,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/shared-lib@portal:../packages/shared-lib::locator=automation-core%40workspace%3A." dependencies: - "@mos-connection/model": ^4.1.1-nightly-master-20240430-072032-ffb8bf6.0 + "@mos-connection/model": ^4.1.1 timeline-state-resolver-types: 9.1.0 tslib: ^2.6.2 type-fest: ^3.13.1 @@ -2689,7 +2689,7 @@ __metadata: "@jstarpl/react-contextmenu": ^2.15.0 "@koa/cors": ^5.0.0 "@koa/router": ^12.0.1 - "@mos-connection/helper": ^4.1.1-nightly-master-20240430-072032-ffb8bf6.0 + "@mos-connection/helper": ^4.1.1 "@nrk/core-icons": ^9.6.0 "@popperjs/core": ^2.11.8 "@shopify/jest-koa-mocks": ^5.1.1 diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index 94aa2a5953..a2daa397aa 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -65,7 +65,7 @@ "production" ], "dependencies": { - "@mos-connection/connector": "4.1.1-nightly-master-20240430-072032-ffb8bf6.0", + "@mos-connection/connector": "4.1.1", "@sofie-automation/server-core-integration": "1.51.0-in-testing.0", "@sofie-automation/shared-lib": "1.51.0-in-testing.0", "tslib": "^2.6.2", diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index b88af543a6..04d198d1b2 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -38,7 +38,7 @@ "/LICENSE" ], "dependencies": { - "@mos-connection/model": "^4.1.1-nightly-master-20240430-072032-ffb8bf6.0", + "@mos-connection/model": "^4.1.1", "timeline-state-resolver-types": "9.1.0", "tslib": "^2.6.2", "type-fest": "^3.13.1" diff --git a/packages/yarn.lock b/packages/yarn.lock index 4926001c7e..34523db9c0 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -3326,37 +3326,37 @@ __metadata: languageName: node linkType: hard -"@mos-connection/connector@npm:4.1.1-nightly-master-20240430-072032-ffb8bf6.0": - version: 4.1.1-nightly-master-20240430-072032-ffb8bf6.0 - resolution: "@mos-connection/connector@npm:4.1.1-nightly-master-20240430-072032-ffb8bf6.0" +"@mos-connection/connector@npm:4.1.1": + version: 4.1.1 + resolution: "@mos-connection/connector@npm:4.1.1" dependencies: - "@mos-connection/helper": 4.1.1-nightly-master-20240430-072032-ffb8bf6.0 - "@mos-connection/model": 4.1.1-nightly-master-20240430-072032-ffb8bf6.0 + "@mos-connection/helper": 4.1.1 + "@mos-connection/model": 4.1.1 iconv-lite: ^0.6.3 tslib: ^2.5.3 xml-js: ^1.6.11 xmlbuilder: ^15.1.1 - checksum: 5a1879e940867380a7fea443c71cb220ad4e932c8acfa34ae1cb0a2ade471d16f44b72293d604a8aa4b227e356b7f3dfb520f822f6e330ab5bc9e48fbfefcab3 + checksum: 5fca3ae81a0c9aced89de56ad60178a1ff725c99807be99e8097a9dd7b77d09045313e87d86312942b788da0afa66611d30aeedd07ff44484c06996cf4258576 languageName: node linkType: hard -"@mos-connection/helper@npm:4.1.1-nightly-master-20240430-072032-ffb8bf6.0": - version: 4.1.1-nightly-master-20240430-072032-ffb8bf6.0 - resolution: "@mos-connection/helper@npm:4.1.1-nightly-master-20240430-072032-ffb8bf6.0" +"@mos-connection/helper@npm:4.1.1": + version: 4.1.1 + resolution: "@mos-connection/helper@npm:4.1.1" dependencies: - "@mos-connection/model": 4.1.1-nightly-master-20240430-072032-ffb8bf6.0 + "@mos-connection/model": 4.1.1 iconv-lite: ^0.6.3 tslib: ^2.5.3 xml-js: ^1.6.11 xmlbuilder: ^15.1.1 - checksum: dcf7d5e800d08c007a4026b37ddcc6ebe77d020a311809bc379a5c7d15657c2d447b4149682aec17161d17217e2c6d5167c7ef81e954a4ed5a6283a7c75a7e0a + checksum: 3ff0982889d33e97bb573de101706aba59936b0413651425c900a433b3f2981668c7099898c3385a3a4c691813b9fb73d22b77647d09c6a4fd5e513fbb18b115 languageName: node linkType: hard -"@mos-connection/model@npm:4.1.1-nightly-master-20240430-072032-ffb8bf6.0, @mos-connection/model@npm:^4.1.1-nightly-master-20240430-072032-ffb8bf6.0": - version: 4.1.1-nightly-master-20240430-072032-ffb8bf6.0 - resolution: "@mos-connection/model@npm:4.1.1-nightly-master-20240430-072032-ffb8bf6.0" - checksum: 07bd3ffc94a8c140d994de445c34158bb9f5f8950f6cea34d59657fe4da1f0f6b5d0c2c7ef1b2ad902b43c846485ab10cb9f19d707233b875fe739d731151bdc +"@mos-connection/model@npm:4.1.1, @mos-connection/model@npm:^4.1.1": + version: 4.1.1 + resolution: "@mos-connection/model@npm:4.1.1" + checksum: 31332dcf977ca6735b6c39729bc1c31c750dc965e540d0dbbde34695c69651dfa1fdb97d8347f81ad6a66ad39062ff84bc2d93e8bcb142fe953e49490cf63d2d languageName: node linkType: hard @@ -4710,7 +4710,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: - "@mos-connection/model": ^4.1.1-nightly-master-20240430-072032-ffb8bf6.0 + "@mos-connection/model": ^4.1.1 timeline-state-resolver-types: 9.1.0 tslib: ^2.6.2 type-fest: ^3.13.1 @@ -17427,7 +17427,7 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "mos-gateway@workspace:mos-gateway" dependencies: - "@mos-connection/connector": 4.1.1-nightly-master-20240430-072032-ffb8bf6.0 + "@mos-connection/connector": 4.1.1 "@sofie-automation/server-core-integration": 1.51.0-in-testing.0 "@sofie-automation/shared-lib": 1.51.0-in-testing.0 tslib: ^2.6.2 From 27ab0263b069af8f86a37cb62ae5ecc8120b9d16 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Wed, 21 Aug 2024 10:26:05 +0200 Subject: [PATCH 418/479] chore: sync package.ts file with Package Manager --- .../shared-lib/src/package-manager/package.ts | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/shared-lib/src/package-manager/package.ts b/packages/shared-lib/src/package-manager/package.ts index ddc485bf8e..0e00371284 100644 --- a/packages/shared-lib/src/package-manager/package.ts +++ b/packages/shared-lib/src/package-manager/package.ts @@ -7,6 +7,10 @@ import { StatusCode } from '../lib/status' +type AccessorId = string +type ExpectedPackageId = string +type PackageContainerId = string + // eslint-disable-next-line @typescript-eslint/no-namespace export namespace ExpectedPackage { export type Any = ExpectedPackageMediaFile | ExpectedPackageQuantelClip | ExpectedPackageJSONData @@ -24,7 +28,7 @@ export namespace ExpectedPackage { /** Generic (used in extends) */ export interface Base { /** Unique id of the expectedPackage */ - _id: string + _id: ExpectedPackageId /** Reference to which timeline-layer(s) the Package is going to be used in. * (Used to route the package to the right playout-device (targets)) */ @@ -54,19 +58,21 @@ export namespace ExpectedPackage { */ sources: { /** Reference to a PackageContainer */ - containerId: string + containerId: PackageContainerId /** Locally defined Accessors, these are combined (deep extended) with the PackageContainer (if it is found) Accessors */ - accessors: { [accessorId: string]: AccessorOnPackage.Any } + accessors: { + [accessorId: AccessorId]: AccessorOnPackage.Any + } }[] /** The sideEffect is used by the Package Manager to generate extra artifacts, such as thumbnails & previews */ sideEffect: { /** Which container previews are to be put into */ - previewContainerId?: string | null // null is used to disable the sideEffect + previewContainerId?: PackageContainerId | null // null is used to disable the sideEffect previewPackageSettings?: SideEffectPreviewSettings | null /** Which container thumbnails are to be put into */ - thumbnailContainerId?: string | null // null is used to disable the sideEffect + thumbnailContainerId?: PackageContainerId | null // null is used to disable the sideEffect thumbnailPackageSettings?: SideEffectThumbnailSettings | null /** Should the package be scanned for loudness */ @@ -110,7 +116,7 @@ export namespace ExpectedPackage { export interface ExpectedPackageMediaFile extends Base { type: PackageType.MEDIA_FILE content: { - /** Local file path on the playout device */ + /** Local file path on the package container */ filePath: string } version: { @@ -120,9 +126,9 @@ export namespace ExpectedPackage { checkSumType?: 'sha' | 'md5' | 'whatever' } sources: { - containerId: string + containerId: PackageContainerId accessors: { - [accessorId: string]: + [accessorId: AccessorId]: | AccessorOnPackage.LocalFolder | AccessorOnPackage.FileShare | AccessorOnPackage.HTTP @@ -149,8 +155,8 @@ export namespace ExpectedPackage { cloneId?: number } sources: { - containerId: string - accessors: { [accessorId: string]: AccessorOnPackage.Quantel } + containerId: PackageContainerId + accessors: { [accessorId: AccessorId]: AccessorOnPackage.Quantel } }[] } @@ -162,9 +168,9 @@ export namespace ExpectedPackage { } version: any // {} sources: { - containerId: string + containerId: PackageContainerId accessors: { - [accessorId: string]: + [accessorId: AccessorId]: | AccessorOnPackage.HTTP | AccessorOnPackage.HTTPProxy | AccessorOnPackage.LocalFolder From 0a4bd7f73b7cc6326b509c3dbfd2947d40b9f793 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Wed, 21 Aug 2024 10:26:30 +0200 Subject: [PATCH 419/479] fix: add ExpectedPackageHtmlTemplate types from Package Manager --- .../shared-lib/src/package-manager/package.ts | 67 ++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/packages/shared-lib/src/package-manager/package.ts b/packages/shared-lib/src/package-manager/package.ts index 0e00371284..35671e5ccd 100644 --- a/packages/shared-lib/src/package-manager/package.ts +++ b/packages/shared-lib/src/package-manager/package.ts @@ -13,7 +13,11 @@ type PackageContainerId = string // eslint-disable-next-line @typescript-eslint/no-namespace export namespace ExpectedPackage { - export type Any = ExpectedPackageMediaFile | ExpectedPackageQuantelClip | ExpectedPackageJSONData + export type Any = + | ExpectedPackageMediaFile + | ExpectedPackageQuantelClip + | ExpectedPackageJSONData + | ExpectedPackageHtmlTemplate export enum PackageType { MEDIA_FILE = 'media_file', @@ -178,6 +182,67 @@ export namespace ExpectedPackage { } }[] } + export interface ExpectedPackageHtmlTemplate extends Base { + type: PackageType.HTML_TEMPLATE + content: { + /** path to the HTML template */ + path: string + /** Add prefix to output artifacts */ + outputPrefix: string + } + version: { + renderer?: { + /** Renderer width, defaults to 1920 */ + width?: number + /** Renderer width, defaults to 1080 */ + height?: number + /** + * Scale the rendered width and height with this value, and also zoom the content accordingly. + * For example, if the width is 1920 and scale is 0.5, the width will be scaled to 960. + * (Defaults to 1) + */ + scale?: number + /** Background color, #RRGGBB, CSS-string, "transparent" or "default" (defaults to "default") */ + background?: string + userAgent?: string + } + + /** + * Convenience settings for a template that follows the typical CasparCG steps; + * update(data); play(); stop(); + * If this is set, steps are overridden */ + casparCG?: { + /** + * Data to send into the update() function of a CasparCG Template. + * Strings will be piped through as-is, objects will be JSON.stringified. + */ + data: { [key: string]: any } | null | string + + /** How long to wait between each action in a CasparCG template, (default: 1000ms) */ + delay?: number + } + + steps?: ( + | { do: 'waitForLoad' } + | { do: 'sleep'; duration: number } + | { do: 'takeScreenshot'; fileName: string } + | { do: 'startRecording'; fileName: string } + | { do: 'stopRecording' } + | { do: 'cropRecording'; fileName: string } + | { do: 'executeJs'; js: string } + )[] + } + sources: { + containerId: PackageContainerId + accessors: { + [accessorId: AccessorId]: + | AccessorOnPackage.LocalFolder + | AccessorOnPackage.FileShare + | AccessorOnPackage.HTTP + | AccessorOnPackage.HTTPProxy + } + }[] + } } /** A PackageContainer defines a place that contains Packages, that can be read or written to. From 433ea997cbb73fccb93c5aa4b3e7d60140210bfc Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 21 Aug 2024 13:01:55 +0200 Subject: [PATCH 420/479] fix(ServerClientAPI): response/resolution is waiting for UserActionsLog update --- meteor/server/api/client.ts | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/meteor/server/api/client.ts b/meteor/server/api/client.ts index 90bdfd170f..9d9aab6cf3 100644 --- a/meteor/server/api/client.ts +++ b/meteor/server/api/client.ts @@ -235,14 +235,21 @@ export namespace ServerClientAPI { const result = await fcn(credentials, userActionMetadata) const completeTime = Date.now() - await UserActionsLog.updateAsync(actionId, { - $set: { - success: true, - doneTime: completeTime, - executionTime: completeTime - startTime, - workerTime: userActionMetadata.workerDuration, - }, - }) + pInitialInsert + .then(async () => + UserActionsLog.updateAsync(actionId, { + $set: { + success: true, + doneTime: completeTime, + executionTime: completeTime - startTime, + workerTime: userActionMetadata.workerDuration, + }, + }) + ) + .catch((err) => { + // If this fails make sure it is handled + logger.warn(`Failed to update UserActionsLog: ${stringifyError(err)}`) + }) return ClientAPI.responseSuccess(result) } catch (e) { From 38d220ce970c45df4896998e023dafca6331c8cf Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 21 Aug 2024 14:51:53 +0100 Subject: [PATCH 421/479] chore: fix failing test --- meteor/server/api/__tests__/userActions/general.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/meteor/server/api/__tests__/userActions/general.test.ts b/meteor/server/api/__tests__/userActions/general.test.ts index b505426674..9a8233aca6 100644 --- a/meteor/server/api/__tests__/userActions/general.test.ts +++ b/meteor/server/api/__tests__/userActions/general.test.ts @@ -13,6 +13,8 @@ require('../../userActions') // include in order to create the Meteor methods ne describe('User Actions - General', () => { beforeEach(async () => { + await UserActionsLog.removeAsync({}) + await setupDefaultStudioEnvironment() }) From ba357b63923bb9e1bcf0559cb6bff9fdb0529cc3 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Fri, 23 Aug 2024 07:02:31 +0200 Subject: [PATCH 422/479] chore: update types --- .../shared-lib/src/package-manager/package.ts | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/shared-lib/src/package-manager/package.ts b/packages/shared-lib/src/package-manager/package.ts index 35671e5ccd..a43bfd7da9 100644 --- a/packages/shared-lib/src/package-manager/package.ts +++ b/packages/shared-lib/src/package-manager/package.ts @@ -23,6 +23,7 @@ export namespace ExpectedPackage { MEDIA_FILE = 'media_file', QUANTEL_CLIP = 'quantel_clip', JSON_DATA = 'json_data', + HTML_TEMPLATE = 'html_template', // TALLY_LABEL = 'tally_label' @@ -192,9 +193,7 @@ export namespace ExpectedPackage { } version: { renderer?: { - /** Renderer width, defaults to 1920 */ width?: number - /** Renderer width, defaults to 1080 */ height?: number /** * Scale the rendered width and height with this value, and also zoom the content accordingly. @@ -225,11 +224,36 @@ export namespace ExpectedPackage { steps?: ( | { do: 'waitForLoad' } | { do: 'sleep'; duration: number } + | { + do: 'sendHTTPCommand' + url: string + /** GET, POST, PUT etc.. */ + method: string + body?: ArrayBuffer | ArrayBufferView | NodeJS.ReadableStream | string | URLSearchParams + + headers?: Record + } | { do: 'takeScreenshot'; fileName: string } | { do: 'startRecording'; fileName: string } | { do: 'stopRecording' } | { do: 'cropRecording'; fileName: string } | { do: 'executeJs'; js: string } + // Store an object in memory + | { + do: 'storeObject' + key: string + /** The value to store into memory. Either an object, or a JSON-stringified object */ + value: Record | string + } + // Modify an object in memory. Path is a dot-separated string + | { do: 'modifyObject'; key: string; path: string; value: any } + // Send an object to the renderer as a postMessage (so basically does a executeJs: window.postMessage(memory[key])) + | { + do: 'injectObject' + key: string + /** The method to receive the value. Defaults to window.postMessage */ + receivingFunction?: string + } )[] } sources: { From 45065e51d9c896883b307ffa554d40d667349dd3 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Fri, 23 Aug 2024 07:22:40 +0200 Subject: [PATCH 423/479] chore: update types --- .../shared-lib/src/package-manager/package.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/shared-lib/src/package-manager/package.ts b/packages/shared-lib/src/package-manager/package.ts index a43bfd7da9..e542f4e342 100644 --- a/packages/shared-lib/src/package-manager/package.ts +++ b/packages/shared-lib/src/package-manager/package.ts @@ -193,7 +193,9 @@ export namespace ExpectedPackage { } version: { renderer?: { + /** Renderer width, defaults to 1920 */ width?: number + /** Renderer height, defaults to 1080 */ height?: number /** * Scale the rendered width and height with this value, and also zoom the content accordingly. @@ -280,7 +282,7 @@ export interface PackageContainer { label: string /** A list of ways to access the PackageContainer. Note: The accessors are different ways to access THE SAME PackageContainer. */ - accessors: { [accessorId: string]: Accessor.Any } + accessors: { [accessorId: AccessorId]: Accessor.Any } } /** Defines different ways of accessing a PackageContainer. @@ -340,10 +342,16 @@ export namespace Accessor { allowWrite: false /** Base url (url to the host), for example http://myhost.com/fileShare/ */ - baseUrl: string + baseUrl?: string /** Name/Id of the network the share exists on. Used to differ between different local networks. Leave empty if globally accessible. */ networkId?: string + + /** If true, assumes that a source never changes once it has been fetched. */ + isImmutable?: boolean + + /** If true, assumes that the source doesn't support HEAD requests and will use GET instead. If false, HEAD requests will be sent to check availability. */ + useGETinsteadOfHEAD?: boolean } /** Definition of access to the HTTP-proxy server that comes with Package Manager. */ export interface HTTPProxy extends Base { @@ -438,11 +446,11 @@ export namespace AccessorOnPackage { } export interface PackageContainerOnPackage extends Omit { - containerId: string + containerId: PackageContainerId /** Short name, for displaying to user */ label: string - accessors: { [accessorId: string]: AccessorOnPackage.Any } + accessors: { [accessorId: AccessorId]: AccessorOnPackage.Any } } // todo: should this be moved into core-integration? From fb1014a0c25d55574d11c081a90c24d5017693ba Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Fri, 23 Aug 2024 08:05:49 +0200 Subject: [PATCH 424/479] fix: add support for prerendered html-templates --- .../Status/package-status/PackageStatus.tsx | 2 + meteor/lib/collections/ExpectedPackages.ts | 49 ++++++++------- .../shared-lib/src/package-manager/helpers.ts | 61 +++++++++++++++++++ .../shared-lib/src/package-manager/package.ts | 2 - 4 files changed, 90 insertions(+), 24 deletions(-) create mode 100644 packages/shared-lib/src/package-manager/helpers.ts diff --git a/meteor/client/ui/Status/package-status/PackageStatus.tsx b/meteor/client/ui/Status/package-status/PackageStatus.tsx index fb4c461cee..74446b8860 100644 --- a/meteor/client/ui/Status/package-status/PackageStatus.tsx +++ b/meteor/client/ui/Status/package-status/PackageStatus.tsx @@ -31,6 +31,8 @@ export const PackageStatus: React.FC<{ return p2.content.title || p2.content.guid || unprotectString(props.package._id) } else if (p2.type === ExpectedPackage.PackageType.JSON_DATA) { return p2.content.path || unprotectString(props.package._id) + } else if (p2.type === ExpectedPackage.PackageType.HTML_TEMPLATE) { + return p2.content.path || unprotectString(props.package._id) } else { assertNever(p2) return unprotectString(props.package._id) diff --git a/meteor/lib/collections/ExpectedPackages.ts b/meteor/lib/collections/ExpectedPackages.ts index b4349899cd..cfffad7842 100644 --- a/meteor/lib/collections/ExpectedPackages.ts +++ b/meteor/lib/collections/ExpectedPackages.ts @@ -1,49 +1,54 @@ import { ExpectedPackage } from '@sofie-automation/blueprints-integration' import { assertNever, literal } from '../lib' import { StudioLight } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { + htmlTemplateGetSteps, + htmlTemplateGetFileNamesFromSteps, +} from '@sofie-automation/shared-lib/dist/package-manager/helpers' import deepExtend from 'deep-extend' export function getPreviewPackageSettings( expectedPackage: ExpectedPackage.Any ): ExpectedPackage.SideEffectPreviewSettings | undefined { - let packagePath: string | undefined - if (expectedPackage.type === ExpectedPackage.PackageType.MEDIA_FILE) { - packagePath = expectedPackage.content.filePath + const packagePath = expectedPackage.content.filePath + if (packagePath) return { path: packagePath + '_preview.webm' } } else if (expectedPackage.type === ExpectedPackage.PackageType.QUANTEL_CLIP) { - packagePath = expectedPackage.content.guid || expectedPackage.content.title + const packagePath = expectedPackage.content.guid || expectedPackage.content.title + if (packagePath) return { path: packagePath + '_preview.webm' } } else if (expectedPackage.type === ExpectedPackage.PackageType.JSON_DATA) { - packagePath = undefined // Not supported + return undefined // Not supported + } else if (expectedPackage.type === ExpectedPackage.PackageType.HTML_TEMPLATE) { + const steps = htmlTemplateGetSteps(expectedPackage.version) + const o = htmlTemplateGetFileNamesFromSteps(steps) + if (o.mainRecording) return { path: o.mainRecording } + else return undefined } else { assertNever(expectedPackage) + return undefined } - if (packagePath) { - return { - path: packagePath + '_preview.webm', - } - } - return undefined } export function getThumbnailPackageSettings( expectedPackage: ExpectedPackage.Any ): ExpectedPackage.SideEffectThumbnailSettings | undefined { - let packagePath: string | undefined - if (expectedPackage.type === ExpectedPackage.PackageType.MEDIA_FILE) { - packagePath = expectedPackage.content.filePath + const packagePath = expectedPackage.content.filePath + if (packagePath) return { path: packagePath + '_thumbnail.png' } } else if (expectedPackage.type === ExpectedPackage.PackageType.QUANTEL_CLIP) { - packagePath = expectedPackage.content.guid || expectedPackage.content.title + const packagePath = expectedPackage.content.guid || expectedPackage.content.title + if (packagePath) return { path: packagePath + '_thumbnail.png' } } else if (expectedPackage.type === ExpectedPackage.PackageType.JSON_DATA) { - packagePath = undefined // Not supported + return undefined // Not supported + } else if (expectedPackage.type === ExpectedPackage.PackageType.HTML_TEMPLATE) { + // temporary implementation: + const steps = htmlTemplateGetSteps(expectedPackage.version) + const o = htmlTemplateGetFileNamesFromSteps(steps) + if (o.mainScreenShot) return { path: o.mainScreenShot } + else return undefined } else { assertNever(expectedPackage) + return undefined } - if (packagePath) { - return { - path: packagePath + '_thumbnail.png', - } - } - return undefined } export function getSideEffect( expectedPackage: ExpectedPackage.Base, diff --git a/packages/shared-lib/src/package-manager/helpers.ts b/packages/shared-lib/src/package-manager/helpers.ts new file mode 100644 index 0000000000..c6eb6d8bb1 --- /dev/null +++ b/packages/shared-lib/src/package-manager/helpers.ts @@ -0,0 +1,61 @@ +import { ExpectedPackage } from './package' + +// Note: These functions are copied from Package Manager + +type Steps = Required['steps'] + +export function htmlTemplateGetSteps(version: ExpectedPackage.ExpectedPackageHtmlTemplate['version']): Steps { + let steps: Steps + if (version.casparCG) { + // Generate a set of steps for standard CasparCG templates + const casparData = version.casparCG.data + const casparDataJSON = typeof casparData === 'string' ? casparData : JSON.stringify(casparData) + steps = [ + { do: 'waitForLoad' }, + { do: 'takeScreenshot', fileName: 'idle.png' }, + { do: 'startRecording', fileName: 'preview.webm' }, + { do: 'executeJs', js: `update(${casparDataJSON})` }, + { do: 'executeJs', js: `play()` }, + { do: 'sleep', duration: 1000 }, + { do: 'takeScreenshot', fileName: 'play.png' }, + { do: 'executeJs', js: `stop()` }, + { do: 'sleep', duration: 1000 }, + { do: 'takeScreenshot', fileName: 'stop.png' }, + { do: 'stopRecording' }, + { do: 'cropRecording', fileName: 'preview-cropped.webm' }, + ] + } else { + steps = version.steps || [] + } + return steps +} +export function htmlTemplateGetFileNamesFromSteps(steps: Steps): { + /** List of all file names that will be output from in the steps */ + fileNames: string[] + /** The "main file", ie the file that will carry the main metadata */ + mainFileName: string | undefined + /** File name of the main (first) screenshot */ + mainScreenShot: string | undefined + /** File name of the main (first) recording */ + mainRecording: string | undefined +} { + const fileNames: string[] = [] + let mainFileName: string | undefined = undefined + let mainScreenShot: string | undefined = undefined + let mainRecording: string | undefined = undefined + + for (const step of steps) { + if (step.do === 'takeScreenshot') { + fileNames.push(step.fileName) + if (!mainFileName) mainFileName = step.fileName + if (!mainScreenShot) mainScreenShot = step.fileName + } else if (step.do === 'startRecording') { + fileNames.push(step.fileName) + mainFileName = step.fileName + if (!mainRecording) mainRecording = step.fileName + } else if (step.do === 'cropRecording') { + fileNames.push(step.fileName) + } + } + return { fileNames, mainFileName, mainScreenShot, mainRecording } +} diff --git a/packages/shared-lib/src/package-manager/package.ts b/packages/shared-lib/src/package-manager/package.ts index e542f4e342..bf2dddd34f 100644 --- a/packages/shared-lib/src/package-manager/package.ts +++ b/packages/shared-lib/src/package-manager/package.ts @@ -188,8 +188,6 @@ export namespace ExpectedPackage { content: { /** path to the HTML template */ path: string - /** Add prefix to output artifacts */ - outputPrefix: string } version: { renderer?: { From 2bbd045b97b7bc90067a6f4473ff6f8a74c3d89d Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 26 Aug 2024 14:52:37 +0100 Subject: [PATCH 425/479] feat: rest api clear multiple sourcelayers SOFIE-3314 (#1232) --- meteor/lib/api/rest/v1/playlists.ts | 8 +-- meteor/server/api/rest/v1/playlists.ts | 30 ++++++++-- packages/openapi/api/actions.yaml | 2 + .../openapi/api/definitions/playlists.yaml | 57 ++++++++++++++++++- .../openapi/src/__tests__/playlists.spec.ts | 25 ++++++++ 5 files changed, 112 insertions(+), 10 deletions(-) diff --git a/meteor/lib/api/rest/v1/playlists.ts b/meteor/lib/api/rest/v1/playlists.ts index d3dcbb7adf..afbf8a3622 100644 --- a/meteor/lib/api/rest/v1/playlists.ts +++ b/meteor/lib/api/rest/v1/playlists.ts @@ -227,19 +227,19 @@ export interface PlaylistsRestAPI { fromPartInstanceId: PartInstanceId | undefined ): Promise> /** - * Clears the specified SourceLayer. + * Clears the specified SourceLayers. * * Throws if specified playlist is not active. * @param connection Connection data including client and header details * @param event User event string * @param rundownPlaylistId Target Playlist. - * @param sourceLayerId Target SourceLayer. + * @param sourceLayerIds Target SourceLayers. */ - clearSourceLayer( + clearSourceLayers( connection: Meteor.Connection, event: string, rundownPlaylistId: RundownPlaylistId, - sourceLayerId: string + sourceLayerIds: string[] ): Promise> /** * Recalls the last sticky Piece on the specified SourceLayer, if there is any. diff --git a/meteor/server/api/rest/v1/playlists.ts b/meteor/server/api/rest/v1/playlists.ts index 7fa1dcfb47..e4c89d7d04 100644 --- a/meteor/server/api/rest/v1/playlists.ts +++ b/meteor/server/api/rest/v1/playlists.ts @@ -458,11 +458,11 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { ) } - async clearSourceLayer( + async clearSourceLayers( connection: Meteor.Connection, event: string, rundownPlaylistId: RundownPlaylistId, - sourceLayerId: string + sourceLayerIds: string[] ): Promise> { const rundownPlaylist = await RundownPlaylists.findOneAsync(rundownPlaylistId) if (!rundownPlaylist) @@ -489,13 +489,13 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { rundownPlaylistId, () => { check(rundownPlaylistId, String) - check(sourceLayerId, String) + check(sourceLayerIds, [String]) }, StudioJobs.StopPiecesOnSourceLayers, { playlistId: rundownPlaylistId, partInstanceId: rundownPlaylist.currentPartInfo.partInstanceId, - sourceLayerIds: [sourceLayerId], + sourceLayerIds: sourceLayerIds, } ) } @@ -786,6 +786,26 @@ export function registerRoutes(registerRoute: APIRegisterHook) } ) + registerRoute<{ playlistId: string }, { sourceLayerIds: string[] }, void>( + 'put', + '/playlists/:playlistId/clear-sourcelayers', + new Map([ + [404, [UserErrorMessage.RundownPlaylistNotFound]], + [412, [UserErrorMessage.InactiveRundown]], + ]), + playlistsAPIFactory, + async (serverAPI, connection, event, params, body) => { + const playlistId = protectString(params.playlistId) + const sourceLayerIds = body?.sourceLayerIds + logger.info(`API POST: clear-sourcelayers ${playlistId} ${sourceLayerIds}`) + + check(playlistId, String) + check(sourceLayerIds, Array) + + return await serverAPI.clearSourceLayers(connection, event, playlistId, sourceLayerIds) + } + ) + registerRoute<{ playlistId: string; sourceLayerId: string }, never, void>( 'delete', '/playlists/:playlistId/sourceLayer/:sourceLayerId', @@ -801,7 +821,7 @@ export function registerRoutes(registerRoute: APIRegisterHook) check(playlistId, String) check(sourceLayerId, String) - return await serverAPI.clearSourceLayer(connection, event, playlistId, sourceLayerId) + return await serverAPI.clearSourceLayers(connection, event, playlistId, [sourceLayerId]) } ) diff --git a/packages/openapi/api/actions.yaml b/packages/openapi/api/actions.yaml index 2c21b0677c..eb12981019 100644 --- a/packages/openapi/api/actions.yaml +++ b/packages/openapi/api/actions.yaml @@ -61,6 +61,8 @@ paths: $ref: 'definitions/playlists.yaml#/resources/setNextSegment' /playlists/{playlistId}/queue-next-segment: $ref: 'definitions/playlists.yaml#/resources/queueNextSegment' + /playlists/{playlistId}/clear-sourcelayers: + $ref: 'definitions/playlists.yaml#/resources/clearSourceLayers' /playlists/{playlistId}/take: $ref: 'definitions/playlists.yaml#/resources/take' /playlists/{playlistId}/sourceLayer/{sourceLayerId}: diff --git a/packages/openapi/api/definitions/playlists.yaml b/packages/openapi/api/definitions/playlists.yaml index bb459f6d52..8a4b050414 100644 --- a/packages/openapi/api/definitions/playlists.yaml +++ b/packages/openapi/api/definitions/playlists.yaml @@ -574,12 +574,67 @@ resources: example: No Next point found, please set a part as Next before doing a TAKE. 500: $ref: '#/components/responses/internalServerError' + + clearSourceLayers: + put: + operationId: clearSourceLayers + tags: + - playlists + summary: Clears the target SourceLayers. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + sourceLayerIds: + description: Target SourceLayers. + type: array + items: + type: string + required: + - sourceLayerIds + example: + sourceLayerIds: + - 'mySourceLayerId' + responses: + 200: + $ref: '#/components/responses/putSuccess' + 404: + $ref: '#/components/responses/playlistNotFound' + 412: + description: Playlist is not active. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + example: Rundown must be active! + 500: + $ref: '#/components/responses/internalServerError' + sourceLayer: delete: + deprecated: true operationId: clearSourceLayer tags: - sourceLayers - summary: Clears the target SourceLayer. + summary: | + Clears the target SourceLayer. + This endpoint is deprecated, use the `clear-sourcelayers` endpoint instead. parameters: - name: playlistId in: path diff --git a/packages/openapi/src/__tests__/playlists.spec.ts b/packages/openapi/src/__tests__/playlists.spec.ts index 7b63d3194c..e9aecdb988 100644 --- a/packages/openapi/src/__tests__/playlists.spec.ts +++ b/packages/openapi/src/__tests__/playlists.spec.ts @@ -152,4 +152,29 @@ describe('Network client', () => { } else { test.todo('Reload playlist can be dependant on test order') } + + test('fails to clear the target SourceLayers with null playlistId', async () => { + await expect( + playlistsApi.clearSourceLayers({ + playlistId: null, + clearSourceLayersRequest: { + sourceLayerIds: ['42'], + }, + }) + ).rejects.toThrow() + }) + + if (testServer) { + test('can clear the target SourceLayers', async () => { + const sofieVersion = await playlistsApi.clearSourceLayers({ + playlistId: playlistIds[0], + clearSourceLayersRequest: { + sourceLayerIds: ['42'], + }, + }) + expect(sofieVersion.status).toBe(200) + }) + } else { + test.todo('Get SourceLayerIds for clear operation') + } }) From a887d69c58413f464bd3bf557b470d60944def7d Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Tue, 27 Aug 2024 09:16:52 +0200 Subject: [PATCH 426/479] chore: fix velocity typings being missing --- meteor/client/ui/ClockView/OverlayScreenSaver.tsx | 1 - meteor/client/ui/Prompter/PrompterView.tsx | 8 ++++---- meteor/lib/typings/velocity.d.ts | 7 +++++++ 3 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 meteor/lib/typings/velocity.d.ts diff --git a/meteor/client/ui/ClockView/OverlayScreenSaver.tsx b/meteor/client/ui/ClockView/OverlayScreenSaver.tsx index 473f21a37b..c1e74cbe7e 100644 --- a/meteor/client/ui/ClockView/OverlayScreenSaver.tsx +++ b/meteor/client/ui/ClockView/OverlayScreenSaver.tsx @@ -3,7 +3,6 @@ import { Clock } from '../StudioScreenSaver/Clock' import { useTracker, useSubscription } from '../../lib/ReactMeteorData/ReactMeteorData' import { MeteorPubSub } from '../../../lib/api/pubsub' import { findNextPlaylist } from '../StudioScreenSaver/StudioScreenSaver' -// @ts-expect-error No types available import Velocity from 'velocity-animate' import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { useSetDocumentClass } from '../util/useSetDocumentClass' diff --git a/meteor/client/ui/Prompter/PrompterView.tsx b/meteor/client/ui/Prompter/PrompterView.tsx index 01f673812a..0215cc676f 100644 --- a/meteor/client/ui/Prompter/PrompterView.tsx +++ b/meteor/client/ui/Prompter/PrompterView.tsx @@ -1,6 +1,5 @@ import React, { PropsWithChildren } from 'react' import _ from 'underscore' -// @ts-expect-error No types available import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import ClassNames from 'classnames' import { Meteor } from 'meteor/meteor' @@ -289,7 +288,7 @@ export class PrompterViewContent extends React.Component(`#partInstance_${partInstanceId}`) if (target) { Velocity(document.body, 'finish') @@ -298,7 +297,8 @@ export class PrompterViewContent extends React.Component('.prompter .live') || document.querySelector('.prompter .next') if (current) { Velocity(document.body, 'finish') @@ -307,7 +307,7 @@ export class PrompterViewContent extends React.Component('.prompter .next') if (next) { Velocity(document.body, 'finish') diff --git a/meteor/lib/typings/velocity.d.ts b/meteor/lib/typings/velocity.d.ts new file mode 100644 index 0000000000..2b3b7e0e86 --- /dev/null +++ b/meteor/lib/typings/velocity.d.ts @@ -0,0 +1,7 @@ +declare module 'velocity-animate' { + function Velocity(target: HTMLElement, command: string, commandProps?: Record): void + function Velocity(target: HTMLElement, command: string, stopAll?: boolean): void + function Velocity(target: HTMLElement, props: Record, animationProps: Record): void + + export default Velocity +} From a98c287f38ec31c9774013fbd6cdcf5c23fadb29 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Mon, 19 Aug 2024 08:02:15 +0200 Subject: [PATCH 427/479] fix: improve userAction-log view, collapse content that is longer than 100 chars or 5 lines --- meteor/client/ui/Status/UserActivity.tsx | 67 +++++++++++++++++++++--- 1 file changed, 60 insertions(+), 7 deletions(-) diff --git a/meteor/client/ui/Status/UserActivity.tsx b/meteor/client/ui/Status/UserActivity.tsx index f4e868b795..f6799f03cc 100644 --- a/meteor/client/ui/Status/UserActivity.tsx +++ b/meteor/client/ui/Status/UserActivity.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useLayoutEffect } from 'react' +import React, { useEffect, useState, useLayoutEffect, useMemo } from 'react' import { useSubscription, useTracker } from '../../lib/ReactMeteorData/react-meteor-data' import { Time, unprotectString } from '../../../lib/lib' import { UserActionsLogItem } from '../../../lib/collections/UserActionsLog' @@ -26,11 +26,62 @@ interface IUserActionsListProps { renderButtons?: (item: UserActionsLogItem) => React.ReactElement } -function prettyPrintJsonString(str: string): string { - try { - return JSON.stringify(JSON.parse(str), undefined, 4) - } catch (_e) { - return str +/** + * pretty-prints JSON content, and collapses it if it's too long. + */ +function CollapseContent(props: { content: any }): JSX.Element { + const [expanded, setExpanded] = useState(false) + + const originalString = useMemo(() => { + try { + let str = JSON.stringify(JSON.parse(props.content), undefined, 2) // Pretty print JSON + + // If the JSON string is a pretty short one, use a condensed JSON instead: + if (str.length < 200) { + str = JSON.stringify(JSON.parse(props.content)) // Condensed JSON + } + return str + } catch (_e) { + // Ignore parsing error + return '' + props.content + } + }, [props.content]) + + /** Position of the 5th line in the string, 0 if not found */ + const indexOf5thLine = useMemo(() => { + let indexOf5thLine = 0 + let foundIndex = 0 + for (let foundCount = 0; foundCount < 10; foundCount++) { + foundIndex = originalString.indexOf('\n', foundIndex + 1) + if (foundIndex === -1) { + break + } else { + if (foundCount >= 5) { + indexOf5thLine = foundIndex + break + } + } + } + return indexOf5thLine + }, [originalString]) + + if (originalString.length < 100 && indexOf5thLine === 0) { + return
{originalString}
+ } else { + const displayStr = expanded + ? originalString + : originalString.substring(0, Math.min(indexOf5thLine || 100, 100)) + '...' + return ( + { + e.preventDefault() + setExpanded(!expanded) + }} + > +
{displayStr}
+
+ ) } } @@ -133,7 +184,9 @@ function UserActionsList(props: Readonly) {
- + {props.renderButtons ? : null} ) From 842b222f45843c3bb96282ba2a7ebe5775cd1d9d Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Mon, 19 Aug 2024 17:50:36 +0200 Subject: [PATCH 428/479] feat: refactor CollapseJSON to a separate component, add ability to add time-component in UserActions view --- meteor/client/lib/collapseJSON.tsx | 84 +++++++++ meteor/client/lib/datePicker.tsx | 187 ++++++++++----------- meteor/client/styles/collapseJSON.scss | 17 ++ meteor/client/ui/Status/UserActivity.tsx | 101 ++++------- meteor/server/publications/organization.ts | 4 +- 5 files changed, 225 insertions(+), 168 deletions(-) create mode 100644 meteor/client/lib/collapseJSON.tsx create mode 100644 meteor/client/styles/collapseJSON.scss diff --git a/meteor/client/lib/collapseJSON.tsx b/meteor/client/lib/collapseJSON.tsx new file mode 100644 index 0000000000..085da42ee9 --- /dev/null +++ b/meteor/client/lib/collapseJSON.tsx @@ -0,0 +1,84 @@ +import React, { useMemo, useState } from 'react' + +/** + * pretty-prints JSON content, and collapses it if it's too long. + */ +export function CollapseJSON({ json }: { json: string }): JSX.Element { + const [expanded, setExpanded] = useState(false) + + const originalString = useMemo(() => { + try { + const obj = JSON.parse(json) + let str = JSON.stringify(obj, undefined, 2) // Pretty print JSON + + // If the JSON string is a pretty short one, use a condensed JSON instead: + if (str.length < 200) { + str = JSON.stringify(obj) // Condensed JSON + } + return str + } catch (_e) { + // Ignore parsing error + return '' + json + } + }, [json]) + + /** Position of the 5th line in the string, 0 if not found */ + const indexOf5thLine = useMemo(() => { + let indexOf5thLine: null | number = null + let foundIndex = 0 + for (let foundCount = 0; foundCount < 10; foundCount++) { + foundIndex = originalString.indexOf('\n', foundIndex + 1) + if (foundIndex === -1) { + break + } else { + if (foundCount >= 5) { + indexOf5thLine = foundIndex + break + } + } + } + return indexOf5thLine + }, [originalString]) + + if (originalString.length < 100 && indexOf5thLine === null) { + return
{originalString}
+ } + + const displayContents = expanded ? ( + <> + {originalString} + + + ) : ( + <> + {originalString.substring(0, Math.min(indexOf5thLine || 100, 100))} + + + ) + + return ( +
 setExpanded(!expanded)} className="collapse-json__block">
+			{displayContents}
+		
+ ) +} diff --git a/meteor/client/lib/datePicker.tsx b/meteor/client/lib/datePicker.tsx index 3a181a5c4f..188d65aeb9 100644 --- a/meteor/client/lib/datePicker.tsx +++ b/meteor/client/lib/datePicker.tsx @@ -1,115 +1,104 @@ -import * as React from 'react' -import * as _ from 'underscore' +import React, { useState, useEffect } from 'react' import DatePicker from 'react-datepicker' import moment from 'moment' import 'react-datepicker/dist/react-datepicker.css' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faChevronRight, faChevronLeft } from '@fortawesome/free-solid-svg-icons' import { Time } from '../../lib/lib' -import { withTranslation, WithTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next' -interface IProps { - from: Time - to: Time +export function DatePickerFromTo({ + from, + to, + onChange, +}: { + from?: Time + to?: Time onChange: (from: Time, to: Time) => void -} -interface IState { - dateFrom: Date - dateTo: Date -} -export const DatePickerFromTo = withTranslation()( - class DatePickerFromTo extends React.Component { - constructor(props: IProps & WithTranslation) { - super(props) +}): JSX.Element { + const { t } = useTranslation() - this.state = { - dateFrom: props.from ? new Date(props.from) : moment().subtract(1, 'days').startOf('day').toDate(), - dateTo: props.to ? new Date(props.to) : moment().startOf('day').toDate(), - } - } - static getDerivedStateFromProps(props: Readonly): IState { - return { - dateFrom: props.from ? new Date(props.from) : moment().subtract(1, 'days').startOf('day').toDate(), - dateTo: props.to ? new Date(props.to) : moment().startOf('day').toDate(), - } - } - triggerOnchange = (state: IState) => { - this.props.onChange(state.dateFrom.valueOf(), state.dateTo.valueOf()) - } - updateData = (o: Partial) => { - this.setState(o as any) + const [localFrom, setLocalFrom] = useState( + from ? new Date(from) : moment().subtract(1, 'days').startOf('day').toDate() + ) + const [localTo, setLocalTo] = useState(to ? new Date(to) : moment().startOf('day').toDate()) - const newState: IState = _.extend(_.clone(this.state), o) - this.triggerOnchange(newState) - } - handleChangeFrom = (date: Date | null) => { - if (date) { - this.updateData({ - dateFrom: date, - }) - } + function onClickPrevious() { + const from = localFrom.valueOf() + const to = localTo.valueOf() + const range = to - from + + setLocalFrom(new Date(from - range)) + setLocalTo(new Date(to - range)) + } + function onClickNext() { + const from = localFrom.valueOf() + const to = localTo.valueOf() + const range = to - from + + setLocalFrom(new Date(from + range)) + setLocalTo(new Date(to + range)) + } + function handleChangeFrom(date: Date | null) { + if (!date) return + if (date.valueOf() >= localTo.valueOf()) { + setLocalTo(moment(date).add(1, 'days').startOf('day').toDate()) } - handleChangeTo = (date: Date | null) => { - if (date) { - this.updateData({ - dateTo: date, - }) - } + setLocalFrom(date) + } + function handleChangeTo(date: Date | null) { + if (!date) return + if (date.valueOf() <= localFrom.valueOf()) { + setLocalFrom(moment(date).subtract(1, 'days').startOf('day').toDate()) } - onClickPrevious = () => { - const from = this.state.dateFrom.valueOf() - const to = this.state.dateTo.valueOf() - const range = to - from + setLocalTo(date) + } - this.updateData({ - dateFrom: new Date(from - range), - dateTo: new Date(to - range), - }) - } - onClickNext = () => { - const from = this.state.dateFrom.valueOf() - const to = this.state.dateTo.valueOf() - const range = to - from + useEffect(() => { + onChange(localFrom.valueOf(), localTo.valueOf()) + }, [localFrom, localTo]) - this.updateData({ - dateFrom: new Date(from + range), - dateTo: new Date(to + range), - }) + useEffect(() => { + if (from) { + setLocalFrom(from ? new Date(from) : moment().subtract(1, 'days').startOf('day').toDate()) } - render(): JSX.Element { - const { t } = this.props - return ( -
- - - - -
- ) + if (to) { + setLocalTo(to ? new Date(to) : moment().startOf('day').toDate()) } - } -) + }, [from, to]) + + return ( +
+ + + + +
+ ) +} diff --git a/meteor/client/styles/collapseJSON.scss b/meteor/client/styles/collapseJSON.scss new file mode 100644 index 0000000000..e7975db3df --- /dev/null +++ b/meteor/client/styles/collapseJSON.scss @@ -0,0 +1,17 @@ +.collapse-json__block { + cursor: pointer; +} + +.collapse-json__collapser { + display: inline-block; + background: #eee; + border-radius: 3px; + line-height: 1.3em; + padding: 0 0.3em; + border: none; +} + +.collapse-json__collapser:focus { + outline: none; + box-shadow: 0 0 0 2px #00feff; +} diff --git a/meteor/client/ui/Status/UserActivity.tsx b/meteor/client/ui/Status/UserActivity.tsx index f6799f03cc..f274541ffc 100644 --- a/meteor/client/ui/Status/UserActivity.tsx +++ b/meteor/client/ui/Status/UserActivity.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useLayoutEffect, useMemo } from 'react' +import React, { useEffect, useState, useLayoutEffect } from 'react' import { useSubscription, useTracker } from '../../lib/ReactMeteorData/react-meteor-data' import { Time, unprotectString } from '../../../lib/lib' import { UserActionsLogItem } from '../../../lib/collections/UserActionsLog' @@ -12,11 +12,13 @@ import classNames from 'classnames' import { getCoreSystem, UserActionsLog } from '../../collections' import Tooltip from 'rc-tooltip' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { CollapseJSON } from '../../lib/collapseJSON' import { faDownload } from '@fortawesome/free-solid-svg-icons' import { downloadBlob } from '../../lib/downloadBlob' -const PARAM_DATE_FORMAT = 'YYYY-MM-DD' +const PARAM_DATE_FORMAT = 'YYYY-MM-DDTHHmm' const PARAM_NAME_FROM_DATE = 'fromDate' +const PARAM_NAME_TO_DATE = 'toDate' interface IUserActionsListProps { logItems: UserActionsLogItem[] @@ -26,65 +28,6 @@ interface IUserActionsListProps { renderButtons?: (item: UserActionsLogItem) => React.ReactElement } -/** - * pretty-prints JSON content, and collapses it if it's too long. - */ -function CollapseContent(props: { content: any }): JSX.Element { - const [expanded, setExpanded] = useState(false) - - const originalString = useMemo(() => { - try { - let str = JSON.stringify(JSON.parse(props.content), undefined, 2) // Pretty print JSON - - // If the JSON string is a pretty short one, use a condensed JSON instead: - if (str.length < 200) { - str = JSON.stringify(JSON.parse(props.content)) // Condensed JSON - } - return str - } catch (_e) { - // Ignore parsing error - return '' + props.content - } - }, [props.content]) - - /** Position of the 5th line in the string, 0 if not found */ - const indexOf5thLine = useMemo(() => { - let indexOf5thLine = 0 - let foundIndex = 0 - for (let foundCount = 0; foundCount < 10; foundCount++) { - foundIndex = originalString.indexOf('\n', foundIndex + 1) - if (foundIndex === -1) { - break - } else { - if (foundCount >= 5) { - indexOf5thLine = foundIndex - break - } - } - } - return indexOf5thLine - }, [originalString]) - - if (originalString.length < 100 && indexOf5thLine === 0) { - return
{originalString}
- } else { - const displayStr = expanded - ? originalString - : originalString.substring(0, Math.min(indexOf5thLine || 100, 100)) + '...' - return ( - { - e.preventDefault() - setExpanded(!expanded) - }} - > -
{displayStr}
-
- ) - } -} - function UserActionsList(props: Readonly) { const { t } = useTranslation() @@ -98,7 +41,7 @@ function UserActionsList(props: Readonly) {
- + {props.renderButtons ? : null} @@ -185,12 +128,19 @@ function UserActionsList(props: Readonly) { {msg.success ? 'Success' : msg.success === false ? 'Error: ' + msg.errorMessage : null} {props.renderButtons ? : null} ) })} + {props.logItems.length >= 10_000 && ( + + + + )}
{rowId} + {!!resetItem && !rowItem.defaults && ( + + )} + {!!resetItem && rowItem.defaults && rowItem.overrideOps.length > 0 && ( + + )} diff --git a/meteor/client/styles/settings.scss b/meteor/client/styles/settings.scss index ceb200dece..12b5aca501 100644 --- a/meteor/client/styles/settings.scss +++ b/meteor/client/styles/settings.scss @@ -46,7 +46,7 @@ .table-item-actions { text-align: right; - min-width: 4em; + min-width: 5em; .action-btn { vertical-align: top; @@ -126,16 +126,16 @@ } } - .settings-studio-showStyleVariants-table__drag { - color: #a2a2a2; - cursor: grab; - margin-right: 10px; + .settings-studio-showStyleVariants-table__drag { + color: #a2a2a2; + cursor: grab; + margin-right: 10px; - svg { - width: 10px; - position: relative; + svg { + width: 10px; + position: relative; + } } - } .settings-studio-accessors { border-left: 0.5em solid #999; diff --git a/meteor/client/ui/Settings/util/OverrideOpHelper.tsx b/meteor/client/ui/Settings/util/OverrideOpHelper.tsx index c1e28ba8a7..0dd8e0af6d 100644 --- a/meteor/client/ui/Settings/util/OverrideOpHelper.tsx +++ b/meteor/client/ui/Settings/util/OverrideOpHelper.tsx @@ -177,7 +177,7 @@ class OverrideOpHelperImpl implements OverrideOpHelperBatcher { clearItemOverrides = (itemId: string, subPath: string): this => { const opPath = `${itemId}.${subPath}` - const newOps = this.#object.overrides.filter((op) => op.path !== opPath) + const newOps = filterOverrideOpsForPrefix(this.#object.overrides, opPath).otherOps this.#object.overrides = newOps From 63939cd9eea06ffb2d018b918b57dc648a679593 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 3 Jun 2024 12:02:03 +0100 Subject: [PATCH 324/479] fix: transition piece hover preview SOFIE-3183 --- .../Renderers/TransitionSourceRenderer.tsx | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/meteor/client/ui/SegmentTimeline/Renderers/TransitionSourceRenderer.tsx b/meteor/client/ui/SegmentTimeline/Renderers/TransitionSourceRenderer.tsx index d2fb76c5a1..a5cb8ec5b1 100644 --- a/meteor/client/ui/SegmentTimeline/Renderers/TransitionSourceRenderer.tsx +++ b/meteor/client/ui/SegmentTimeline/Renderers/TransitionSourceRenderer.tsx @@ -77,8 +77,8 @@ export class TransitionSourceRenderer extends CustomLayerItemRenderer ) : null} - {this.props.showMiniInspector && !this.state.iconFailed && this.props.itemElement !== null && content && ( - + {this.props.showMiniInspector && !this.state.iconFailed && content?.preview && ( + )} ) @@ -86,25 +86,21 @@ export class TransitionSourceRenderer extends CustomLayerItemRenderer) { +}: Readonly<{ preview: string; position: IFloatingInspectorPosition }>) { const ref = useRef(null) const { style: floatingInspectorStyle } = useInspectorPosition(position, ref) return ( - - {content.preview && ( -
- -
- )} + +
+ +
) } From 1d8bed77e4262a12bba46eecf0631cbcae96fb34 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 3 Jun 2024 12:36:30 +0100 Subject: [PATCH 325/479] fix: allow blueprint assets to be gif SOFIE-3183 --- meteor/server/api/blueprints/http.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/meteor/server/api/blueprints/http.ts b/meteor/server/api/blueprints/http.ts index 9912805949..b59f322f98 100644 --- a/meteor/server/api/blueprints/http.ts +++ b/meteor/server/api/blueprints/http.ts @@ -191,7 +191,7 @@ blueprintsRouter.get('/assets/(.*)', async (ctx) => { // for now just check it's a png to prevent snapshots being downloaded const filePath = ctx.params[0] - if (filePath.match(/\.(png|svg)?$/)) { + if (filePath.match(/\.(png|svg|gif)?$/)) { const userId = ctx.headers.authorization ? ctx.headers.authorization.split(' ')[1] : '' try { const dataStream = retrieveBlueprintAsset({ userId: protectString(userId) }, filePath) @@ -200,6 +200,8 @@ blueprintsRouter.get('/assets/(.*)', async (ctx) => { ctx.response.type = 'image/svg+xml' } else if (extension === '.png') { ctx.response.type = 'image/png' + } else if (extension === '.gif') { + ctx.response.type = 'image/gif' } // assets are supposed to have a unique ID/file name, if the asset changes, so must the filename ctx.set('Cache-Control', `public, max-age=${BLUEPRINT_ASSET_MAX_AGE}, immutable`) @@ -207,10 +209,8 @@ blueprintsRouter.get('/assets/(.*)', async (ctx) => { ctx.body = dataStream } catch { ctx.statusCode = 404 // Probably - ctx.end() } } else { ctx.statusCode = 403 - ctx.end() } }) From 68b137489b1f3cfb6af290fe7ae542ac8448f50c Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 3 Jun 2024 14:47:29 +0100 Subject: [PATCH 326/479] fix: improve UX of object table, to indicate which rows have overrides and are custom --- .../forms/SchemaFormTable/ArrayTableRow.tsx | 2 +- .../lib/forms/SchemaFormTable/ObjectTable.tsx | 25 ++++++++++++++++++- .../lib/forms/SchemaTableSummaryRow.tsx | 25 ++++++++++++++++--- meteor/client/styles/settings.scss | 18 ++++++------- .../ui/Settings/util/OverrideOpHelper.tsx | 2 +- 5 files changed, 56 insertions(+), 16 deletions(-) diff --git a/meteor/client/lib/forms/SchemaFormTable/ArrayTableRow.tsx b/meteor/client/lib/forms/SchemaFormTable/ArrayTableRow.tsx index 9a905ec7ef..3ce5d5f39f 100644 --- a/meteor/client/lib/forms/SchemaFormTable/ArrayTableRow.tsx +++ b/meteor/client/lib/forms/SchemaFormTable/ArrayTableRow.tsx @@ -56,7 +56,7 @@ export function ArrayTableRow({ summaryFields={summaryFields} rowId={rowId} showRowId={false} - object={rowObject} + rowItem={rowItem} isEdited={isExpanded} editItem={toggleExpanded} removeItem={confirmRemove} diff --git a/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx b/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx index b70e6e0f57..3b18d5d2ad 100644 --- a/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx +++ b/meteor/client/lib/forms/SchemaFormTable/ObjectTable.tsx @@ -167,6 +167,28 @@ export const SchemaFormObjectTable = ({ [t, tableOverrideHelper] ) + const confirmReset = useCallback( + (rowId: number | string) => { + doModalDialog({ + title: t('Reset this item?'), + yes: t('Reset'), + no: t('Cancel'), + onAccept: () => { + tableOverrideHelper() + .clearItemOverrides(rowId + '', '') + .commit() + }, + message: ( + +

{t('Are you sure you want to reset all overrides for the selected row?')}

+

{t('Please note: This action is irreversible!')}

+
+ ), + }) + }, + [t, tableOverrideHelper] + ) + const title = getSchemaUIField(schema, SchemaFormUIField.Title) const description = getSchemaUIField(schema, SchemaFormUIField.Description) const titleElement = title && ( @@ -219,10 +241,11 @@ export const SchemaFormObjectTable = ({ summaryFields={summaryFields} rowId={rowItem.id} showRowId={false} - object={rowItem.computed} + rowItem={rowItem} isEdited={isExpanded(rowItem.id)} editItem={toggleExpanded} removeItem={confirmRemove} + resetItem={confirmReset} /> {isExpanded(rowItem.id) && ( { summaryFields: SchemaSummaryField[] showRowId: boolean rowId: T - object: any + rowItem: WrappedOverridableItemNormal isEdited: boolean editItem: (rowId: T) => void removeItem: (rowId: T) => void + resetItem?: (rowId: T) => void } export function SchemaTableSummaryRow({ summaryFields, showRowId, rowId, - object, + rowItem, isEdited, editItem, removeItem, + resetItem, }: Readonly>): JSX.Element { + const { t } = useTranslation() + const editItem2 = useCallback(() => editItem(rowId), [editItem, rowId]) const removeItem2 = useCallback(() => removeItem(rowId), [removeItem, rowId]) + const resetItem2 = useCallback(() => resetItem?.(rowId), [resetItem, rowId]) return (
{rowId} + {!!resetItem && !rowItem.defaults && ( + + )} + {!!resetItem && rowItem.defaults && rowItem.overrideOps.length > 0 && ( + + )} diff --git a/meteor/client/styles/settings.scss b/meteor/client/styles/settings.scss index ceb200dece..12b5aca501 100644 --- a/meteor/client/styles/settings.scss +++ b/meteor/client/styles/settings.scss @@ -46,7 +46,7 @@ .table-item-actions { text-align: right; - min-width: 4em; + min-width: 5em; .action-btn { vertical-align: top; @@ -126,16 +126,16 @@ } } - .settings-studio-showStyleVariants-table__drag { - color: #a2a2a2; - cursor: grab; - margin-right: 10px; + .settings-studio-showStyleVariants-table__drag { + color: #a2a2a2; + cursor: grab; + margin-right: 10px; - svg { - width: 10px; - position: relative; + svg { + width: 10px; + position: relative; + } } - } .settings-studio-accessors { border-left: 0.5em solid #999; diff --git a/meteor/client/ui/Settings/util/OverrideOpHelper.tsx b/meteor/client/ui/Settings/util/OverrideOpHelper.tsx index 34918ecc5b..146f962b3c 100644 --- a/meteor/client/ui/Settings/util/OverrideOpHelper.tsx +++ b/meteor/client/ui/Settings/util/OverrideOpHelper.tsx @@ -154,7 +154,7 @@ class OverrideOpHelperImpl implements OverrideOpHelperBatcher { clearItemOverrides = (itemId: string, subPath: string): this => { const opPath = `${itemId}.${subPath}` - const newOps = this.#object.overrides.filter((op) => op.path !== opPath) + const newOps = filterOverrideOpsForPrefix(this.#object.overrides, opPath).otherOps this.#object.overrides = newOps From 1b17a1d946fb0b23b720b110e1f714324901dde5 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 28 May 2024 15:16:52 +0100 Subject: [PATCH 327/479] fix: emit ddp publciation remove messages before add SOFIE-3172 --- meteor/server/lib/customPublication/publish.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/meteor/server/lib/customPublication/publish.ts b/meteor/server/lib/customPublication/publish.ts index 7806bcd945..39a04e9878 100644 --- a/meteor/server/lib/customPublication/publish.ts +++ b/meteor/server/lib/customPublication/publish.ts @@ -74,6 +74,10 @@ export class CustomPublishMeteor }> { changed(changes: CustomPublishChanges): void { if (!this.#isReady) throw new Meteor.Error(500, 'CustomPublish has not been initialised') + for (const id of changes.removed.values()) { + this._meteorSubscription.removed(this._collectionName, unprotectString(id)) + } + for (const doc of changes.added.values()) { this._meteorSubscription.added(this._collectionName, unprotectString(doc._id), doc) } @@ -81,10 +85,6 @@ export class CustomPublishMeteor }> { for (const doc of changes.changed.values()) { this._meteorSubscription.changed(this._collectionName, unprotectString(doc._id), doc) } - - for (const id of changes.removed.values()) { - this._meteorSubscription.removed(this._collectionName, unprotectString(id)) - } } } From c4e2ad0282a7b683f3d19b8b9b5b541bf6518cbe Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 28 May 2024 15:18:11 +0100 Subject: [PATCH 328/479] fix: invalidate deviceTriggerPreviews when the filterChain changes SOFIE-3172 --- .../StudioDeviceTriggerManager.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts b/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts index 03282c892b..c7eed9a955 100644 --- a/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts +++ b/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts @@ -71,22 +71,23 @@ export class StudioDeviceTriggerManager { showStyleBaseId: { $in: [showStyleBaseId, null], }, - }).map((pair) => convertDocument(pair)) - const triggeredActions = allTriggeredActions.filter((pair) => - Object.values(pair.triggers).find((trigger) => isDeviceTrigger(trigger)) - ) + }) const upsertedDeviceTriggerMountedActionIds: DeviceTriggerMountedActionId[] = [] const touchedActionIds: DeviceActionId[] = [] - for (const triggeredAction of triggeredActions) { + for (const rawTriggeredAction of allTriggeredActions) { + const triggeredAction = convertDocument(rawTriggeredAction) + + if (!Object.values(triggeredAction.triggers).find(isDeviceTrigger)) continue + const addedPreviewIds: PreviewWrappedAdLibId[] = [] Object.entries(triggeredAction.actions).forEach(([key, action]) => { - // Since the compiled aciton is cached using this actionId as a key, having the action - // and the filterChain length allows for a quicker invalidation without doing a deepEquals + // Since the compiled action is cached using this actionId as a key, having the action + // and the filterChain allows for a quicker invalidation without doing a deepEquals const actionId = protectString( - `${studioId}_${triggeredAction._id}_${key}_${action.action}_${action.filterChain.length}` + `${studioId}_${triggeredAction._id}_${key}_${action.action}_${JSON.stringify(action.filterChain)}` ) const existingAction = actionManager.getAction(actionId) let thisAction: ExecutableAction @@ -191,6 +192,8 @@ export class StudioDeviceTriggerManager { } }) + console.log(`updating ${triggeredAction._id}, gen ${addedPreviewIds}`) + DeviceTriggerMountedActionAdlibsPreview.remove({ triggeredActionId: triggeredAction._id, _id: { From c1bb47fe3958c245b708f9698c458b3d94055a3b Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 3 Jun 2024 18:05:11 +0100 Subject: [PATCH 329/479] fix: `DataCloneError: [object Object] could not be cloned` when propogating some errors from job-worker SOFIE-2903 --- meteor/server/api/client.ts | 13 ++---- packages/corelib/src/__tests__/lib.spec.ts | 2 +- packages/corelib/src/error.ts | 42 ++++++++++++++----- .../job-worker/src/playout/adlibAction.ts | 2 +- .../job-worker/src/workers/events/child.ts | 9 ++-- .../job-worker/src/workers/ingest/child.ts | 9 ++-- .../job-worker/src/workers/parent-base.ts | 7 +++- .../job-worker/src/workers/studio/child.ts | 10 +++-- packages/shared-lib/src/lib/stringifyError.ts | 2 + 9 files changed, 62 insertions(+), 34 deletions(-) diff --git a/meteor/server/api/client.ts b/meteor/server/api/client.ts index 74a0cfd3a2..90bdfd170f 100644 --- a/meteor/server/api/client.ts +++ b/meteor/server/api/client.ts @@ -12,7 +12,7 @@ import { isInTestWrite, triggerWriteAccessBecauseNoCheckNecessary } from '../sec import { PeripheralDeviceContentWriteAccess } from '../security/peripheralDevice' import { endTrace, sendTrace, startTrace } from './integration/influx' import { interpollateTranslation, translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' -import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/error' +import { UserError } from '@sofie-automation/corelib/dist/error' import { StudioJobFunc } from '@sofie-automation/corelib/dist/worker/studio' import { QueueStudioJob } from '../worker/worker' import { profiler } from './profiler' @@ -37,16 +37,9 @@ import { UserActionsLog } from '../collections' import { executePeripheralDeviceFunctionWithCustomTimeout } from './peripheralDevice/executeFunction' function rewrapError(methodName: string, e: any): ClientAPI.ClientResponseError { - let userError: UserError - if (UserError.isUserError(e)) { - userError = e - } else { - // Rewrap errors as a UserError - const err = e instanceof Error ? e : new Error(stringifyError(e)) - userError = UserError.from(err, UserErrorMessage.InternalError, undefined, e.error) - } + const userError = UserError.fromUnknown(e) - logger.info(`UserAction "${methodName}" failed: ${stringifyError(userError)}`) + logger.info(`UserAction "${methodName}" failed: ${userError.toErrorString()}`) // Forward the error to the caller return ClientAPI.responseError(userError, userError.errorCode) diff --git a/packages/corelib/src/__tests__/lib.spec.ts b/packages/corelib/src/__tests__/lib.spec.ts index f273fd60a1..01c28d9f9b 100644 --- a/packages/corelib/src/__tests__/lib.spec.ts +++ b/packages/corelib/src/__tests__/lib.spec.ts @@ -132,7 +132,7 @@ describe('Lib', () => { expect(stringifyError(obj)).toMatch(/anotherProp.*abc/) // UserError: - const userError = UserError.fromUnknown(error, UserErrorMessage.ValidationFailed, {}, 42) + const userError = UserError.from(error, UserErrorMessage.ValidationFailed, {}, 42) // The stringification should trigger .toString() -> .toJSON() in UserError: const str = stringifyError(userError) expect(str).toMatch(/^UserError: /) diff --git a/packages/corelib/src/error.ts b/packages/corelib/src/error.ts index 82c53e9622..69e8fa58ff 100644 --- a/packages/corelib/src/error.ts +++ b/packages/corelib/src/error.ts @@ -1,5 +1,6 @@ import { ITranslatableMessage } from '@sofie-automation/blueprints-integration' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' +import { interpollateTranslation, translateMessage } from './TranslatableMessage' // Mock 't' function for i18next to find the keys function t(key: string): string { @@ -109,7 +110,18 @@ const UserErrorMessagesTranslations: { [key in UserErrorMessage]: string } = { [UserErrorMessage.ValidationFailed]: t('Validation failed!'), } -export class UserError { +export interface UserErrorObj { + readonly errorCode: number + + /** The raw Error that was thrown */ + readonly rawError: Error + /** The UserErrorMessage key (for matching certain error) */ + readonly key: UserErrorMessage + /** The translatable string for the key */ + readonly message: ITranslatableMessage +} + +export class UserError implements UserErrorObj { public readonly errorCode: number private constructor( @@ -133,15 +145,19 @@ export class UserError { static from(err: Error, key: UserErrorMessage, args?: { [k: string]: any }, errCode?: number): UserError { return new UserError(err, key, { key: UserErrorMessagesTranslations[key], args }, errCode) } - /** Create a UserError with a custom error for the log */ - static fromUnknown( - err: unknown, - key: UserErrorMessage, - args?: { [k: string]: any }, - errorCode?: number - ): UserError { + /** Create a UserError from an unknown possibly error input */ + static fromUnknown(err: unknown, errorCode?: number): UserError { + if (err instanceof UserError) return err + if (this.isUserError(err)) + return new UserError(new Error(err.rawError.toString()), err.key, err.message, err.errorCode) + const err2 = err instanceof Error ? err : new Error(stringifyError(err)) - return new UserError(err2, key, { key: UserErrorMessagesTranslations[key], args }, errorCode) + return new UserError( + err2, + UserErrorMessage.InternalError, + { key: UserErrorMessagesTranslations[UserErrorMessage.InternalError] }, + errorCode + ) } /** Create a UserError duplicating the same error for the log */ @@ -162,7 +178,7 @@ export class UserError { } } - static toJSON(e: UserError): string { + static toJSON(e: UserErrorObj): string { return JSON.stringify({ rawError: stringifyError(e.rawError), message: e.message, @@ -171,7 +187,11 @@ export class UserError { }) } - static isUserError(e: unknown): e is UserError { + static isUserError(e: unknown): e is UserErrorObj { return !(e instanceof Error) && !!e && typeof e === 'object' && 'rawError' in e && 'message' in e && 'key' in e } + + toErrorString(): string { + return `${translateMessage(this.message, interpollateTranslation)}\n${stringifyError(this.rawError)}` + } } diff --git a/packages/job-worker/src/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index 0d31ae94f1..6b474aaf6d 100644 --- a/packages/job-worker/src/playout/adlibAction.ts +++ b/packages/job-worker/src/playout/adlibAction.ts @@ -165,7 +165,7 @@ export async function executeActionInner( ) } catch (err) { logger.error(`Error in showStyleBlueprint.executeAction: ${stringifyError(err)}`) - throw UserError.fromUnknown(err, UserErrorMessage.InternalError) + throw UserError.fromUnknown(err) } await applyAnyExecutionSideEffects(context, cache, actionContext, now) diff --git a/packages/job-worker/src/workers/events/child.ts b/packages/job-worker/src/workers/events/child.ts index 76ba53001f..55745e5331 100644 --- a/packages/job-worker/src/workers/events/child.ts +++ b/packages/job-worker/src/workers/events/child.ts @@ -15,13 +15,13 @@ import { JobContextImpl, QueueJobFunc } from '../context' import { AnyLockEvent, LocksManager } from '../locks' import { FastTrackTimelineFunc, LogLineWithSourceFunc } from '../../main' import { interceptLogging, logger } from '../../logging' -import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { setupInfluxDb } from '../../influx' import { getEventsQueueName } from '@sofie-automation/corelib/dist/worker/events' import { ExternalMessageQueueRunner } from '../../events/ExternalMessageQueue' import { WorkerJobResult } from '../parent-base' import { endTrace, sendTrace, startTrace } from '@sofie-automation/corelib/dist/influxdb' import { getPrometheusMetricsString, setupPrometheusMetrics } from '@sofie-automation/corelib/dist/prometheus' +import { UserError } from '@sofie-automation/corelib/dist/error' interface StaticData { readonly mongoClient: MongoClient @@ -141,10 +141,13 @@ export class EventsWorkerChild { error: null, } } catch (e) { - logger.error(`Events job "${jobName}" errored: ${stringifyError(e)}`) + const userError = UserError.fromUnknown(e) + + logger.error(`Events job "${jobName}" errored: ${userError.toErrorString()}`) + return { result: null, - error: e, + error: UserError.toJSON(userError), } } } else { diff --git a/packages/job-worker/src/workers/ingest/child.ts b/packages/job-worker/src/workers/ingest/child.ts index 92dd841503..d00f8a4ae8 100644 --- a/packages/job-worker/src/workers/ingest/child.ts +++ b/packages/job-worker/src/workers/ingest/child.ts @@ -9,12 +9,12 @@ import { JobContextImpl, QueueJobFunc } from '../context' import { AnyLockEvent, LocksManager } from '../locks' import { FastTrackTimelineFunc, LogLineWithSourceFunc } from '../../main' import { interceptLogging, logger } from '../../logging' -import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { setupInfluxDb } from '../../influx' import { getIngestQueueName } from '@sofie-automation/corelib/dist/worker/ingest' import { WorkerJobResult } from '../parent-base' import { endTrace, sendTrace, startTrace } from '@sofie-automation/corelib/dist/influxdb' import { getPrometheusMetricsString, setupPrometheusMetrics } from '@sofie-automation/corelib/dist/prometheus' +import { UserError } from '@sofie-automation/corelib/dist/error' interface StaticData { readonly mongoClient: MongoClient @@ -123,10 +123,13 @@ export class IngestWorkerChild { error: null, } } catch (e) { - logger.error(`Ingest job "${jobName}" errored: ${stringifyError(e)}`) + const userError = UserError.fromUnknown(e) + + logger.error(`Ingest job "${jobName}" errored: ${userError.toErrorString()}`) + return { result: null, - error: e, + error: UserError.toJSON(userError), } } } else { diff --git a/packages/job-worker/src/workers/parent-base.ts b/packages/job-worker/src/workers/parent-base.ts index b3444867a8..7fd820f536 100644 --- a/packages/job-worker/src/workers/parent-base.ts +++ b/packages/job-worker/src/workers/parent-base.ts @@ -20,6 +20,7 @@ import { Promisify, ThreadedClassManager } from 'threadedclass' import { StatusCode } from '@sofie-automation/blueprints-integration' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' import { WorkerThreadStatus } from '@sofie-automation/corelib/dist/dataModel/WorkerThreads' +import { UserError } from '@sofie-automation/corelib/dist/error' export enum ThreadStatus { Closed = 0, @@ -56,7 +57,7 @@ export interface WorkerParentBaseOptions extends WorkerParentOptions { * Wrap up the result of a job to an object. This allows us to use special types in the error, as anything thrown can get mangled by threadedClass or other wrappings */ export interface WorkerJobResult { - error: any + error: string | null // string must be formatted as a UserError result: any } @@ -301,7 +302,9 @@ export abstract class WorkerParentBase { job.id, startTime, endTime, - result.error, + result.error + ? UserError.tryFromJSON(result.error) ?? new Error(result.error) + : null, result.result ) diff --git a/packages/job-worker/src/workers/studio/child.ts b/packages/job-worker/src/workers/studio/child.ts index a30e5af11e..d582e03e80 100644 --- a/packages/job-worker/src/workers/studio/child.ts +++ b/packages/job-worker/src/workers/studio/child.ts @@ -9,12 +9,13 @@ import { QueueJobFunc, JobContextImpl } from '../context' import { AnyLockEvent, LocksManager } from '../locks' import { FastTrackTimelineFunc, LogLineWithSourceFunc } from '../../main' import { interceptLogging, logger } from '../../logging' -import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { setupInfluxDb } from '../../influx' import { getStudioQueueName } from '@sofie-automation/corelib/dist/worker/studio' import { WorkerJobResult } from '../parent-base' import { endTrace, sendTrace, startTrace } from '@sofie-automation/corelib/dist/influxdb' import { getPrometheusMetricsString, setupPrometheusMetrics } from '@sofie-automation/corelib/dist/prometheus' +import { UserError } from '@sofie-automation/corelib/dist/error' +import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' interface StaticData { readonly mongoClient: MongoClient @@ -123,11 +124,14 @@ export class StudioWorkerChild { error: null, } } catch (e) { - logger.error(`Studio job "${jobName}" errored: ${stringifyError(e)}`) + const userError = UserError.fromUnknown(e) + console.log('border', userError.toErrorString(), stringifyError(e)) + + logger.error(`Studio job "${jobName}" errored: ${userError.toErrorString()}`) return { result: null, - error: e, + error: UserError.toJSON(userError), } } } else { diff --git a/packages/shared-lib/src/lib/stringifyError.ts b/packages/shared-lib/src/lib/stringifyError.ts index 87bb26b2ea..69a5cd7270 100644 --- a/packages/shared-lib/src/lib/stringifyError.ts +++ b/packages/shared-lib/src/lib/stringifyError.ts @@ -39,5 +39,7 @@ export function stringifyError(error: unknown, noStack = false): string { } } + if (str.startsWith('Error: ')) str = str.slice('Error: '.length) + return str } From 7f618c75bb6a94e28e4a9c00db4688a77a1bc184 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 3 Jun 2024 18:07:06 +0100 Subject: [PATCH 330/479] fix: `DataCloneError: [object Object] could not be cloned` when propogating some errors from job-worker SOFIE-2903 --- meteor/server/api/client.ts | 13 ++---- packages/corelib/src/__tests__/lib.spec.ts | 2 +- packages/corelib/src/error.ts | 42 ++++++++++++++----- .../job-worker/src/playout/adlibAction.ts | 2 +- .../job-worker/src/workers/events/child.ts | 9 ++-- .../job-worker/src/workers/ingest/child.ts | 9 ++-- .../job-worker/src/workers/parent-base.ts | 7 +++- .../job-worker/src/workers/studio/child.ts | 19 +++------ packages/shared-lib/src/lib/stringifyError.ts | 2 + 9 files changed, 61 insertions(+), 44 deletions(-) diff --git a/meteor/server/api/client.ts b/meteor/server/api/client.ts index 74a0cfd3a2..90bdfd170f 100644 --- a/meteor/server/api/client.ts +++ b/meteor/server/api/client.ts @@ -12,7 +12,7 @@ import { isInTestWrite, triggerWriteAccessBecauseNoCheckNecessary } from '../sec import { PeripheralDeviceContentWriteAccess } from '../security/peripheralDevice' import { endTrace, sendTrace, startTrace } from './integration/influx' import { interpollateTranslation, translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' -import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/error' +import { UserError } from '@sofie-automation/corelib/dist/error' import { StudioJobFunc } from '@sofie-automation/corelib/dist/worker/studio' import { QueueStudioJob } from '../worker/worker' import { profiler } from './profiler' @@ -37,16 +37,9 @@ import { UserActionsLog } from '../collections' import { executePeripheralDeviceFunctionWithCustomTimeout } from './peripheralDevice/executeFunction' function rewrapError(methodName: string, e: any): ClientAPI.ClientResponseError { - let userError: UserError - if (UserError.isUserError(e)) { - userError = e - } else { - // Rewrap errors as a UserError - const err = e instanceof Error ? e : new Error(stringifyError(e)) - userError = UserError.from(err, UserErrorMessage.InternalError, undefined, e.error) - } + const userError = UserError.fromUnknown(e) - logger.info(`UserAction "${methodName}" failed: ${stringifyError(userError)}`) + logger.info(`UserAction "${methodName}" failed: ${userError.toErrorString()}`) // Forward the error to the caller return ClientAPI.responseError(userError, userError.errorCode) diff --git a/packages/corelib/src/__tests__/lib.spec.ts b/packages/corelib/src/__tests__/lib.spec.ts index f273fd60a1..01c28d9f9b 100644 --- a/packages/corelib/src/__tests__/lib.spec.ts +++ b/packages/corelib/src/__tests__/lib.spec.ts @@ -132,7 +132,7 @@ describe('Lib', () => { expect(stringifyError(obj)).toMatch(/anotherProp.*abc/) // UserError: - const userError = UserError.fromUnknown(error, UserErrorMessage.ValidationFailed, {}, 42) + const userError = UserError.from(error, UserErrorMessage.ValidationFailed, {}, 42) // The stringification should trigger .toString() -> .toJSON() in UserError: const str = stringifyError(userError) expect(str).toMatch(/^UserError: /) diff --git a/packages/corelib/src/error.ts b/packages/corelib/src/error.ts index 5ff76c0242..46570681a6 100644 --- a/packages/corelib/src/error.ts +++ b/packages/corelib/src/error.ts @@ -1,5 +1,6 @@ import { ITranslatableMessage } from '@sofie-automation/blueprints-integration' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' +import { interpollateTranslation, translateMessage } from './TranslatableMessage' // Mock 't' function for i18next to find the keys function t(key: string): string { @@ -115,7 +116,18 @@ const UserErrorMessagesTranslations: { [key in UserErrorMessage]: string } = { [UserErrorMessage.BucketNotFound]: t(`Bucket not found!`), } -export class UserError { +export interface UserErrorObj { + readonly errorCode: number + + /** The raw Error that was thrown */ + readonly rawError: Error + /** The UserErrorMessage key (for matching certain error) */ + readonly key: UserErrorMessage + /** The translatable string for the key */ + readonly message: ITranslatableMessage +} + +export class UserError implements UserErrorObj { public readonly errorCode: number private constructor( @@ -139,15 +151,19 @@ export class UserError { static from(err: Error, key: UserErrorMessage, args?: { [k: string]: any }, errCode?: number): UserError { return new UserError(err, key, { key: UserErrorMessagesTranslations[key], args }, errCode) } - /** Create a UserError with a custom error for the log */ - static fromUnknown( - err: unknown, - key: UserErrorMessage, - args?: { [k: string]: any }, - errorCode?: number - ): UserError { + /** Create a UserError from an unknown possibly error input */ + static fromUnknown(err: unknown, errorCode?: number): UserError { + if (err instanceof UserError) return err + if (this.isUserError(err)) + return new UserError(new Error(err.rawError.toString()), err.key, err.message, err.errorCode) + const err2 = err instanceof Error ? err : new Error(stringifyError(err)) - return new UserError(err2, key, { key: UserErrorMessagesTranslations[key], args }, errorCode) + return new UserError( + err2, + UserErrorMessage.InternalError, + { key: UserErrorMessagesTranslations[UserErrorMessage.InternalError] }, + errorCode + ) } /** Create a UserError duplicating the same error for the log */ @@ -168,7 +184,7 @@ export class UserError { } } - static toJSON(e: UserError): string { + static toJSON(e: UserErrorObj): string { return JSON.stringify({ rawError: stringifyError(e.rawError), message: e.message, @@ -177,7 +193,11 @@ export class UserError { }) } - static isUserError(e: unknown): e is UserError { + static isUserError(e: unknown): e is UserErrorObj { return !(e instanceof Error) && !!e && typeof e === 'object' && 'rawError' in e && 'message' in e && 'key' in e } + + toErrorString(): string { + return `${translateMessage(this.message, interpollateTranslation)}\n${stringifyError(this.rawError)}` + } } diff --git a/packages/job-worker/src/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index 932bceb3dc..19b19d94b4 100644 --- a/packages/job-worker/src/playout/adlibAction.ts +++ b/packages/job-worker/src/playout/adlibAction.ts @@ -211,7 +211,7 @@ export async function executeActionInner( ) } catch (err) { logger.error(`Error in showStyleBlueprint.executeAction: ${stringifyError(err)}`) - throw UserError.fromUnknown(err, UserErrorMessage.InternalError) + throw UserError.fromUnknown(err) } await applyAnyExecutionSideEffects(context, playoutModel, actionContext, now) diff --git a/packages/job-worker/src/workers/events/child.ts b/packages/job-worker/src/workers/events/child.ts index 76ba53001f..4dd8b04690 100644 --- a/packages/job-worker/src/workers/events/child.ts +++ b/packages/job-worker/src/workers/events/child.ts @@ -15,13 +15,13 @@ import { JobContextImpl, QueueJobFunc } from '../context' import { AnyLockEvent, LocksManager } from '../locks' import { FastTrackTimelineFunc, LogLineWithSourceFunc } from '../../main' import { interceptLogging, logger } from '../../logging' -import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { setupInfluxDb } from '../../influx' import { getEventsQueueName } from '@sofie-automation/corelib/dist/worker/events' import { ExternalMessageQueueRunner } from '../../events/ExternalMessageQueue' import { WorkerJobResult } from '../parent-base' import { endTrace, sendTrace, startTrace } from '@sofie-automation/corelib/dist/influxdb' import { getPrometheusMetricsString, setupPrometheusMetrics } from '@sofie-automation/corelib/dist/prometheus' +import { UserError } from '@sofie-automation/corelib/dist/error' interface StaticData { readonly mongoClient: MongoClient @@ -141,10 +141,13 @@ export class EventsWorkerChild { error: null, } } catch (e) { - logger.error(`Events job "${jobName}" errored: ${stringifyError(e)}`) + const userError = UserError.fromUnknown(e) + + logger.info(`Events job "${jobName}" errored: ${userError.toErrorString()}`) + return { result: null, - error: e, + error: UserError.toJSON(userError), } } } else { diff --git a/packages/job-worker/src/workers/ingest/child.ts b/packages/job-worker/src/workers/ingest/child.ts index 92dd841503..1a56da68a8 100644 --- a/packages/job-worker/src/workers/ingest/child.ts +++ b/packages/job-worker/src/workers/ingest/child.ts @@ -9,12 +9,12 @@ import { JobContextImpl, QueueJobFunc } from '../context' import { AnyLockEvent, LocksManager } from '../locks' import { FastTrackTimelineFunc, LogLineWithSourceFunc } from '../../main' import { interceptLogging, logger } from '../../logging' -import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { setupInfluxDb } from '../../influx' import { getIngestQueueName } from '@sofie-automation/corelib/dist/worker/ingest' import { WorkerJobResult } from '../parent-base' import { endTrace, sendTrace, startTrace } from '@sofie-automation/corelib/dist/influxdb' import { getPrometheusMetricsString, setupPrometheusMetrics } from '@sofie-automation/corelib/dist/prometheus' +import { UserError } from '@sofie-automation/corelib/dist/error' interface StaticData { readonly mongoClient: MongoClient @@ -123,10 +123,13 @@ export class IngestWorkerChild { error: null, } } catch (e) { - logger.error(`Ingest job "${jobName}" errored: ${stringifyError(e)}`) + const userError = UserError.fromUnknown(e) + + logger.info(`Ingest job "${jobName}" errored: ${userError.toErrorString()}`) + return { result: null, - error: e, + error: UserError.toJSON(userError), } } } else { diff --git a/packages/job-worker/src/workers/parent-base.ts b/packages/job-worker/src/workers/parent-base.ts index b3444867a8..7fd820f536 100644 --- a/packages/job-worker/src/workers/parent-base.ts +++ b/packages/job-worker/src/workers/parent-base.ts @@ -20,6 +20,7 @@ import { Promisify, ThreadedClassManager } from 'threadedclass' import { StatusCode } from '@sofie-automation/blueprints-integration' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' import { WorkerThreadStatus } from '@sofie-automation/corelib/dist/dataModel/WorkerThreads' +import { UserError } from '@sofie-automation/corelib/dist/error' export enum ThreadStatus { Closed = 0, @@ -56,7 +57,7 @@ export interface WorkerParentBaseOptions extends WorkerParentOptions { * Wrap up the result of a job to an object. This allows us to use special types in the error, as anything thrown can get mangled by threadedClass or other wrappings */ export interface WorkerJobResult { - error: any + error: string | null // string must be formatted as a UserError result: any } @@ -301,7 +302,9 @@ export abstract class WorkerParentBase { job.id, startTime, endTime, - result.error, + result.error + ? UserError.tryFromJSON(result.error) ?? new Error(result.error) + : null, result.result ) diff --git a/packages/job-worker/src/workers/studio/child.ts b/packages/job-worker/src/workers/studio/child.ts index d913fa97ac..4d87dd3314 100644 --- a/packages/job-worker/src/workers/studio/child.ts +++ b/packages/job-worker/src/workers/studio/child.ts @@ -9,14 +9,13 @@ import { QueueJobFunc, JobContextImpl } from '../context' import { AnyLockEvent, LocksManager } from '../locks' import { FastTrackTimelineFunc, LogLineWithSourceFunc } from '../../main' import { interceptLogging, logger } from '../../logging' -import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { setupInfluxDb } from '../../influx' import { getStudioQueueName } from '@sofie-automation/corelib/dist/worker/studio' import { WorkerJobResult } from '../parent-base' import { endTrace, sendTrace, startTrace } from '@sofie-automation/corelib/dist/influxdb' import { getPrometheusMetricsString, setupPrometheusMetrics } from '@sofie-automation/corelib/dist/prometheus' import { UserError } from '@sofie-automation/corelib/dist/error' -import { interpollateTranslation } from '@sofie-automation/corelib/dist/TranslatableMessage' +import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' interface StaticData { readonly mongoClient: MongoClient @@ -125,20 +124,14 @@ export class StudioWorkerChild { error: null, } } catch (e) { - if (UserError.isUserError(e)) { - logger.info( - `Studio job "${jobName}" failed: ${interpollateTranslation( - e.message.key, - e.message.args - )} ${stringifyError(e.rawError)}` - ) - } else { - logger.error(`Studio job "${jobName}" errored: ${stringifyError(e)}`) - } + const userError = UserError.fromUnknown(e) + console.log('border', userError.toErrorString(), stringifyError(e)) + + logger.info(`Studio job "${jobName}" errored: ${userError.toErrorString()}`) return { result: null, - error: e, + error: UserError.toJSON(userError), } } } else { diff --git a/packages/shared-lib/src/lib/stringifyError.ts b/packages/shared-lib/src/lib/stringifyError.ts index 87bb26b2ea..69a5cd7270 100644 --- a/packages/shared-lib/src/lib/stringifyError.ts +++ b/packages/shared-lib/src/lib/stringifyError.ts @@ -39,5 +39,7 @@ export function stringifyError(error: unknown, noStack = false): string { } } + if (str.startsWith('Error: ')) str = str.slice('Error: '.length) + return str } From 1a44ff8bea971922fc55fbb03686467e9bd3b0c9 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 4 Jun 2024 13:43:51 +0100 Subject: [PATCH 331/479] fix: action triggers disappearing when adlibbing and immediately taking a part #1197 SOFIE-3182 --- .../StudioDeviceTriggerManager.ts | 2 -- .../api/deviceTriggers/StudioObserver.ts | 23 +++---------------- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts b/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts index c7eed9a955..92a78df145 100644 --- a/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts +++ b/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts @@ -192,8 +192,6 @@ export class StudioDeviceTriggerManager { } }) - console.log(`updating ${triggeredAction._id}, gen ${addedPreviewIds}`) - DeviceTriggerMountedActionAdlibsPreview.remove({ triggeredActionId: triggeredAction._id, _id: { diff --git a/meteor/server/api/deviceTriggers/StudioObserver.ts b/meteor/server/api/deviceTriggers/StudioObserver.ts index 15029b43d7..3576935658 100644 --- a/meteor/server/api/deviceTriggers/StudioObserver.ts +++ b/meteor/server/api/deviceTriggers/StudioObserver.ts @@ -11,7 +11,6 @@ import EventEmitter from 'events' import { Meteor } from 'meteor/meteor' import _ from 'underscore' import { MongoCursor } from '../../../lib/collections/lib' -import { DBPartInstance } from '../../../lib/collections/PartInstances' import { DBRundownPlaylist } from '../../../lib/collections/RundownPlaylists' import { DBRundown } from '../../../lib/collections/Rundowns' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' @@ -20,7 +19,7 @@ import { observerChain } from '../../publications/lib/observerChain' import { ContentCache } from './reactiveContentCache' import { RundownContentObserver } from './RundownContentObserver' import { RundownsObserver } from './RundownsObserver' -import { PartInstances, RundownPlaylists, Rundowns, ShowStyleBases } from '../../collections' +import { RundownPlaylists, Rundowns, ShowStyleBases } from '../../collections' type ChangedHandler = (showStyleBaseId: ShowStyleBaseId, cache: ContentCache) => () => void @@ -36,12 +35,6 @@ const rundownPlaylistFieldSpecifier = literal< nextPartInfo: 1, }) -type PartInstanceFields = '_id' | 'rundownId' -const partInstanceFieldSpecifier = literal>>({ - _id: 1, - rundownId: 1, -}) - type RundownFields = '_id' | 'showStyleBaseId' const rundownFieldSpecifier = literal>>({ _id: 1, @@ -84,16 +77,6 @@ export class StudioObserver extends EventEmitter { } ) as Promise>> ) - .next('activePartInstance', async (chain) => { - const activePartInstanceId = - chain.activePlaylist.currentPartInfo?.partInstanceId ?? - chain.activePlaylist.nextPartInfo?.partInstanceId - if (!activePartInstanceId) return null - return PartInstances.findWithCursor( - { _id: activePartInstanceId }, - { projection: partInstanceFieldSpecifier, limit: 1 } - ) as Promise>> - }) .end(this.updatePlaylistInStudio) } @@ -102,12 +85,12 @@ export class StudioObserver extends EventEmitter { ( state: { activePlaylist: Pick - activePartInstance: Pick } | null ): void => { const activePlaylistId = state?.activePlaylist?._id const activationId = state?.activePlaylist?.activationId - const currentRundownId = state?.activePartInstance?.rundownId + const currentRundownId = + state?.activePlaylist?.currentPartInfo?.rundownId ?? state?.activePlaylist?.nextPartInfo?.rundownId if (!activePlaylistId || !activationId || !currentRundownId) { this.#showStyleOfRundownLiveQuery?.stop() From d01cd991865485d97435ffb1065c574653bf508f Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 4 Jun 2024 13:43:51 +0100 Subject: [PATCH 332/479] fix: action triggers disappearing when adlibbing and immediately taking a part #1197 SOFIE-3182 --- .../StudioDeviceTriggerManager.ts | 2 -- .../api/deviceTriggers/StudioObserver.ts | 23 +++---------------- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts b/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts index c7eed9a955..92a78df145 100644 --- a/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts +++ b/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts @@ -192,8 +192,6 @@ export class StudioDeviceTriggerManager { } }) - console.log(`updating ${triggeredAction._id}, gen ${addedPreviewIds}`) - DeviceTriggerMountedActionAdlibsPreview.remove({ triggeredActionId: triggeredAction._id, _id: { diff --git a/meteor/server/api/deviceTriggers/StudioObserver.ts b/meteor/server/api/deviceTriggers/StudioObserver.ts index 2d918373b6..95a26bda08 100644 --- a/meteor/server/api/deviceTriggers/StudioObserver.ts +++ b/meteor/server/api/deviceTriggers/StudioObserver.ts @@ -11,7 +11,6 @@ import EventEmitter from 'events' import { Meteor } from 'meteor/meteor' import _ from 'underscore' import { MongoCursor } from '../../../lib/collections/lib' -import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' @@ -20,7 +19,7 @@ import { observerChain } from '../../publications/lib/observerChain' import { ContentCache } from './reactiveContentCache' import { RundownContentObserver } from './RundownContentObserver' import { RundownsObserver } from './RundownsObserver' -import { PartInstances, RundownPlaylists, Rundowns, ShowStyleBases } from '../../collections' +import { RundownPlaylists, Rundowns, ShowStyleBases } from '../../collections' type ChangedHandler = (showStyleBaseId: ShowStyleBaseId, cache: ContentCache) => () => void @@ -36,12 +35,6 @@ const rundownPlaylistFieldSpecifier = literal< nextPartInfo: 1, }) -type PartInstanceFields = '_id' | 'rundownId' -const partInstanceFieldSpecifier = literal>>({ - _id: 1, - rundownId: 1, -}) - type RundownFields = '_id' | 'showStyleBaseId' const rundownFieldSpecifier = literal>>({ _id: 1, @@ -90,16 +83,6 @@ export class StudioObserver extends EventEmitter { } ) as Promise>> ) - .next('activePartInstance', async (chain) => { - const activePartInstanceId = - chain.activePlaylist.currentPartInfo?.partInstanceId ?? - chain.activePlaylist.nextPartInfo?.partInstanceId - if (!activePartInstanceId) return null - return PartInstances.findWithCursor( - { _id: activePartInstanceId }, - { projection: partInstanceFieldSpecifier, limit: 1 } - ) as Promise>> - }) .end(this.updatePlaylistInStudio) } @@ -108,12 +91,12 @@ export class StudioObserver extends EventEmitter { ( state: { activePlaylist: Pick - activePartInstance: Pick } | null ): void => { const activePlaylistId = state?.activePlaylist?._id const activationId = state?.activePlaylist?.activationId - const currentRundownId = state?.activePartInstance?.rundownId + const currentRundownId = + state?.activePlaylist?.currentPartInfo?.rundownId ?? state?.activePlaylist?.nextPartInfo?.rundownId if (!activePlaylistId || !activationId || !currentRundownId) { this.#showStyleOfRundownLiveQuery?.stop() From 167a6252fa7036ae5c1239f8af35292c8147e7de Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Tue, 4 Jun 2024 16:56:59 +0200 Subject: [PATCH 333/479] feat(LSG): sort adlibs to match GUI --- .../src/liveStatusServer.ts | 1 + .../src/topics/adLibsTopic.ts | 135 ++++++++++++------ .../src/topics/helpers/contentSorting.ts | 40 ++++++ 3 files changed, 136 insertions(+), 40 deletions(-) create mode 100644 packages/live-status-gateway/src/topics/helpers/contentSorting.ts diff --git a/packages/live-status-gateway/src/liveStatusServer.ts b/packages/live-status-gateway/src/liveStatusServer.ts index ea18611f8a..5cdda17720 100644 --- a/packages/live-status-gateway/src/liveStatusServer.ts +++ b/packages/live-status-gateway/src/liveStatusServer.ts @@ -117,6 +117,7 @@ export class LiveStatusServer { await showStyleBaseHandler.subscribe(adLibsTopic) await partsHandler.subscribe(adLibsTopic) + await segmentsHandler.subscribe(adLibsTopic) await playlistHandler.subscribe(adLibsTopic) await adLibActionsHandler.subscribe(adLibsTopic) await adLibsHandler.subscribe(adLibsTopic) diff --git a/packages/live-status-gateway/src/topics/adLibsTopic.ts b/packages/live-status-gateway/src/topics/adLibsTopic.ts index e2e29efc55..856014a7e9 100644 --- a/packages/live-status-gateway/src/topics/adLibsTopic.ts +++ b/packages/live-status-gateway/src/topics/adLibsTopic.ts @@ -19,7 +19,11 @@ import { AdLibsHandler } from '../collections/adLibsHandler' import { GlobalAdLibsHandler } from '../collections/globalAdLibsHandler' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { PartsHandler } from '../collections/partsHandler' -import { PartId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PartId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { WithSortingMetadata, sortContent } from './helpers/contentSorting' +import { isDeepStrictEqual } from 'util' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import { SegmentsHandler } from '../collections/segmentsHandler' const THROTTLE_PERIOD_MS = 100 @@ -69,6 +73,7 @@ export class AdLibsTopic private _adLibActions: AdLibAction[] | undefined private _abLibs: AdLibPiece[] | undefined private _parts: ReadonlyMap = new Map() + private _segments: ReadonlyMap = new Map() private _globalAdLibActions: RundownBaselineAdLibAction[] | undefined private _globalAdLibs: RundownBaselineAdLibItem[] | undefined private throttledSendStatusToAll: () => void @@ -87,8 +92,8 @@ export class AdLibsTopic } sendStatus(subscribers: Iterable): void { - const adLibs: AdLibStatus[] = [] - const globalAdLibs: GlobalAdLibStatus[] = [] + const adLibs: WithSortingMetadata[] = [] + const globalAdLibs: WithSortingMetadata[] = [] if (this._adLibActions) { adLibs.push( @@ -108,16 +113,24 @@ export class AdLibsTopic ) : [] const segmentId = this._parts.get(action.partId)?.segmentId - return literal({ + const name = interpollateTranslation(action.display.label.key, action.display.label.args) + return literal>({ + obj: { + id: unprotectString(action._id), + name, + partId: unprotectString(action.partId), + segmentId: unprotectString(segmentId) ?? 'invalid', + sourceLayer: sourceLayerName ?? 'invalid', + outputLayer: outputLayerName ?? 'invalid', + actionType: triggerModes, + tags: action.display.tags, + publicData: action.userData, // tmp, publicData was introduced in R51 + }, id: unprotectString(action._id), - name: interpollateTranslation(action.display.label.key, action.display.label.args), - partId: unprotectString(action.partId), - segmentId: unprotectString(segmentId) ?? 'invalid', - sourceLayer: sourceLayerName ?? 'invalid', - outputLayer: outputLayerName ?? 'invalid', - actionType: triggerModes, - tags: action.display.tags, - publicData: action.userData, // tmp, publicData was introduced in R51 + label: name, + _rank: action.display._rank, + partId: action.partId, + segmentId: segmentId, }) }) ) @@ -129,16 +142,23 @@ export class AdLibsTopic const sourceLayerName = this._sourceLayersMap.get(adLib.sourceLayerId) const outputLayerName = this._outputLayersMap.get(adLib.outputLayerId) const segmentId = adLib.partId ? this._parts.get(adLib.partId)?.segmentId : undefined - return literal({ + return literal>({ + obj: { + id: unprotectString(adLib._id), + name: adLib.name, + partId: unprotectString(adLib.partId) ?? 'invalid', + segmentId: unprotectString(segmentId) ?? 'invalid', + sourceLayer: sourceLayerName ?? 'invalid', + outputLayer: outputLayerName ?? 'invalid', + actionType: [], + tags: adLib.tags, + publicData: adLib.metaData, // tmp, publicData was introduced in R51 + }, id: unprotectString(adLib._id), - name: adLib.name, - partId: unprotectString(adLib.partId) ?? 'invalid', - segmentId: unprotectString(segmentId) ?? 'invalid', - sourceLayer: sourceLayerName ?? 'invalid', - outputLayer: outputLayerName ?? 'invalid', - actionType: [], - tags: adLib.tags, - publicData: adLib.metaData, // tmp, publicData was introduced in R51 + label: adLib.name, + _rank: adLib._rank, + partId: adLib.partId, + segmentId: segmentId, }) }) ) @@ -161,14 +181,21 @@ export class AdLibsTopic }) ) : [] - return literal({ + const name = interpollateTranslation(action.display.label.key, action.display.label.args) + return literal>({ + obj: { + id: unprotectString(action._id), + name, + sourceLayer: sourceLayerName ?? 'invalid', + outputLayer: outputLayerName ?? 'invalid', + actionType: triggerModes, + tags: action.display.tags, + publicData: action.userData, // tmp, publicData was introduced in R51 + }, id: unprotectString(action._id), - name: interpollateTranslation(action.display.label.key, action.display.label.args), - sourceLayer: sourceLayerName ?? 'invalid', - outputLayer: outputLayerName ?? 'invalid', - actionType: triggerModes, - tags: action.display.tags, - publicData: action.userData, // tmp, publicData was introduced in R51 + label: name, + rundownId: action.rundownId, + _rank: action.display._rank, }) }) ) @@ -179,14 +206,20 @@ export class AdLibsTopic ...this._globalAdLibs.map((adLib) => { const sourceLayerName = this._sourceLayersMap.get(adLib.sourceLayerId) const outputLayerName = this._outputLayersMap.get(adLib.outputLayerId) - return literal({ + return literal>({ + obj: { + id: unprotectString(adLib._id), + name: adLib.name, + sourceLayer: sourceLayerName ?? 'invalid', + outputLayer: outputLayerName ?? 'invalid', + actionType: [], + tags: adLib.tags, + publicData: adLib.metaData, // tmp, publicData was introduced in R51 + }, id: unprotectString(adLib._id), - name: adLib.name, - sourceLayer: sourceLayerName ?? 'invalid', - outputLayer: outputLayerName ?? 'invalid', - actionType: [], - tags: adLib.tags, - publicData: adLib.metaData, // tmp, publicData was introduced in R51 + label: adLib.name, + rundownId: adLib.rundownId, + _rank: adLib._rank, }) }) ) @@ -196,8 +229,13 @@ export class AdLibsTopic ? { event: 'adLibs', rundownPlaylistId: unprotectString(this._activePlaylist._id), - adLibs, - globalAdLibs, + adLibs: sortContent(adLibs, this._activePlaylist.rundownIdsInOrder, this._segments, this._parts), + globalAdLibs: sortContent( + globalAdLibs, + this._activePlaylist.rundownIdsInOrder, + this._segments, + this._parts + ), } : { event: 'adLibs', rundownPlaylistId: null, adLibs: [], globalAdLibs: [] } @@ -216,14 +254,21 @@ export class AdLibsTopic | AdLibPiece[] | RundownBaselineAdLibItem[] | DBPart[] + | DBSegment[] | undefined ): Promise { switch (source) { case PlaylistHandler.name: { - const previousPlaylistId = this._activePlaylist?._id - this._activePlaylist = data as DBRundownPlaylist | undefined + const previousPlaylist = this._activePlaylist this.logUpdateReceived('playlist', source) - if (previousPlaylistId === this._activePlaylist?._id) return + this._activePlaylist = data as DBRundownPlaylist | undefined + // PlaylistHandler is quite chatty (will update on every take), so let's make sure there's a point + // in sending a status + if ( + previousPlaylist?._id === this._activePlaylist?._id && + isDeepStrictEqual(previousPlaylist?.rundownIdsInOrder, this._activePlaylist?.rundownIdsInOrder) + ) + return break } case AdLibActionsHandler.name: { @@ -257,6 +302,16 @@ export class AdLibsTopic this._outputLayersMap = showStyleBaseExt?.outputLayerNamesById ?? new Map() break } + case SegmentsHandler.name: { + const segments = data ? (data as DBPart[]) : [] + this.logUpdateReceived('segments', source) + const newSegments = new Map() + segments.forEach((segment) => { + newSegments.set(segment._id, segment) + }) + this._segments = newSegments + break + } case PartsHandler.name: { const parts = data ? (data as DBPart[]) : [] this.logUpdateReceived('parts', source) diff --git a/packages/live-status-gateway/src/topics/helpers/contentSorting.ts b/packages/live-status-gateway/src/topics/helpers/contentSorting.ts new file mode 100644 index 0000000000..bb86774a34 --- /dev/null +++ b/packages/live-status-gateway/src/topics/helpers/contentSorting.ts @@ -0,0 +1,40 @@ +import { PartId, RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' + +export type WithSortingMetadata = { + obj: T + id: string + label: string + _rank?: number + rundownId?: RundownId + segmentId?: SegmentId + partId?: PartId +} + +export function sortContent( + contentData: WithSortingMetadata[], + rundownIdsInOrder: RundownId[], + segments: ReadonlyMap, + parts: ReadonlyMap +): T[] { + function comparator(a: WithSortingMetadata, b: WithSortingMetadata): number { + if (a.rundownId !== b.rundownId) + return ( + (a.rundownId ? rundownIdsInOrder.indexOf(a.rundownId) : -1) - + (b.rundownId ? rundownIdsInOrder.indexOf(b.rundownId) : -1) + ) + if (a.segmentId !== b.segmentId) return getRank(segments, a.segmentId) - getRank(segments, b.segmentId) + if (a.partId !== b.partId) return getRank(parts, a.partId) - getRank(parts, b.partId) + if (a.label !== b.label) return a.label?.localeCompare(b.label) + // if everything else fails, fall back to sorting on the ID for a stable sort + return a.id.localeCompare(b.id) + } + + return contentData.sort(comparator).map((res) => res.obj) +} + +function getRank(map: ReadonlyMap, key: Key | undefined): number { + if (key === undefined) return -1 + return map.get(key)?._rank ?? -1 +} From 4224b02be70851ca2058d85c73571d9085cbf97f Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Tue, 4 Jun 2024 17:01:07 +0200 Subject: [PATCH 334/479] fix: also sort on "own" rank --- .../live-status-gateway/src/topics/helpers/contentSorting.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/live-status-gateway/src/topics/helpers/contentSorting.ts b/packages/live-status-gateway/src/topics/helpers/contentSorting.ts index bb86774a34..f00acaf538 100644 --- a/packages/live-status-gateway/src/topics/helpers/contentSorting.ts +++ b/packages/live-status-gateway/src/topics/helpers/contentSorting.ts @@ -26,6 +26,7 @@ export function sortContent( ) if (a.segmentId !== b.segmentId) return getRank(segments, a.segmentId) - getRank(segments, b.segmentId) if (a.partId !== b.partId) return getRank(parts, a.partId) - getRank(parts, b.partId) + if (a._rank !== b._rank) return (a._rank ?? 0) - (b._rank ?? 0) if (a.label !== b.label) return a.label?.localeCompare(b.label) // if everything else fails, fall back to sorting on the ID for a stable sort return a.id.localeCompare(b.id) From 67d5916f64f0a543ebf8e93caee3243917458bd7 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 5 Jun 2024 10:45:09 +0200 Subject: [PATCH 335/479] fix: make content sorting consistent with Core --- .../src/topics/adLibsTopic.ts | 33 +++++----- .../src/topics/helpers/contentSorting.ts | 61 +++++++++++-------- 2 files changed, 52 insertions(+), 42 deletions(-) diff --git a/packages/live-status-gateway/src/topics/adLibsTopic.ts b/packages/live-status-gateway/src/topics/adLibsTopic.ts index 856014a7e9..def55c0bf8 100644 --- a/packages/live-status-gateway/src/topics/adLibsTopic.ts +++ b/packages/live-status-gateway/src/topics/adLibsTopic.ts @@ -20,7 +20,7 @@ import { GlobalAdLibsHandler } from '../collections/globalAdLibsHandler' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { PartsHandler } from '../collections/partsHandler' import { PartId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { WithSortingMetadata, sortContent } from './helpers/contentSorting' +import { WithSortingMetadata, getRank, sortContent } from './helpers/contentSorting' import { isDeepStrictEqual } from 'util' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { SegmentsHandler } from '../collections/segmentsHandler' @@ -128,9 +128,10 @@ export class AdLibsTopic }, id: unprotectString(action._id), label: name, - _rank: action.display._rank, - partId: action.partId, - segmentId: segmentId, + itemRank: action.display._rank, + partRank: getRank(this._parts, action.partId), + segmentRank: getRank(this._segments, segmentId), + rundownRank: this._activePlaylist?.rundownIdsInOrder.indexOf(action.rundownId), }) }) ) @@ -156,9 +157,10 @@ export class AdLibsTopic }, id: unprotectString(adLib._id), label: adLib.name, - _rank: adLib._rank, - partId: adLib.partId, - segmentId: segmentId, + itemRank: adLib._rank, + partRank: getRank(this._parts, adLib.partId), + segmentRank: getRank(this._segments, segmentId), + rundownRank: this._activePlaylist?.rundownIdsInOrder.indexOf(adLib.rundownId), }) }) ) @@ -194,8 +196,8 @@ export class AdLibsTopic }, id: unprotectString(action._id), label: name, - rundownId: action.rundownId, - _rank: action.display._rank, + rundownRank: this._activePlaylist?.rundownIdsInOrder.indexOf(action.rundownId), + itemRank: action.display._rank, }) }) ) @@ -218,8 +220,8 @@ export class AdLibsTopic }, id: unprotectString(adLib._id), label: adLib.name, - rundownId: adLib.rundownId, - _rank: adLib._rank, + rundownRank: this._activePlaylist?.rundownIdsInOrder.indexOf(adLib.rundownId), + itemRank: adLib._rank, }) }) ) @@ -229,13 +231,8 @@ export class AdLibsTopic ? { event: 'adLibs', rundownPlaylistId: unprotectString(this._activePlaylist._id), - adLibs: sortContent(adLibs, this._activePlaylist.rundownIdsInOrder, this._segments, this._parts), - globalAdLibs: sortContent( - globalAdLibs, - this._activePlaylist.rundownIdsInOrder, - this._segments, - this._parts - ), + adLibs: sortContent(adLibs), + globalAdLibs: sortContent(globalAdLibs), } : { event: 'adLibs', rundownPlaylistId: null, adLibs: [], globalAdLibs: [] } diff --git a/packages/live-status-gateway/src/topics/helpers/contentSorting.ts b/packages/live-status-gateway/src/topics/helpers/contentSorting.ts index f00acaf538..73675017c7 100644 --- a/packages/live-status-gateway/src/topics/helpers/contentSorting.ts +++ b/packages/live-status-gateway/src/topics/helpers/contentSorting.ts @@ -1,41 +1,54 @@ -import { PartId, RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' -import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' - export type WithSortingMetadata = { obj: T id: string label: string - _rank?: number - rundownId?: RundownId - segmentId?: SegmentId - partId?: PartId + itemRank?: number + rundownRank?: number + segmentRank?: number + partRank?: number } -export function sortContent( - contentData: WithSortingMetadata[], - rundownIdsInOrder: RundownId[], - segments: ReadonlyMap, - parts: ReadonlyMap -): T[] { +export function sortContent(contentData: WithSortingMetadata[]): T[] { function comparator(a: WithSortingMetadata, b: WithSortingMetadata): number { - if (a.rundownId !== b.rundownId) - return ( - (a.rundownId ? rundownIdsInOrder.indexOf(a.rundownId) : -1) - - (b.rundownId ? rundownIdsInOrder.indexOf(b.rundownId) : -1) - ) - if (a.segmentId !== b.segmentId) return getRank(segments, a.segmentId) - getRank(segments, b.segmentId) - if (a.partId !== b.partId) return getRank(parts, a.partId) - getRank(parts, b.partId) - if (a._rank !== b._rank) return (a._rank ?? 0) - (b._rank ?? 0) + a.rundownRank = a.rundownRank ?? Number.POSITIVE_INFINITY + b.rundownRank = b.rundownRank ?? Number.POSITIVE_INFINITY + if (a.rundownRank > b.rundownRank) return 1 + if (a.rundownRank < b.rundownRank) return -1 + + // Sort by segment rank, where applicable: + a.segmentRank = a.segmentRank ?? Number.POSITIVE_INFINITY + b.segmentRank = b.segmentRank ?? Number.POSITIVE_INFINITY + if (a.segmentRank > b.segmentRank) return 1 + if (a.segmentRank < b.segmentRank) return -1 + + // Sort by part rank, where applicable: + a.partRank = a.partRank ?? Number.POSITIVE_INFINITY + b.partRank = b.partRank ?? Number.POSITIVE_INFINITY + if (a.partRank > b.partRank) return 1 + if (a.partRank < b.partRank) return -1 + + a.itemRank = a.itemRank ?? Number.POSITIVE_INFINITY + b.itemRank = b.itemRank ?? Number.POSITIVE_INFINITY + if (a.itemRank > b.itemRank) return 1 + if (a.itemRank < b.itemRank) return -1 + if (a.label !== b.label) return a.label?.localeCompare(b.label) + // if everything else fails, fall back to sorting on the ID for a stable sort - return a.id.localeCompare(b.id) + // As a last resort, sort by ids: + if (a.id > b.id) return 1 + if (a.id < b.id) return -1 + + return 0 } return contentData.sort(comparator).map((res) => res.obj) } -function getRank(map: ReadonlyMap, key: Key | undefined): number { +export function getRank( + map: ReadonlyMap, + key: Key | undefined +): number { if (key === undefined) return -1 return map.get(key)?._rank ?? -1 } From 5667c0ffc4f28a92f477d4c2927875bec04a4301 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 5 Jun 2024 11:45:49 +0200 Subject: [PATCH 336/479] fix(LSG): sorting, make getRank return undefined if rank could not be established --- .../src/topics/helpers/contentSorting.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/live-status-gateway/src/topics/helpers/contentSorting.ts b/packages/live-status-gateway/src/topics/helpers/contentSorting.ts index 73675017c7..52a48f2851 100644 --- a/packages/live-status-gateway/src/topics/helpers/contentSorting.ts +++ b/packages/live-status-gateway/src/topics/helpers/contentSorting.ts @@ -48,7 +48,7 @@ export function sortContent(contentData: WithSortingMetadata[]): T[] { export function getRank( map: ReadonlyMap, key: Key | undefined -): number { - if (key === undefined) return -1 - return map.get(key)?._rank ?? -1 +): number | undefined { + if (key === undefined) return undefined + return map.get(key)?._rank } From 65102f67cd941c38c268d6edbed502b3f9c02524 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Fri, 7 Jun 2024 11:43:40 +0200 Subject: [PATCH 337/479] fix: REST API UserAction log entries are incorrect and in an inconveniant format. Also, Client address is set to the server host and not the actual user agent IP address. --- meteor/server/api/rest/koa.ts | 34 +++++++++++++++++++++++++++++- meteor/server/api/rest/v1/index.ts | 11 +++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/meteor/server/api/rest/koa.ts b/meteor/server/api/rest/koa.ts index efec4e22ef..b55880a28a 100644 --- a/meteor/server/api/rest/koa.ts +++ b/meteor/server/api/rest/koa.ts @@ -4,6 +4,7 @@ import KoaRouter from '@koa/router' import { WebApp } from 'meteor/webapp' import { Meteor } from 'meteor/meteor' import { getRandomString } from '@sofie-automation/corelib/dist/lib' +import _ from 'underscore' declare module 'http' { interface IncomingMessage { @@ -42,6 +43,31 @@ export function bindKoaRouter(koaRouter: KoaRouter, bindPath: string): void { app.use(koaRouter.routes()).use(koaRouter.allowedMethods()) } +const REVERSE_PROXY_COUNT = process.env.HTTP_FORWARDED_COUNT ? parseInt(process.env.HTTP_FORWARDED_COUNT) : 0 + +function getClientAddrFromXForwarded(headerVal: undefined | string | string[]): string | undefined { + if (headerVal === undefined) return undefined + if (typeof headerVal !== 'string') { + headerVal = _.last(headerVal) as string + } + const remoteAddresses = headerVal.split(/\s*,\s*/) + return remoteAddresses[remoteAddresses.length - REVERSE_PROXY_COUNT] ?? remoteAddresses[0] +} + +function getClientAddrFromForwarded(forwardedVal: string | undefined): string | undefined { + if (forwardedVal === undefined) return undefined + const allProxies = forwardedVal.split(/\s*,\s*/) + const proxyInfo = allProxies[allProxies.length - REVERSE_PROXY_COUNT] ?? allProxies[0] + const directives = proxyInfo?.split(/\s*;\s*/) + for (const directive of directives) { + let match: RegExpMatchArray | null + if ((match = directive.match(/^for=("\[)?([\w.:])+(\]")?/))) { + return match[2] + } + } + return undefined +} + export const makeMeteorConnectionFromKoa = ( ctx: Koa.ParameterizedContext< Koa.DefaultState, @@ -57,7 +83,13 @@ export const makeMeteorConnectionFromKoa = ( onClose: () => { /* no-op */ }, - clientAddress: ctx.req.headers.host || 'unknown', + clientAddress: + // This replicates Meteor behavior which uses the HTTP_FORWARDED_COUNT to extract the "world-facing" + // IP address of the Client User Agent + getClientAddrFromForwarded(ctx.req.headers.forwarded) || + getClientAddrFromXForwarded(ctx.req.headers['x-forwarded-for']) || + ctx.req.socket.remoteAddress || + 'unknown', httpHeaders: ctx.req.headers as Record, } } diff --git a/meteor/server/api/rest/v1/index.ts b/meteor/server/api/rest/v1/index.ts index df128e127c..c92db9c4c1 100644 --- a/meteor/server/api/rest/v1/index.ts +++ b/meteor/server/api/rest/v1/index.ts @@ -98,7 +98,9 @@ function restAPIUserEvent( unknown > ): string { - return `rest_api_${ctx.method}_${ctx.URL.origin}/api/v1.0${ctx.URL.pathname}}` + // due to how the router is mounted, the ctx.URL.pathname does not contain the /api portion, + // but contains the API version portion + return `REST API: ${ctx.method} /api${ctx.URL.pathname} ${ctx.URL.origin}` } class ServerRestAPI implements RestAPI { @@ -1251,6 +1253,13 @@ function sofieAPIRequest( ) { koaRouter[method](route, async (ctx, next) => { try { + logger.info(`REST APIv1: ${ctx.socket.remoteAddress} ${method} "${ctx.url}"`, { + url: ctx.url, + method, + remoteAddress: ctx.socket.remoteAddress, + remotePort: ctx.socket.remotePort, + headers: ctx.headers, + }) const serverAPI = new ServerRestAPI() const response = await handler( serverAPI, From cc0524d94d5a5a67950c03604ca581155e12302e Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Fri, 7 Jun 2024 11:51:20 +0200 Subject: [PATCH 338/479] chore: lint --- meteor/server/api/rest/v1/index.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/meteor/server/api/rest/v1/index.ts b/meteor/server/api/rest/v1/index.ts index c92db9c4c1..df128e127c 100644 --- a/meteor/server/api/rest/v1/index.ts +++ b/meteor/server/api/rest/v1/index.ts @@ -98,9 +98,7 @@ function restAPIUserEvent( unknown > ): string { - // due to how the router is mounted, the ctx.URL.pathname does not contain the /api portion, - // but contains the API version portion - return `REST API: ${ctx.method} /api${ctx.URL.pathname} ${ctx.URL.origin}` + return `rest_api_${ctx.method}_${ctx.URL.origin}/api/v1.0${ctx.URL.pathname}}` } class ServerRestAPI implements RestAPI { @@ -1253,13 +1251,6 @@ function sofieAPIRequest( ) { koaRouter[method](route, async (ctx, next) => { try { - logger.info(`REST APIv1: ${ctx.socket.remoteAddress} ${method} "${ctx.url}"`, { - url: ctx.url, - method, - remoteAddress: ctx.socket.remoteAddress, - remotePort: ctx.socket.remotePort, - headers: ctx.headers, - }) const serverAPI = new ServerRestAPI() const response = await handler( serverAPI, From 70ddd11431771b4cb71d74851c3d56161a8a1490 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Fri, 7 Jun 2024 11:56:20 +0200 Subject: [PATCH 339/479] fix: userEvent for REST API calls does not look sensible --- meteor/server/api/rest/v1/index.ts | 84 ++++++++++-------------------- 1 file changed, 28 insertions(+), 56 deletions(-) diff --git a/meteor/server/api/rest/v1/index.ts b/meteor/server/api/rest/v1/index.ts index df128e127c..f6452b0b5b 100644 --- a/meteor/server/api/rest/v1/index.ts +++ b/meteor/server/api/rest/v1/index.ts @@ -98,7 +98,8 @@ function restAPIUserEvent( unknown > ): string { - return `rest_api_${ctx.method}_${ctx.URL.origin}/api/v1.0${ctx.URL.pathname}}` + // the ctx.URL.pathname will contain `/v1.0`, but will not contain `/api` + return `REST API: ${ctx.method} /api${ctx.URL.pathname} ${ctx.URL.origin}` } class ServerRestAPI implements RestAPI { @@ -191,19 +192,17 @@ class ServerRestAPI implements RestAPI { }) const segmentAdLibPiece = AdLibPieces.findOneAsync(adLibId as PieceId, { projection: { _id: 1 } }) const bucketAdLibPiece = BucketAdLibs.findOneAsync(adLibId as BucketAdLibId, { projection: { _id: 1 } }) - const [baselineAdLibDoc, segmentAdLibDoc, bucketAdLibDoc, adLibAction, baselineAdLibAction] = await Promise.all( - [ - baselineAdLibPiece, - segmentAdLibPiece, - bucketAdLibPiece, - AdLibActions.findOneAsync(adLibId as AdLibActionId, { - projection: { _id: 1, actionId: 1, userData: 1 }, - }), - RundownBaselineAdLibActions.findOneAsync(adLibId as RundownBaselineAdLibActionId, { - projection: { _id: 1, actionId: 1, userData: 1 }, - }), - ] - ) + const [baselineAdLibDoc, segmentAdLibDoc, bucketAdLibDoc, adLibAction, baselineAdLibAction] = await Promise.all([ + baselineAdLibPiece, + segmentAdLibPiece, + bucketAdLibPiece, + AdLibActions.findOneAsync(adLibId as AdLibActionId, { + projection: { _id: 1, actionId: 1, userData: 1 }, + }), + RundownBaselineAdLibActions.findOneAsync(adLibId as RundownBaselineAdLibActionId, { + projection: { _id: 1, actionId: 1, userData: 1 }, + }), + ]) const adLibActionDoc = adLibAction ?? baselineAdLibAction const regularAdLibDoc = baselineAdLibDoc ?? segmentAdLibDoc ?? bucketAdLibDoc if (regularAdLibDoc) { @@ -214,10 +213,7 @@ class ServerRestAPI implements RestAPI { }) if (!rundownPlaylist) return ClientAPI.responseError( - UserError.from( - new Error(`Rundown playlist does not exist`), - UserErrorMessage.RundownPlaylistNotFound - ), + UserError.from(new Error(`Rundown playlist does not exist`), UserErrorMessage.RundownPlaylistNotFound), 404 ) if (rundownPlaylist.currentPartInfo === null) @@ -253,10 +249,7 @@ class ServerRestAPI implements RestAPI { if (!rundownPlaylist) return ClientAPI.responseError( - UserError.from( - new Error(`Rundown playlist does not exist`), - UserErrorMessage.RundownPlaylistNotFound - ), + UserError.from(new Error(`Rundown playlist does not exist`), UserErrorMessage.RundownPlaylistNotFound), 404 ) if (!rundownPlaylist.activationId) @@ -370,10 +363,7 @@ class ServerRestAPI implements RestAPI { }, false) return success ? {} - : UserError.from( - new Error(`Failed to reload playlist ${rundownPlaylistId}`), - UserErrorMessage.InternalError - ) + : UserError.from(new Error(`Failed to reload playlist ${rundownPlaylistId}`), UserErrorMessage.InternalError) } ) } @@ -508,10 +498,7 @@ class ServerRestAPI implements RestAPI { check(routeSetId, String) check(state, Boolean) - const access = await StudioContentWriteAccess.routeSet( - ServerRestAPI.getCredentials(connection), - studioId - ) + const access = await StudioContentWriteAccess.routeSet(ServerRestAPI.getCredentials(connection), studioId) return ServerPlayoutAPI.switchRouteSet(access, routeSetId, state) } ) @@ -600,10 +587,7 @@ class ServerRestAPI implements RestAPI { const device = await PeripheralDevices.findOneAsync(deviceId) if (!device) return ClientAPI.responseError( - UserError.from( - new Error(`Device ${deviceId} does not exist`), - UserErrorMessage.PeripheralDeviceNotFound - ), + UserError.from(new Error(`Device ${deviceId} does not exist`), UserErrorMessage.PeripheralDeviceNotFound), 404 ) return ClientAPI.responseSuccess(APIPeripheralDeviceFrom(device)) @@ -618,10 +602,7 @@ class ServerRestAPI implements RestAPI { const device = await PeripheralDevices.findOneAsync(deviceId) if (!device) return ClientAPI.responseError( - UserError.from( - new Error(`Device ${deviceId} does not exist`), - UserErrorMessage.PeripheralDeviceNotFound - ), + UserError.from(new Error(`Device ${deviceId} does not exist`), UserErrorMessage.PeripheralDeviceNotFound), 404 ) @@ -716,10 +697,7 @@ class ServerRestAPI implements RestAPI { if (device.studioId !== undefined && device.studioId !== studio._id) { return ClientAPI.responseError( - UserError.from( - new Error(`Device already attached to studio`), - UserErrorMessage.DeviceAlreadyAttachedToStudio - ), + UserError.from(new Error(`Device already attached to studio`), UserErrorMessage.DeviceAlreadyAttachedToStudio), 412 ) } @@ -798,10 +776,9 @@ class ServerRestAPI implements RestAPI { const existingShowStyle = await ShowStyleBases.findOneAsync(showStyleBaseId) if (existingShowStyle) { - const rundowns = (await Rundowns.findFetchAsync( - { showStyleBaseId }, - { projection: { playlistId: 1 } } - )) as Array> + const rundowns = (await Rundowns.findFetchAsync({ showStyleBaseId }, { projection: { playlistId: 1 } })) as Array< + Pick + > const playlists = (await RundownPlaylists.findFetchAsync( { _id: { $in: rundowns.map((r) => r.playlistId) } }, { @@ -840,10 +817,9 @@ class ServerRestAPI implements RestAPI { _event: string, showStyleBaseId: ShowStyleBaseId ): Promise> { - const rundowns = (await Rundowns.findFetchAsync( - { showStyleBaseId }, - { projection: { playlistId: 1 } } - )) as Array> + const rundowns = (await Rundowns.findFetchAsync({ showStyleBaseId }, { projection: { playlistId: 1 } })) as Array< + Pick + > const playlists = (await RundownPlaylists.findFetchAsync( { _id: { $in: rundowns.map((r) => r.playlistId) } }, { @@ -1274,9 +1250,7 @@ function sofieAPIRequest( } errMsg = translateMessage(msgConcat, interpollateTranslation) } else { - logger.error( - `${method.toUpperCase()} for route ${route} returned unexpected error code ${errCode} - ${errMsg}` - ) + logger.error(`${method.toUpperCase()} for route ${route} returned unexpected error code ${errCode} - ${errMsg}`) } logger.error(`${method.toUpperCase()} failed for route ${route}: ${errCode} - ${errMsg}`) @@ -1355,9 +1329,7 @@ sofieAPIRequest<{ playlistId: string }, { adLibId: string; actionType?: string } ]), async (serverAPI, connection, event, params, body) => { const rundownPlaylistId = protectString(params.playlistId) - const adLibId = protectString( - body.adLibId - ) + const adLibId = protectString(body.adLibId) const actionTypeObj = body const triggerMode = actionTypeObj ? (actionTypeObj as { actionType: string }).actionType : undefined logger.info(`API POST: execute-adlib ${rundownPlaylistId} ${adLibId} - triggerMode: ${triggerMode}`) From d3dbb5115993c4c2e799cd7cfdd1015dbb17fc5a Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Fri, 7 Jun 2024 11:58:30 +0200 Subject: [PATCH 340/479] chore: lint --- meteor/server/api/rest/v1/index.ts | 81 ++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 26 deletions(-) diff --git a/meteor/server/api/rest/v1/index.ts b/meteor/server/api/rest/v1/index.ts index f6452b0b5b..64165a15f3 100644 --- a/meteor/server/api/rest/v1/index.ts +++ b/meteor/server/api/rest/v1/index.ts @@ -192,17 +192,19 @@ class ServerRestAPI implements RestAPI { }) const segmentAdLibPiece = AdLibPieces.findOneAsync(adLibId as PieceId, { projection: { _id: 1 } }) const bucketAdLibPiece = BucketAdLibs.findOneAsync(adLibId as BucketAdLibId, { projection: { _id: 1 } }) - const [baselineAdLibDoc, segmentAdLibDoc, bucketAdLibDoc, adLibAction, baselineAdLibAction] = await Promise.all([ - baselineAdLibPiece, - segmentAdLibPiece, - bucketAdLibPiece, - AdLibActions.findOneAsync(adLibId as AdLibActionId, { - projection: { _id: 1, actionId: 1, userData: 1 }, - }), - RundownBaselineAdLibActions.findOneAsync(adLibId as RundownBaselineAdLibActionId, { - projection: { _id: 1, actionId: 1, userData: 1 }, - }), - ]) + const [baselineAdLibDoc, segmentAdLibDoc, bucketAdLibDoc, adLibAction, baselineAdLibAction] = await Promise.all( + [ + baselineAdLibPiece, + segmentAdLibPiece, + bucketAdLibPiece, + AdLibActions.findOneAsync(adLibId as AdLibActionId, { + projection: { _id: 1, actionId: 1, userData: 1 }, + }), + RundownBaselineAdLibActions.findOneAsync(adLibId as RundownBaselineAdLibActionId, { + projection: { _id: 1, actionId: 1, userData: 1 }, + }), + ] + ) const adLibActionDoc = adLibAction ?? baselineAdLibAction const regularAdLibDoc = baselineAdLibDoc ?? segmentAdLibDoc ?? bucketAdLibDoc if (regularAdLibDoc) { @@ -213,7 +215,10 @@ class ServerRestAPI implements RestAPI { }) if (!rundownPlaylist) return ClientAPI.responseError( - UserError.from(new Error(`Rundown playlist does not exist`), UserErrorMessage.RundownPlaylistNotFound), + UserError.from( + new Error(`Rundown playlist does not exist`), + UserErrorMessage.RundownPlaylistNotFound + ), 404 ) if (rundownPlaylist.currentPartInfo === null) @@ -249,7 +254,10 @@ class ServerRestAPI implements RestAPI { if (!rundownPlaylist) return ClientAPI.responseError( - UserError.from(new Error(`Rundown playlist does not exist`), UserErrorMessage.RundownPlaylistNotFound), + UserError.from( + new Error(`Rundown playlist does not exist`), + UserErrorMessage.RundownPlaylistNotFound + ), 404 ) if (!rundownPlaylist.activationId) @@ -363,7 +371,10 @@ class ServerRestAPI implements RestAPI { }, false) return success ? {} - : UserError.from(new Error(`Failed to reload playlist ${rundownPlaylistId}`), UserErrorMessage.InternalError) + : UserError.from( + new Error(`Failed to reload playlist ${rundownPlaylistId}`), + UserErrorMessage.InternalError + ) } ) } @@ -498,7 +509,10 @@ class ServerRestAPI implements RestAPI { check(routeSetId, String) check(state, Boolean) - const access = await StudioContentWriteAccess.routeSet(ServerRestAPI.getCredentials(connection), studioId) + const access = await StudioContentWriteAccess.routeSet( + ServerRestAPI.getCredentials(connection), + studioId + ) return ServerPlayoutAPI.switchRouteSet(access, routeSetId, state) } ) @@ -587,7 +601,10 @@ class ServerRestAPI implements RestAPI { const device = await PeripheralDevices.findOneAsync(deviceId) if (!device) return ClientAPI.responseError( - UserError.from(new Error(`Device ${deviceId} does not exist`), UserErrorMessage.PeripheralDeviceNotFound), + UserError.from( + new Error(`Device ${deviceId} does not exist`), + UserErrorMessage.PeripheralDeviceNotFound + ), 404 ) return ClientAPI.responseSuccess(APIPeripheralDeviceFrom(device)) @@ -602,7 +619,10 @@ class ServerRestAPI implements RestAPI { const device = await PeripheralDevices.findOneAsync(deviceId) if (!device) return ClientAPI.responseError( - UserError.from(new Error(`Device ${deviceId} does not exist`), UserErrorMessage.PeripheralDeviceNotFound), + UserError.from( + new Error(`Device ${deviceId} does not exist`), + UserErrorMessage.PeripheralDeviceNotFound + ), 404 ) @@ -697,7 +717,10 @@ class ServerRestAPI implements RestAPI { if (device.studioId !== undefined && device.studioId !== studio._id) { return ClientAPI.responseError( - UserError.from(new Error(`Device already attached to studio`), UserErrorMessage.DeviceAlreadyAttachedToStudio), + UserError.from( + new Error(`Device already attached to studio`), + UserErrorMessage.DeviceAlreadyAttachedToStudio + ), 412 ) } @@ -776,9 +799,10 @@ class ServerRestAPI implements RestAPI { const existingShowStyle = await ShowStyleBases.findOneAsync(showStyleBaseId) if (existingShowStyle) { - const rundowns = (await Rundowns.findFetchAsync({ showStyleBaseId }, { projection: { playlistId: 1 } })) as Array< - Pick - > + const rundowns = (await Rundowns.findFetchAsync( + { showStyleBaseId }, + { projection: { playlistId: 1 } } + )) as Array> const playlists = (await RundownPlaylists.findFetchAsync( { _id: { $in: rundowns.map((r) => r.playlistId) } }, { @@ -817,9 +841,10 @@ class ServerRestAPI implements RestAPI { _event: string, showStyleBaseId: ShowStyleBaseId ): Promise> { - const rundowns = (await Rundowns.findFetchAsync({ showStyleBaseId }, { projection: { playlistId: 1 } })) as Array< - Pick - > + const rundowns = (await Rundowns.findFetchAsync( + { showStyleBaseId }, + { projection: { playlistId: 1 } } + )) as Array> const playlists = (await RundownPlaylists.findFetchAsync( { _id: { $in: rundowns.map((r) => r.playlistId) } }, { @@ -1250,7 +1275,9 @@ function sofieAPIRequest( } errMsg = translateMessage(msgConcat, interpollateTranslation) } else { - logger.error(`${method.toUpperCase()} for route ${route} returned unexpected error code ${errCode} - ${errMsg}`) + logger.error( + `${method.toUpperCase()} for route ${route} returned unexpected error code ${errCode} - ${errMsg}` + ) } logger.error(`${method.toUpperCase()} failed for route ${route}: ${errCode} - ${errMsg}`) @@ -1329,7 +1356,9 @@ sofieAPIRequest<{ playlistId: string }, { adLibId: string; actionType?: string } ]), async (serverAPI, connection, event, params, body) => { const rundownPlaylistId = protectString(params.playlistId) - const adLibId = protectString(body.adLibId) + const adLibId = protectString( + body.adLibId + ) const actionTypeObj = body const triggerMode = actionTypeObj ? (actionTypeObj as { actionType: string }).actionType : undefined logger.info(`API POST: execute-adlib ${rundownPlaylistId} ${adLibId} - triggerMode: ${triggerMode}`) From c5d8c7e7a78c59d316950dcac648fb312284a04f Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Fri, 7 Jun 2024 12:12:46 +0200 Subject: [PATCH 341/479] fix: simplify splits for DoS-safety --- meteor/server/api/rest/koa.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/meteor/server/api/rest/koa.ts b/meteor/server/api/rest/koa.ts index b55880a28a..fd980d33c8 100644 --- a/meteor/server/api/rest/koa.ts +++ b/meteor/server/api/rest/koa.ts @@ -50,18 +50,18 @@ function getClientAddrFromXForwarded(headerVal: undefined | string | string[]): if (typeof headerVal !== 'string') { headerVal = _.last(headerVal) as string } - const remoteAddresses = headerVal.split(/\s*,\s*/) - return remoteAddresses[remoteAddresses.length - REVERSE_PROXY_COUNT] ?? remoteAddresses[0] + const remoteAddresses = headerVal.split(',') + return remoteAddresses[remoteAddresses.length - REVERSE_PROXY_COUNT]?.trim() ?? remoteAddresses[0]?.trim() } function getClientAddrFromForwarded(forwardedVal: string | undefined): string | undefined { if (forwardedVal === undefined) return undefined - const allProxies = forwardedVal.split(/\s*,\s*/) + const allProxies = forwardedVal.split(',') const proxyInfo = allProxies[allProxies.length - REVERSE_PROXY_COUNT] ?? allProxies[0] - const directives = proxyInfo?.split(/\s*;\s*/) + const directives = proxyInfo?.trim().split(';') for (const directive of directives) { let match: RegExpMatchArray | null - if ((match = directive.match(/^for=("\[)?([\w.:])+(\]")?/))) { + if ((match = directive.trim().match(/^for=("\[)?([\w.:])+(\]")?/))) { return match[2] } } From 813ae80e2a0d21b7bee07a17365f5f90dfa9753e Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Fri, 7 Jun 2024 11:43:40 +0200 Subject: [PATCH 342/479] fix: REST API UserAction log entries are incorrect and in an inconveniant format. Also, Client address is set to the server host and not the actual user agent IP address. --- meteor/server/api/rest/koa.ts | 34 +++++++++++++++++++++++++++++- meteor/server/api/rest/v1/index.ts | 15 +++++++++---- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/meteor/server/api/rest/koa.ts b/meteor/server/api/rest/koa.ts index e1eddd0b52..51fb114e13 100644 --- a/meteor/server/api/rest/koa.ts +++ b/meteor/server/api/rest/koa.ts @@ -4,6 +4,7 @@ import KoaRouter from '@koa/router' import { WebApp } from 'meteor/webapp' import { Meteor } from 'meteor/meteor' import { getRandomString } from '@sofie-automation/corelib/dist/lib' +import _ from 'underscore' declare module 'http' { interface IncomingMessage { @@ -43,6 +44,31 @@ export function bindKoaRouter(koaRouter: KoaRouter, bindPath: string): void { app.use(koaRouter.routes()).use(koaRouter.allowedMethods()) } +const REVERSE_PROXY_COUNT = process.env.HTTP_FORWARDED_COUNT ? parseInt(process.env.HTTP_FORWARDED_COUNT) : 0 + +function getClientAddrFromXForwarded(headerVal: undefined | string | string[]): string | undefined { + if (headerVal === undefined) return undefined + if (typeof headerVal !== 'string') { + headerVal = _.last(headerVal) as string + } + const remoteAddresses = headerVal.split(/\s*,\s*/) + return remoteAddresses[remoteAddresses.length - REVERSE_PROXY_COUNT] ?? remoteAddresses[0] +} + +function getClientAddrFromForwarded(forwardedVal: string | undefined): string | undefined { + if (forwardedVal === undefined) return undefined + const allProxies = forwardedVal.split(/\s*,\s*/) + const proxyInfo = allProxies[allProxies.length - REVERSE_PROXY_COUNT] ?? allProxies[0] + const directives = proxyInfo?.split(/\s*;\s*/) + for (const directive of directives) { + let match: RegExpMatchArray | null + if ((match = directive.match(/^for=("\[)?([\w.:])+(\]")?/))) { + return match[2] + } + } + return undefined +} + export const makeMeteorConnectionFromKoa = ( ctx: Koa.ParameterizedContext< Koa.DefaultState, @@ -58,7 +84,13 @@ export const makeMeteorConnectionFromKoa = ( onClose: () => { /* no-op */ }, - clientAddress: ctx.req.headers.host || 'unknown', + clientAddress: + // This replicates Meteor behavior which uses the HTTP_FORWARDED_COUNT to extract the "world-facing" + // IP address of the Client User Agent + getClientAddrFromForwarded(ctx.req.headers.forwarded) || + getClientAddrFromXForwarded(ctx.req.headers['x-forwarded-for']) || + ctx.req.socket.remoteAddress || + 'unknown', httpHeaders: ctx.req.headers as Record, } } diff --git a/meteor/server/api/rest/v1/index.ts b/meteor/server/api/rest/v1/index.ts index 594bee5667..8bb845a264 100644 --- a/meteor/server/api/rest/v1/index.ts +++ b/meteor/server/api/rest/v1/index.ts @@ -27,7 +27,9 @@ function restAPIUserEvent( unknown > ): string { - return `rest_api_${ctx.method}_${ctx.URL.origin}/api/v1.0${ctx.URL.pathname}}` + // due to how the router is mounted, the ctx.URL.pathname does not contain the /api portion, + // but contains the API version portion + return `REST API: ${ctx.method} /api${ctx.URL.pathname} ${ctx.URL.origin}` } class APIContext implements ServerAPIContext { @@ -112,6 +114,13 @@ function sofieAPIRequest( ) { koaRouter[method](route, async (ctx, next) => { try { + logger.info(`REST APIv1: ${ctx.socket.remoteAddress} ${method} "${ctx.url}"`, { + url: ctx.url, + method, + remoteAddress: ctx.socket.remoteAddress, + remotePort: ctx.socket.remotePort, + headers: ctx.headers, + }) const context = new APIContext() const serverAPI = serverAPIFactory.createServerAPI(context) const response = await handler( @@ -136,9 +145,7 @@ function sofieAPIRequest( } errMsg = translateMessage(msgConcat, interpollateTranslation) } else { - logger.error( - `${method.toUpperCase()} for route ${route} returned unexpected error code ${errCode} - ${errMsg}` - ) + logger.error(`${method.toUpperCase()} for route ${route} returned unexpected error code ${errCode} - ${errMsg}`) } logger.error(`${method.toUpperCase()} failed for route ${route}: ${errCode} - ${errMsg}`) From 2ec6cfaadf3bf8776d02a78310f7c7ef487c3164 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Fri, 7 Jun 2024 11:51:20 +0200 Subject: [PATCH 343/479] chore: lint --- meteor/server/api/rest/v1/index.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/meteor/server/api/rest/v1/index.ts b/meteor/server/api/rest/v1/index.ts index 8bb845a264..a6be080176 100644 --- a/meteor/server/api/rest/v1/index.ts +++ b/meteor/server/api/rest/v1/index.ts @@ -27,9 +27,7 @@ function restAPIUserEvent( unknown > ): string { - // due to how the router is mounted, the ctx.URL.pathname does not contain the /api portion, - // but contains the API version portion - return `REST API: ${ctx.method} /api${ctx.URL.pathname} ${ctx.URL.origin}` + return `rest_api_${ctx.method}_${ctx.URL.origin}/api/v1.0${ctx.URL.pathname}}` } class APIContext implements ServerAPIContext { @@ -114,13 +112,6 @@ function sofieAPIRequest( ) { koaRouter[method](route, async (ctx, next) => { try { - logger.info(`REST APIv1: ${ctx.socket.remoteAddress} ${method} "${ctx.url}"`, { - url: ctx.url, - method, - remoteAddress: ctx.socket.remoteAddress, - remotePort: ctx.socket.remotePort, - headers: ctx.headers, - }) const context = new APIContext() const serverAPI = serverAPIFactory.createServerAPI(context) const response = await handler( From 8b4bb5d47ab3d0a945a322ec563bb704e57667dd Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Fri, 7 Jun 2024 12:12:46 +0200 Subject: [PATCH 344/479] fix: simplify splits for DoS-safety --- meteor/server/api/rest/koa.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/meteor/server/api/rest/koa.ts b/meteor/server/api/rest/koa.ts index 51fb114e13..87f8b0b660 100644 --- a/meteor/server/api/rest/koa.ts +++ b/meteor/server/api/rest/koa.ts @@ -51,18 +51,18 @@ function getClientAddrFromXForwarded(headerVal: undefined | string | string[]): if (typeof headerVal !== 'string') { headerVal = _.last(headerVal) as string } - const remoteAddresses = headerVal.split(/\s*,\s*/) - return remoteAddresses[remoteAddresses.length - REVERSE_PROXY_COUNT] ?? remoteAddresses[0] + const remoteAddresses = headerVal.split(',') + return remoteAddresses[remoteAddresses.length - REVERSE_PROXY_COUNT]?.trim() ?? remoteAddresses[0]?.trim() } function getClientAddrFromForwarded(forwardedVal: string | undefined): string | undefined { if (forwardedVal === undefined) return undefined - const allProxies = forwardedVal.split(/\s*,\s*/) + const allProxies = forwardedVal.split(',') const proxyInfo = allProxies[allProxies.length - REVERSE_PROXY_COUNT] ?? allProxies[0] - const directives = proxyInfo?.split(/\s*;\s*/) + const directives = proxyInfo?.trim().split(';') for (const directive of directives) { let match: RegExpMatchArray | null - if ((match = directive.match(/^for=("\[)?([\w.:])+(\]")?/))) { + if ((match = directive.trim().match(/^for=("\[)?([\w.:])+(\]")?/))) { return match[2] } } From cfc178a36f21f1ccc98c8bcd208a02ea04d13f3a Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Fri, 7 Jun 2024 15:23:43 +0200 Subject: [PATCH 345/479] fix: userEvent for REST API calls does not look sensible --- meteor/server/api/rest/v1/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meteor/server/api/rest/v1/index.ts b/meteor/server/api/rest/v1/index.ts index a6be080176..08d35d5b89 100644 --- a/meteor/server/api/rest/v1/index.ts +++ b/meteor/server/api/rest/v1/index.ts @@ -27,7 +27,7 @@ function restAPIUserEvent( unknown > ): string { - return `rest_api_${ctx.method}_${ctx.URL.origin}/api/v1.0${ctx.URL.pathname}}` + return `REST API: ${ctx.method} /api${ctx.URL.pathname} ${ctx.URL.origin}` } class APIContext implements ServerAPIContext { From 02de6b4390c4c57c86f906abba479bf1fc71adaa Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Mon, 10 Jun 2024 12:41:17 +0200 Subject: [PATCH 346/479] chore: fix log level --- packages/job-worker/src/workers/parent-base.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/job-worker/src/workers/parent-base.ts b/packages/job-worker/src/workers/parent-base.ts index b3444867a8..9695be095e 100644 --- a/packages/job-worker/src/workers/parent-base.ts +++ b/packages/job-worker/src/workers/parent-base.ts @@ -305,7 +305,7 @@ export abstract class WorkerParentBase { result.result ) - logger.debug(`Completed work ${job.id} in ${endTime - startTime}ms`) + logger.verbose(`Completed work ${job.id} in ${endTime - startTime}ms`) } catch (e: unknown) { let error: Error if (e instanceof Error) { From 0425d8f5a8b289922d146d0fe5241e08ade06935 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Mon, 10 Jun 2024 12:41:00 +0200 Subject: [PATCH 347/479] fix: introduce MeteorApply as alternative to Meteor.apply, to log when a method is sent late, due to other blocking methods on the client. --- meteor/lib/MeteorApply.ts | 150 ++++++++++++++++++++++++++++++++++++++ meteor/lib/lib.ts | 8 +- meteor/lib/logging.ts | 8 +- 3 files changed, 158 insertions(+), 8 deletions(-) create mode 100644 meteor/lib/MeteorApply.ts diff --git a/meteor/lib/MeteorApply.ts b/meteor/lib/MeteorApply.ts new file mode 100644 index 0000000000..607793399e --- /dev/null +++ b/meteor/lib/MeteorApply.ts @@ -0,0 +1,150 @@ +import { Meteor } from 'meteor/meteor' +import { logger } from './logging' + +/* + * MeteorApply is a wrapper around Meteor.apply(), and logs a warning if the method is sent late. + * + * Because Meteor methods are generally executed in order, it might be useful to know if a method is sent late. + * ref: https://guide.meteor.com/methods#methods-vs-rest + * + * This only works if all method-calls on the client are done through this function, + * as separate calls to Meteor.apply() or Meteor.call() will bypass the queueing, and cause the + * time-measurements and logged warnings to be incorrect or omitted. + */ + +/** + * Synonym for Meteor.apply. + * Logs a warning if the method is sent late + */ +export async function MeteorApply( + callName: Parameters[0], + args: Parameters[1], + options?: Parameters[2], + sendOptions?: SendOptions +): Promise { + if (!Meteor.isClient) throw new Error('MeteorApply should only be called client side!') + + return new Promise((resolve, reject) => { + const queuedMethod: QueuedMeteorMethod = { + queueTime: Date.now(), + running: false, + callName, + args, + options, + sendOptions, + reject, + resolve, + } + meteorMethodQueue.push(queuedMethod) + checkMethodQueue() + }) +} +const meteorMethodQueue: QueuedMeteorMethod[] = [] + +function checkMethodQueue() { + const nextMethod = meteorMethodQueue[0] + if (!nextMethod) return + if (!nextMethod.running) { + // Time to send the method + nextMethod.running = true + + const sendTime = Date.now() + const timeBetweenTriggerAndSend = sendTime - nextMethod.queueTime + if (timeBetweenTriggerAndSend > (nextMethod.sendOptions?.warnSendTime ?? 1000)) { + logWarning( + `Method "${ + nextMethod.callName + }" was sent ${timeBetweenTriggerAndSend}ms after it was triggered (at ${new Date().toISOString()})` + ) + } + + Meteor.apply(nextMethod.callName, nextMethod.args, nextMethod.options, (err, res) => { + meteorMethodQueue.shift() + setTimeout(() => { + checkMethodQueue() + }, 0) + + const completeTime = Date.now() + const timeBetweenSendAndComplete = completeTime - sendTime + if (timeBetweenSendAndComplete > (nextMethod.sendOptions?.warnCompleteTime ?? 1000)) { + logWarning( + `Method "${ + nextMethod.callName + }" was completed ${timeBetweenSendAndComplete}ms after it was sent (at ${new Date().toISOString()})` + ) + } + + if (err) { + nextMethod.reject(err) + } else { + nextMethod.resolve(res) + } + }) + } +} + +let loggedWarningCount = 0 +let tooManyWarnings = false +let skippedWarningCount = 0 +const MAX_LOG_COUNT = 10 +const SKIP_WARNING_COOL_DOWN = 60 * 5 // seconds +function logWarning(message: string) { + if (!tooManyWarnings) { + loggedWarningCount++ + + if (loggedWarningCount > MAX_LOG_COUNT) { + // To avoid a flood of warnings (which, when sent to server via a method call, can cause slowness itself), + // we will only log the first {MAX_LOG_COUNT} warnings, and then ignore further warnings for a while. + + logger.warn( + `Has logged too many warnings (${loggedWarningCount}) about late Meteor methods. Will ignore further warnings for ${SKIP_WARNING_COOL_DOWN} seconds.` + ) + tooManyWarnings = true + setTimeout(() => { + if (skippedWarningCount > 0) { + logger.warn( + `Will start logging warnings about late Meteor methods again. (Ignored ${skippedWarningCount} warnings.)` + ) + } + tooManyWarnings = false + skippedWarningCount = 0 + loggedWarningCount = 0 + }, SKIP_WARNING_COOL_DOWN * 1000) + } else { + logger.warn(message) + } + } else { + // Ignore the warning + skippedWarningCount++ + } +} +// Clear the warning count every hour, just to avoid +setInterval(() => { + if (!tooManyWarnings) { + loggedWarningCount = 0 + } +}, 3600 * 1000) + +interface QueuedMeteorMethod { + callName: Parameters[0] + args: Parameters[1] + options?: Parameters[2] + + reject: (reason: any) => void + resolve: (value: unknown) => void + + sendOptions?: SendOptions + + queueTime: number + running: boolean +} +export interface SendOptions { + /** Log a warning if the method was sent later than this time. Defaults to 1000. [milliseconds] */ + warnSendTime?: number + /** Log a warning if the method was completed later than this time. Defaults to 1000. [milliseconds] */ + warnCompleteTime?: number +} +if (Meteor.isClient) { + // @ts-expect-error hack for dev + window.MeteorApply = MeteorApply +} diff --git a/meteor/lib/lib.ts b/meteor/lib/lib.ts index 44cda8e927..7e2dd6e266 100644 --- a/meteor/lib/lib.ts +++ b/meteor/lib/lib.ts @@ -9,6 +9,7 @@ import { MongoQuery as CoreLibMongoQuery } from '@sofie-automation/corelib/dist/ import { Time, TimeDuration } from '@sofie-automation/shared-lib/dist/lib/lib' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { ReactiveVar } from 'meteor/reactive-var' +import { MeteorApply } from './MeteorApply' export { Time, TimeDuration } // Legacy compatability @@ -34,12 +35,7 @@ export async function MeteorPromiseApply( args: Parameters[1], options?: Parameters[2] ): Promise { - return new Promise((resolve, reject) => { - Meteor.apply(callName, args, options, (err, res) => { - if (err) reject(err) - else resolve(res) - }) - }) + return MeteorApply(callName, args, options) } // The diff is currently only used client-side diff --git a/meteor/lib/logging.ts b/meteor/lib/logging.ts index a35e9cbad0..1721dfaf45 100644 --- a/meteor/lib/logging.ts +++ b/meteor/lib/logging.ts @@ -1,5 +1,6 @@ import { Meteor } from 'meteor/meteor' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' +import { MeteorApply } from './MeteorApply' export const LOGGER_METHOD_NAME = 'logger' @@ -35,7 +36,9 @@ if (Meteor.isServer) { const stringifiedArgs: string[] = args.map((arg) => { return stringifyError(arg) }) - return Meteor.call(LOGGER_METHOD_NAME, type, ...stringifiedArgs) + + Meteor.call(LOGGER_METHOD_NAME, type, ...stringifiedArgs) + return logger } } @@ -67,7 +70,8 @@ if (Meteor.isServer) { const stringifiedArgs: string[] = args.map((arg) => { return stringifyError(arg) }) - Meteor.call(LOGGER_METHOD_NAME, type, `Client ${type}`, ...stringifiedArgs) + MeteorApply(LOGGER_METHOD_NAME, [type, `Client ${type}`, ...stringifiedArgs]).catch(console.error) + return logger } return logger } From fcd733166ec3ca0f8ffca31e4507b3f0ccbebfc2 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Mon, 10 Jun 2024 12:52:57 +0200 Subject: [PATCH 348/479] chore: only call MeteorApply client side --- meteor/lib/lib.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/meteor/lib/lib.ts b/meteor/lib/lib.ts index 7e2dd6e266..4c48ddfd77 100644 --- a/meteor/lib/lib.ts +++ b/meteor/lib/lib.ts @@ -35,7 +35,16 @@ export async function MeteorPromiseApply( args: Parameters[1], options?: Parameters[2] ): Promise { - return MeteorApply(callName, args, options) + if (Meteor.isServer) { + return new Promise((resolve, reject) => { + Meteor.apply(callName, args, options, (err, res) => { + if (err) reject(err) + else resolve(res) + }) + }) + } else { + return MeteorApply(callName, args, options) + } } // The diff is currently only used client-side From 50e8cd26b8da3394eaa9f5fd9909d3f5d8dfe73c Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 11 Jun 2024 14:01:28 +0200 Subject: [PATCH 349/479] fix: invalidate deviceTriggers when changing triggermode SOFIE-3253 --- meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts b/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts index 92a78df145..fc1b6ac0c2 100644 --- a/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts +++ b/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts @@ -87,7 +87,7 @@ export class StudioDeviceTriggerManager { // Since the compiled action is cached using this actionId as a key, having the action // and the filterChain allows for a quicker invalidation without doing a deepEquals const actionId = protectString( - `${studioId}_${triggeredAction._id}_${key}_${action.action}_${JSON.stringify(action.filterChain)}` + `${studioId}_${triggeredAction._id}_${key}_${JSON.stringify(action)}` ) const existingAction = actionManager.getAction(actionId) let thisAction: ExecutableAction From 188c9855ce4398076960daa8a14f0f4009b145eb Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Tue, 11 Jun 2024 14:39:44 +0200 Subject: [PATCH 350/479] fix: filter out virtual pieces in isPieceInstanceActive --- .../src/collections/pieceInstancesHandler.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts index f68fe377b5..79d1a399d9 100644 --- a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts +++ b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts @@ -187,9 +187,11 @@ export class PieceInstancesHandler private isPieceInstanceActive(pieceInstance: PieceInstance) { return ( pieceInstance.reportedStoppedPlayback == null && + pieceInstance.piece.virtual !== true && (pieceInstance.partInstanceId === this._currentPlaylist?.previousPartInfo?.partInstanceId || // a piece from previous part instance may be active during transition pieceInstance.partInstanceId === this._currentPlaylist?.currentPartInfo?.partInstanceId) && (pieceInstance.reportedStartedPlayback != null || // has been reported to have started by the Playout Gateway + pieceInstance.plannedStartedPlayback != null || // a time to stop playing has been set by Core (pieceInstance.partInstanceId === this._currentPlaylist?.currentPartInfo?.partInstanceId && pieceInstance.piece.enable.start === 0) || // this is to speed things up immediately after a part instance is taken when not yet reported by the Playout Gateway pieceInstance.infinite?.fromPreviousPart) // infinites from previous part also are on air from the start of the current part From 312820d2332f5aac4acacfe53f16a7ae4abed500 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Tue, 11 Jun 2024 16:34:22 +0200 Subject: [PATCH 351/479] chore: comment in pieceInstancesHandler is wrong --- .../src/collections/pieceInstancesHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts index 79d1a399d9..c1739c8a6b 100644 --- a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts +++ b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts @@ -191,7 +191,7 @@ export class PieceInstancesHandler (pieceInstance.partInstanceId === this._currentPlaylist?.previousPartInfo?.partInstanceId || // a piece from previous part instance may be active during transition pieceInstance.partInstanceId === this._currentPlaylist?.currentPartInfo?.partInstanceId) && (pieceInstance.reportedStartedPlayback != null || // has been reported to have started by the Playout Gateway - pieceInstance.plannedStartedPlayback != null || // a time to stop playing has been set by Core + pieceInstance.plannedStartedPlayback != null || // a time to start playing has been set by Core (pieceInstance.partInstanceId === this._currentPlaylist?.currentPartInfo?.partInstanceId && pieceInstance.piece.enable.start === 0) || // this is to speed things up immediately after a part instance is taken when not yet reported by the Playout Gateway pieceInstance.infinite?.fromPreviousPart) // infinites from previous part also are on air from the start of the current part From 416b7915884b08ef705599ebcd992235a059bd19 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Tue, 11 Jun 2024 18:04:27 +0200 Subject: [PATCH 352/479] chore: add comments explaining the syntax of the headers --- meteor/server/api/rest/koa.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/meteor/server/api/rest/koa.ts b/meteor/server/api/rest/koa.ts index 87f8b0b660..0e399f37dd 100644 --- a/meteor/server/api/rest/koa.ts +++ b/meteor/server/api/rest/koa.ts @@ -46,6 +46,9 @@ export function bindKoaRouter(koaRouter: KoaRouter, bindPath: string): void { const REVERSE_PROXY_COUNT = process.env.HTTP_FORWARDED_COUNT ? parseInt(process.env.HTTP_FORWARDED_COUNT) : 0 +// X-Forwarded-For (a de-facto standard) has the following syntax by convention +// X-Forwarded-For: 203.0.113.195, 2001:db8:85a3:8d3:1319:8a2e:370:7348 +// X-Forwarded-For: 203.0.113.195,2001:db8:85a3:8d3:1319:8a2e:370:7348,198.51.100.178 function getClientAddrFromXForwarded(headerVal: undefined | string | string[]): string | undefined { if (headerVal === undefined) return undefined if (typeof headerVal !== 'string') { @@ -55,6 +58,9 @@ function getClientAddrFromXForwarded(headerVal: undefined | string | string[]): return remoteAddresses[remoteAddresses.length - REVERSE_PROXY_COUNT]?.trim() ?? remoteAddresses[0]?.trim() } +// Forwarded uses the following syntax: +// Forwarded: for=192.0.2.60;proto=http;by=203.0.113.43 +// Forwarded: for=192.0.2.43, for="[2001:db8:cafe::17]" function getClientAddrFromForwarded(forwardedVal: string | undefined): string | undefined { if (forwardedVal === undefined) return undefined const allProxies = forwardedVal.split(',') From f3ae3e66a2ff0ce77bb14433be29e816f971433f Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Tue, 11 Jun 2024 18:32:21 +0200 Subject: [PATCH 353/479] chore: lint --- meteor/server/api/rest/v1/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/meteor/server/api/rest/v1/index.ts b/meteor/server/api/rest/v1/index.ts index 08d35d5b89..559f5c4f73 100644 --- a/meteor/server/api/rest/v1/index.ts +++ b/meteor/server/api/rest/v1/index.ts @@ -136,7 +136,9 @@ function sofieAPIRequest( } errMsg = translateMessage(msgConcat, interpollateTranslation) } else { - logger.error(`${method.toUpperCase()} for route ${route} returned unexpected error code ${errCode} - ${errMsg}`) + logger.error( + `${method.toUpperCase()} for route ${route} returned unexpected error code ${errCode} - ${errMsg}` + ) } logger.error(`${method.toUpperCase()} failed for route ${route}: ${errCode} - ${errMsg}`) From bfcf08e2fe2629b9c25a6c8a1fce256858d86e00 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 12 Jun 2024 10:50:57 +0200 Subject: [PATCH 354/479] feat: break out of maintain focus on wheel event --- meteor/client/lib/viewPort.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/meteor/client/lib/viewPort.ts b/meteor/client/lib/viewPort.ts index 1bcfc3b9f0..0cf58ebe12 100644 --- a/meteor/client/lib/viewPort.ts +++ b/meteor/client/lib/viewPort.ts @@ -33,6 +33,11 @@ export function maintainFocusOnPartInstance( quitFocusOnPart() } } + document.addEventListener('wheel', onWheelWhenMaintainingFocus, { + once: true, + capture: true, + passive: true, + }) focusInterval = setInterval(focus, 500) focus() } @@ -41,7 +46,14 @@ export function isMaintainingFocus(): boolean { return !!focusInterval } +function onWheelWhenMaintainingFocus() { + quitFocusOnPart() +} + function quitFocusOnPart() { + document.removeEventListener('wheel', onWheelWhenMaintainingFocus, { + capture: true, + }) if (!_dontClearInterval && focusInterval) { clearInterval(focusInterval) focusInterval = undefined From 68172c565af3aaea7a2517abc9623b2eefd77fba Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 12 Jun 2024 15:33:28 +0200 Subject: [PATCH 355/479] fix: allways show trigger popups --- meteor/client/lib/popperUtils.ts | 4 +-- .../TriggeredActionsEditor.scss | 26 +++++++++++++++++++ .../actionSelector/ActionSelector.tsx | 5 +++- .../filterPreviews/FilterEditor.tsx | 7 +++-- .../triggerEditors/TriggerEditor.tsx | 5 +++- 5 files changed, 40 insertions(+), 7 deletions(-) diff --git a/meteor/client/lib/popperUtils.ts b/meteor/client/lib/popperUtils.ts index 302c5b111a..c2aa6c33f2 100644 --- a/meteor/client/lib/popperUtils.ts +++ b/meteor/client/lib/popperUtils.ts @@ -8,9 +8,7 @@ export const sameWidth = { fn: ({ state }: { state: any }): void => { const targetWidth = Math.max(208, state.rects.reference.width + 10) state.styles.popper.width = `${targetWidth}px` - state.styles.popper.transform = `translate(${ - state.rects.reference.x - 10 + (state.rects.reference.width + 10 - targetWidth) / 2 - }px, ${Math.ceil(state.modifiersData.popperOffsets.y)}px)` + state.styles.popper.transform = state.styles.popper.transform + ' translate(4px, 0)' }, effect: ({ state }: { state: any }): void => { state.elements.popper.style.width = `${state.elements.reference.offsetWidth + 10}px` diff --git a/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.scss b/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.scss index 50b5549e94..cee4ed9e01 100644 --- a/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.scss +++ b/meteor/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.scss @@ -118,6 +118,14 @@ $triggered-action-color: #146985; display: block; min-width: 228px; + padding-top: 40px !important; + padding-bottom: 10px !important; + + &[data-popper-placement="top"] { + padding-bottom: 40px !important; + padding-top: 10px !important; + } + .triggered-action-entry__trigger-editor__triggers-preview { margin: 1em 0 0; padding: 0; @@ -334,6 +342,15 @@ $triggered-action-color: #146985; > .triggered-action-entry__action-editor { box-sizing: content-box; min-width: 208px; + + padding-top: 40px !important; + padding-bottom: 10px !important; + + &[data-popper-placement="top"] { + padding-bottom: 40px !important; + padding-top: 10px !important; + } + > * { box-sizing: border-box; } @@ -342,6 +359,15 @@ $triggered-action-color: #146985; > .triggered-action-entry__action__filter-editor { box-sizing: content-box; min-width: 208px; + + padding-top: 40px !important; + padding-bottom: 10px !important; + + &[data-popper-placement="top"] { + padding-bottom: 40px !important; + padding-top: 10px !important; + } + > * { box-sizing: border-box; } diff --git a/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx b/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx index 2d5f121e50..3a45be4756 100644 --- a/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx +++ b/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx @@ -12,6 +12,7 @@ import { faAngleRight, faTrash } from '@fortawesome/free-solid-svg-icons' import { AdLibActionEditor } from './actionEditors/AdLibActionEditor' import { DeviceActions } from '@sofie-automation/shared-lib/dist/core/model/ShowStyle' import { catchError } from '../../../../../../lib/lib' +import { preventOverflow } from '@popperjs/core' interface IProps { action: SomeAction @@ -500,7 +501,9 @@ export const ActionSelector = function ActionSelector({ const [referenceElement, setReferenceElement] = useState(null) const [popperElement, setPopperElement] = useState(null) const { styles, attributes, update } = usePopper(referenceElement, popperElement, { + placement: 'bottom', modifiers: [ + preventOverflow, { name: 'offset', options: { @@ -558,7 +561,7 @@ export const ActionSelector = function ActionSelector({ {opened ? (
= function FilterEditor(props: IProp const [referenceElement, setReferenceElement] = useState(null) const [popperElement, setPopperElement] = useState(null) const { styles, attributes, update } = usePopper(referenceElement, popperElement, { + placement: 'bottom', modifiers: [ + preventOverflow, { name: 'offset', options: { @@ -50,7 +53,7 @@ export const FilterEditor: React.FC = function FilterEditor(props: IProp if ( popperElement && referenceElement && - !composedPath.includes(popperElement) && + !composedPath.find((item) => item instanceof HTMLElement && item.className === popperElement.className) && !composedPath.includes(referenceElement) ) { onClose(index) @@ -88,7 +91,7 @@ export const FilterEditor: React.FC = function FilterEditor(props: IProp {opened ? (
(null) const [popperElement, setPopperElement] = useState(null) const { styles, attributes, update } = usePopper(referenceElement, popperElement, { + placement: 'bottom', modifiers: [ + preventOverflow, { name: 'offset', options: { @@ -168,7 +171,7 @@ export const TriggerEditor = function TriggerEditor({ {triggerPreview} {opened ? (
Date: Thu, 13 Jun 2024 10:49:35 +0200 Subject: [PATCH 356/479] fix(DashboardPieceButton): hover previews are not positioned correctly --- meteor/client/ui/Shelf/DashboardPieceButton.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/meteor/client/ui/Shelf/DashboardPieceButton.tsx b/meteor/client/ui/Shelf/DashboardPieceButton.tsx index 4cd8afe7da..aea46688fb 100644 --- a/meteor/client/ui/Shelf/DashboardPieceButton.tsx +++ b/meteor/client/ui/Shelf/DashboardPieceButton.tsx @@ -68,6 +68,7 @@ export class DashboardPieceButtonBase extends MeteorReactComponent< IState > { private element: HTMLDivElement | null = null + /** positionAndSize needs to be in document coordinate space */ private positionAndSize: { top: number left: number @@ -206,8 +207,8 @@ export class DashboardPieceButtonBase extends MeteorReactComponent< if (this.element) { const { top, left, width, height } = this.element.getBoundingClientRect() this.positionAndSize = { - top, - left, + top: window.scrollY + top, + left: window.scrollX + left, width, height, } @@ -224,8 +225,8 @@ export class DashboardPieceButtonBase extends MeteorReactComponent< if (this.element) { const { top, left, width, height } = this.element.getBoundingClientRect() this.positionAndSize = { - top, - left, + top: window.scrollY + top, + left: window.scrollX + left, width, height, } From e5a022a7f2b8b0e9abeb792d48bab741beeac567 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Thu, 13 Jun 2024 10:49:35 +0200 Subject: [PATCH 357/479] fix(DashboardPieceButton): hover previews are not positioned correctly (cherry picked from commit 8f8d147a42b3e80f87f9d9f575f81a048521d989) --- meteor/client/ui/Shelf/DashboardPieceButton.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/meteor/client/ui/Shelf/DashboardPieceButton.tsx b/meteor/client/ui/Shelf/DashboardPieceButton.tsx index fcf8776159..7b085f6b99 100644 --- a/meteor/client/ui/Shelf/DashboardPieceButton.tsx +++ b/meteor/client/ui/Shelf/DashboardPieceButton.tsx @@ -67,6 +67,7 @@ export class DashboardPieceButtonBase extends React.Component< IState > { private element: HTMLDivElement | null = null + /** positionAndSize needs to be in document coordinate space */ private positionAndSize: { top: number left: number @@ -204,8 +205,8 @@ export class DashboardPieceButtonBase extends React.Component< if (this.element) { const { top, left, width, height } = this.element.getBoundingClientRect() this.positionAndSize = { - top, - left, + top: window.scrollY + top, + left: window.scrollX + left, width, height, } @@ -222,8 +223,8 @@ export class DashboardPieceButtonBase extends React.Component< if (this.element) { const { top, left, width, height } = this.element.getBoundingClientRect() this.positionAndSize = { - top, - left, + top: window.scrollY + top, + left: window.scrollX + left, width, height, } From 3d4a2ff4276e674c5c9c2b26b8b6c7287ae730a9 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 19 Jun 2024 12:25:16 +0200 Subject: [PATCH 358/479] chore(docs): add info about OAuth in HTTP integration --- packages/documentation/docs/user-guide/supported-devices.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/documentation/docs/user-guide/supported-devices.md b/packages/documentation/docs/user-guide/supported-devices.md index b5a2763851..be8f91a8bd 100644 --- a/packages/documentation/docs/user-guide/supported-devices.md +++ b/packages/documentation/docs/user-guide/supported-devices.md @@ -18,7 +18,7 @@ We support almost all features of these devices except fairlight audio, camera c ## CasparCG Server -Tested and developed against [a fork of version 2.1](https://github.com/nrkno/sofie-casparcg-server) with more support for version 2.3 being added in the future. +Tested and developed against [a fork of version 2.4](https://github.com/nrkno/sofie-casparcg-server) * Video playback * Graphics playback @@ -29,6 +29,8 @@ Tested and developed against [a fork of version 2.1](https://github.com/nrkno/so ## HTTP Protocol * GET/POST/PUT/DELETE methods +* Pre-shared "Bearer" token authorization +* OAuth 2.0 Client Credentials flow * Interval based watcher for status monitoring ## Blackmagic Design HyperDeck @@ -75,8 +77,6 @@ _Note: some features are controlled through the Package Manager_ * Control nodes -_Note: this is not currently used in production by anyone we know of_ - ## Sisyfos * On-air controls From 58261e54ffe87e448b0de80b007ddf7a06dd924b Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Wed, 19 Jun 2024 13:46:40 +0200 Subject: [PATCH 359/479] fix: live-status-gateway ignored settings from Core, so logLevel ended up always being the default --- .../live-status-gateway/src/coreHandler.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/live-status-gateway/src/coreHandler.ts b/packages/live-status-gateway/src/coreHandler.ts index 3c5130f7b5..2843fa1014 100644 --- a/packages/live-status-gateway/src/coreHandler.ts +++ b/packages/live-status-gateway/src/coreHandler.ts @@ -137,8 +137,9 @@ export class CoreHandler { } // setup observers const observer = this.core.observe(PeripheralDevicePubSubCollectionsNames.peripheralDeviceForDevice) - observer.added = (id) => this.onDeviceChanged(id) - observer.changed = (id) => this.onDeviceChanged(id) + observer.added = () => this.onDeviceChanged() + observer.changed = () => this.onDeviceChanged() + this.onDeviceChanged() // set initial settings this.setupObserverForPeripheralDeviceCommands(this) } async destroy(): Promise { @@ -178,15 +179,18 @@ export class CoreHandler { this._onConnected = fcn } - onDeviceChanged(id: PeripheralDeviceId): void { - if (id !== this.core.deviceId) return + onDeviceChanged(): void { const col = this.core.getCollection( PeripheralDevicePubSubCollectionsNames.peripheralDeviceForDevice ) if (!col) throw new Error('collection "peripheralDeviceForDevice" not found!') - const device = col.findOne(id) - this.deviceSettings = device?.deviceSettings || {} + const device = col.findOne(this.core.deviceId) + if (!device) { + throw new Error(`No "peripheralDeviceForDevice" with id "${this.core.deviceId}" found!`) + } + + this.deviceSettings = device.deviceSettings || {} const logLevel = this.deviceSettings['debugLogging'] ? 'debug' : 'info' if (logLevel !== this.logger.level) { this.logger.level = logLevel @@ -198,7 +202,7 @@ export class CoreHandler { this.logger.info('Loglevel: ' + this.logger.level) } - const studioId = device?.studioId + const studioId = device.studioId if (studioId === undefined) { throw new Error(`Live status gateway must be attached to a studio`) } From 8682eca8004aedb092a1dca7f7c856c0814eaf66 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 19 Jun 2024 13:52:41 +0100 Subject: [PATCH 360/479] fix: live status gateway including pieces which have been pruned and will never be played SOFIE-3301 (#1206) --- .../src/collections/partInstancesHandler.ts | 10 +++ .../src/collections/pieceInstancesHandler.ts | 71 +++++++++++++++++-- .../src/collections/showStyleBaseHandler.ts | 4 +- .../src/liveStatusServer.ts | 2 + .../topics/__tests__/activePlaylist.spec.ts | 1 + 5 files changed, 81 insertions(+), 7 deletions(-) diff --git a/packages/live-status-gateway/src/collections/partInstancesHandler.ts b/packages/live-status-gateway/src/collections/partInstancesHandler.ts index 79ae9464f6..d2b9e7e5f2 100644 --- a/packages/live-status-gateway/src/collections/partInstancesHandler.ts +++ b/packages/live-status-gateway/src/collections/partInstancesHandler.ts @@ -11,6 +11,7 @@ import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { PartInstanceId, RundownId, RundownPlaylistActivationId } from '@sofie-automation/corelib/dist/dataModel/Ids' export interface SelectedPartInstances { + previous: DBPartInstance | undefined current: DBPartInstance | undefined next: DBPartInstance | undefined firstInSegmentPlayout: DBPartInstance | undefined @@ -36,6 +37,7 @@ export class PartInstancesHandler super(PartInstancesHandler.name, CollectionName.PartInstances, CorelibPubSub.partInstances, logger, coreHandler) this.observerName = this._name this._collectionData = { + previous: undefined, current: undefined, next: undefined, firstInSegmentPlayout: undefined, @@ -54,6 +56,9 @@ export class PartInstancesHandler if (!this._collectionName || !this._collectionData) return false const collection = this._core.getCollection(this._collectionName) if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) + const previousPartInstance = this._currentPlaylist?.previousPartInfo?.partInstanceId + ? collection.findOne(this._currentPlaylist.previousPartInfo.partInstanceId) + : undefined const currentPartInstance = this._currentPlaylist?.currentPartInfo?.partInstanceId ? collection.findOne(this._currentPlaylist.currentPartInfo.partInstanceId) : undefined @@ -70,6 +75,10 @@ export class PartInstancesHandler ) as DBPartInstance let hasAnythingChanged = false + if (previousPartInstance !== this._collectionData.previous) { + this._collectionData.previous = previousPartInstance + hasAnythingChanged = true + } if (currentPartInstance !== this._collectionData.current) { this._collectionData.current = currentPartInstance hasAnythingChanged = true @@ -91,6 +100,7 @@ export class PartInstancesHandler private clearCollectionData() { if (!this._collectionName || !this._collectionData) return + this._collectionData.previous = undefined this._collectionData.current = undefined this._collectionData.next = undefined this._collectionData.firstInSegmentPlayout = undefined diff --git a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts index c1739c8a6b..1cb7a69af5 100644 --- a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts +++ b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts @@ -10,6 +10,12 @@ import throttleToNextTick from '@sofie-automation/shared-lib/dist/lib/throttleTo import _ = require('underscore') import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { PartInstanceId, PieceInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { processAndPrunePieceInstanceTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' +import { ShowStyleBaseExt, ShowStyleBaseHandler } from './showStyleBaseHandler' +import { PlaylistHandler } from './playlistHandler' +import { SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' +import { PartInstancesHandler, SelectedPartInstances } from './partInstancesHandler' +import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' export type PieceInstanceMin = Omit @@ -20,7 +26,7 @@ export interface SelectedPieceInstances { // Pieces present in the current part instance currentPartInstance: PieceInstanceMin[] - // Pieces present in the current part instance + // Pieces present in the next part instance nextPartInstance: PieceInstanceMin[] } @@ -33,6 +39,8 @@ export class PieceInstancesHandler private _partInstanceIds: PartInstanceId[] = [] private _activationId: string | undefined private _subscriptionPending = false + private _sourceLayers: SourceLayers = {} + private _partInstances: SelectedPartInstances | undefined private _throttledUpdateAndNotify = throttleToNextTick(() => { this.updateCollectionData() @@ -61,20 +69,45 @@ export class PieceInstancesHandler this._throttledUpdateAndNotify() } + private processAndPrunePieceInstanceTimings( + partInstance: DBPartInstance | undefined, + pieceInstances: PieceInstance[] + ): PieceInstance[] { + // Approximate when 'now' is in the PartInstance, so that any adlibbed Pieces will be timed roughly correctly + const partStarted = partInstance?.timings?.plannedStartedPlayback + const nowInPart = partStarted === undefined ? 0 : Date.now() - partStarted + + return processAndPrunePieceInstanceTimings(this._sourceLayers, pieceInstances, nowInPart, false, false) + } + private updateCollectionData(): boolean { if (!this._collectionName || !this._collectionData) return false const collection = this._core.getCollection(this._collectionName) if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) - const active = this._currentPlaylist?.currentPartInfo?.partInstanceId - ? collection.find((pieceInstance: PieceInstance) => this.isPieceInstanceActive(pieceInstance)) + + const inPreviousPartInstance = this._currentPlaylist?.previousPartInfo?.partInstanceId + ? this.processAndPrunePieceInstanceTimings( + this._partInstances?.previous, + collection.find({ partInstanceId: this._currentPlaylist.previousPartInfo.partInstanceId }) + ) : [] const inCurrentPartInstance = this._currentPlaylist?.currentPartInfo?.partInstanceId - ? collection.find({ partInstanceId: this._currentPlaylist.currentPartInfo.partInstanceId }) + ? this.processAndPrunePieceInstanceTimings( + this._partInstances?.current, + collection.find({ partInstanceId: this._currentPlaylist.currentPartInfo.partInstanceId }) + ) : [] const inNextPartInstance = this._currentPlaylist?.nextPartInfo?.partInstanceId - ? collection.find({ partInstanceId: this._currentPlaylist.nextPartInfo.partInstanceId }) + ? this.processAndPrunePieceInstanceTimings( + undefined, + collection.find({ partInstanceId: this._currentPlaylist.nextPartInfo.partInstanceId }) + ) : [] + const active = [...inPreviousPartInstance, ...inCurrentPartInstance].filter((pieceInstance) => + this.isPieceInstanceActive(pieceInstance) + ) + let hasAnythingChanged = false if (!areElementsShallowEqual(this._collectionData.active, active)) { this._collectionData.active = active @@ -107,7 +140,32 @@ export class PieceInstancesHandler this._collectionData.nextPartInstance = [] } - async update(source: string, data: DBRundownPlaylist | undefined): Promise { + async update( + source: string, + data: DBRundownPlaylist | ShowStyleBaseExt | SelectedPartInstances | undefined + ): Promise { + switch (source) { + case PlaylistHandler.name: + return this.updateRundownPlaylist(source, data as DBRundownPlaylist | undefined) + case ShowStyleBaseHandler.name: { + this.logUpdateReceived('showStyleBase', source) + const showStyleBaseExt = data as ShowStyleBaseExt | undefined + this._sourceLayers = showStyleBaseExt?.sourceLayers ?? {} + this._throttledUpdateAndNotify() + break + } + case PartInstancesHandler.name: { + this.logUpdateReceived('partInstances', source) + this._partInstances = data as SelectedPartInstances + this._throttledUpdateAndNotify() + break + } + default: + throw new Error(`${this._name} received unsupported update from ${source}}`) + } + } + + private async updateRundownPlaylist(source: string, data: DBRundownPlaylist | undefined): Promise { const prevPartInstanceIds = this._partInstanceIds const prevActivationId = this._activationId @@ -188,6 +246,7 @@ export class PieceInstancesHandler return ( pieceInstance.reportedStoppedPlayback == null && pieceInstance.piece.virtual !== true && + pieceInstance.disabled !== true && (pieceInstance.partInstanceId === this._currentPlaylist?.previousPartInfo?.partInstanceId || // a piece from previous part instance may be active during transition pieceInstance.partInstanceId === this._currentPlaylist?.currentPartInfo?.partInstanceId) && (pieceInstance.reportedStartedPlayback != null || // has been reported to have started by the Playout Gateway diff --git a/packages/live-status-gateway/src/collections/showStyleBaseHandler.ts b/packages/live-status-gateway/src/collections/showStyleBaseHandler.ts index 8a01c78273..e59d39d4c3 100644 --- a/packages/live-status-gateway/src/collections/showStyleBaseHandler.ts +++ b/packages/live-status-gateway/src/collections/showStyleBaseHandler.ts @@ -12,6 +12,7 @@ import { IOutputLayer, ISourceLayer } from '@sofie-automation/blueprints-integra export interface ShowStyleBaseExt extends DBShowStyleBase { sourceLayerNamesById: ReadonlyMap outputLayerNamesById: ReadonlyMap + sourceLayers: SourceLayers } export class ShowStyleBaseHandler @@ -116,10 +117,11 @@ export class ShowStyleBaseHandler if (outputLayer === undefined || outputLayer === null) continue this._outputLayersMap.set(layerId, outputLayer.name) } - const showStyleBaseExt = { + const showStyleBaseExt: ShowStyleBaseExt = { ...showStyleBase, sourceLayerNamesById: this._sourceLayersMap, outputLayerNamesById: this._outputLayersMap, + sourceLayers, } this._collectionData = showStyleBaseExt } diff --git a/packages/live-status-gateway/src/liveStatusServer.ts b/packages/live-status-gateway/src/liveStatusServer.ts index 5cdda17720..70590e22b0 100644 --- a/packages/live-status-gateway/src/liveStatusServer.ts +++ b/packages/live-status-gateway/src/liveStatusServer.ts @@ -96,6 +96,8 @@ export class LiveStatusServer { await partInstancesHandler.subscribe(globalAdLibActionsHandler) await partInstancesHandler.subscribe(adLibsHandler) await partInstancesHandler.subscribe(globalAdLibsHandler) + await showStyleBaseHandler.subscribe(pieceInstancesHandler) + await partInstancesHandler.subscribe(pieceInstancesHandler) // add observers for websocket topic updates await studioHandler.subscribe(studioTopic) diff --git a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts index cffa88b3d5..09f9220fe6 100644 --- a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts @@ -12,6 +12,7 @@ import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' function makeEmptyTestPartInstances(): SelectedPartInstances { return { + previous: undefined, current: undefined, firstInSegmentPlayout: undefined, inCurrentSegment: [], From 65d0d35c586e080a08f5837e18e4583ef940770c Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 19 Jun 2024 13:52:41 +0100 Subject: [PATCH 361/479] fix: live status gateway including pieces which have been pruned and will never be played SOFIE-3301 (#1206) --- .../src/collections/partInstancesHandler.ts | 10 +++ .../src/collections/pieceInstancesHandler.ts | 71 +++++++++++++++++-- .../src/collections/showStyleBaseHandler.ts | 4 +- .../src/liveStatusServer.ts | 2 + .../topics/__tests__/activePlaylist.spec.ts | 1 + 5 files changed, 81 insertions(+), 7 deletions(-) diff --git a/packages/live-status-gateway/src/collections/partInstancesHandler.ts b/packages/live-status-gateway/src/collections/partInstancesHandler.ts index 1ebd7fdec9..acca11e154 100644 --- a/packages/live-status-gateway/src/collections/partInstancesHandler.ts +++ b/packages/live-status-gateway/src/collections/partInstancesHandler.ts @@ -11,6 +11,7 @@ import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { PartInstanceId, RundownId, RundownPlaylistActivationId } from '@sofie-automation/corelib/dist/dataModel/Ids' export interface SelectedPartInstances { + previous: DBPartInstance | undefined current: DBPartInstance | undefined next: DBPartInstance | undefined firstInSegmentPlayout: DBPartInstance | undefined @@ -36,6 +37,7 @@ export class PartInstancesHandler super(PartInstancesHandler.name, CollectionName.PartInstances, CorelibPubSub.partInstances, logger, coreHandler) this.observerName = this._name this._collectionData = { + previous: undefined, current: undefined, next: undefined, firstInSegmentPlayout: undefined, @@ -54,6 +56,9 @@ export class PartInstancesHandler if (!this._collectionName || !this._collectionData) return false const collection = this._core.getCollection(this._collectionName) if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) + const previousPartInstance = this._currentPlaylist?.previousPartInfo?.partInstanceId + ? collection.findOne(this._currentPlaylist.previousPartInfo.partInstanceId) + : undefined const currentPartInstance = this._currentPlaylist?.currentPartInfo?.partInstanceId ? collection.findOne(this._currentPlaylist.currentPartInfo.partInstanceId) : undefined @@ -70,6 +75,10 @@ export class PartInstancesHandler ) as DBPartInstance let hasAnythingChanged = false + if (previousPartInstance !== this._collectionData.previous) { + this._collectionData.previous = previousPartInstance + hasAnythingChanged = true + } if (currentPartInstance !== this._collectionData.current) { this._collectionData.current = currentPartInstance hasAnythingChanged = true @@ -91,6 +100,7 @@ export class PartInstancesHandler private clearCollectionData() { if (!this._collectionName || !this._collectionData) return + this._collectionData.previous = undefined this._collectionData.current = undefined this._collectionData.next = undefined this._collectionData.firstInSegmentPlayout = undefined diff --git a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts index 9392adfdd1..d1391a72dd 100644 --- a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts +++ b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts @@ -10,6 +10,12 @@ import throttleToNextTick from '@sofie-automation/shared-lib/dist/lib/throttleTo import _ = require('underscore') import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { PartInstanceId, PieceInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { processAndPrunePieceInstanceTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' +import { ShowStyleBaseExt, ShowStyleBaseHandler } from './showStyleBaseHandler' +import { PlaylistHandler } from './playlistHandler' +import { SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' +import { PartInstancesHandler, SelectedPartInstances } from './partInstancesHandler' +import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' export type PieceInstanceMin = Omit @@ -20,7 +26,7 @@ export interface SelectedPieceInstances { // Pieces present in the current part instance currentPartInstance: PieceInstanceMin[] - // Pieces present in the current part instance + // Pieces present in the next part instance nextPartInstance: PieceInstanceMin[] } @@ -33,6 +39,8 @@ export class PieceInstancesHandler private _partInstanceIds: PartInstanceId[] = [] private _activationId: string | undefined private _subscriptionPending = false + private _sourceLayers: SourceLayers = {} + private _partInstances: SelectedPartInstances | undefined private _throttledUpdateAndNotify = throttleToNextTick(() => { this.updateCollectionData() @@ -61,20 +69,45 @@ export class PieceInstancesHandler this._throttledUpdateAndNotify() } + private processAndPrunePieceInstanceTimings( + partInstance: DBPartInstance | undefined, + pieceInstances: PieceInstance[] + ): PieceInstance[] { + // Approximate when 'now' is in the PartInstance, so that any adlibbed Pieces will be timed roughly correctly + const partStarted = partInstance?.timings?.plannedStartedPlayback + const nowInPart = partStarted === undefined ? 0 : Date.now() - partStarted + + return processAndPrunePieceInstanceTimings(this._sourceLayers, pieceInstances, nowInPart, false, false) + } + private updateCollectionData(): boolean { if (!this._collectionName || !this._collectionData) return false const collection = this._core.getCollection(this._collectionName) if (!collection) throw new Error(`collection '${this._collectionName}' not found!`) - const active = this._currentPlaylist?.currentPartInfo?.partInstanceId - ? collection.find((pieceInstance: PieceInstance) => this.isPieceInstanceActive(pieceInstance)) + + const inPreviousPartInstance = this._currentPlaylist?.previousPartInfo?.partInstanceId + ? this.processAndPrunePieceInstanceTimings( + this._partInstances?.previous, + collection.find({ partInstanceId: this._currentPlaylist.previousPartInfo.partInstanceId }) + ) : [] const inCurrentPartInstance = this._currentPlaylist?.currentPartInfo?.partInstanceId - ? collection.find({ partInstanceId: this._currentPlaylist.currentPartInfo.partInstanceId }) + ? this.processAndPrunePieceInstanceTimings( + this._partInstances?.current, + collection.find({ partInstanceId: this._currentPlaylist.currentPartInfo.partInstanceId }) + ) : [] const inNextPartInstance = this._currentPlaylist?.nextPartInfo?.partInstanceId - ? collection.find({ partInstanceId: this._currentPlaylist.nextPartInfo.partInstanceId }) + ? this.processAndPrunePieceInstanceTimings( + undefined, + collection.find({ partInstanceId: this._currentPlaylist.nextPartInfo.partInstanceId }) + ) : [] + const active = [...inPreviousPartInstance, ...inCurrentPartInstance].filter((pieceInstance) => + this.isPieceInstanceActive(pieceInstance) + ) + let hasAnythingChanged = false if (!areElementsShallowEqual(this._collectionData.active, active)) { this._collectionData.active = active @@ -107,7 +140,32 @@ export class PieceInstancesHandler this._collectionData.nextPartInstance = [] } - async update(source: string, data: DBRundownPlaylist | undefined): Promise { + async update( + source: string, + data: DBRundownPlaylist | ShowStyleBaseExt | SelectedPartInstances | undefined + ): Promise { + switch (source) { + case PlaylistHandler.name: + return this.updateRundownPlaylist(source, data as DBRundownPlaylist | undefined) + case ShowStyleBaseHandler.name: { + this.logUpdateReceived('showStyleBase', source) + const showStyleBaseExt = data as ShowStyleBaseExt | undefined + this._sourceLayers = showStyleBaseExt?.sourceLayers ?? {} + this._throttledUpdateAndNotify() + break + } + case PartInstancesHandler.name: { + this.logUpdateReceived('partInstances', source) + this._partInstances = data as SelectedPartInstances + this._throttledUpdateAndNotify() + break + } + default: + throw new Error(`${this._name} received unsupported update from ${source}}`) + } + } + + private async updateRundownPlaylist(source: string, data: DBRundownPlaylist | undefined): Promise { const prevPartInstanceIds = this._partInstanceIds const prevActivationId = this._activationId @@ -180,6 +238,7 @@ export class PieceInstancesHandler return ( pieceInstance.reportedStoppedPlayback == null && pieceInstance.piece.virtual !== true && + pieceInstance.disabled !== true && (pieceInstance.partInstanceId === this._currentPlaylist?.previousPartInfo?.partInstanceId || // a piece from previous part instance may be active during transition pieceInstance.partInstanceId === this._currentPlaylist?.currentPartInfo?.partInstanceId) && (pieceInstance.reportedStartedPlayback != null || // has been reported to have started by the Playout Gateway diff --git a/packages/live-status-gateway/src/collections/showStyleBaseHandler.ts b/packages/live-status-gateway/src/collections/showStyleBaseHandler.ts index c41e1e10f3..2cdcdd6541 100644 --- a/packages/live-status-gateway/src/collections/showStyleBaseHandler.ts +++ b/packages/live-status-gateway/src/collections/showStyleBaseHandler.ts @@ -12,6 +12,7 @@ import { IOutputLayer, ISourceLayer } from '@sofie-automation/blueprints-integra export interface ShowStyleBaseExt extends DBShowStyleBase { sourceLayerNamesById: ReadonlyMap outputLayerNamesById: ReadonlyMap + sourceLayers: SourceLayers } export class ShowStyleBaseHandler @@ -113,10 +114,11 @@ export class ShowStyleBaseHandler if (outputLayer === undefined || outputLayer === null) continue this._outputLayersMap.set(layerId, outputLayer.name) } - const showStyleBaseExt = { + const showStyleBaseExt: ShowStyleBaseExt = { ...showStyleBase, sourceLayerNamesById: this._sourceLayersMap, outputLayerNamesById: this._outputLayersMap, + sourceLayers, } this._collectionData = showStyleBaseExt } diff --git a/packages/live-status-gateway/src/liveStatusServer.ts b/packages/live-status-gateway/src/liveStatusServer.ts index 5cdda17720..70590e22b0 100644 --- a/packages/live-status-gateway/src/liveStatusServer.ts +++ b/packages/live-status-gateway/src/liveStatusServer.ts @@ -96,6 +96,8 @@ export class LiveStatusServer { await partInstancesHandler.subscribe(globalAdLibActionsHandler) await partInstancesHandler.subscribe(adLibsHandler) await partInstancesHandler.subscribe(globalAdLibsHandler) + await showStyleBaseHandler.subscribe(pieceInstancesHandler) + await partInstancesHandler.subscribe(pieceInstancesHandler) // add observers for websocket topic updates await studioHandler.subscribe(studioTopic) diff --git a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts index 91f28d6d08..b50890c6b9 100644 --- a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts @@ -12,6 +12,7 @@ import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' function makeEmptyTestPartInstances(): SelectedPartInstances { return { + previous: undefined, current: undefined, firstInSegmentPlayout: undefined, inCurrentSegment: [], From fb0d2c8ef2e316bfe53cfa7160711101ede29ac4 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Wed, 19 Jun 2024 14:55:10 +0200 Subject: [PATCH 362/479] fix: group sends together, for increased performance A local test shows that stringifying messages infividually for each recipient cand easilly take several milliseconds. --- .../src/topics/activePiecesTopic.ts | 4 +--- .../src/topics/activePlaylistTopic.ts | 4 +--- .../src/topics/adLibsTopic.ts | 4 +--- .../src/topics/segmentsTopic.ts | 4 +--- .../src/topics/studioTopic.ts | 5 ++--- packages/live-status-gateway/src/wsHandler.ts | 22 +++++++++++++++---- 6 files changed, 24 insertions(+), 19 deletions(-) diff --git a/packages/live-status-gateway/src/topics/activePiecesTopic.ts b/packages/live-status-gateway/src/topics/activePiecesTopic.ts index e90d8e71af..fcb53adf4f 100644 --- a/packages/live-status-gateway/src/topics/activePiecesTopic.ts +++ b/packages/live-status-gateway/src/topics/activePiecesTopic.ts @@ -60,9 +60,7 @@ export class ActivePiecesTopic activePieces: [], }) - for (const subscriber of subscribers) { - this.sendMessage(subscriber, message) - } + this.sendMessage(subscribers, message) } async update( diff --git a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts index 98c3e46f1b..f0e6213e63 100644 --- a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts +++ b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts @@ -153,9 +153,7 @@ export class ActivePlaylistTopic publicData: undefined, }) - for (const subscriber of subscribers) { - this.sendMessage(subscriber, message) - } + this.sendMessage(subscribers, message) } private isDataInconsistent() { diff --git a/packages/live-status-gateway/src/topics/adLibsTopic.ts b/packages/live-status-gateway/src/topics/adLibsTopic.ts index def55c0bf8..da78725c0b 100644 --- a/packages/live-status-gateway/src/topics/adLibsTopic.ts +++ b/packages/live-status-gateway/src/topics/adLibsTopic.ts @@ -236,9 +236,7 @@ export class AdLibsTopic } : { event: 'adLibs', rundownPlaylistId: null, adLibs: [], globalAdLibs: [] } - for (const subscriber of subscribers) { - this.sendMessage(subscriber, adLibsStatus) - } + this.sendMessage(subscribers, adLibsStatus) } async update( diff --git a/packages/live-status-gateway/src/topics/segmentsTopic.ts b/packages/live-status-gateway/src/topics/segmentsTopic.ts index 5532528058..93dd706d11 100644 --- a/packages/live-status-gateway/src/topics/segmentsTopic.ts +++ b/packages/live-status-gateway/src/topics/segmentsTopic.ts @@ -75,9 +75,7 @@ export class SegmentsTopic }), } - for (const subscriber of subscribers) { - this.sendMessage(subscriber, segmentsStatus) - } + this.sendMessage(subscribers, segmentsStatus) } async update(source: string, data: DBRundownPlaylist | DBSegment[] | DBPart[] | undefined): Promise { diff --git a/packages/live-status-gateway/src/topics/studioTopic.ts b/packages/live-status-gateway/src/topics/studioTopic.ts index 4998a1afd1..945317718d 100644 --- a/packages/live-status-gateway/src/topics/studioTopic.ts +++ b/packages/live-status-gateway/src/topics/studioTopic.ts @@ -54,9 +54,8 @@ export class StudioTopic name: '', playlists: [], } - for (const subscriber of subscribers) { - this.sendMessage(subscriber, studioStatus) - } + + this.sendMessage(subscribers, studioStatus) } async update(source: string, data: DBStudio | DBRundownPlaylist[] | undefined): Promise { diff --git a/packages/live-status-gateway/src/wsHandler.ts b/packages/live-status-gateway/src/wsHandler.ts index 90a43f64e4..072da1ad3e 100644 --- a/packages/live-status-gateway/src/wsHandler.ts +++ b/packages/live-status-gateway/src/wsHandler.ts @@ -34,15 +34,22 @@ export abstract class WebSocketTopicBase { this._logger.error(`Process ${this._name} message not expected '${JSON.stringify(msg)}'`) } - sendMessage(ws: WebSocket, msg: object): void { + sendMessage(recipients: WebSocket | Iterable, msg: object): void { const msgStr = JSON.stringify(msg) - this._logger.debug(`Send ${this._name} message '${msgStr}'`) - ws.send(msgStr) + + recipients = isIterable(recipients) ? recipients : [recipients] + + let count = 0 + for (const ws of recipients) { + count++ + ws.send(msgStr) + } + this._logger.silly(`Send ${this._name} message '${msgStr}' to ${count} recipients`) } sendHeartbeat(ws: WebSocket): void { const msgStr = JSON.stringify({ event: 'heartbeat' }) - this._logger.silly(`Send ${this._name} message '${msgStr}'`) + // this._logger.silly(`Send ${this._name} message '${msgStr}'`) ws.send(msgStr) } @@ -166,3 +173,10 @@ export interface CollectionObserver { observerName: string update(source: string, data: T | undefined): Promise } +function isIterable(obj: T | Iterable): obj is Iterable { + // checks for null and undefined + if (obj == null) { + return false + } + return typeof (obj as any)[Symbol.iterator] === 'function' +} From b88a07f339cb14c16f3d8a6e3757dd2861520d4d Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 19 Jun 2024 14:34:41 +0100 Subject: [PATCH 363/479] chore: fix type errors SOFIE-3301 --- .../src/collections/pieceInstancesHandler.ts | 16 +++++++++------- .../src/topics/helpers/pieceStatus.ts | 6 +++--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts index d1391a72dd..732bf79a67 100644 --- a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts +++ b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts @@ -16,8 +16,9 @@ import { PlaylistHandler } from './playlistHandler' import { SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { PartInstancesHandler, SelectedPartInstances } from './partInstancesHandler' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' +import { ReadonlyDeep } from 'type-fest' -export type PieceInstanceMin = Omit +export type PieceInstanceMin = Omit, 'reportedStartedPlayback' | 'reportedStoppedPlayback'> export interface SelectedPieceInstances { // Pieces reported by the Playout Gateway as active @@ -72,7 +73,7 @@ export class PieceInstancesHandler private processAndPrunePieceInstanceTimings( partInstance: DBPartInstance | undefined, pieceInstances: PieceInstance[] - ): PieceInstance[] { + ): ReadonlyDeep[] { // Approximate when 'now' is in the PartInstance, so that any adlibbed Pieces will be timed roughly correctly const partStarted = partInstance?.timings?.plannedStartedPlayback const nowInPart = partStarted === undefined ? 0 : Date.now() - partStarted @@ -117,10 +118,11 @@ export class PieceInstancesHandler !areElementsShallowEqual(this._collectionData.currentPartInstance, inCurrentPartInstance) && (this._collectionData.currentPartInstance.length !== inCurrentPartInstance.length || this._collectionData.currentPartInstance.some((pieceInstance, index) => { - return !arePropertiesShallowEqual(inCurrentPartInstance[index], pieceInstance, [ - 'reportedStartedPlayback', - 'reportedStoppedPlayback', - ]) + return !arePropertiesShallowEqual>( + inCurrentPartInstance[index], + pieceInstance, + ['reportedStartedPlayback', 'reportedStoppedPlayback'] + ) })) ) { this._collectionData.currentPartInstance = inCurrentPartInstance @@ -234,7 +236,7 @@ export class PieceInstancesHandler } } - private isPieceInstanceActive(pieceInstance: PieceInstance) { + private isPieceInstanceActive(pieceInstance: ReadonlyDeep) { return ( pieceInstance.reportedStoppedPlayback == null && pieceInstance.piece.virtual !== true && diff --git a/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts b/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts index 02005aa377..62ea6fde94 100644 --- a/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts +++ b/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts @@ -1,18 +1,18 @@ -import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { unprotectString } from '@sofie-automation/server-core-integration' import { ShowStyleBaseExt } from '../../collections/showStyleBaseHandler' +import { PieceInstanceMin } from '../../collections/pieceInstancesHandler' export interface PieceStatus { id: string name: string sourceLayer: string outputLayer: string - tags: string[] | undefined + tags: readonly string[] | undefined publicData: unknown } export function toPieceStatus( - pieceInstance: PieceInstance, + pieceInstance: PieceInstanceMin, showStyleBaseExt: ShowStyleBaseExt | undefined ): PieceStatus { const sourceLayerName = showStyleBaseExt?.sourceLayerNamesById.get(pieceInstance.piece.sourceLayerId) From 2dcca215403cc0256c78f5f8f5f6befa2a1ca683 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 20 Jun 2024 09:58:33 +0100 Subject: [PATCH 364/479] fix: include timestamp when logging to file (#1207) --- packages/live-status-gateway/src/index.ts | 26 ++++++++++---------- packages/playout-gateway/src/index.ts | 29 ++++++++++------------- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/packages/live-status-gateway/src/index.ts b/packages/live-status-gateway/src/index.ts index e9c51e769d..89094e926c 100644 --- a/packages/live-status-gateway/src/index.ts +++ b/packages/live-status-gateway/src/index.ts @@ -6,6 +6,16 @@ import { stringifyError } from '@sofie-automation/server-core-integration' console.log('process started') // This is a message all Sofie processes log upon startup +// custom json stringifier +const { splat, combine, printf } = Winston.format +const myFormat = combine( + splat(), + printf((obj) => { + obj.localTimestamp = new Date().toISOString() + return JSON.stringify(obj) // make single line + }) +) + // Setup logging -------------------------------------- let logger: Winston.Logger if (logPath) { @@ -19,7 +29,7 @@ if (logPath) { handleExceptions: true, handleRejections: true, filename: logPath, - format: Winston.format.json(), + format: myFormat, }) logger = Winston.createLogger({ @@ -37,20 +47,12 @@ if (logPath) { } } } else { - // custom json stringifier - const { splat, combine, printf } = Winston.format - const myFormat = printf((obj) => { - obj.localTimestamp = getCurrentTime() - obj.randomId = Math.round(Math.random() * 10000) - return JSON.stringify(obj) // make single line - }) - // Log json to console const transportConsole = new Winston.transports.Console({ level: logLevel || 'silly', handleExceptions: true, handleRejections: true, - format: combine(splat(), myFormat), + format: myFormat, }) logger = Winston.createLogger({ @@ -66,10 +68,6 @@ if (logPath) { } } } -function getCurrentTime() { - const v = Date.now() - return new Date(v).toISOString() -} // Because the default NodeJS-handler sucks and wont display error properly process.on('warning', (e: any) => { diff --git a/packages/playout-gateway/src/index.ts b/packages/playout-gateway/src/index.ts index d0ca133122..b47a3baaac 100644 --- a/packages/playout-gateway/src/index.ts +++ b/packages/playout-gateway/src/index.ts @@ -6,6 +6,16 @@ import { stringifyError } from '@sofie-automation/server-core-integration' console.log('process started') // This is a message all Sofie processes log upon startup +// custom json stringifier +const { splat, combine, printf } = Winston.format +const myFormat = combine( + splat(), + printf((obj) => { + obj.localTimestamp = new Date().toISOString() + return JSON.stringify(obj) // make single line + }) +) + // Setup logging -------------------------------------- let logger: Winston.Logger if (logPath) { @@ -19,7 +29,7 @@ if (logPath) { handleExceptions: true, handleRejections: true, filename: logPath, - format: Winston.format.json(), + format: myFormat, }) logger = Winston.createLogger({ @@ -37,20 +47,12 @@ if (logPath) { } } } else { - // custom json stringifier - const { splat, combine, printf } = Winston.format - const myFormat = printf((obj) => { - obj.localTimestamp = getCurrentTime() - obj.randomId = Math.round(Math.random() * 10000) - return JSON.stringify(obj) // make single line - }) - // Log json to console const transportConsole = new Winston.transports.Console({ level: logLevel || 'silly', handleExceptions: true, handleRejections: true, - format: combine(splat(), myFormat), + format: myFormat, }) logger = Winston.createLogger({ @@ -66,13 +68,6 @@ if (logPath) { } } } -function getCurrentTime() { - const v = Date.now() - // if (c && c.coreHandler && c.coreHandler.core) { - // v = c.coreHandler.core.getCurrentTime() - // } - return new Date(v).toISOString() -} // Because the default NodeJS-handler sucks and wont display error properly process.on('warning', (e: any) => { From ae1adeba0c9f992797a59a7a3ec1697897b5bd45 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Thu, 20 Jun 2024 13:37:21 +0200 Subject: [PATCH 365/479] fix(LSG): further optimize sending of messages --- packages/live-status-gateway/src/wsHandler.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/live-status-gateway/src/wsHandler.ts b/packages/live-status-gateway/src/wsHandler.ts index 072da1ad3e..6e2926afc8 100644 --- a/packages/live-status-gateway/src/wsHandler.ts +++ b/packages/live-status-gateway/src/wsHandler.ts @@ -35,22 +35,23 @@ export abstract class WebSocketTopicBase { } sendMessage(recipients: WebSocket | Iterable, msg: object): void { - const msgStr = JSON.stringify(msg) - recipients = isIterable(recipients) ? recipients : [recipients] let count = 0 + let msgStr = '' for (const ws of recipients) { + if (!msgStr) msgStr = JSON.stringify(msg) // Optimization: only stringify if there are any recipients count++ ws.send(msgStr) } this._logger.silly(`Send ${this._name} message '${msgStr}' to ${count} recipients`) } - sendHeartbeat(ws: WebSocket): void { + sendHeartbeat(recipients: Set): void { const msgStr = JSON.stringify({ event: 'heartbeat' }) - // this._logger.silly(`Send ${this._name} message '${msgStr}'`) - ws.send(msgStr) + for (const ws of recipients.values()) { + ws.send(msgStr) + } } protected logUpdateReceived(collectionName: string, source: string, extraInfo?: string): void { From 282cbec6e9de1948bbb6d82ccd3f0f56efa7fc64 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 20 Jun 2024 15:25:13 +0100 Subject: [PATCH 366/479] fix: avoid adding `originalId` to timeline-objects during lookahead --- packages/job-worker/src/playout/lib.ts | 31 +++++-------------- .../src/playout/timeline/generate.ts | 9 +++++- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/packages/job-worker/src/playout/lib.ts b/packages/job-worker/src/playout/lib.ts index f4f1ddc68a..d677898e75 100644 --- a/packages/job-worker/src/playout/lib.ts +++ b/packages/job-worker/src/playout/lib.ts @@ -10,7 +10,6 @@ import { logger } from '../logging' import { getCurrentTime } from '../lib' import { MongoQuery } from '../db' import { mongoWhere } from '@sofie-automation/corelib/dist/mongo' -import _ = require('underscore') import { setNextPart } from './setNext' import { selectNextPart } from './selectNextPart' @@ -147,34 +146,20 @@ export function substituteObjectIds( return enable } -export function prefixSingleObjectId( - obj: T, - prefix: string, - ignoreOriginal?: never -): string { - let id = obj.id - if (!ignoreOriginal) { - if (!obj.originalId) { - obj.originalId = obj.id - } - id = obj.originalId - } - return prefix + id +export function prefixSingleObjectId(obj: T, prefix: string): string { + return prefix + (obj.originalId ?? obj.id) } -export function prefixAllObjectIds( - objList: T[], - prefix: string, - ignoreOriginal?: never -): T[] { +export function prefixAllObjectIds(objList: T[], prefix: string): T[] { const idMap: { [oldId: string]: string | undefined } = {} - _.each(objList, (o) => { - idMap[o.id] = prefixSingleObjectId(o, prefix, ignoreOriginal) - }) + for (const obj of objList) { + idMap[obj.id] = prefixSingleObjectId(obj, prefix) + } return objList.map((rawObj) => { const obj = clone(rawObj) - obj.id = prefixSingleObjectId(obj, prefix, ignoreOriginal) + if (!obj.originalId) obj.originalId = obj.id + obj.id = prefixSingleObjectId(obj, prefix) obj.enable = substituteObjectIds(obj.enable, idMap) if (typeof obj.inGroup === 'string') { diff --git a/packages/job-worker/src/playout/timeline/generate.ts b/packages/job-worker/src/playout/timeline/generate.ts index cb9e55df68..3467383690 100644 --- a/packages/job-worker/src/playout/timeline/generate.ts +++ b/packages/job-worker/src/playout/timeline/generate.ts @@ -439,7 +439,14 @@ async function getTimelineRundown( return { objs: timelineObjs.map((timelineObj) => { return { - ...omit(timelineObj, 'pieceInstanceId', 'infinitePieceInstanceId', 'partInstanceId'), // temporary fields from OnGenerateTimelineObj + ...omit( + timelineObj, + // temporary fields from OnGenerateTimelineObj + 'pieceInstanceId', + 'infinitePieceInstanceId', + 'partInstanceId', + 'originalId' + ), objectType: TimelineObjType.RUNDOWN, } }), From e60b4ec32216df1be6e96cb1160ec40027323d30 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 21 Jun 2024 08:00:23 +0100 Subject: [PATCH 367/479] chore: update test snapshots --- .../__tests__/__snapshots__/lookahead.test.ts.snap | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/job-worker/src/playout/lookahead/__tests__/__snapshots__/lookahead.test.ts.snap b/packages/job-worker/src/playout/lookahead/__tests__/__snapshots__/lookahead.test.ts.snap index 2c5a5b3313..2c5ef924bf 100644 --- a/packages/job-worker/src/playout/lookahead/__tests__/__snapshots__/lookahead.test.ts.snap +++ b/packages/job-worker/src/playout/lookahead/__tests__/__snapshots__/lookahead.test.ts.snap @@ -15,7 +15,6 @@ exports[`Lookahead got some objects 1`] = ` "isLookahead": true, "layer": "PRELOAD_lookahead", "lookaheadForLayer": "PRELOAD", - "originalId": "obj0", "pieceInstanceId": "piece0", "priority": 0.1, }, @@ -32,7 +31,6 @@ exports[`Lookahead got some objects 1`] = ` "isLookahead": true, "layer": "PRELOAD_lookahead", "lookaheadForLayer": "PRELOAD", - "originalId": "obj1", "pieceInstanceId": "piece1", "priority": 0.1, }, @@ -48,7 +46,6 @@ exports[`Lookahead got some objects 1`] = ` "isLookahead": true, "layer": "PRELOAD_lookahead", "lookaheadForLayer": "PRELOAD", - "originalId": "obj2", "pieceInstanceId": "piece0", "priority": 0.07500000000000001, }, @@ -64,7 +61,6 @@ exports[`Lookahead got some objects 1`] = ` "isLookahead": true, "layer": "PRELOAD_lookahead", "lookaheadForLayer": "PRELOAD", - "originalId": "obj3", "pieceInstanceId": "piece0", "priority": 0.05, }, @@ -80,7 +76,6 @@ exports[`Lookahead got some objects 1`] = ` "isLookahead": true, "layer": "PRELOAD_lookahead", "lookaheadForLayer": "PRELOAD", - "originalId": "obj4", "pieceInstanceId": "piece0", "priority": 0.025, }, @@ -96,7 +91,6 @@ exports[`Lookahead got some objects 1`] = ` "id": "lookahead_timed0_piece1obj5", "isLookahead": true, "layer": "WHEN_CLEAR", - "originalId": "obj5", "pieceInstanceId": "piece1", "priority": 0.1, }, @@ -112,7 +106,6 @@ exports[`Lookahead got some objects 1`] = ` "id": "lookahead_timed1_piece0obj6", "isLookahead": true, "layer": "WHEN_CLEAR", - "originalId": "obj6", "pieceInstanceId": "piece0", "priority": 0.1, }, @@ -127,7 +120,6 @@ exports[`Lookahead got some objects 1`] = ` "id": "lookahead_future0_piece1obj7", "isLookahead": true, "layer": "WHEN_CLEAR", - "originalId": "obj7", "pieceInstanceId": "piece1", "priority": 0.07500000000000001, }, @@ -142,7 +134,6 @@ exports[`Lookahead got some objects 1`] = ` "id": "lookahead_future1_piece1obj8", "isLookahead": true, "layer": "WHEN_CLEAR", - "originalId": "obj8", "pieceInstanceId": "piece1", "priority": 0.05, }, @@ -157,7 +148,6 @@ exports[`Lookahead got some objects 1`] = ` "id": "lookahead_future2_piece0obj9", "isLookahead": true, "layer": "WHEN_CLEAR", - "originalId": "obj9", "pieceInstanceId": "piece0", "priority": 0.025, }, From 32fb01aed6d3d1c1d4f24e6a21f8dd94649572aa Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 21 Jun 2024 14:07:58 +0100 Subject: [PATCH 368/479] chore: remove unnecessary parameter --- .../job-worker/src/blueprints/context/watchedPackages.ts | 7 +++---- packages/job-worker/src/ingest/bucket/import.ts | 2 +- packages/job-worker/src/playout/adlibAction.ts | 2 +- packages/job-worker/src/playout/timeline/generate.ts | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/job-worker/src/blueprints/context/watchedPackages.ts b/packages/job-worker/src/blueprints/context/watchedPackages.ts index afbc1a3c99..3ae20ec9fe 100644 --- a/packages/job-worker/src/blueprints/context/watchedPackages.ts +++ b/packages/job-worker/src/blueprints/context/watchedPackages.ts @@ -5,7 +5,7 @@ import { } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' import { PackageInfoDB } from '@sofie-automation/corelib/dist/dataModel/PackageInfos' import { JobContext } from '../../jobs' -import { ExpectedPackageId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { ExpectedPackageId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { Filter as FilterQuery } from 'mongodb' import { PackageInfo } from '@sofie-automation/blueprints-integration' import { unprotectObjectArray } from '@sofie-automation/corelib/dist/protectedString' @@ -41,16 +41,15 @@ export class WatchedPackagesHelper { */ static async create( context: JobContext, - studioId: StudioId, filter: FilterQuery> ): Promise { // Load all the packages and the infos that are watched const watchedPackages = await context.directCollections.ExpectedPackages.findFetch({ ...filter, - studioId: studioId, + studioId: context.studioId, } as any) // TODO: don't use any here const watchedPackageInfos = await context.directCollections.PackageInfos.findFetch({ - studioId: studioId, + studioId: context.studioId, packageId: { $in: watchedPackages.map((p) => p._id) }, }) diff --git a/packages/job-worker/src/ingest/bucket/import.ts b/packages/job-worker/src/ingest/bucket/import.ts index af4edd32b5..e2d44faac2 100644 --- a/packages/job-worker/src/ingest/bucket/import.ts +++ b/packages/job-worker/src/ingest/bucket/import.ts @@ -244,7 +244,7 @@ async function generateBucketAdlibForVariant( // pieceId: BucketAdLibId | BucketAdLibActionId, payload: IngestAdlib ): Promise { - const watchedPackages = await WatchedPackagesHelper.create(context, context.studio._id, { + const watchedPackages = await WatchedPackagesHelper.create(context, { // We don't know what the `pieceId` will be, but we do know the `externalId` pieceExternalId: payload.externalId, fromPieceType: { diff --git a/packages/job-worker/src/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index 19b19d94b4..736825c3ba 100644 --- a/packages/job-worker/src/playout/adlibAction.ts +++ b/packages/job-worker/src/playout/adlibAction.ts @@ -70,7 +70,7 @@ export async function executeAdlibActionAndSaveModel( throw UserError.create(UserErrorMessage.ActionsNotSupported) } - const watchedPackages = await WatchedPackagesHelper.create(context, context.studio._id, { + const watchedPackages = await WatchedPackagesHelper.create(context, { pieceId: data.actionDocId, fromPieceType: { $in: [ diff --git a/packages/job-worker/src/playout/timeline/generate.ts b/packages/job-worker/src/playout/timeline/generate.ts index 3467383690..ae055d97a1 100644 --- a/packages/job-worker/src/playout/timeline/generate.ts +++ b/packages/job-worker/src/playout/timeline/generate.ts @@ -96,7 +96,7 @@ export async function updateStudioTimeline( const studioBlueprint = context.studioBlueprint if (studioBlueprint) { - const watchedPackages = await WatchedPackagesHelper.create(context, studio._id, { + const watchedPackages = await WatchedPackagesHelper.create(context, { fromPieceType: ExpectedPackageDBType.STUDIO_BASELINE_OBJECTS, }) From 5b38f57e51ebc86b86c66b695b47fc26edad28d7 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Mon, 24 Jun 2024 08:41:42 +0200 Subject: [PATCH 369/479] chore: lint --- packages/live-status-gateway/src/coreHandler.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/live-status-gateway/src/coreHandler.ts b/packages/live-status-gateway/src/coreHandler.ts index 2843fa1014..183c9212b0 100644 --- a/packages/live-status-gateway/src/coreHandler.ts +++ b/packages/live-status-gateway/src/coreHandler.ts @@ -15,11 +15,7 @@ import { PeripheralDeviceType, } from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI' import { protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' -import { - PeripheralDeviceCommandId, - PeripheralDeviceId, - StudioId, -} from '@sofie-automation/shared-lib/dist/core/model/Ids' +import { PeripheralDeviceCommandId, StudioId } from '@sofie-automation/shared-lib/dist/core/model/Ids' import { StatusCode } from '@sofie-automation/shared-lib/dist/lib/status' import { PeripheralDeviceCommand } from '@sofie-automation/shared-lib/dist/core/model/PeripheralDeviceCommand' import { LiveStatusGatewayConfig } from './generated/options' From bff9864cb4764770d7512a31ffb6cd1af5a2ca2c Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Mon, 24 Jun 2024 08:43:34 +0200 Subject: [PATCH 370/479] chore: fix --- packages/live-status-gateway/src/topics/root.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/live-status-gateway/src/topics/root.ts b/packages/live-status-gateway/src/topics/root.ts index 444ec7fe23..6307aae73a 100644 --- a/packages/live-status-gateway/src/topics/root.ts +++ b/packages/live-status-gateway/src/topics/root.ts @@ -53,7 +53,7 @@ export class RootChannel extends WebSocketTopicBase implements WebSocketTopic { constructor(logger: Logger) { super('Root', logger) - this._heartbeat = setInterval(() => this._subscribers.forEach((ws) => this.sendHeartbeat(ws)), 2000) + this._heartbeat = setInterval(() => this.sendHeartbeat(this._subscribers), 2000) } close(): void { From bf81baf9ff520ad6a9fc9b6378ce6ceeac320645 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Mon, 24 Jun 2024 10:57:46 +0200 Subject: [PATCH 371/479] fix: refactor VirtualElement to be a FC --- meteor/client/lib/VirtualElement.tsx | 309 +++++++++++++-------------- meteor/package.json | 2 +- meteor/yarn.lock | 16 +- tsconfig.json | 3 + 4 files changed, 163 insertions(+), 167 deletions(-) create mode 100644 tsconfig.json diff --git a/meteor/client/lib/VirtualElement.tsx b/meteor/client/lib/VirtualElement.tsx index 6c00e4e433..1b825b7292 100644 --- a/meteor/client/lib/VirtualElement.tsx +++ b/meteor/client/lib/VirtualElement.tsx @@ -1,29 +1,6 @@ -import * as React from 'react' +import React, { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react' import { InView } from 'react-intersection-observer' -export interface IProps { - initialShow?: boolean - placeholderHeight?: number - _debug?: boolean - placeholderClassName?: string - width?: string | number - margin?: string - id?: string | undefined - className?: string -} - -declare global { - interface Window { - requestIdleCallback( - callback: Function, - options?: { - timeout: number - } - ): number - cancelIdleCallback(callback: number) - } -} - interface IElementMeasurements { width: string | number clientHeight: number @@ -34,160 +11,172 @@ interface IElementMeasurements { id: string | undefined } -interface IState extends IElementMeasurements { - inView: boolean - isMeasured: boolean -} - const OPTIMIZE_PERIOD = 5000 +const IDLE_CALLBACK_TIMEOUT = 100 + /** * This is a component that allows optimizing the amount of elements present in the DOM through replacing them * with placeholders when they aren't visible in the viewport. * * @export - * @class VirtualElement - * @extends {React.Component} + * @param {(React.PropsWithChildren<{ + * initialShow?: boolean + * placeholderHeight?: number + * _debug?: boolean + * placeholderClassName?: string + * width?: string | number + * margin?: string + * id?: string | undefined + * className?: string + * }>)} { + * initialShow, + * placeholderHeight, + * placeholderClassName, + * width, + * margin, + * id, + * className, + * children, + * } + * @return {*} {(JSX.Element | null)} */ -export class VirtualElement extends React.Component, IState> { - private el: HTMLElement | null = null - private instance: HTMLElement | null = null - private optimizeTimeout: NodeJS.Timer | null = null - private refreshSizingTimeout: NodeJS.Timer | null = null - private styleObj: CSSStyleDeclaration | undefined - - constructor(props: IProps) { - super(props) - this.state = { - inView: props.initialShow || false, - isMeasured: false, - clientHeight: 0, - width: 'auto', - marginBottom: undefined, - marginTop: undefined, - marginLeft: undefined, - marginRight: undefined, - id: undefined, +export function VirtualElement({ + initialShow, + placeholderHeight, + placeholderClassName, + width, + margin, + id, + className, + children, +}: React.PropsWithChildren<{ + initialShow?: boolean + placeholderHeight?: number + _debug?: boolean + placeholderClassName?: string + width?: string | number + margin?: string + id?: string | undefined + className?: string +}>): JSX.Element | null { + const [inView, setInView] = useState(initialShow ?? false) + const [isShowingChildren, setIsShowingChildren] = useState(inView) + const [measurements, setMeasurements] = useState(null) + const [ref, setRef] = useState(null) + const [childRef, setChildRef] = useState(null) + + const isMeasured = !!measurements + + const styleObj = useMemo( + () => ({ + width: width ?? measurements?.width ?? 'auto', + height: (measurements?.clientHeight ?? placeholderHeight ?? '0') + 'px', + marginTop: measurements?.marginTop, + marginLeft: measurements?.marginLeft, + marginRight: measurements?.marginRight, + marginBottom: measurements?.marginBottom, + }), + [width, measurements, placeholderHeight] + ) + + const onVisibleChanged = useCallback((visible: boolean) => { + setInView(visible) + }, []) + + useEffect(() => { + if (inView === true) { + setIsShowingChildren(true) + return } - } - private visibleChanged = (inView: boolean) => { - this.props._debug && console.log(this.props.id, 'Changed', inView) - if (this.optimizeTimeout) { - clearTimeout(this.optimizeTimeout) - this.optimizeTimeout = null - } - if (inView && !this.state.inView) { - this.setState({ - inView, - }) - } else if (!inView && this.state.inView) { - this.optimizeTimeout = setTimeout(() => { - this.optimizeTimeout = null - const measurements = this.measureElement() || undefined - this.setState({ - inView, - - isMeasured: measurements ? true : false, - ...measurements, - } as IState) - }, OPTIMIZE_PERIOD) - } - } + let idleCallback: number | undefined + const optimizeTimeout = window.setTimeout(() => { + idleCallback = window.requestIdleCallback( + () => { + if (childRef) { + setMeasurements(measureElement(childRef)) + } + setIsShowingChildren(false) + }, + { + timeout: IDLE_CALLBACK_TIMEOUT, + } + ) + }, OPTIMIZE_PERIOD) - private measureElement = (): IElementMeasurements | null => { - if (this.el) { - const style = this.styleObj || window.getComputedStyle(this.el) - this.styleObj = style - this.props._debug && console.log(this.props.id, 'Re-measuring child', this.el.clientHeight) - - return { - width: style.width || 'auto', - clientHeight: this.el.clientHeight, - marginTop: style.marginTop || undefined, - marginBottom: style.marginBottom || undefined, - marginLeft: style.marginLeft || undefined, - marginRight: style.marginRight || undefined, - id: this.el.id, + return () => { + if (idleCallback) { + window.cancelIdleCallback(idleCallback) } - } - - return null - } - private refreshSizing = () => { - this.refreshSizingTimeout = null - const measurements = this.measureElement() - if (measurements) { - this.setState({ - isMeasured: true, - ...measurements, - }) + window.clearTimeout(optimizeTimeout) } - } + }, [childRef, inView]) - private findChildElement = () => { - if (!this.el || !this.el.parentElement) { - const el = this.instance ? (this.instance.firstElementChild as HTMLElement) : null - if (el && !el.classList.contains('virtual-element-placeholder')) { - this.el = el - this.styleObj = undefined - this.refreshSizingTimeout = setTimeout(this.refreshSizing, 250) - } - } - } + const showPlaceholder = !isShowingChildren && (!initialShow || isMeasured) - private setRef = (instance: HTMLElement | null) => { - this.instance = instance - this.findChildElement() - } + useLayoutEffect(() => { + if (!ref || showPlaceholder) return - componentDidUpdate(_: IProps, prevState: IState): void { - if (this.state.inView && prevState.inView !== this.state.inView) { - this.findChildElement() - } - } + const el = ref?.firstElementChild + if (!el || el.classList.contains('virtual-element-placeholder') || !(el instanceof HTMLElement)) return - componentWillUnmount(): void { - if (this.optimizeTimeout) clearTimeout(this.optimizeTimeout) - if (this.refreshSizingTimeout) clearTimeout(this.refreshSizingTimeout) - } + setChildRef(el) - render(): JSX.Element { - this.props._debug && - console.log( - this.props.id, - this.state.inView, - this.props.initialShow, - this.state.isMeasured, - !this.state.inView && (!this.props.initialShow || this.state.isMeasured) + let idleCallback: number | undefined + const refreshSizingTimeout = window.setTimeout(() => { + idleCallback = window.requestIdleCallback( + () => { + setMeasurements(measureElement(el)) + }, + { + timeout: IDLE_CALLBACK_TIMEOUT, + } ) - return ( - -
- {!this.state.inView && (!this.props.initialShow || this.state.isMeasured) ? ( -
- ) : ( - this.props.children - )} -
-
- ) + }, 1000) + + return () => { + if (idleCallback) { + window.cancelIdleCallback(idleCallback) + } + window.clearTimeout(refreshSizingTimeout) + } + }, [ref, showPlaceholder]) + + return ( + +
+ {showPlaceholder ? ( +
+ ) : ( + children + )} +
+
+ ) +} + +function measureElement(el: HTMLElement): IElementMeasurements | null { + const style = window.getComputedStyle(el) + const clientRect = el.getBoundingClientRect() + + return { + width: style.width || 'auto', + clientHeight: clientRect.height, + marginTop: style.marginTop || undefined, + marginBottom: style.marginBottom || undefined, + marginLeft: style.marginLeft || undefined, + marginRight: style.marginRight || undefined, + id: el.id, } } diff --git a/meteor/package.json b/meteor/package.json index 4c89f893c4..84850a5b53 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -93,7 +93,7 @@ "react-dom": "^18.2.0", "react-hotkeys": "^2.0.0", "react-i18next": "^11.18.6", - "react-intersection-observer": "^9.4.3", + "react-intersection-observer": "^9.10.3", "react-moment": "^0.9.7", "react-popper": "^2.2.5", "react-router-dom": "^5.3.3", diff --git a/meteor/yarn.lock b/meteor/yarn.lock index dc218fdee4..a757a7df1e 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -3558,7 +3558,7 @@ __metadata: react-dom: ^18.2.0 react-hotkeys: ^2.0.0 react-i18next: ^11.18.6 - react-intersection-observer: ^9.4.3 + react-intersection-observer: ^9.10.3 react-moment: ^0.9.7 react-popper: ^2.2.5 react-router-dom: ^5.3.3 @@ -10856,12 +10856,16 @@ __metadata: languageName: node linkType: hard -"react-intersection-observer@npm:^9.4.3": - version: 9.4.3 - resolution: "react-intersection-observer@npm:9.4.3" +"react-intersection-observer@npm:^9.10.3": + version: 9.10.3 + resolution: "react-intersection-observer@npm:9.10.3" peerDependencies: - react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 - checksum: ac31c6c76ce72019a1fb50fe6b53ef6429d98b9e9b937f92d16635ef8586392c7058bb61526a8fe6bea6ce36c006015fe2d8893bac43eda6157b9e6b17ad9b68 + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + react-dom: + optional: true + checksum: 482c89a432e582749f3cb3dd696e08638a92e41fbcb81bcb3dc3cadebcf8b40bc47e7a52d2a7e8c4f9eb2a3c1c29b4cb0f21007c1540da05893b5abb11d7a761 languageName: node linkType: hard diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..4086555c7b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./meteor/tsconfig.json", +} \ No newline at end of file From de6ee9b3590cbdb6270c291cc41ad15c23652726 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 24 Jun 2024 13:54:25 +0100 Subject: [PATCH 372/479] feat: Rundown `source` property SOFIE-2963 (#1210) --- meteor/__mocks__/defaultCollectionObjects.ts | 7 +- meteor/__mocks__/helpers/database.ts | 7 +- .../lib/__tests__/rundownTiming.test.ts | 7 +- .../ui/ClipTrimPanel/ClipTrimDialog.tsx | 6 +- .../ui/RundownList/RundownListItemView.tsx | 4 +- .../ui/RundownList/RundownPlaylistUi.tsx | 4 +- meteor/client/ui/RundownView.tsx | 6 +- .../client/ui/RundownView/RundownNotifier.tsx | 10 +- .../ui/RundownView/RundownSystemStatus.tsx | 6 +- meteor/client/ui/Shelf/SystemStatusPanel.tsx | 4 +- .../__tests__/externalMessageQueue.test.ts | 7 +- .../api/__tests__/peripheralDevice.test.ts | 7 +- meteor/server/api/ingest/actions.ts | 2 +- meteor/server/api/ingest/debug.ts | 27 +++-- .../api/ingest/genericDevice/actions.ts | 4 +- meteor/server/api/ingest/http.ts | 11 +- meteor/server/api/ingest/lib.ts | 22 ++-- .../mosDevice/__tests__/actions.test.ts | 3 +- meteor/server/api/ingest/mosDevice/actions.ts | 21 ++-- .../api/ingest/mosDevice/mosIntegration.ts | 22 ++-- meteor/server/api/ingest/packageInfo.ts | 1 - meteor/server/api/ingest/rundownInput.ts | 34 +++--- meteor/server/migration/X_X_X.ts | 60 +++++++++- .../__tests__/publication.test.ts | 6 +- .../segmentPartNotesUI/publication.ts | 4 +- .../reactiveContentCache.ts | 4 +- .../corelib/src/dataModel/PeripheralDevice.ts | 10 -- packages/corelib/src/dataModel/Rundown.ts | 38 ++++++- .../src/playout/__tests__/infinites.test.ts | 4 +- packages/corelib/src/worker/ingest.ts | 10 +- .../src/__mocks__/defaultCollectionObjects.ts | 12 +- .../src/__mocks__/presetCollections.ts | 7 +- .../__tests__/externalMessageQueue.test.ts | 12 +- packages/job-worker/src/events/handle.ts | 8 +- .../src/ingest/__tests__/ingest.test.ts | 84 ++++++-------- .../src/ingest/__tests__/updateNext.test.ts | 5 +- .../src/ingest/generationRundown.ts | 31 ++---- .../src/ingest/ingestRundownJobs.ts | 28 ++--- .../src/ingest/model/IngestModel.ts | 5 +- .../model/implementation/IngestModelImpl.ts | 9 +- .../__snapshots__/mosIngest.test.ts.snap | 105 +++++++++++++----- .../mosDevice/__tests__/mosIngest.test.ts | 65 +++-------- .../src/ingest/mosDevice/mosRundownJobs.ts | 6 +- .../__snapshots__/playout.test.ts.snap | 8 +- .../src/playout/__tests__/helpers/rundowns.ts | 6 +- .../lookahead/__tests__/lookahead.test.ts | 5 +- .../playout/lookahead/__tests__/util.test.ts | 5 +- .../__tests__/PlayoutRundownModelImpl.spec.ts | 4 +- .../__tests__/SavePlayoutModel.spec.ts | 4 +- packages/job-worker/src/playout/setNext.ts | 3 +- packages/job-worker/src/playout/snapshot.ts | 6 +- packages/job-worker/src/rundownPlaylists.ts | 1 - 52 files changed, 447 insertions(+), 330 deletions(-) diff --git a/meteor/__mocks__/defaultCollectionObjects.ts b/meteor/__mocks__/defaultCollectionObjects.ts index 8c27d3d611..48c860b4b6 100644 --- a/meteor/__mocks__/defaultCollectionObjects.ts +++ b/meteor/__mocks__/defaultCollectionObjects.ts @@ -60,7 +60,6 @@ export function defaultRundown( showStyleVariantId: ShowStyleVariantId ): DBRundown { return { - peripheralDeviceId: ingestDeviceId, studioId: studioId, showStyleBaseId: showStyleBaseId, showStyleVariantId: showStyleVariantId, @@ -83,10 +82,14 @@ export function defaultRundown( core: '', }, - externalNRCSName: 'mock', timing: { type: 'none' as any, }, + source: { + type: 'nrcs', + peripheralDeviceId: ingestDeviceId, + nrcsName: 'mock', + }, } } diff --git a/meteor/__mocks__/helpers/database.ts b/meteor/__mocks__/helpers/database.ts index 1f1b6d1ff8..0581105b4c 100644 --- a/meteor/__mocks__/helpers/database.ts +++ b/meteor/__mocks__/helpers/database.ts @@ -610,7 +610,6 @@ export async function setupDefaultRundown( const sourceLayerIds = Object.keys(applyAndValidateOverrides(env.showStyleBase.sourceLayersWithOverrides).obj) const rundown: DBRundown = { - peripheralDeviceId: env.ingestDevice._id, organizationId: null, studioId: env.studio._id, showStyleBaseId: env.showStyleBase._id, @@ -635,7 +634,11 @@ export async function setupDefaultRundown( core: '', }, - externalNRCSName: 'mock', + source: { + type: 'nrcs', + peripheralDeviceId: env.ingestDevice._id, + nrcsName: 'mock', + }, } await Rundowns.mutableCollection.insertAsync(rundown) diff --git a/meteor/client/lib/__tests__/rundownTiming.test.ts b/meteor/client/lib/__tests__/rundownTiming.test.ts index e3e899c05e..1897d369b6 100644 --- a/meteor/client/lib/__tests__/rundownTiming.test.ts +++ b/meteor/client/lib/__tests__/rundownTiming.test.ts @@ -85,12 +85,15 @@ function makeMockRundown(id: string, playlist: DBRundownPlaylist) { studioId: protectString('studio0'), showStyleBaseId: protectString(''), showStyleVariantId: protectString('variant0'), - peripheralDeviceId: protectString(''), created: 0, modified: 0, importVersions: {} as any, name: 'test', - externalNRCSName: 'mockNRCS', + source: { + type: 'nrcs', + peripheralDeviceId: protectString(''), + nrcsName: 'mockNRCS', + }, organizationId: protectString(''), playlistId: playlist._id, }) diff --git a/meteor/client/ui/ClipTrimPanel/ClipTrimDialog.tsx b/meteor/client/ui/ClipTrimPanel/ClipTrimDialog.tsx index 92cda537e5..45ba2b7f75 100644 --- a/meteor/client/ui/ClipTrimPanel/ClipTrimDialog.tsx +++ b/meteor/client/ui/ClipTrimPanel/ClipTrimDialog.tsx @@ -9,7 +9,7 @@ import { NotificationCenter, Notification, NoticeLevel } from '../../../lib/noti import { protectString } from '../../../lib/lib' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { ClientAPI } from '../../../lib/api/client' -import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { Rundown, getRundownNrcsName } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { UIStudio } from '../../../lib/api/studios' import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' @@ -87,7 +87,7 @@ export function ClipTrimDialog({ {selectedPiece.name}:  {t( "Trimming this clip has timed out. It's possible that the story is currently locked for writing in {{nrcsName}} and will eventually be updated. Make sure that the story is not being edited by other users.", - { nrcsName: rundown?.externalNRCSName || 'NRCS' } + { nrcsName: getRundownNrcsName(rundown) } )} ), @@ -139,7 +139,7 @@ export function ClipTrimDialog({ {selectedPiece.name}:  {t( "Trimming this clip is taking longer than expected. It's possible that the story is locked for writing in {{nrcsName}}.", - { nrcsName: rundown?.externalNRCSName || 'NRCS' } + { nrcsName: getRundownNrcsName(rundown) } )} ), diff --git a/meteor/client/ui/RundownList/RundownListItemView.tsx b/meteor/client/ui/RundownList/RundownListItemView.tsx index 78dec50833..7b24652ab9 100644 --- a/meteor/client/ui/RundownList/RundownListItemView.tsx +++ b/meteor/client/ui/RundownList/RundownListItemView.tsx @@ -2,7 +2,7 @@ import Tooltip from 'rc-tooltip' import React from 'react' import { useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' -import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { Rundown, getRundownNrcsName } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { getAllowStudio } from '../../lib/localStorage' import { RundownUtils } from '../../lib/rundown' import { iconDragHandle, iconRemove, iconResync } from './icons' @@ -196,7 +196,7 @@ export default React.memo(function RundownListItemView({ {confirmReSyncRundownHandler ? ( + ))} +

+
+ ) +}) diff --git a/meteor/client/ui/RundownList/RundownListItem.tsx b/meteor/client/ui/RundownList/RundownListItem.tsx index 2b7ac93ec7..5eaf623d08 100644 --- a/meteor/client/ui/RundownList/RundownListItem.tsx +++ b/meteor/client/ui/RundownList/RundownListItem.tsx @@ -120,7 +120,10 @@ export function RundownListItem({ showStyleName={showStyleLabel} showStyleBaseURL={userCanConfigure ? getShowStyleBaseLink(rundown.showStyleBaseId) : undefined} confirmDeleteRundownHandler={ - (rundown.orphaned && getAllowStudio()) || userCanConfigure || getAllowService() + (getAllowStudio() && + (rundown.orphaned || rundown.source.type === 'testing' || rundown.source.type === 'snapshot')) || + userCanConfigure || + getAllowService() ? () => confirmDeleteRundown(rundown, t) : undefined } diff --git a/meteor/client/ui/Settings/ShowStyle/VariantListItem.tsx b/meteor/client/ui/Settings/ShowStyle/VariantListItem.tsx index 5ea44b54f3..b016706aa3 100644 --- a/meteor/client/ui/Settings/ShowStyle/VariantListItem.tsx +++ b/meteor/client/ui/Settings/ShowStyle/VariantListItem.tsx @@ -169,6 +169,22 @@ export const VariantListItem = ({
+
+ +
+

{t('Blueprint Configuration')}

diff --git a/meteor/lib/api/pubsub.ts b/meteor/lib/api/pubsub.ts index 6cb848b0b7..4c7ff3b813 100644 --- a/meteor/lib/api/pubsub.ts +++ b/meteor/lib/api/pubsub.ts @@ -126,12 +126,12 @@ export enum MeteorPubSub { uiShowStyleBase = 'uiShowStyleBase', /** * Fetch the simplified playout UI view of the specified Studio. - * If the id is null, nothing will be returned + * If the id is null, all studios will be returned */ uiStudio = 'uiStudio', /** * Fetch the simplified playout UI view of the TriggeredActions in the specified ShowStyleBase. - * If the id is null, nothing will be returned + * If the id is null, only global TriggeredActions will be returned */ uiTriggeredActions = 'uiTriggeredActions', diff --git a/meteor/lib/api/showStyles.ts b/meteor/lib/api/showStyles.ts index d2cd45dc23..fee84cccfc 100644 --- a/meteor/lib/api/showStyles.ts +++ b/meteor/lib/api/showStyles.ts @@ -1,4 +1,4 @@ -import { ShowStyleBaseId, ShowStyleVariantId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { ShowStyleBaseId, ShowStyleVariantId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { HotkeyDefinition, OutputLayers, SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' @@ -10,6 +10,8 @@ export interface NewShowStylesAPI { removeShowStyleBase(showStyleBaseId: ShowStyleBaseId): Promise removeShowStyleVariant(showStyleVariantId: ShowStyleVariantId): Promise reorderShowStyleVariant(showStyleVariantId: ShowStyleVariantId, newRank: number): Promise + + getCreateAdlibTestingRundownOptions(): Promise } export enum ShowStylesAPIMethods { @@ -20,6 +22,8 @@ export enum ShowStylesAPIMethods { 'removeShowStyleBase' = 'showstyles.removeShowStyleBase', 'removeShowStyleVariant' = 'showstyles.removeShowStyleVariant', 'reorderShowStyleVariant' = 'showstyles.reorderShowStyleVariant', + + getCreateAdlibTestingRundownOptions = 'showstyles.getCreateAdlibTestingRundownOptions', } /** @@ -41,3 +45,10 @@ export interface UIShowStyleBase { /** "Layers" in the GUI */ sourceLayers: SourceLayers } + +export interface CreateAdlibTestingRundownOption { + studioId: StudioId + showStyleVariantId: ShowStyleVariantId + + label: string +} diff --git a/meteor/lib/api/userActions.ts b/meteor/lib/api/userActions.ts index ef5303932a..735d8fb3a1 100644 --- a/meteor/lib/api/userActions.ts +++ b/meteor/lib/api/userActions.ts @@ -22,6 +22,7 @@ import { RundownPlaylistId, SegmentId, ShowStyleBaseId, + ShowStyleVariantId, SnapshotId, StudioId, } from '@sofie-automation/corelib/dist/dataModel/Ids' @@ -327,6 +328,13 @@ export interface NewUserActionAPI extends MethodContext { playlistId: RundownPlaylistId, rundownId: RundownId ): Promise> + + createAdlibTestingRundownForShowStyleVariant( + userEvent: string, + eventTime: Time, + studioId: StudioId, + showStyleVariantId: ShowStyleVariantId + ): Promise> } export enum UserActionAPIMethods { @@ -407,6 +415,8 @@ export enum UserActionAPIMethods { 'disablePeripheralSubDevice' = 'userAction.system.disablePeripheralSubDevice', 'activateAdlibTestingMode' = 'userAction.activateAdlibTestingMode', + + 'createAdlibTestingRundownForShowStyleVariant' = 'userAction.createAdlibTestingRundownForShowStyleVariant', } export interface ReloadRundownPlaylistResponse { diff --git a/meteor/lib/clientUserAction.ts b/meteor/lib/clientUserAction.ts index 6b82913b56..268939bf03 100644 --- a/meteor/lib/clientUserAction.ts +++ b/meteor/lib/clientUserAction.ts @@ -114,6 +114,8 @@ function userActionToLabel(userAction: UserAction, t: i18next.TFunction) { return t('Refreshing debug states') case UserAction.ACTIVATE_ADLIB_TESTING: return t('Rehearsal Mode') + case UserAction.CREATE_ADLIB_TESTING_RUNDOWN: + return t('Creating Adlib Testing Rundown') default: assertNever(userAction) } diff --git a/meteor/lib/userAction.ts b/meteor/lib/userAction.ts index bdd534376c..7b24ec3df9 100644 --- a/meteor/lib/userAction.ts +++ b/meteor/lib/userAction.ts @@ -50,4 +50,5 @@ export enum UserAction { PERIPHERAL_DEVICE_REFRESH_DEBUG_STATES, ACTIVATE_ADLIB_TESTING, QUEUE_NEXT_SEGMENT, + CREATE_ADLIB_TESTING_RUNDOWN, } diff --git a/meteor/server/api/ingest/actions.ts b/meteor/server/api/ingest/actions.ts index a8877fd29a..cdabc65157 100644 --- a/meteor/server/api/ingest/actions.ts +++ b/meteor/server/api/ingest/actions.ts @@ -1,10 +1,12 @@ -import { getPeripheralDeviceFromRundown } from './lib' +import { getPeripheralDeviceFromRundown, runIngestOperation } from './lib' import { MOSDeviceActions } from './mosDevice/actions' import { Meteor } from 'meteor/meteor' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { TriggerReloadDataResponse } from '../../../lib/api/userActions' import { GenericDeviceActions } from './genericDevice/actions' import { PeripheralDeviceType } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' +import { IngestJobs } from '@sofie-automation/corelib/dist/worker/ingest' +import { assertNever } from '@sofie-automation/corelib/dist/lib' /* This file contains actions that can be performed on an ingest-device @@ -14,19 +16,46 @@ export namespace IngestActions { * Trigger a reload of a rundown */ export async function reloadRundown( - rundown: Pick + rundown: Pick ): Promise { - const device = await getPeripheralDeviceFromRundown(rundown) + const rundownSourceType = rundown.source.type + switch (rundown.source.type) { + case 'snapshot': + throw new Meteor.Error(400, `Cannot reload a snapshot rundown`) + case 'http': { + await runIngestOperation(rundown.studioId, IngestJobs.RegenerateRundown, { + rundownExternalId: rundown.externalId, + }) - // The Rundown.orphaned flag will be reset by the response update + return TriggerReloadDataResponse.COMPLETED + } + case 'testing': { + await runIngestOperation(rundown.studioId, IngestJobs.CreateAdlibTestingRundownForShowStyleVariant, { + showStyleVariantId: rundown.showStyleVariantId, + }) - // TODO: refactor this into something nicer perhaps? - if (device.type === PeripheralDeviceType.MOS) { - return MOSDeviceActions.reloadRundown(device, rundown) - } else if (device.type === PeripheralDeviceType.SPREADSHEET || device.type === PeripheralDeviceType.INEWS) { - return GenericDeviceActions.reloadRundown(device, rundown) - } else { - throw new Meteor.Error(400, `The device ${device._id} does not support the method "reloadRundown"`) + return TriggerReloadDataResponse.COMPLETED + } + case 'nrcs': { + const device = await getPeripheralDeviceFromRundown(rundown) + + // The Rundown.orphaned flag will be reset by the response update + + // TODO: refactor this into something nicer perhaps? + if (device.type === PeripheralDeviceType.MOS) { + return MOSDeviceActions.reloadRundown(device, rundown) + } else if ( + device.type === PeripheralDeviceType.SPREADSHEET || + device.type === PeripheralDeviceType.INEWS + ) { + return GenericDeviceActions.reloadRundown(device, rundown) + } else { + throw new Meteor.Error(400, `The device ${device._id} does not support the method "reloadRundown"`) + } + } + default: + assertNever(rundown.source) + throw new Meteor.Error(400, `Cannot reload rundown from source "${rundownSourceType}"`) } } } diff --git a/meteor/server/api/showStyles.ts b/meteor/server/api/showStyles.ts index da9095ade1..db8a018da2 100644 --- a/meteor/server/api/showStyles.ts +++ b/meteor/server/api/showStyles.ts @@ -1,6 +1,6 @@ import { check } from '../../lib/check' import { registerClassToMeteorMethods } from '../methods' -import { NewShowStylesAPI, ShowStylesAPIMethods } from '../../lib/api/showStyles' +import { CreateAdlibTestingRundownOption, NewShowStylesAPI, ShowStylesAPIMethods } from '../../lib/api/showStyles' import { Meteor } from 'meteor/meteor' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' @@ -17,7 +17,8 @@ import { } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { IBlueprintConfig } from '@sofie-automation/blueprints-integration' import { OrganizationId, ShowStyleBaseId, ShowStyleVariantId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { RundownLayouts, ShowStyleBases, ShowStyleVariants } from '../collections' +import { RundownLayouts, ShowStyleBases, ShowStyleVariants, Studios } from '../collections' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' export interface ShowStyleCompound extends Omit { showStyleVariantId: ShowStyleVariantId @@ -213,6 +214,70 @@ export async function reorderShowStyleVariant( }) } +async function getCreateAdlibTestingRundownOptions(): Promise { + const [studios, showStyleBases, showStyleVariants] = await Promise.all([ + Studios.findFetchAsync( + {}, + { + projection: { + _id: 1, + name: 1, + supportedShowStyleBase: 1, + }, + } + ) as Promise[]>, + ShowStyleBases.findFetchAsync( + {}, + { + projection: { + _id: 1, + name: 1, + }, + } + ) as Promise[]>, + ShowStyleVariants.findFetchAsync( + {}, + { + projection: { + _id: 1, + showStyleBaseId: 1, + name: 1, + canGenerateAdlibTestingRundown: 1, + }, + sort: { + _rank: 1, + }, + } + ) as Promise[]>, + ]) + + const options: CreateAdlibTestingRundownOption[] = [] + + for (const studio of studios) { + for (const showStyleBase of showStyleBases) { + if (!studio.supportedShowStyleBase.includes(showStyleBase._id)) continue + + for (const showStyleVariant of showStyleVariants) { + if (!showStyleVariant.canGenerateAdlibTestingRundown) continue + if (showStyleVariant.showStyleBaseId !== showStyleBase._id) continue + + // Generate a descriptive label, but as minimal as possible + let label = showStyleVariant.name + if (showStyleBases.length > 1) label = `${showStyleBase.name} - ${label}` + if (studios.length > 1) label = `${label} (${studio.name})` + + options.push({ + studioId: studio._id, + showStyleVariantId: showStyleVariant._id, + label, + }) + } + } + } + + return options +} + class ServerShowStylesAPI extends MethodContextAPI implements NewShowStylesAPI { async insertShowStyleBase() { return insertShowStyleBase(this) @@ -235,5 +300,9 @@ class ServerShowStylesAPI extends MethodContextAPI implements NewShowStylesAPI { async reorderShowStyleVariant(showStyleVariantId: ShowStyleVariantId, newRank: number) { return reorderShowStyleVariant(this, showStyleVariantId, newRank) } + + async getCreateAdlibTestingRundownOptions() { + return getCreateAdlibTestingRundownOptions() + } } registerClassToMeteorMethods(ShowStylesAPIMethods, ServerShowStylesAPI, false) diff --git a/meteor/server/api/userActions.ts b/meteor/server/api/userActions.ts index a868ffca1c..0c07d95244 100644 --- a/meteor/server/api/userActions.ts +++ b/meteor/server/api/userActions.ts @@ -43,11 +43,14 @@ import { RundownPlaylistId, SegmentId, ShowStyleBaseId, + ShowStyleVariantId, StudioId, } from '@sofie-automation/corelib/dist/dataModel/Ids' import { IngestDataCache, Parts, Pieces, Rundowns } from '../collections' import { IngestCacheType } from '@sofie-automation/corelib/dist/dataModel/IngestDataCache' import { verifyHashedToken } from './singleUseTokens' +import { runIngestOperation } from './ingest/lib' +import { IngestJobs } from '@sofie-automation/corelib/dist/worker/ingest' async function pieceSetInOutPoints( access: VerifiedRundownPlaylistContentAccess, @@ -1218,5 +1221,30 @@ class ServerUserActionAPI } ) } + + async createAdlibTestingRundownForShowStyleVariant( + userEvent: string, + eventTime: number, + studioId: StudioId, + showStyleVariantId: ShowStyleVariantId + ) { + const jobName = IngestJobs.CreateAdlibTestingRundownForShowStyleVariant + return ServerClientAPI.runUserActionInLog( + this, + userEvent, + eventTime, + `worker.ingest.${jobName}`, + [showStyleVariantId], + async (_credentials) => { + check(studioId, String) + check(showStyleVariantId, String) + + // TODO - checkAccessToStudio? + return runIngestOperation(studioId, IngestJobs.CreateAdlibTestingRundownForShowStyleVariant, { + showStyleVariantId, + }) + } + ) + } } registerClassToMeteorMethods(UserActionAPIMethods, ServerUserActionAPI, false) diff --git a/packages/blueprints-integration/src/api/showStyle.ts b/packages/blueprints-integration/src/api/showStyle.ts index 31eaf07267..52ec4559fd 100644 --- a/packages/blueprints-integration/src/api/showStyle.ts +++ b/packages/blueprints-integration/src/api/showStyle.ts @@ -19,7 +19,7 @@ import type { IOnTakeContext, IOnSetAsNextContext, } from '../context' -import type { IngestAdlib, ExtendedIngestRundown, IngestSegment } from '../ingest' +import type { IngestAdlib, ExtendedIngestRundown, IngestSegment, IngestRundown } from '../ingest' import type { IBlueprintExternalMessageQueueObj } from '../message' import type { MigrationStepShowStyle } from '../migrations' import type { @@ -88,6 +88,15 @@ export interface ShowStyleBlueprintManifest BlueprintResultSegment | Promise + /** + * Generate an Adlib Testing IngestRundown for the specified ShowStyleVariant. + * This is used to generate a rundown which can be used for testing adlibs, or minimal use of Sofie without a rundown from an NRCS. + */ + generateAdlibTestingIngestRundown?: ( + context: IShowStyleUserContext, + showStyleVariant: IBlueprintShowStyleVariant + ) => IngestRundown | Promise + /** * Allows the blueprint to custom-modify the PartInstance, on ingest data update (this is run after getSegment()) * diff --git a/packages/corelib/src/dataModel/Rundown.ts b/packages/corelib/src/dataModel/Rundown.ts index 1ceea1f684..c0e47ca652 100644 --- a/packages/corelib/src/dataModel/Rundown.ts +++ b/packages/corelib/src/dataModel/Rundown.ts @@ -90,7 +90,7 @@ export interface Rundown { } /** A description of where a Rundown originated from */ -export type RundownSource = RundownSourceNrcs | RundownSourceSnapshot | RundownSourceHttp +export type RundownSource = RundownSourceNrcs | RundownSourceSnapshot | RundownSourceHttp | RundownSourceTesting /** A description of the external NRCS source of a Rundown */ export interface RundownSourceNrcs { @@ -110,6 +110,12 @@ export interface RundownSourceSnapshot { export interface RundownSourceHttp { type: 'http' } +/** A description of the Adlib Testing source of a Rundown */ +export interface RundownSourceTesting { + type: 'testing' + /** The ShowStyleVariant the Rundown is created for */ + showStyleVariantId: ShowStyleVariantId +} export function getRundownNrcsName(rundown: ReadonlyDeep> | undefined): string { if (rundown?.source?.type === 'nrcs' && rundown.source.nrcsName) { diff --git a/packages/corelib/src/dataModel/ShowStyleVariant.ts b/packages/corelib/src/dataModel/ShowStyleVariant.ts index 35bdefd7be..8a072b9250 100644 --- a/packages/corelib/src/dataModel/ShowStyleVariant.ts +++ b/packages/corelib/src/dataModel/ShowStyleVariant.ts @@ -21,5 +21,8 @@ export interface DBShowStyleVariant { /** Config values are used by the Blueprints */ blueprintConfigWithOverrides: ObjectWithOverrides + /** Whether a testing rundown can be generated for this variant */ + canGenerateAdlibTestingRundown?: boolean + _rundownVersionHash: string } diff --git a/packages/corelib/src/error.ts b/packages/corelib/src/error.ts index 46570681a6..87206b3b42 100644 --- a/packages/corelib/src/error.ts +++ b/packages/corelib/src/error.ts @@ -58,6 +58,8 @@ export enum UserErrorMessage { AdlibTestingNotAllowed = 42, AdlibTestingAlreadyActive = 43, BucketNotFound = 44, + AdlibTestingRundownsNotSupported = 45, + AdlibTestingRundownsGenerationFailed = 46, } const UserErrorMessagesTranslations: { [key in UserErrorMessage]: string } = { @@ -114,6 +116,8 @@ const UserErrorMessagesTranslations: { [key in UserErrorMessage]: string } = { [UserErrorMessage.AdlibTestingNotAllowed]: t(`Rehearsal mode is not allowed`), [UserErrorMessage.AdlibTestingAlreadyActive]: t(`Rehearsal mode is already active`), [UserErrorMessage.BucketNotFound]: t(`Bucket not found!`), + [UserErrorMessage.AdlibTestingRundownsNotSupported]: t(`Adlib rundowns are not supported for this ShowStyle!`), + [UserErrorMessage.AdlibTestingRundownsGenerationFailed]: t(`Failed to generate adlib rundown! {{message}}`), } export interface UserErrorObj { diff --git a/packages/corelib/src/worker/ingest.ts b/packages/corelib/src/worker/ingest.ts index fc4e471951..4e5ba19e0d 100644 --- a/packages/corelib/src/worker/ingest.ts +++ b/packages/corelib/src/worker/ingest.ts @@ -126,6 +126,11 @@ export enum IngestJobs { BucketRemoveAdlibPiece = 'bucketRemoveAdlibPiece', BucketRemoveAdlibAction = 'bucketRemoveAdlibAction', BucketEmpty = 'bucketEmpty', + + /** + * Create a testing rundown for the specified ShowStyleVariant + */ + CreateAdlibTestingRundownForShowStyleVariant = 'createAdlibTestingRundownForShowStyleVariant', } export interface IngestPropsBase { @@ -260,13 +265,17 @@ export interface BucketEmptyProps { bucketId: BucketId } +export interface CreateAdlibTestingRundownForShowStyleVariantProps { + showStyleVariantId: ShowStyleVariantId +} + /** * Set of valid functions, of form: * `id: (data) => return` */ export type IngestJobFunc = { [IngestJobs.RemoveRundown]: (data: IngestRemoveRundownProps) => void - [IngestJobs.UpdateRundown]: (data: IngestUpdateRundownProps) => void + [IngestJobs.UpdateRundown]: (data: IngestUpdateRundownProps) => RundownId [IngestJobs.UpdateRundownMetaData]: (data: IngestUpdateRundownMetaDataProps) => void [IngestJobs.RemoveSegment]: (data: IngestRemoveSegmentProps) => void [IngestJobs.UpdateSegment]: (data: IngestUpdateSegmentProps) => void @@ -302,6 +311,10 @@ export type IngestJobFunc = { [IngestJobs.BucketRemoveAdlibPiece]: (data: BucketRemoveAdlibPieceProps) => void [IngestJobs.BucketRemoveAdlibAction]: (data: BucketRemoveAdlibActionProps) => void [IngestJobs.BucketEmpty]: (data: BucketEmptyProps) => void + + [IngestJobs.CreateAdlibTestingRundownForShowStyleVariant]: ( + data: CreateAdlibTestingRundownForShowStyleVariantProps + ) => RundownId } // Future: there should probably be a queue per rundown or something. To be improved later diff --git a/packages/job-worker/src/__mocks__/context.ts b/packages/job-worker/src/__mocks__/context.ts index 4b32701891..9e2751de4e 100644 --- a/packages/job-worker/src/__mocks__/context.ts +++ b/packages/job-worker/src/__mocks__/context.ts @@ -76,8 +76,8 @@ export class MockJobContext implements JobContext { #mockCollections: Readonly #studio: ReadonlyDeep - #studioBlueprint: StudioBlueprintManifest - #showStyleBlueprint: ShowStyleBlueprintManifest + #studioBlueprint: ReadonlyDeep + #showStyleBlueprint: ReadonlyDeep constructor( jobCollections: Readonly, @@ -115,6 +115,14 @@ export class MockJobContext implements JobContext { } } + get rawStudioBlueprint(): ReadonlyDeep { + return this.#studioBlueprint + } + + get rawShowStyleBlueprint(): ReadonlyDeep { + return this.#showStyleBlueprint + } + trackCache(_model: BaseModel): void { // TODO // throw new Error('Method not implemented.') @@ -202,7 +210,7 @@ export class MockJobContext implements JobContext { return compound } - async getShowStyleBlueprint(id: ShowStyleBaseId): Promise { + async getShowStyleBlueprint(id: ShowStyleBaseId): Promise> { const showStyle = await this.getShowStyleBase(id) return { @@ -225,7 +233,7 @@ export class MockJobContext implements JobContext { setStudio(studio: ReadonlyDeep): void { this.#studio = clone(studio) } - setShowStyleBlueprint(blueprint: ShowStyleBlueprintManifest): void { + setShowStyleBlueprint(blueprint: ReadonlyDeep): void { this.#showStyleBlueprint = blueprint } updateShowStyleBlueprint(blueprint: Partial): void { @@ -234,7 +242,7 @@ export class MockJobContext implements JobContext { ...blueprint, } } - setStudioBlueprint(blueprint: StudioBlueprintManifest): void { + setStudioBlueprint(blueprint: ReadonlyDeep): void { this.#studioBlueprint = blueprint } updateStudioBlueprint(blueprint: Partial): void { diff --git a/packages/job-worker/src/ingest/__tests__/selectShowStyleVariant.test.ts b/packages/job-worker/src/ingest/__tests__/selectShowStyleVariant.test.ts new file mode 100644 index 0000000000..9fd132c416 --- /dev/null +++ b/packages/job-worker/src/ingest/__tests__/selectShowStyleVariant.test.ts @@ -0,0 +1,274 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { ExtendedIngestRundown, IBlueprintShowStyleBase } from '@sofie-automation/blueprints-integration' +import '../../__mocks__/_extendJest' +import { MockJobContext, setupDefaultJobEnvironment } from '../../__mocks__/context' +import { selectShowStyleVariant } from '../selectShowStyleVariant' +import { StudioUserContext } from '../../blueprints/context' +import { setupMockShowStyleCompound, setupMockShowStyleVariant } from '../../__mocks__/presetCollections' +import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' + +describe('selectShowStyleVariant', () => { + function generateIngestRundown(): ExtendedIngestRundown { + return { + externalId: 'rd0', + name: 'Rundown', + type: 'mock', + segments: [], + coreData: undefined, + } + } + function createBlueprintContext(context: MockJobContext): StudioUserContext { + return new StudioUserContext( + { + name: 'test', + identifier: 'test', + tempSendUserNotesIntoBlackHole: true, + }, + context.studio, + context.getStudioBlueprintConfig() + ) + } + + describe('rundown source: testing', () => { + test('success', async () => { + const context = setupDefaultJobEnvironment() + const showStyleCompound = await setupMockShowStyleCompound(context) + context.setStudio({ + ...context.studio, + supportedShowStyleBase: [showStyleCompound._id], + }) + + const ingestRundown = generateIngestRundown() + const blueprintContext = createBlueprintContext(context) + + const selectedShowStyle = await selectShowStyleVariant(context, blueprintContext, ingestRundown, { + type: 'testing', + showStyleVariantId: showStyleCompound.showStyleVariantId, + }) + + expect(selectedShowStyle).toBeTruthy() + expect(selectedShowStyle!.variant._id).toEqual(showStyleCompound.showStyleVariantId) + expect(selectedShowStyle!.base._id).toEqual(showStyleCompound._id) + expect(selectedShowStyle!.compound).toEqual(showStyleCompound) + }) + + test('none defined for studio', async () => { + const context = setupDefaultJobEnvironment() + const showStyleCompound = await setupMockShowStyleCompound(context) + context.setStudio({ + ...context.studio, + supportedShowStyleBase: [], + }) + + const ingestRundown = generateIngestRundown() + const blueprintContext = createBlueprintContext(context) + + const selectedShowStyle = await selectShowStyleVariant(context, blueprintContext, ingestRundown, { + type: 'testing', + showStyleVariantId: showStyleCompound.showStyleVariantId, + }) + + expect(selectedShowStyle).toBeNull() + }) + + test('unknown id', async () => { + const context = setupDefaultJobEnvironment() + const showStyleCompound = await setupMockShowStyleCompound(context) + context.setStudio({ + ...context.studio, + supportedShowStyleBase: [showStyleCompound._id], + }) + + const ingestRundown = generateIngestRundown() + const blueprintContext = createBlueprintContext(context) + + const selectedShowStyle = await selectShowStyleVariant(context, blueprintContext, ingestRundown, { + type: 'testing', + showStyleVariantId: protectString('fakeId'), + }) + + expect(selectedShowStyle).toBeNull() + }) + }) + + describe('through blueprints', () => { + function mockBlueprintMethods(context: MockJobContext) { + const mockGetShowStyleId = jest.fn(context.rawStudioBlueprint.getShowStyleId) + context.setStudioBlueprint({ + ...context.studioBlueprint.blueprint, + getShowStyleId: mockGetShowStyleId, + }) + + const mockGetShowStyleVariantId = jest.fn(context.rawShowStyleBlueprint.getShowStyleVariantId) + context.setShowStyleBlueprint({ + ...context.rawShowStyleBlueprint, + getShowStyleVariantId: mockGetShowStyleVariantId, + }) + + return { + mockGetShowStyleId, + mockGetShowStyleVariantId, + } + } + + test('success', async () => { + const context = setupDefaultJobEnvironment() + const showStyleCompound = await setupMockShowStyleCompound(context) + const showStyleCompoundVariant2 = await setupMockShowStyleVariant(context, showStyleCompound._id) + const showStyleCompound2 = await setupMockShowStyleCompound(context) + context.setStudio({ + ...context.studio, + supportedShowStyleBase: [showStyleCompound._id, showStyleCompound2._id], + }) + + const { mockGetShowStyleId, mockGetShowStyleVariantId } = mockBlueprintMethods(context) + + const ingestRundown = generateIngestRundown() + const blueprintContext = createBlueprintContext(context) + + const selectedShowStyle = await selectShowStyleVariant(context, blueprintContext, ingestRundown, { + type: 'http', + }) + + expect(selectedShowStyle).toBeTruthy() + expect(selectedShowStyle!.variant._id).toEqual(showStyleCompound.showStyleVariantId) + expect(selectedShowStyle!.base._id).toEqual(showStyleCompound._id) + expect(selectedShowStyle!.compound).toEqual(showStyleCompound) + + expect(mockGetShowStyleId).toHaveBeenCalledTimes(1) + expect(mockGetShowStyleId.mock.calls[0][1]).toMatchObject[]>([ + { _id: unprotectString(showStyleCompound._id) }, + { _id: unprotectString(showStyleCompound2._id) }, + ]) + expect(mockGetShowStyleId.mock.calls[0][2]).toEqual(ingestRundown) + expect(mockGetShowStyleVariantId).toHaveBeenCalledTimes(1) + expect(mockGetShowStyleVariantId.mock.calls[0][1]).toMatchObject[]>([ + { _id: unprotectString(showStyleCompound.showStyleVariantId) }, + { _id: unprotectString(showStyleCompoundVariant2._id) }, + ]) + expect(mockGetShowStyleVariantId.mock.calls[0][2]).toEqual(ingestRundown) + }) + + test('no show style bases', async () => { + const context = setupDefaultJobEnvironment() + context.setStudio({ + ...context.studio, + supportedShowStyleBase: [protectString('fakeId')], + }) + + const { mockGetShowStyleId, mockGetShowStyleVariantId } = mockBlueprintMethods(context) + + const ingestRundown = generateIngestRundown() + const blueprintContext = createBlueprintContext(context) + + const selectedShowStyle = await selectShowStyleVariant(context, blueprintContext, ingestRundown, { + type: 'http', + }) + + expect(selectedShowStyle).toBeNull() + + expect(mockGetShowStyleId).toHaveBeenCalledTimes(0) + expect(mockGetShowStyleVariantId).toHaveBeenCalledTimes(0) + }) + + test('blueprint returns unknown base id', async () => { + const context = setupDefaultJobEnvironment() + const showStyleCompound = await setupMockShowStyleCompound(context) + context.setStudio({ + ...context.studio, + supportedShowStyleBase: [showStyleCompound._id], + }) + + const { mockGetShowStyleId, mockGetShowStyleVariantId } = mockBlueprintMethods(context) + + const ingestRundown = generateIngestRundown() + const blueprintContext = createBlueprintContext(context) + + mockGetShowStyleId.mockImplementation(() => 'badId') + + const selectedShowStyle = await selectShowStyleVariant(context, blueprintContext, ingestRundown, { + type: 'http', + }) + + expect(selectedShowStyle).toBeNull() + + expect(mockGetShowStyleId).toHaveBeenCalledTimes(1) + expect(mockGetShowStyleVariantId).toHaveBeenCalledTimes(0) + }) + + test('blueprint returns null for base id', async () => { + const context = setupDefaultJobEnvironment() + const showStyleCompound = await setupMockShowStyleCompound(context) + context.setStudio({ + ...context.studio, + supportedShowStyleBase: [showStyleCompound._id], + }) + + const { mockGetShowStyleId, mockGetShowStyleVariantId } = mockBlueprintMethods(context) + + const ingestRundown = generateIngestRundown() + const blueprintContext = createBlueprintContext(context) + + mockGetShowStyleId.mockImplementation(() => null) + + const selectedShowStyle = await selectShowStyleVariant(context, blueprintContext, ingestRundown, { + type: 'http', + }) + + expect(selectedShowStyle).toBeNull() + + expect(mockGetShowStyleId).toHaveBeenCalledTimes(1) + expect(mockGetShowStyleVariantId).toHaveBeenCalledTimes(0) + }) + + test('blueprint returns unknown variant id', async () => { + const context = setupDefaultJobEnvironment() + const showStyleCompound = await setupMockShowStyleCompound(context) + context.setStudio({ + ...context.studio, + supportedShowStyleBase: [showStyleCompound._id], + }) + + const { mockGetShowStyleId, mockGetShowStyleVariantId } = mockBlueprintMethods(context) + + const ingestRundown = generateIngestRundown() + const blueprintContext = createBlueprintContext(context) + + mockGetShowStyleVariantId.mockImplementation(() => 'badId') + + const selectedShowStyle = await selectShowStyleVariant(context, blueprintContext, ingestRundown, { + type: 'http', + }) + + expect(selectedShowStyle).toBeNull() + + expect(mockGetShowStyleId).toHaveBeenCalledTimes(1) + expect(mockGetShowStyleVariantId).toHaveBeenCalledTimes(1) + }) + + test('blueprint returns null for variant id', async () => { + const context = setupDefaultJobEnvironment() + const showStyleCompound = await setupMockShowStyleCompound(context) + context.setStudio({ + ...context.studio, + supportedShowStyleBase: [showStyleCompound._id], + }) + + const { mockGetShowStyleId, mockGetShowStyleVariantId } = mockBlueprintMethods(context) + + const ingestRundown = generateIngestRundown() + const blueprintContext = createBlueprintContext(context) + + mockGetShowStyleVariantId.mockImplementation(() => null) + + const selectedShowStyle = await selectShowStyleVariant(context, blueprintContext, ingestRundown, { + type: 'http', + }) + + expect(selectedShowStyle).toBeNull() + + expect(mockGetShowStyleId).toHaveBeenCalledTimes(1) + expect(mockGetShowStyleVariantId).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/packages/job-worker/src/ingest/createAdlibTestingRundown.ts b/packages/job-worker/src/ingest/createAdlibTestingRundown.ts new file mode 100644 index 0000000000..e24a942db4 --- /dev/null +++ b/packages/job-worker/src/ingest/createAdlibTestingRundown.ts @@ -0,0 +1,74 @@ +import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/error' +import type { CreateAdlibTestingRundownForShowStyleVariantProps } from '@sofie-automation/corelib/dist/worker/ingest' +import type { JobContext } from '../jobs' +import type { RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { convertShowStyleVariantToBlueprints } from '../blueprints/context/lib' +import { ShowStyleUserContext } from '../blueprints/context' +import { WatchedPackagesHelper } from '../blueprints/context/watchedPackages' +import { handleUpdatedRundown } from './ingestRundownJobs' +import type { + IShowStyleUserContext, + IBlueprintShowStyleVariant, + IngestRundown, +} from '@sofie-automation/blueprints-integration' +import { logger } from '../logging' + +export async function handleCreateAdlibTestingRundownForShowStyleVariant( + context: JobContext, + data: CreateAdlibTestingRundownForShowStyleVariantProps +): Promise { + const showStyleVariant = await context.getShowStyleVariant(data.showStyleVariantId) + const showStyleCompound = await context.getShowStyleCompound(showStyleVariant._id) + const showStyleBlueprint = await context.getShowStyleBlueprint(showStyleCompound._id) + + const generateAdlibTestingIngestRundown = + showStyleBlueprint.blueprint.generateAdlibTestingIngestRundown || fallbackBlueprintMethod + const blueprintContext = new ShowStyleUserContext( + { + name: `Create Adlib Testing Rundown`, + identifier: `studioId=${context.studioId},showStyleBaseId=${showStyleCompound._id},showStyleVariantId=${showStyleCompound.showStyleVariantId}`, + tempSendUserNotesIntoBlackHole: true, // TODO-CONTEXT + }, + context, + showStyleCompound, + WatchedPackagesHelper.empty(context) // No packages to provide here, as this is before there is a rundown + ) + + const ingestRundown = await Promise.resolve() + .then(async () => + generateAdlibTestingIngestRundown(blueprintContext, convertShowStyleVariantToBlueprints(showStyleVariant)) + ) + .catch(async (e) => { + throw UserError.from(e, UserErrorMessage.AdlibTestingRundownsGenerationFailed, { message: e.toString() }) + }) + + // Prefix the externalId to avoid conflicts with real rundowns, and ensure it has a sensible value + ingestRundown.externalId = `testing:${ingestRundown.externalId || showStyleVariant._id}` + + logger.info( + `Creating adlib testing rundown "${ingestRundown.name}" for showStyleVariant "${showStyleVariant.name}"` + ) + + return handleUpdatedRundown(context, { + rundownExternalId: ingestRundown.externalId, + ingestRundown, + isCreateAction: true, + rundownSource: { + type: 'testing', + showStyleVariantId: showStyleVariant._id, + }, + }) +} + +function fallbackBlueprintMethod( + _context: IShowStyleUserContext, + showStyleVariant: IBlueprintShowStyleVariant +): IngestRundown { + return { + externalId: '', + name: `Rehearsal: ${showStyleVariant.name}`, + type: 'rehearsal', + payload: {}, + segments: [], // No contents + } +} diff --git a/packages/job-worker/src/ingest/generationRundown.ts b/packages/job-worker/src/ingest/generationRundown.ts index 8918f1323e..f41ed7db49 100644 --- a/packages/job-worker/src/ingest/generationRundown.ts +++ b/packages/job-worker/src/ingest/generationRundown.ts @@ -64,7 +64,12 @@ export async function updateRundownFromIngestData( context.getStudioBlueprintConfig() ) // TODO-CONTEXT save any user notes from selectShowStyleContext - const showStyle = await selectShowStyleVariant(context, selectShowStyleContext, extendedIngestRundown) + const showStyle = await selectShowStyleVariant( + context, + selectShowStyleContext, + extendedIngestRundown, + rundownSource + ) if (!showStyle) { logger.debug('Blueprint rejected the rundown') throw new Error('Blueprint rejected the rundown') @@ -149,7 +154,12 @@ export async function updateRundownMetadataFromIngestData( ) // TODO-CONTEXT save any user notes from selectShowStyleContext - const showStyle = await selectShowStyleVariant(context, selectShowStyleContext, extendedIngestRundown) + const showStyle = await selectShowStyleVariant( + context, + selectShowStyleContext, + extendedIngestRundown, + rundownSource + ) if (!showStyle) { logger.debug('Blueprint rejected the rundown') throw new Error('Blueprint rejected the rundown') diff --git a/packages/job-worker/src/ingest/ingestPartJobs.ts b/packages/job-worker/src/ingest/ingestPartJobs.ts index 3267e460d0..2886deeb24 100644 --- a/packages/job-worker/src/ingest/ingestPartJobs.ts +++ b/packages/job-worker/src/ingest/ingestPartJobs.ts @@ -9,7 +9,7 @@ import { IngestRemovePartProps, IngestUpdatePartProps } from '@sofie-automation/ * Remove a Part from a Segment */ export async function handleRemovedPart(context: JobContext, data: IngestRemovePartProps): Promise { - return runIngestJob( + await runIngestJob( context, data, (ingestRundown) => { @@ -41,7 +41,7 @@ export async function handleRemovedPart(context: JobContext, data: IngestRemoveP * Insert or update a Part in a Segment */ export async function handleUpdatedPart(context: JobContext, data: IngestUpdatePartProps): Promise { - return runIngestJob( + await runIngestJob( context, data, (ingestRundown) => { diff --git a/packages/job-worker/src/ingest/ingestRundownJobs.ts b/packages/job-worker/src/ingest/ingestRundownJobs.ts index 394af550b3..12c13dfb82 100644 --- a/packages/job-worker/src/ingest/ingestRundownJobs.ts +++ b/packages/job-worker/src/ingest/ingestRundownJobs.ts @@ -2,7 +2,7 @@ import { JobContext } from '../jobs' import { logger } from '../logging' import { updateRundownFromIngestData, updateRundownMetadataFromIngestData } from './generationRundown' import { makeNewIngestRundown } from './ingestCache' -import { canRundownBeUpdated } from './lib' +import { canRundownBeUpdated, getRundownId } from './lib' import { CommitIngestData, runIngestJob, runWithRundownLock, UpdateIngestRundownAction } from './lock' import { removeRundownFromDb } from '../rundownPlaylists' import { literal } from '@sofie-automation/corelib/dist/lib' @@ -16,12 +16,13 @@ import { UserUnsyncRundownProps, } from '@sofie-automation/corelib/dist/worker/ingest' import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/error' +import { RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' /** * Attempt to remove a rundown, or orphan it */ export async function handleRemovedRundown(context: JobContext, data: IngestRemoveRundownProps): Promise { - return runIngestJob( + await runIngestJob( context, data, () => { @@ -49,24 +50,19 @@ export async function handleRemovedRundown(context: JobContext, data: IngestRemo * User requested removing a rundown */ export async function handleUserRemoveRundown(context: JobContext, data: UserRemoveRundownProps): Promise { - /** - * As the user requested a delete, it may not be from an ingest gateway, and have a bad relationship between _id and externalId. - * Because of this, we must do some more manual steps to ensure it is done correctly, and with as close to correct locking as is reasonable - */ const tmpRundown = await context.directCollections.Rundowns.findOne(data.rundownId) if (!tmpRundown || tmpRundown.studioId !== context.studioId) { // Either not found, or belongs to someone else return } - if (tmpRundown.source.type === 'nrcs') { - // Its a real rundown, so defer to the proper route for deletion - return handleRemovedRundown(context, { - rundownExternalId: tmpRundown.externalId, - forceDelete: data.force, - }) - } else { - // Its from a snapshot, so we need to use a lighter locking flow + if (tmpRundown._id !== getRundownId(context.studioId, tmpRundown.externalId)) { + /** + * If the rundown is not created via an ingest method, there can be a bad relationship between _id and externalId, which causes the rundown to not be found. + * This typically happens when a rundown is restored from a snapshot. + * When this happens, we need to remove the rundown directly. + */ + return runWithRundownLock(context, data.rundownId, async (rundown, lock) => { if (rundown) { // It's from a snapshot, so should be removed directly, as that means it cannot run ingest operations @@ -84,13 +80,19 @@ export async function handleUserRemoveRundown(context: JobContext, data: UserRem } } }) + } else { + // The ids match, meaning the typical ingest operation flow will work + return handleRemovedRundown(context, { + rundownExternalId: tmpRundown.externalId, + forceDelete: data.force, + }) } } /** * Insert or update a rundown with a new IngestRundown */ -export async function handleUpdatedRundown(context: JobContext, data: IngestUpdateRundownProps): Promise { +export async function handleUpdatedRundown(context: JobContext, data: IngestUpdateRundownProps): Promise { return runIngestJob( context, data, @@ -123,7 +125,7 @@ export async function handleUpdatedRundownMetaData( context: JobContext, data: IngestUpdateRundownMetaDataProps ): Promise { - return runIngestJob( + await runIngestJob( context, data, (ingestRundown) => { @@ -148,7 +150,7 @@ export async function handleUpdatedRundownMetaData( * Regnerate a Rundown from the cached IngestRundown */ export async function handleRegenerateRundown(context: JobContext, data: IngestRegenerateRundownProps): Promise { - return runIngestJob( + await runIngestJob( context, data, (ingestRundown) => { diff --git a/packages/job-worker/src/ingest/ingestSegmentJobs.ts b/packages/job-worker/src/ingest/ingestSegmentJobs.ts index 741fd0faec..bfe4fc20a1 100644 --- a/packages/job-worker/src/ingest/ingestSegmentJobs.ts +++ b/packages/job-worker/src/ingest/ingestSegmentJobs.ts @@ -20,7 +20,7 @@ import { * Regnerate a Segment from the cached IngestSegment */ export async function handleRegenerateSegment(context: JobContext, data: IngestRegenerateSegmentProps): Promise { - return runIngestJob( + await runIngestJob( context, data, (ingestRundown) => { @@ -51,7 +51,7 @@ export async function handleRegenerateSegment(context: JobContext, data: IngestR * Attempt to remove a segment, or orphan it */ export async function handleRemovedSegment(context: JobContext, data: IngestRemoveSegmentProps): Promise { - return runIngestJob( + await runIngestJob( context, data, (ingestRundown) => { @@ -98,7 +98,7 @@ export async function handleRemovedSegment(context: JobContext, data: IngestRemo */ export async function handleUpdatedSegment(context: JobContext, data: IngestUpdateSegmentProps): Promise { const segmentExternalId = data.ingestSegment.externalId - return runIngestJob( + await runIngestJob( context, data, (ingestRundown) => { @@ -128,7 +128,7 @@ export async function handleUpdatedSegmentRanks( context: JobContext, data: IngestUpdateSegmentRanksProps ): Promise { - return runIngestJob( + await runIngestJob( context, data, (ingestRundown) => { @@ -175,7 +175,7 @@ export async function handleRemoveOrphanedSegemnts( context: JobContext, data: RemoveOrphanedSegmentsProps ): Promise { - return runIngestJob( + await runIngestJob( context, data, (ingestRundown) => ingestRundown ?? UpdateIngestRundownAction.DELETE, diff --git a/packages/job-worker/src/ingest/lock.ts b/packages/job-worker/src/ingest/lock.ts index 2f209c3aa3..381c0e2ef5 100644 --- a/packages/job-worker/src/ingest/lock.ts +++ b/packages/job-worker/src/ingest/lock.ts @@ -59,7 +59,7 @@ export async function runIngestJob( newIngestRundown: LocalIngestRundown | undefined, oldIngestRundown: LocalIngestRundown | undefined ) => Promise -): Promise { +): Promise { if (!data.rundownExternalId) { throw new Error(`Job is missing rundownExternalId`) } @@ -128,6 +128,8 @@ export async function runIngestJob( } if (resultingError) throw resultingError + + return rundownId }) } diff --git a/packages/job-worker/src/ingest/mosDevice/mosRundownJobs.ts b/packages/job-worker/src/ingest/mosDevice/mosRundownJobs.ts index 666ba0ae1d..b0f9e50a90 100644 --- a/packages/job-worker/src/ingest/mosDevice/mosRundownJobs.ts +++ b/packages/job-worker/src/ingest/mosDevice/mosRundownJobs.ts @@ -27,7 +27,7 @@ export async function handleMosRundownData(context: JobContext, data: MosRundown if (parseMosString(data.mosRunningOrder.ID) !== data.rundownExternalId) throw new Error('mosRunningOrder.ID and rundownExternalId mismatch!') - return runIngestJob( + await runIngestJob( context, data, (ingestRundown) => { @@ -99,7 +99,7 @@ export async function handleMosRundownData(context: JobContext, data: MosRundown * Update the payload of a mos rundown, without changing any parts or segments */ export async function handleMosRundownMetadata(context: JobContext, data: MosRundownMetadataProps): Promise { - return runIngestJob( + await runIngestJob( context, data, (ingestRundown) => { @@ -144,7 +144,7 @@ export async function handleMosRundownStatus(context: JobContext, data: MosRundo * Update the ready to air state of a mos rundown */ export async function handleMosRundownReadyToAir(context: JobContext, data: MosRundownReadyToAirProps): Promise { - return runIngestJob( + await runIngestJob( context, data, (ingestRundown) => { diff --git a/packages/job-worker/src/ingest/mosDevice/mosStoryJobs.ts b/packages/job-worker/src/ingest/mosDevice/mosStoryJobs.ts index aaa5e50b76..2e4cac9be0 100644 --- a/packages/job-worker/src/ingest/mosDevice/mosStoryJobs.ts +++ b/packages/job-worker/src/ingest/mosDevice/mosStoryJobs.ts @@ -43,7 +43,7 @@ export async function handleMosFullStory(context: JobContext, data: MosFullStory const partExternalId = parseMosString(data.story.ID) - return runIngestJob( + await runIngestJob( context, data, (ingestRundown) => { @@ -84,7 +84,7 @@ export async function handleMosFullStory(context: JobContext, data: MosFullStory export async function handleMosDeleteStory(context: JobContext, data: MosDeleteStoryProps): Promise { if (data.stories.length === 0) return - return runIngestJob( + await runIngestJob( context, data, (ingestRundown) => { @@ -129,7 +129,7 @@ export async function handleMosDeleteStory(context: JobContext, data: MosDeleteS * Insert a mos story before the referenced existing story */ export async function handleMosInsertStories(context: JobContext, data: MosInsertStoryProps): Promise { - return runIngestJob( + await runIngestJob( context, data, (ingestRundown) => { @@ -208,7 +208,7 @@ export async function handleMosSwapStories(context: JobContext, data: MosSwapSto throw new Error(`Cannot swap part ${story0Str} with itself in rundown ${data.rundownExternalId}`) } - return runIngestJob( + await runIngestJob( context, data, (ingestRundown) => { @@ -246,7 +246,7 @@ export async function handleMosSwapStories(context: JobContext, data: MosSwapSto * Move a list of mos stories */ export async function handleMosMoveStories(context: JobContext, data: MosMoveStoryProps): Promise { - return runIngestJob( + await runIngestJob( context, data, (ingestRundown) => { diff --git a/packages/job-worker/src/ingest/packageInfo.ts b/packages/job-worker/src/ingest/packageInfo.ts index ba915032b4..8015744f02 100644 --- a/packages/job-worker/src/ingest/packageInfo.ts +++ b/packages/job-worker/src/ingest/packageInfo.ts @@ -44,7 +44,7 @@ export async function handleUpdatedPackageInfoForRundown( return } - return runIngestJob( + await runIngestJob( context, data, (ingestRundown) => { diff --git a/packages/job-worker/src/ingest/selectShowStyleVariant.ts b/packages/job-worker/src/ingest/selectShowStyleVariant.ts index 6c46b1248f..92b542628c 100644 --- a/packages/job-worker/src/ingest/selectShowStyleVariant.ts +++ b/packages/job-worker/src/ingest/selectShowStyleVariant.ts @@ -3,12 +3,12 @@ import { ShowStyleBaseId, ShowStyleVariantId } from '@sofie-automation/corelib/d import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { logger } from '../logging' import { createShowStyleCompound } from '../showStyles' -import _ = require('underscore') import { StudioUserContext } from '../blueprints/context' import { ProcessedShowStyleBase, ProcessedShowStyleVariant, JobContext, ProcessedShowStyleCompound } from '../jobs' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { ReadonlyDeep } from 'type-fest' import { convertShowStyleBaseToBlueprints, convertShowStyleVariantToBlueprints } from '../blueprints/context/lib' +import { RundownSource, RundownSourceTesting } from '@sofie-automation/corelib/dist/dataModel/Rundown' export interface SelectedShowStyleVariant { variant: ReadonlyDeep @@ -26,16 +26,57 @@ export interface SelectedShowStyleVariant { export async function selectShowStyleVariant( context: JobContext, blueprintContext: StudioUserContext, - ingestRundown: ExtendedIngestRundown + ingestRundown: ExtendedIngestRundown, + rundownSource: RundownSource ): Promise { - const studio = blueprintContext.studio + const studio = context.studio if (!studio.supportedShowStyleBase.length) { logger.debug(`Studio "${studio._id}" does not have any supportedShowStyleBase`) return null } + if (rundownSource.type === 'testing') { + return selectShowStyleVariantFromRundownSource(context, rundownSource) + } + + return selectShowStyleVariantWithBlueprints(context, blueprintContext, ingestRundown) +} + +async function selectShowStyleVariantFromRundownSource( + context: JobContext, + rundownSource: RundownSourceTesting +): Promise { + const showStyleVariant = await context.getShowStyleVariant(rundownSource.showStyleVariantId).catch(() => null) + if (!showStyleVariant) { + logger.debug(`ShowStyleVariant "${rundownSource.showStyleVariantId}" not found`) + return null + } + + const showStyleBase = await context.getShowStyleBase(showStyleVariant.showStyleBaseId).catch(() => null) + if (!showStyleBase) { + logger.debug(`ShowStyleBase "${showStyleVariant.showStyleBaseId}" not found`) + return null + } + + const compound = createShowStyleCompound(showStyleBase, showStyleVariant) + if (!compound) throw new Error(`no showStyleCompound for "${showStyleVariant._id}"`) + + return { + variant: showStyleVariant, + base: showStyleBase, + compound, + } +} + +async function selectShowStyleVariantWithBlueprints( + context: JobContext, + blueprintContext: StudioUserContext, + ingestRundown: ExtendedIngestRundown +): Promise { + const studio = context.studio + const showStyleBases = await context.getShowStyleBases() - let showStyleBase = _.first(showStyleBases) + let showStyleBase: ReadonlyDeep | undefined = showStyleBases[0] if (!showStyleBase) { logger.debug( `No showStyleBases matching with supportedShowStyleBase [${studio.supportedShowStyleBase}] from studio "${studio._id}"` @@ -64,7 +105,7 @@ export async function selectShowStyleVariant( logger.debug(`StudioBlueprint for studio "${studio._id}" returned showStyleId = null`) return null } - showStyleBase = _.find(showStyleBases, (s) => s._id === showStyleId) + showStyleBase = showStyleBases.find((s) => s._id === showStyleId) if (!showStyleBase) { logger.debug( `No ShowStyleBase found matching showStyleId "${showStyleId}", from studio "${studio._id}" blueprint` @@ -96,8 +137,13 @@ export async function selectShowStyleVariant( logger.debug(`StudioBlueprint for studio "${studio._id}" returned variantId = null in .getShowStyleVariantId`) return null } else { - const showStyleVariant = _.find(showStyleVariants, (s) => s._id === variantId) - if (!showStyleVariant) throw new Error(`Blueprint returned variantId "${variantId}", which was not found!`) + const showStyleVariant = showStyleVariants.find((s) => s._id === variantId) + if (!showStyleVariant) { + logger.debug( + `No ShowStyleVariant found matching showStyleId "${variantId}", from studio "${studio._id}" blueprint` + ) + return null + } const compound = createShowStyleCompound(showStyleBase, showStyleVariant) if (!compound) throw new Error(`no showStyleCompound for "${showStyleVariant._id}"`) diff --git a/packages/job-worker/src/rundown.ts b/packages/job-worker/src/rundown.ts index 549eec73cd..ec50085bb6 100644 --- a/packages/job-worker/src/rundown.ts +++ b/packages/job-worker/src/rundown.ts @@ -14,8 +14,8 @@ export function allowedToMoveRundownOutOfPlaylist( if (!playlist.activationId) return true - return ( - !playlist.activationId || - (playlist.currentPartInfo?.rundownId !== rundown._id && playlist.nextPartInfo?.rundownId !== rundown._id) - ) + // Can't delete last rundown in playlist when it is active + if (playlist.rundownIdsInOrder.length === 1) return false + + return playlist.currentPartInfo?.rundownId !== rundown._id && playlist.nextPartInfo?.rundownId !== rundown._id } diff --git a/packages/job-worker/src/workers/ingest/jobs.ts b/packages/job-worker/src/workers/ingest/jobs.ts index c5360ae7b1..1fa5a5b922 100644 --- a/packages/job-worker/src/workers/ingest/jobs.ts +++ b/packages/job-worker/src/workers/ingest/jobs.ts @@ -39,6 +39,7 @@ import { handleBucketRemoveAdlibPiece, } from '../../ingest/bucket/bucketAdlibs' import { handleBucketItemImport, handleBucketItemRegenerate } from '../../ingest/bucket/import' +import { handleCreateAdlibTestingRundownForShowStyleVariant } from '../../ingest/createAdlibTestingRundown' type ExecutableFunction = ( context: JobContext, @@ -87,4 +88,6 @@ export const ingestJobHandlers: IngestJobHandlers = { [IngestJobs.BucketRemoveAdlibPiece]: handleBucketRemoveAdlibPiece, [IngestJobs.BucketRemoveAdlibAction]: handleBucketRemoveAdlibAction, [IngestJobs.BucketEmpty]: handleBucketEmpty, + + [IngestJobs.CreateAdlibTestingRundownForShowStyleVariant]: handleCreateAdlibTestingRundownForShowStyleVariant, } From 1aca769f021edd3f3577174b226e2c7abf756635 Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Tue, 25 Jun 2024 13:02:00 +0000 Subject: [PATCH 374/479] chore: bump mos-connection --- packages/mos-gateway/package.json | 2 +- packages/yarn.lock | 33 +++++++++++++++++++------------ 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index 7946085e7d..ebc8f1b64b 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -65,7 +65,7 @@ "production" ], "dependencies": { - "@mos-connection/connector": "^4.1.0", + "@mos-connection/connector": "4.1.1-nightly-master-20240430-072032-ffb8bf6.0", "@sofie-automation/server-core-integration": "1.51.0-in-development", "@sofie-automation/shared-lib": "1.51.0-in-development", "tslib": "^2.6.2", diff --git a/packages/yarn.lock b/packages/yarn.lock index f07ad0122c..3b35626b65 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -4447,34 +4447,41 @@ __metadata: languageName: node linkType: hard -"@mos-connection/connector@npm:^4.1.0": - version: 4.1.0 - resolution: "@mos-connection/connector@npm:4.1.0" +"@mos-connection/connector@npm:4.1.1-nightly-master-20240430-072032-ffb8bf6.0": + version: 4.1.1-nightly-master-20240430-072032-ffb8bf6.0 + resolution: "@mos-connection/connector@npm:4.1.1-nightly-master-20240430-072032-ffb8bf6.0" dependencies: - "@mos-connection/helper": 4.1.0 - "@mos-connection/model": 4.1.0 + "@mos-connection/helper": 4.1.1-nightly-master-20240430-072032-ffb8bf6.0 + "@mos-connection/model": 4.1.1-nightly-master-20240430-072032-ffb8bf6.0 iconv-lite: ^0.6.3 tslib: ^2.5.3 xml-js: ^1.6.11 xmlbuilder: ^15.1.1 - checksum: 05a187b61aaa143b647afa51cd7e8632ae5a1843ecfd02ccce53f78647dfacd74267a836f4c47237e716c204df177e012461c366006b2c204973e81f3b2355e9 + checksum: 5a1879e940867380a7fea443c71cb220ad4e932c8acfa34ae1cb0a2ade471d16f44b72293d604a8aa4b227e356b7f3dfb520f822f6e330ab5bc9e48fbfefcab3 languageName: node linkType: hard -"@mos-connection/helper@npm:4.1.0": - version: 4.1.0 - resolution: "@mos-connection/helper@npm:4.1.0" +"@mos-connection/helper@npm:4.1.1-nightly-master-20240430-072032-ffb8bf6.0": + version: 4.1.1-nightly-master-20240430-072032-ffb8bf6.0 + resolution: "@mos-connection/helper@npm:4.1.1-nightly-master-20240430-072032-ffb8bf6.0" dependencies: - "@mos-connection/model": 4.1.0 + "@mos-connection/model": 4.1.1-nightly-master-20240430-072032-ffb8bf6.0 iconv-lite: ^0.6.3 tslib: ^2.5.3 xml-js: ^1.6.11 xmlbuilder: ^15.1.1 - checksum: 46df4a6bb603da9d8759096f3c262d5353534f81978bfabeb3320a78af92242e86985b11bd00d407c1bf027b3fd3916eaa0f79ac9453959d7afaafbd185fba74 + checksum: dcf7d5e800d08c007a4026b37ddcc6ebe77d020a311809bc379a5c7d15657c2d447b4149682aec17161d17217e2c6d5167c7ef81e954a4ed5a6283a7c75a7e0a + languageName: node + linkType: hard + +"@mos-connection/model@npm:4.1.1-nightly-master-20240430-072032-ffb8bf6.0": + version: 4.1.1-nightly-master-20240430-072032-ffb8bf6.0 + resolution: "@mos-connection/model@npm:4.1.1-nightly-master-20240430-072032-ffb8bf6.0" + checksum: 07bd3ffc94a8c140d994de445c34158bb9f5f8950f6cea34d59657fe4da1f0f6b5d0c2c7ef1b2ad902b43c846485ab10cb9f19d707233b875fe739d731151bdc languageName: node linkType: hard -"@mos-connection/model@npm:4.1.0, @mos-connection/model@npm:^4.1.0": +"@mos-connection/model@npm:^4.1.0": version: 4.1.0 resolution: "@mos-connection/model@npm:4.1.0" checksum: b8e1f83d87c342959165956c19b4a8d03f2c7ae0b1caf02cd00451deb977c0c5ea4ad5b9c8915ba0cea199ce071efbbcfe2329b5e737f89f1725820740d12f60 @@ -18716,7 +18723,7 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "mos-gateway@workspace:mos-gateway" dependencies: - "@mos-connection/connector": ^4.1.0 + "@mos-connection/connector": 4.1.1-nightly-master-20240430-072032-ffb8bf6.0 "@sofie-automation/server-core-integration": 1.51.0-in-development "@sofie-automation/shared-lib": 1.51.0-in-development tslib: ^2.6.2 From a0ea9b48997fe39e8db5ababef744c103a68509f Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 26 Jun 2024 01:26:56 +0200 Subject: [PATCH 375/479] fix(LSG): Token "examples" does not exist when running `yarn gendocs` --- .../live-status-gateway/api/schemas/adLibs.yaml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/live-status-gateway/api/schemas/adLibs.yaml b/packages/live-status-gateway/api/schemas/adLibs.yaml index b98a84e17f..5cb6d6df69 100644 --- a/packages/live-status-gateway/api/schemas/adLibs.yaml +++ b/packages/live-status-gateway/api/schemas/adLibs.yaml @@ -30,7 +30,7 @@ $defs: adLibs: $ref: '#/$defs/adLib/examples' globalAdLibs: - $ref: '#/$defs/adLib/examples' + $ref: '#/$defs/adLibBase/examples' adLib: allOf: - $ref: '#/$defs/adLibBase' @@ -41,6 +41,18 @@ $defs: partId: description: Unique id of the part this adLib belongs to required: [segmentId, partId] + examples: + - id: 'C6K_yIMuGFUk8X_L9A9_jRT6aq4_' + name: Music video clip + sourceLayer: Video Clip + actionType: + - name: pvw + label: Preview + tags: ['music_video'] + segmentId: 'n1mOVd5_K5tt4sfk6HYfTuwumGQ_' + partId: 'H5CBGYjThrMSmaYvRaa5FVKJIzk_' + publicData: + fileName: MV000123.mxf globalAdLib: $ref: '#/$defs/adLibBase' adLibBase: From 87065d7af229cd2e3bf1db1f9217d6d8b8945fee Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 26 Jun 2024 01:26:56 +0200 Subject: [PATCH 376/479] fix(LSG): Token "examples" does not exist when running `yarn gendocs` (cherry picked from commit a0ea9b48997fe39e8db5ababef744c103a68509f) --- .../live-status-gateway/api/schemas/adLibs.yaml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/live-status-gateway/api/schemas/adLibs.yaml b/packages/live-status-gateway/api/schemas/adLibs.yaml index b98a84e17f..5cb6d6df69 100644 --- a/packages/live-status-gateway/api/schemas/adLibs.yaml +++ b/packages/live-status-gateway/api/schemas/adLibs.yaml @@ -30,7 +30,7 @@ $defs: adLibs: $ref: '#/$defs/adLib/examples' globalAdLibs: - $ref: '#/$defs/adLib/examples' + $ref: '#/$defs/adLibBase/examples' adLib: allOf: - $ref: '#/$defs/adLibBase' @@ -41,6 +41,18 @@ $defs: partId: description: Unique id of the part this adLib belongs to required: [segmentId, partId] + examples: + - id: 'C6K_yIMuGFUk8X_L9A9_jRT6aq4_' + name: Music video clip + sourceLayer: Video Clip + actionType: + - name: pvw + label: Preview + tags: ['music_video'] + segmentId: 'n1mOVd5_K5tt4sfk6HYfTuwumGQ_' + partId: 'H5CBGYjThrMSmaYvRaa5FVKJIzk_' + publicData: + fileName: MV000123.mxf globalAdLib: $ref: '#/$defs/adLibBase' adLibBase: From 0dd6a1f61be57f0177b0b1953c489ee9c28f9215 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Mon, 24 Jun 2024 10:57:46 +0200 Subject: [PATCH 377/479] fix: refactor VirtualElement to be a FC (cherry picked from commit bf81baf9ff520ad6a9fc9b6378ce6ceeac320645) --- meteor/client/lib/VirtualElement.tsx | 309 +++++++++++++-------------- meteor/package.json | 4 +- meteor/yarn.lock | 14 +- tsconfig.json | 3 + 4 files changed, 161 insertions(+), 169 deletions(-) create mode 100644 tsconfig.json diff --git a/meteor/client/lib/VirtualElement.tsx b/meteor/client/lib/VirtualElement.tsx index d88c148088..1b825b7292 100644 --- a/meteor/client/lib/VirtualElement.tsx +++ b/meteor/client/lib/VirtualElement.tsx @@ -1,29 +1,6 @@ -import * as React from 'react' +import React, { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react' import { InView } from 'react-intersection-observer' -export interface IProps { - initialShow?: boolean - placeholderHeight?: number - _debug?: boolean - placeholderClassName?: string - width?: string | number - margin?: string - id?: string | undefined - className?: string -} - -declare global { - interface Window { - requestIdleCallback( - callback: Function, - options?: { - timeout: number - } - ): number - cancelIdleCallback(callback: number): void - } -} - interface IElementMeasurements { width: string | number clientHeight: number @@ -34,160 +11,172 @@ interface IElementMeasurements { id: string | undefined } -interface IState extends IElementMeasurements { - inView: boolean - isMeasured: boolean -} - const OPTIMIZE_PERIOD = 5000 +const IDLE_CALLBACK_TIMEOUT = 100 + /** * This is a component that allows optimizing the amount of elements present in the DOM through replacing them * with placeholders when they aren't visible in the viewport. * * @export - * @class VirtualElement - * @extends {React.Component} + * @param {(React.PropsWithChildren<{ + * initialShow?: boolean + * placeholderHeight?: number + * _debug?: boolean + * placeholderClassName?: string + * width?: string | number + * margin?: string + * id?: string | undefined + * className?: string + * }>)} { + * initialShow, + * placeholderHeight, + * placeholderClassName, + * width, + * margin, + * id, + * className, + * children, + * } + * @return {*} {(JSX.Element | null)} */ -export class VirtualElement extends React.Component, IState> { - private el: HTMLElement | null = null - private instance: HTMLElement | null = null - private optimizeTimeout: NodeJS.Timer | null = null - private refreshSizingTimeout: NodeJS.Timer | null = null - private styleObj: CSSStyleDeclaration | undefined - - constructor(props: IProps) { - super(props) - this.state = { - inView: props.initialShow || false, - isMeasured: false, - clientHeight: 0, - width: 'auto', - marginBottom: undefined, - marginTop: undefined, - marginLeft: undefined, - marginRight: undefined, - id: undefined, +export function VirtualElement({ + initialShow, + placeholderHeight, + placeholderClassName, + width, + margin, + id, + className, + children, +}: React.PropsWithChildren<{ + initialShow?: boolean + placeholderHeight?: number + _debug?: boolean + placeholderClassName?: string + width?: string | number + margin?: string + id?: string | undefined + className?: string +}>): JSX.Element | null { + const [inView, setInView] = useState(initialShow ?? false) + const [isShowingChildren, setIsShowingChildren] = useState(inView) + const [measurements, setMeasurements] = useState(null) + const [ref, setRef] = useState(null) + const [childRef, setChildRef] = useState(null) + + const isMeasured = !!measurements + + const styleObj = useMemo( + () => ({ + width: width ?? measurements?.width ?? 'auto', + height: (measurements?.clientHeight ?? placeholderHeight ?? '0') + 'px', + marginTop: measurements?.marginTop, + marginLeft: measurements?.marginLeft, + marginRight: measurements?.marginRight, + marginBottom: measurements?.marginBottom, + }), + [width, measurements, placeholderHeight] + ) + + const onVisibleChanged = useCallback((visible: boolean) => { + setInView(visible) + }, []) + + useEffect(() => { + if (inView === true) { + setIsShowingChildren(true) + return } - } - private visibleChanged = (inView: boolean) => { - this.props._debug && console.log(this.props.id, 'Changed', inView) - if (this.optimizeTimeout) { - clearTimeout(this.optimizeTimeout) - this.optimizeTimeout = null - } - if (inView && !this.state.inView) { - this.setState({ - inView, - }) - } else if (!inView && this.state.inView) { - this.optimizeTimeout = setTimeout(() => { - this.optimizeTimeout = null - const measurements = this.measureElement() || undefined - this.setState({ - inView, - - isMeasured: measurements ? true : false, - ...measurements, - } as IState) - }, OPTIMIZE_PERIOD) - } - } + let idleCallback: number | undefined + const optimizeTimeout = window.setTimeout(() => { + idleCallback = window.requestIdleCallback( + () => { + if (childRef) { + setMeasurements(measureElement(childRef)) + } + setIsShowingChildren(false) + }, + { + timeout: IDLE_CALLBACK_TIMEOUT, + } + ) + }, OPTIMIZE_PERIOD) - private measureElement = (): IElementMeasurements | null => { - if (this.el) { - const style = this.styleObj || window.getComputedStyle(this.el) - this.styleObj = style - this.props._debug && console.log(this.props.id, 'Re-measuring child', this.el.clientHeight) - - return { - width: style.width || 'auto', - clientHeight: this.el.clientHeight, - marginTop: style.marginTop || undefined, - marginBottom: style.marginBottom || undefined, - marginLeft: style.marginLeft || undefined, - marginRight: style.marginRight || undefined, - id: this.el.id, + return () => { + if (idleCallback) { + window.cancelIdleCallback(idleCallback) } - } - - return null - } - private refreshSizing = () => { - this.refreshSizingTimeout = null - const measurements = this.measureElement() - if (measurements) { - this.setState({ - isMeasured: true, - ...measurements, - }) + window.clearTimeout(optimizeTimeout) } - } + }, [childRef, inView]) - private findChildElement = () => { - if (!this.el || !this.el.parentElement) { - const el = this.instance ? (this.instance.firstElementChild as HTMLElement) : null - if (el && !el.classList.contains('virtual-element-placeholder')) { - this.el = el - this.styleObj = undefined - this.refreshSizingTimeout = setTimeout(this.refreshSizing, 250) - } - } - } + const showPlaceholder = !isShowingChildren && (!initialShow || isMeasured) - private setRef = (instance: HTMLElement | null) => { - this.instance = instance - this.findChildElement() - } + useLayoutEffect(() => { + if (!ref || showPlaceholder) return - componentDidUpdate(_: IProps, prevState: IState): void { - if (this.state.inView && prevState.inView !== this.state.inView) { - this.findChildElement() - } - } + const el = ref?.firstElementChild + if (!el || el.classList.contains('virtual-element-placeholder') || !(el instanceof HTMLElement)) return - componentWillUnmount(): void { - if (this.optimizeTimeout) clearTimeout(this.optimizeTimeout) - if (this.refreshSizingTimeout) clearTimeout(this.refreshSizingTimeout) - } + setChildRef(el) - render(): JSX.Element { - this.props._debug && - console.log( - this.props.id, - this.state.inView, - this.props.initialShow, - this.state.isMeasured, - !this.state.inView && (!this.props.initialShow || this.state.isMeasured) + let idleCallback: number | undefined + const refreshSizingTimeout = window.setTimeout(() => { + idleCallback = window.requestIdleCallback( + () => { + setMeasurements(measureElement(el)) + }, + { + timeout: IDLE_CALLBACK_TIMEOUT, + } ) - return ( - -
- {!this.state.inView && (!this.props.initialShow || this.state.isMeasured) ? ( -
- ) : ( - this.props.children - )} -
-
- ) + }, 1000) + + return () => { + if (idleCallback) { + window.cancelIdleCallback(idleCallback) + } + window.clearTimeout(refreshSizingTimeout) + } + }, [ref, showPlaceholder]) + + return ( + +
+ {showPlaceholder ? ( +
+ ) : ( + children + )} +
+
+ ) +} + +function measureElement(el: HTMLElement): IElementMeasurements | null { + const style = window.getComputedStyle(el) + const clientRect = el.getBoundingClientRect() + + return { + width: style.width || 'auto', + clientHeight: clientRect.height, + marginTop: style.marginTop || undefined, + marginBottom: style.marginBottom || undefined, + marginLeft: style.marginLeft || undefined, + marginRight: style.marginRight || undefined, + id: el.id, } } diff --git a/meteor/package.json b/meteor/package.json index 263cd5f550..7f0a19a370 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -94,7 +94,7 @@ "react-focus-bounder": "^1.1.6", "react-hotkeys": "^2.0.0", "react-i18next": "^11.18.6", - "react-intersection-observer": "^9.6.0", + "react-intersection-observer": "^9.10.3", "react-moment": "^0.9.7", "react-popper": "^2.3.0", "react-router-dom": "^5.3.4", @@ -207,4 +207,4 @@ "@sofie-automation/shared-lib": "portal:../packages/shared-lib" }, "packageManager": "yarn@3.5.0" -} +} \ No newline at end of file diff --git a/meteor/yarn.lock b/meteor/yarn.lock index b64f2c41bf..1ab50d7512 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -2786,7 +2786,7 @@ __metadata: react-focus-bounder: ^1.1.6 react-hotkeys: ^2.0.0 react-i18next: ^11.18.6 - react-intersection-observer: ^9.6.0 + react-intersection-observer: ^9.10.3 react-moment: ^0.9.7 react-popper: ^2.3.0 react-router-dom: ^5.3.4 @@ -10149,16 +10149,16 @@ __metadata: languageName: node linkType: hard -"react-intersection-observer@npm:^9.6.0": - version: 9.6.0 - resolution: "react-intersection-observer@npm:9.6.0" +"react-intersection-observer@npm:^9.10.3": + version: 9.10.3 + resolution: "react-intersection-observer@npm:9.10.3" peerDependencies: - react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 - react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: react-dom: optional: true - checksum: fba57f1601b6c08ea0345de23c4e41b1e0042834dbef62fa419fc18952f9ceb577d180ef3d8c8e6b01c3d5cfc3265ae5a007a532892c376461bf655b0c764ef7 + checksum: 482c89a432e582749f3cb3dd696e08638a92e41fbcb81bcb3dc3cadebcf8b40bc47e7a52d2a7e8c4f9eb2a3c1c29b4cb0f21007c1540da05893b5abb11d7a761 languageName: node linkType: hard diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..4086555c7b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./meteor/tsconfig.json", +} \ No newline at end of file From 53bea2a75f366f0c2ef487da13195fb6f7f70a36 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 26 Jun 2024 16:19:35 +0200 Subject: [PATCH 378/479] fix(LSG): make AsyncAPI stuff build again --- meteor/package.json | 2 +- .../live-status-gateway/api/asyncapi.yaml | 24 +++++- .../api/schemas/adLibs.yaml | 24 +++--- packages/live-status-gateway/package.json | 6 +- packages/yarn.lock | 86 +++++++++++++------ 5 files changed, 100 insertions(+), 42 deletions(-) diff --git a/meteor/package.json b/meteor/package.json index 7f0a19a370..8e6feb2938 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -207,4 +207,4 @@ "@sofie-automation/shared-lib": "portal:../packages/shared-lib" }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} diff --git a/packages/live-status-gateway/api/asyncapi.yaml b/packages/live-status-gateway/api/asyncapi.yaml index 1f7d363471..52dd541133 100644 --- a/packages/live-status-gateway/api/asyncapi.yaml +++ b/packages/live-status-gateway/api/asyncapi.yaml @@ -1,4 +1,5 @@ -asyncapi: 2.5.0 +asyncapi: 2.6.0 +id: 'urn:nrk:no:sofie:live-status-gateway:server' info: title: Sofie Live Status Service description: This service provides subscriptions for status updates from Sofie @@ -6,6 +7,9 @@ info: license: name: MIT License url: http://opensource.org/licenses/MIT + contact: + name: Sofie TV Automation - NRK Team + url: https://github.com/nrkno/sofie-core/issues servers: development: @@ -14,6 +18,13 @@ servers: protocol: ws protocolVersion: '13' +defaultContentType: application/json + +tags: + - name: sofie + - name: tv-automation + - name: broadcast + channels: /: publish: @@ -41,6 +52,7 @@ components: messages: ping: name: ping + messageId: ping summary: Ping server to determine whether connection is alive description: Client can ping server to determine whether connection is alive, server responds with pong. This is an application level ping as opposed to default ping in websockets standard which is server initiated payload: @@ -49,17 +61,20 @@ components: $ref: '#/components/messages/pong' pong: name: pong + messageId: pong summary: Pong is a response to ping message description: Server pong response to a ping to determine whether connection is alive. This is an application level pong as opposed to default pong in websockets standard which is sent by client in response to a ping payload: $ref: './schemas/root.yaml#/$defs/pong' heartbeat: name: heartbeat + messageId: heartbeat description: Server heartbeat sent if no subscription traffic within 1 second (approximately) payload: $ref: './schemas/root.yaml#/$defs/heartbeat' subscribe: name: subscribe + messageId: subscribe description: Subscribe to a topic payload: $ref: './schemas/root.yaml#/$defs/subscribe' @@ -67,38 +82,45 @@ components: $ref: './schemas/root.yaml#/$defs/subscriptionStatus' unsubscribe: name: unsubscribe + messageId: unsubscribe description: Unsubscribe from a topic payload: $ref: './schemas/root.yaml#/$defs/unsubscribe' x-response: $ref: './schemas/root.yaml#/$defs/subscriptionStatus' subscriptionStatus: + messageId: subscriptionStatus description: Subscription status response to subscribe or unsubscribe payload: $ref: './schemas/root.yaml#/$defs/subscriptionStatus' studio: name: studio + messageId: studioUpdate description: Studio status payload: $ref: './schemas/studio.yaml#/$defs/studio' activePlaylist: name: activePlaylist + messageId: activePlaylistUpdate description: Active Playlist status payload: $ref: './schemas/activePlaylist.yaml#/$defs/activePlaylist' activePieces: name: activePieces + messageId: activePiecesUpdate summary: Active Pieces status description: Pieces from the active Playlist that are currently active (on air) payload: $ref: './schemas/activePieces.yaml#/$defs/activePieces' segments: name: segments + messageId: segmentsUpdate description: Segments in active Playlist payload: $ref: './schemas/segments.yaml#/$defs/segments' adLibs: name: adLibs + messageId: adLibsUpdate description: AdLibs in active Playlist payload: $ref: './schemas/adLibs.yaml#/$defs/adLibs' diff --git a/packages/live-status-gateway/api/schemas/adLibs.yaml b/packages/live-status-gateway/api/schemas/adLibs.yaml index 5cb6d6df69..b69918ae1f 100644 --- a/packages/live-status-gateway/api/schemas/adLibs.yaml +++ b/packages/live-status-gateway/api/schemas/adLibs.yaml @@ -42,19 +42,20 @@ $defs: description: Unique id of the part this adLib belongs to required: [segmentId, partId] examples: - - id: 'C6K_yIMuGFUk8X_L9A9_jRT6aq4_' - name: Music video clip - sourceLayer: Video Clip - actionType: - - name: pvw - label: Preview - tags: ['music_video'] - segmentId: 'n1mOVd5_K5tt4sfk6HYfTuwumGQ_' - partId: 'H5CBGYjThrMSmaYvRaa5FVKJIzk_' - publicData: - fileName: MV000123.mxf + - id: 'C6K_yIMuGFUk8X_L9A9_jRT6aq4_' + name: Music video clip + sourceLayer: Video Clip + actionType: + - name: pvw + label: Preview + tags: ['music_video'] + segmentId: 'n1mOVd5_K5tt4sfk6HYfTuwumGQ_' + partId: 'H5CBGYjThrMSmaYvRaa5FVKJIzk_' + publicData: + fileName: MV000123.mxf globalAdLib: $ref: '#/$defs/adLibBase' + additionalProperties: false adLibBase: type: object properties: @@ -92,7 +93,6 @@ $defs: publicData: description: Optional arbitrary data required: [id, name, sourceLayer, actionType] - additionalProperties: false examples: - id: 'C6K_yIMuGFUk8X_L9A9_jRT6aq4_' name: Music video clip diff --git a/packages/live-status-gateway/package.json b/packages/live-status-gateway/package.json index 0f243e4255..50e8a2e0df 100644 --- a/packages/live-status-gateway/package.json +++ b/packages/live-status-gateway/package.json @@ -66,9 +66,9 @@ "ws": "^8.16.0" }, "devDependencies": { - "@asyncapi/generator": "^1.17.7", - "@asyncapi/html-template": "^2.1.7", - "@asyncapi/nodejs-ws-template": "^0.9.33", + "@asyncapi/generator": "^1.17.25", + "@asyncapi/html-template": "^2.3.9", + "@asyncapi/nodejs-ws-template": "^0.9.36", "type-fest": "^4.10.2" }, "lint-staged": { diff --git a/packages/yarn.lock b/packages/yarn.lock index 3b35626b65..1dea0e04c8 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -409,11 +409,11 @@ __metadata: languageName: node linkType: hard -"@asyncapi/generator-react-sdk@npm:^1.0.11, @asyncapi/generator-react-sdk@npm:^1.0.9": - version: 1.0.11 - resolution: "@asyncapi/generator-react-sdk@npm:1.0.11" +"@asyncapi/generator-react-sdk@npm:^1.0.18, @asyncapi/generator-react-sdk@npm:^1.0.20": + version: 1.0.20 + resolution: "@asyncapi/generator-react-sdk@npm:1.0.20" dependencies: - "@asyncapi/parser": ^3.0.7 + "@asyncapi/parser": ^3.1.0 "@babel/core": 7.12.9 "@babel/preset-env": ^7.12.7 "@babel/preset-react": ^7.12.7 @@ -423,16 +423,16 @@ __metadata: react: ^17.0.1 rollup: ^2.60.1 source-map-support: ^0.5.19 - checksum: b7201b8a95effa259f061fa45a7750a7527afb3969c2d23089241814ad4fcf925b8aa0b91057c2da793532f89156c880378e0d32229bcd1c7fdbc4f25544dabc + checksum: 59473d68935b939027c09da821fac794034020880dc3451c1486c63a6c486e7807abc604d97271dfcf15d4d0f060f8f119e5877c6b81b143661af77f8ad3f545 languageName: node linkType: hard -"@asyncapi/generator@npm:^1.17.7": - version: 1.17.7 - resolution: "@asyncapi/generator@npm:1.17.7" +"@asyncapi/generator@npm:^1.17.25": + version: 1.17.25 + resolution: "@asyncapi/generator@npm:1.17.25" dependencies: - "@asyncapi/generator-react-sdk": ^1.0.11 - "@asyncapi/parser": ^3.0.5 + "@asyncapi/generator-react-sdk": ^1.0.18 + "@asyncapi/parser": ^3.0.14 "@npmcli/arborist": 5.6.3 "@smoya/multi-parser": ^5.0.0 ajv: ^8.12.0 @@ -458,33 +458,33 @@ __metadata: bin: ag: cli.js asyncapi-generator: cli.js - checksum: 4e916d409a2c431848481b29a77d5c5c00489712bef19b24ad0d00df94490596212f0e8e700b2a819d5381dc3ed913ab13efff2d713ed565c45bd58a738fceeb + checksum: 8ae96b0b59d4b13fe0e2493ddd3ee2738284e0d19eb15b8b05e64be7b110871c888cf979e357b234b2b4770eae9438d30fde8425e142acc9ea6e81b3e75bfaea languageName: node linkType: hard -"@asyncapi/html-template@npm:^2.1.7": - version: 2.1.7 - resolution: "@asyncapi/html-template@npm:2.1.7" +"@asyncapi/html-template@npm:^2.3.9": + version: 2.3.9 + resolution: "@asyncapi/html-template@npm:2.3.9" dependencies: - "@asyncapi/generator-react-sdk": ^1.0.9 - "@asyncapi/parser": ^3.0.5 + "@asyncapi/generator-react-sdk": ^1.0.20 + "@asyncapi/parser": ^3.1.0 "@asyncapi/react-component": ^1.2.13 highlight.js: 10.7.3 puppeteer: ^14.1.0 react-dom: ^17.0.2 rimraf: ^3.0.2 sync-fetch: ^0.5.2 - checksum: b3cda2c6f4a9a4146f85f8a3f6f7a72720453daa5e642844433965b81fadb94d77139b4ce9291ab91c90f4859e0195f0818b6f6e281745a5dc3fbb16eb130c4c + checksum: b48a51027decb9e1ebbeaafebbc2539f7b91c2fca0b241201d7cbd7bb10cd44206ecaa756001a252599e631ae80ef7e64f4617a7fba057684db0a0547e0033f2 languageName: node linkType: hard -"@asyncapi/nodejs-ws-template@npm:^0.9.33": - version: 0.9.33 - resolution: "@asyncapi/nodejs-ws-template@npm:0.9.33" +"@asyncapi/nodejs-ws-template@npm:^0.9.36": + version: 0.9.36 + resolution: "@asyncapi/nodejs-ws-template@npm:0.9.36" dependencies: "@asyncapi/generator-filters": ^2.1.0 "@asyncapi/generator-hooks": ^0.1.0 - checksum: 847b7249223ce7733191b0ec3cd1f462e69ed55badcf32a1c6cc3a0a343864ee7f311529e4e4ce7dd0b783772ea5ab83f5e6c1a90c7c67ac413187a40703ba6c + checksum: 7bcc54c6b3066c5115ebd2fe5cac0328f52b4828e06dbf3cbbb05f3e9d10ab605de0f609ced4ac17f0d5ff18093a81c1051251f451d0e4a2378b768e01408b45 languageName: node linkType: hard @@ -501,7 +501,34 @@ __metadata: languageName: node linkType: hard -"@asyncapi/parser@npm:^3.0.5, @asyncapi/parser@npm:^3.0.7, parserapiv3@npm:@asyncapi/parser@^3.0.7": +"@asyncapi/parser@npm:^3.0.14, @asyncapi/parser@npm:^3.1.0": + version: 3.1.0 + resolution: "@asyncapi/parser@npm:3.1.0" + dependencies: + "@asyncapi/specs": ^6.7.1 + "@openapi-contrib/openapi-schema-to-json-schema": ~3.2.0 + "@stoplight/json": ^3.20.2 + "@stoplight/json-ref-readers": ^1.2.2 + "@stoplight/json-ref-resolver": ^3.1.5 + "@stoplight/spectral-core": ^1.16.1 + "@stoplight/spectral-functions": ^1.7.2 + "@stoplight/spectral-parsers": ^1.0.2 + "@stoplight/spectral-ref-resolver": ^1.0.3 + "@stoplight/types": ^13.12.0 + "@types/json-schema": ^7.0.11 + "@types/urijs": ^1.19.19 + ajv: ^8.11.0 + ajv-errors: ^3.0.0 + ajv-formats: ^2.1.1 + avsc: ^5.7.5 + js-yaml: ^4.1.0 + jsonpath-plus: ^7.2.0 + node-fetch: 2.6.7 + checksum: fcec3ecae6e4ceeed3814d7c0456b5189e5ba350a53c9cdacb7b9963637d42aba51606d2b9e4f924c64a624b4250dd2a6d457138a9742cc95280ce439f0f477d + languageName: node + linkType: hard + +"@asyncapi/parser@npm:^3.0.7, parserapiv3@npm:@asyncapi/parser@^3.0.7": version: 3.0.7 resolution: "@asyncapi/parser@npm:3.0.7" dependencies: @@ -589,6 +616,15 @@ __metadata: languageName: node linkType: hard +"@asyncapi/specs@npm:^6.7.1": + version: 6.7.1 + resolution: "@asyncapi/specs@npm:6.7.1" + dependencies: + "@types/json-schema": ^7.0.11 + checksum: be09a276552f27fde349977e465555d961a6a27b369655c68ef31679bc921f4d492a438bb836160eec05ba78adfed9661e0a3ac39c739348f29036a8d62e64b7 + languageName: node + linkType: hard + "@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.0, @babel/code-frame@npm:^7.23.5, @babel/code-frame@npm:^7.8.3": version: 7.23.5 resolution: "@babel/code-frame@npm:7.23.5" @@ -16645,9 +16681,9 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "live-status-gateway@workspace:live-status-gateway" dependencies: - "@asyncapi/generator": ^1.17.7 - "@asyncapi/html-template": ^2.1.7 - "@asyncapi/nodejs-ws-template": ^0.9.33 + "@asyncapi/generator": ^1.17.25 + "@asyncapi/html-template": ^2.3.9 + "@asyncapi/nodejs-ws-template": ^0.9.36 "@sofie-automation/blueprints-integration": 1.51.0-in-development "@sofie-automation/corelib": 1.51.0-in-development "@sofie-automation/server-core-integration": 1.51.0-in-development From 7949020bfcb615d9033d0e8ba66b729238ee4f4e Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 26 Jun 2024 16:31:57 +0200 Subject: [PATCH 379/479] chore: synchronize package versions --- meteor/package.json | 2 +- meteor/yarn.lock | 22 +++++++++++----------- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 11 ++--------- 4 files changed, 15 insertions(+), 22 deletions(-) diff --git a/meteor/package.json b/meteor/package.json index 8e6feb2938..34cabd8ce9 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -48,7 +48,7 @@ "@jstarpl/react-contextmenu": "^2.15.0", "@koa/cors": "^5.0.0", "@koa/router": "^12.0.1", - "@mos-connection/helper": "^4.1.0", + "@mos-connection/helper": "^4.1.1-nightly-master-20240430-072032-ffb8bf6.0", "@nrk/core-icons": "^9.6.0", "@popperjs/core": "^2.11.8", "@slack/webhook": "^6.1.0", diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 1ab50d7512..4216c09816 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1020,23 +1020,23 @@ __metadata: languageName: node linkType: hard -"@mos-connection/helper@npm:^4.1.0": - version: 4.1.0 - resolution: "@mos-connection/helper@npm:4.1.0" +"@mos-connection/helper@npm:^4.1.1-nightly-master-20240430-072032-ffb8bf6.0": + version: 4.1.1-nightly-master-20240430-072032-ffb8bf6.0 + resolution: "@mos-connection/helper@npm:4.1.1-nightly-master-20240430-072032-ffb8bf6.0" dependencies: - "@mos-connection/model": 4.1.0 + "@mos-connection/model": 4.1.1-nightly-master-20240430-072032-ffb8bf6.0 iconv-lite: ^0.6.3 tslib: ^2.5.3 xml-js: ^1.6.11 xmlbuilder: ^15.1.1 - checksum: 46df4a6bb603da9d8759096f3c262d5353534f81978bfabeb3320a78af92242e86985b11bd00d407c1bf027b3fd3916eaa0f79ac9453959d7afaafbd185fba74 + checksum: dcf7d5e800d08c007a4026b37ddcc6ebe77d020a311809bc379a5c7d15657c2d447b4149682aec17161d17217e2c6d5167c7ef81e954a4ed5a6283a7c75a7e0a languageName: node linkType: hard -"@mos-connection/model@npm:4.1.0, @mos-connection/model@npm:^4.1.0": - version: 4.1.0 - resolution: "@mos-connection/model@npm:4.1.0" - checksum: b8e1f83d87c342959165956c19b4a8d03f2c7ae0b1caf02cd00451deb977c0c5ea4ad5b9c8915ba0cea199ce071efbbcfe2329b5e737f89f1725820740d12f60 +"@mos-connection/model@npm:4.1.1-nightly-master-20240430-072032-ffb8bf6.0, @mos-connection/model@npm:^4.1.1-nightly-master-20240430-072032-ffb8bf6.0": + version: 4.1.1-nightly-master-20240430-072032-ffb8bf6.0 + resolution: "@mos-connection/model@npm:4.1.1-nightly-master-20240430-072032-ffb8bf6.0" + checksum: 07bd3ffc94a8c140d994de445c34158bb9f5f8950f6cea34d59657fe4da1f0f6b5d0c2c7ef1b2ad902b43c846485ab10cb9f19d707233b875fe739d731151bdc languageName: node linkType: hard @@ -1417,7 +1417,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/shared-lib@portal:../packages/shared-lib::locator=automation-core%40workspace%3A." dependencies: - "@mos-connection/model": ^4.1.0 + "@mos-connection/model": ^4.1.1-nightly-master-20240430-072032-ffb8bf6.0 timeline-state-resolver-types: 9.1.0-nightly-release51-20240228-132329-b7ceb6950.0 tslib: ^2.6.2 type-fest: ^3.13.1 @@ -2689,7 +2689,7 @@ __metadata: "@jstarpl/react-contextmenu": ^2.15.0 "@koa/cors": ^5.0.0 "@koa/router": ^12.0.1 - "@mos-connection/helper": ^4.1.0 + "@mos-connection/helper": ^4.1.1-nightly-master-20240430-072032-ffb8bf6.0 "@nrk/core-icons": ^9.6.0 "@popperjs/core": ^2.11.8 "@shopify/jest-koa-mocks": ^5.1.1 diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 8c2743d4d8..d2c4be6f57 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -38,7 +38,7 @@ "/LICENSE" ], "dependencies": { - "@mos-connection/model": "^4.1.0", + "@mos-connection/model": "^4.1.1-nightly-master-20240430-072032-ffb8bf6.0", "timeline-state-resolver-types": "9.1.0-nightly-release51-20240228-132329-b7ceb6950.0", "tslib": "^2.6.2", "type-fest": "^3.13.1" diff --git a/packages/yarn.lock b/packages/yarn.lock index 1dea0e04c8..4ba6b53efc 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -4510,20 +4510,13 @@ __metadata: languageName: node linkType: hard -"@mos-connection/model@npm:4.1.1-nightly-master-20240430-072032-ffb8bf6.0": +"@mos-connection/model@npm:4.1.1-nightly-master-20240430-072032-ffb8bf6.0, @mos-connection/model@npm:^4.1.1-nightly-master-20240430-072032-ffb8bf6.0": version: 4.1.1-nightly-master-20240430-072032-ffb8bf6.0 resolution: "@mos-connection/model@npm:4.1.1-nightly-master-20240430-072032-ffb8bf6.0" checksum: 07bd3ffc94a8c140d994de445c34158bb9f5f8950f6cea34d59657fe4da1f0f6b5d0c2c7ef1b2ad902b43c846485ab10cb9f19d707233b875fe739d731151bdc languageName: node linkType: hard -"@mos-connection/model@npm:^4.1.0": - version: 4.1.0 - resolution: "@mos-connection/model@npm:4.1.0" - checksum: b8e1f83d87c342959165956c19b4a8d03f2c7ae0b1caf02cd00451deb977c0c5ea4ad5b9c8915ba0cea199ce071efbbcfe2329b5e737f89f1725820740d12f60 - languageName: node - linkType: hard - "@msgpack/msgpack@npm:^2.7.1": version: 2.8.0 resolution: "@msgpack/msgpack@npm:2.8.0" @@ -5874,7 +5867,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: - "@mos-connection/model": ^4.1.0 + "@mos-connection/model": ^4.1.1-nightly-master-20240430-072032-ffb8bf6.0 timeline-state-resolver-types: 9.1.0-nightly-release51-20240228-132329-b7ceb6950.0 tslib: ^2.6.2 type-fest: ^3.13.1 From faaf202703fe6c7829cd2216882ed96c6d2a1e26 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 27 Jun 2024 15:59:58 +0100 Subject: [PATCH 380/479] fix(LSG): fix async-api generation --- .github/workflows/node.yaml | 2 -- ...ator-react-sdk-npm-1.0.20-af18b2f42b.patch | 14 +++++++++++++ packages/package.json | 6 +++++- packages/yarn.lock | 20 ++++++++++++++++++- 4 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 packages/.yarn/patches/@asyncapi-generator-react-sdk-npm-1.0.20-af18b2f42b.patch diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index cc3655a953..a498458894 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -528,7 +528,6 @@ jobs: asyncapi-generation: name: AsyncAPI Generation runs-on: ubuntu-latest - continue-on-error: true timeout-minutes: 15 steps: - uses: actions/checkout@v4 @@ -555,7 +554,6 @@ jobs: openapi-generation: name: OpenAPI Generation runs-on: ubuntu-latest - continue-on-error: true timeout-minutes: 15 steps: - uses: actions/checkout@v4 diff --git a/packages/.yarn/patches/@asyncapi-generator-react-sdk-npm-1.0.20-af18b2f42b.patch b/packages/.yarn/patches/@asyncapi-generator-react-sdk-npm-1.0.20-af18b2f42b.patch new file mode 100644 index 0000000000..8d71b66241 --- /dev/null +++ b/packages/.yarn/patches/@asyncapi-generator-react-sdk-npm-1.0.20-af18b2f42b.patch @@ -0,0 +1,14 @@ +diff --git a/lib/transpiler/transpiler.js b/lib/transpiler/transpiler.js +index 3c26e5852787f0246065f6206ba0901ee32b16a7..370266a29f2c618ae48c50f4acc7462ac6cc0b4f 100644 +--- a/lib/transpiler/transpiler.js ++++ b/lib/transpiler/transpiler.js +@@ -90,7 +90,8 @@ function transpileFiles(directory, outputDir, options) { + dir: outputDir, + exports: "auto", + paths: { +- 'react/jsx-runtime': 'react/cjs/react-jsx-runtime.production.min', ++ // Make sure it finds the correct version of react https://github.com/asyncapi/generator-react-sdk/pull/242 ++ 'react/jsx-runtime': require.resolve('react/cjs/react-jsx-runtime.production.min'), + }, + sanitizeFileName: false, + })]; diff --git a/packages/package.json b/packages/package.json index c379c53211..ac9e9c160b 100644 --- a/packages/package.json +++ b/packages/package.json @@ -64,5 +64,9 @@ "typescript": "~4.9.5" }, "name": "packages", - "packageManager": "yarn@3.5.0" + "packageManager": "yarn@3.5.0", + "resolutions": { + "@asyncapi/generator-react-sdk@^1.0.20": "patch:@asyncapi/generator-react-sdk@npm%3A1.0.20#./.yarn/patches/@asyncapi-generator-react-sdk-npm-1.0.20-af18b2f42b.patch", + "@asyncapi/generator-react-sdk@^1.0.18": "patch:@asyncapi/generator-react-sdk@npm%3A1.0.20#./.yarn/patches/@asyncapi-generator-react-sdk-npm-1.0.20-af18b2f42b.patch" + } } diff --git a/packages/yarn.lock b/packages/yarn.lock index 4ba6b53efc..62cc6ba31c 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -409,7 +409,7 @@ __metadata: languageName: node linkType: hard -"@asyncapi/generator-react-sdk@npm:^1.0.18, @asyncapi/generator-react-sdk@npm:^1.0.20": +"@asyncapi/generator-react-sdk@npm:1.0.20": version: 1.0.20 resolution: "@asyncapi/generator-react-sdk@npm:1.0.20" dependencies: @@ -427,6 +427,24 @@ __metadata: languageName: node linkType: hard +"@asyncapi/generator-react-sdk@patch:@asyncapi/generator-react-sdk@npm%3A1.0.20#./.yarn/patches/@asyncapi-generator-react-sdk-npm-1.0.20-af18b2f42b.patch::locator=packages%40workspace%3A.": + version: 1.0.20 + resolution: "@asyncapi/generator-react-sdk@patch:@asyncapi/generator-react-sdk@npm%3A1.0.20#./.yarn/patches/@asyncapi-generator-react-sdk-npm-1.0.20-af18b2f42b.patch::version=1.0.20&hash=4ff360&locator=packages%40workspace%3A." + dependencies: + "@asyncapi/parser": ^3.1.0 + "@babel/core": 7.12.9 + "@babel/preset-env": ^7.12.7 + "@babel/preset-react": ^7.12.7 + "@rollup/plugin-babel": ^5.2.1 + babel-plugin-source-map-support: ^2.1.3 + prop-types: ^15.7.2 + react: ^17.0.1 + rollup: ^2.60.1 + source-map-support: ^0.5.19 + checksum: dc34d29fd0e043ed225adf920f0899b4885425504a9aae507317cc4d9d4edff98adbadb3618fb813a52e147b4084eca1ac40618427fb6c4c71c5943e1f053c0e + languageName: node + linkType: hard + "@asyncapi/generator@npm:^1.17.25": version: 1.17.25 resolution: "@asyncapi/generator@npm:1.17.25" From 0e6a301b4d57fc40121d1ffca1cb4e06a8ccedda Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 27 Jun 2024 16:06:26 +0100 Subject: [PATCH 381/479] chore: yarn dedupe --- packages/yarn.lock | 1565 +++----------------------------------------- 1 file changed, 79 insertions(+), 1486 deletions(-) diff --git a/packages/yarn.lock b/packages/yarn.lock index 62cc6ba31c..274d7fb2f3 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -66,15 +66,6 @@ __metadata: languageName: node linkType: hard -"@algolia/cache-browser-local-storage@npm:4.20.0": - version: 4.20.0 - resolution: "@algolia/cache-browser-local-storage@npm:4.20.0" - dependencies: - "@algolia/cache-common": 4.20.0 - checksum: b9ca7e190ab77ddf4d30d22223345f69fc89899aa6887ee716e4ffcef14c8c9d28b782cb7cc96a0f04eed95a989878a6feca5b9aa6add0cd1846222c3308bb65 - languageName: node - linkType: hard - "@algolia/cache-browser-local-storage@npm:4.23.3": version: 4.23.3 resolution: "@algolia/cache-browser-local-storage@npm:4.23.3" @@ -84,13 +75,6 @@ __metadata: languageName: node linkType: hard -"@algolia/cache-common@npm:4.20.0": - version: 4.20.0 - resolution: "@algolia/cache-common@npm:4.20.0" - checksum: a46377de8a309feea109aae1283fc9157c73766a4c51e3085870a1fc49f6e33698814379f3bbdf475713fa0663dace86fc90f0466e64469b1b885a0538abace4 - languageName: node - linkType: hard - "@algolia/cache-common@npm:4.23.3": version: 4.23.3 resolution: "@algolia/cache-common@npm:4.23.3" @@ -98,15 +82,6 @@ __metadata: languageName: node linkType: hard -"@algolia/cache-in-memory@npm:4.20.0": - version: 4.20.0 - resolution: "@algolia/cache-in-memory@npm:4.20.0" - dependencies: - "@algolia/cache-common": 4.20.0 - checksum: 3d67dcfae431605c8b9b1502f14865722f13b97b2822e1e3ed53bbf7bf66a120a825ccf5ed03476ebdf4aa15482dad5bfc6c2c93d81f07f862c373c689f49317 - languageName: node - linkType: hard - "@algolia/cache-in-memory@npm:4.23.3": version: 4.23.3 resolution: "@algolia/cache-in-memory@npm:4.23.3" @@ -116,17 +91,6 @@ __metadata: languageName: node linkType: hard -"@algolia/client-account@npm:4.20.0": - version: 4.20.0 - resolution: "@algolia/client-account@npm:4.20.0" - dependencies: - "@algolia/client-common": 4.20.0 - "@algolia/client-search": 4.20.0 - "@algolia/transporter": 4.20.0 - checksum: b59e9c7a324bbfba4abdab3f41d333522eb1abce7dab74e69d297acd9ee2a3c60e82e5e9db42e6a46b5ea26a35728533e6e4ff846c631b588ceb73d14dcbc5fb - languageName: node - linkType: hard - "@algolia/client-account@npm:4.23.3": version: 4.23.3 resolution: "@algolia/client-account@npm:4.23.3" @@ -138,18 +102,6 @@ __metadata: languageName: node linkType: hard -"@algolia/client-analytics@npm:4.20.0": - version: 4.20.0 - resolution: "@algolia/client-analytics@npm:4.20.0" - dependencies: - "@algolia/client-common": 4.20.0 - "@algolia/client-search": 4.20.0 - "@algolia/requester-common": 4.20.0 - "@algolia/transporter": 4.20.0 - checksum: 0be4120ab72162e0640e49eedddff81bfc2c590e9a9322d1788b8c01e06fdabcaaaa9cd75b5b516e502deb888d3ba2285ac5e1c3bb91fc9eb552a24a716dc6e3 - languageName: node - linkType: hard - "@algolia/client-analytics@npm:4.23.3": version: 4.23.3 resolution: "@algolia/client-analytics@npm:4.23.3" @@ -162,16 +114,6 @@ __metadata: languageName: node linkType: hard -"@algolia/client-common@npm:4.20.0": - version: 4.20.0 - resolution: "@algolia/client-common@npm:4.20.0" - dependencies: - "@algolia/requester-common": 4.20.0 - "@algolia/transporter": 4.20.0 - checksum: 88a27b5f8bba38349e1dbe47634e2ee159a413ff1a3baf6a65fbf244835f8d368e9f0a5ccce8bfe94ec405b38608be5bed45bcb140517f3aba6fe3b7045db373 - languageName: node - linkType: hard - "@algolia/client-common@npm:4.23.3": version: 4.23.3 resolution: "@algolia/client-common@npm:4.23.3" @@ -182,17 +124,6 @@ __metadata: languageName: node linkType: hard -"@algolia/client-personalization@npm:4.20.0": - version: 4.20.0 - resolution: "@algolia/client-personalization@npm:4.20.0" - dependencies: - "@algolia/client-common": 4.20.0 - "@algolia/requester-common": 4.20.0 - "@algolia/transporter": 4.20.0 - checksum: ddb92ebe135564e03db6ac75da7fdc1c7500a0deffb7e41d5a02a413216a06daea008f8062dab606ba8af4c3c34e550354f48e6ea7b048882c385d915643799a - languageName: node - linkType: hard - "@algolia/client-personalization@npm:4.23.3": version: 4.23.3 resolution: "@algolia/client-personalization@npm:4.23.3" @@ -204,17 +135,6 @@ __metadata: languageName: node linkType: hard -"@algolia/client-search@npm:4.20.0": - version: 4.20.0 - resolution: "@algolia/client-search@npm:4.20.0" - dependencies: - "@algolia/client-common": 4.20.0 - "@algolia/requester-common": 4.20.0 - "@algolia/transporter": 4.20.0 - checksum: 9fb6624dab6753f336f3207ee2af3558baeec4772ef739b6f6ed6a754c366e2e8d62cbf1cf8b28d5f763bec276a0a5fc36db2bf6f53a707890a411afcf550e92 - languageName: node - linkType: hard - "@algolia/client-search@npm:4.23.3": version: 4.23.3 resolution: "@algolia/client-search@npm:4.23.3" @@ -233,13 +153,6 @@ __metadata: languageName: node linkType: hard -"@algolia/logger-common@npm:4.20.0": - version: 4.20.0 - resolution: "@algolia/logger-common@npm:4.20.0" - checksum: 06ed28f76b630c8e7597534b15138ab6f71c10dfc6e13f1fb1b76965b39c88fd1d9cb3fe6bb9d046de6533ebcbe5ad92e751bc36fabe98ceda39d1d5f47bb637 - languageName: node - linkType: hard - "@algolia/logger-common@npm:4.23.3": version: 4.23.3 resolution: "@algolia/logger-common@npm:4.23.3" @@ -247,15 +160,6 @@ __metadata: languageName: node linkType: hard -"@algolia/logger-console@npm:4.20.0": - version: 4.20.0 - resolution: "@algolia/logger-console@npm:4.20.0" - dependencies: - "@algolia/logger-common": 4.20.0 - checksum: 721dffe37563e2998d4c361f09a05736b4baa141bfb7da25d50f890ba8257ac99845dd94b43d0d6db38e2fdab96508a726e184a00e5b1e83ef18a16da6fc716c - languageName: node - linkType: hard - "@algolia/logger-console@npm:4.23.3": version: 4.23.3 resolution: "@algolia/logger-console@npm:4.23.3" @@ -284,15 +188,6 @@ __metadata: languageName: node linkType: hard -"@algolia/requester-browser-xhr@npm:4.20.0": - version: 4.20.0 - resolution: "@algolia/requester-browser-xhr@npm:4.20.0" - dependencies: - "@algolia/requester-common": 4.20.0 - checksum: 669790c7dfd491318976b9d61d98d9785880d7385ba33669f3f8b9c66ea88320bcded82d34f58b5df74b2cb8beb62ef48a28d39117f7997be84348c9fa7f6132 - languageName: node - linkType: hard - "@algolia/requester-browser-xhr@npm:4.23.3": version: 4.23.3 resolution: "@algolia/requester-browser-xhr@npm:4.23.3" @@ -302,13 +197,6 @@ __metadata: languageName: node linkType: hard -"@algolia/requester-common@npm:4.20.0": - version: 4.20.0 - resolution: "@algolia/requester-common@npm:4.20.0" - checksum: 8580ffd2be146bbdb5d4a57668bba4a5014f406cb2e5c65f596db6babab46c48d30c6e4732034ee1f987970aa27dcdab567959d654fa5fa74c4bcaf98312a724 - languageName: node - linkType: hard - "@algolia/requester-common@npm:4.23.3": version: 4.23.3 resolution: "@algolia/requester-common@npm:4.23.3" @@ -316,15 +204,6 @@ __metadata: languageName: node linkType: hard -"@algolia/requester-node-http@npm:4.20.0": - version: 4.20.0 - resolution: "@algolia/requester-node-http@npm:4.20.0" - dependencies: - "@algolia/requester-common": 4.20.0 - checksum: 7857114b59c67e0d22e8a7ff3f755d11534a1602a4fc80802d3b35802777880a4980420914ea4a6e3e21198f5bacb95906289ce1bb9372458bf6a60a723bee59 - languageName: node - linkType: hard - "@algolia/requester-node-http@npm:4.23.3": version: 4.23.3 resolution: "@algolia/requester-node-http@npm:4.23.3" @@ -334,17 +213,6 @@ __metadata: languageName: node linkType: hard -"@algolia/transporter@npm:4.20.0": - version: 4.20.0 - resolution: "@algolia/transporter@npm:4.20.0" - dependencies: - "@algolia/cache-common": 4.20.0 - "@algolia/logger-common": 4.20.0 - "@algolia/requester-common": 4.20.0 - checksum: f834d5c8fcb7dfa9b7044cb81e9fab44a32f9dd0c3868a0f85fe0de4f4d27ad11fdc9c3c78541bc944c2593f4be56517a8ce593309d062b8a46ca0d6fcb5dcbc - languageName: node - linkType: hard - "@algolia/transporter@npm:4.23.3": version: 4.23.3 resolution: "@algolia/transporter@npm:4.23.3" @@ -519,7 +387,7 @@ __metadata: languageName: node linkType: hard -"@asyncapi/parser@npm:^3.0.14, @asyncapi/parser@npm:^3.1.0": +"@asyncapi/parser@npm:^3.0.14, @asyncapi/parser@npm:^3.0.7, @asyncapi/parser@npm:^3.1.0": version: 3.1.0 resolution: "@asyncapi/parser@npm:3.1.0" dependencies: @@ -546,33 +414,6 @@ __metadata: languageName: node linkType: hard -"@asyncapi/parser@npm:^3.0.7, parserapiv3@npm:@asyncapi/parser@^3.0.7": - version: 3.0.7 - resolution: "@asyncapi/parser@npm:3.0.7" - dependencies: - "@asyncapi/specs": ^6.5.0 - "@openapi-contrib/openapi-schema-to-json-schema": ~3.2.0 - "@stoplight/json": ^3.20.2 - "@stoplight/json-ref-readers": ^1.2.2 - "@stoplight/json-ref-resolver": ^3.1.5 - "@stoplight/spectral-core": ^1.16.1 - "@stoplight/spectral-functions": ^1.7.2 - "@stoplight/spectral-parsers": ^1.0.2 - "@stoplight/spectral-ref-resolver": ^1.0.3 - "@stoplight/types": ^13.12.0 - "@types/json-schema": ^7.0.11 - "@types/urijs": ^1.19.19 - ajv: ^8.11.0 - ajv-errors: ^3.0.0 - ajv-formats: ^2.1.1 - avsc: ^5.7.5 - js-yaml: ^4.1.0 - jsonpath-plus: ^7.2.0 - node-fetch: 2.6.7 - checksum: 8297f415c43c0e539cdeaaa5ef48c920f24e9af2efae8f7d14c8f1768238bce02075f7f649d29f953eeb9634e8d6692d08e831e3c2fab11bb6c8a98ccbd8f67a - languageName: node - linkType: hard - "@asyncapi/protobuf-schema-parser@npm:^3.0.0, @asyncapi/protobuf-schema-parser@npm:^3.2.4": version: 3.2.4 resolution: "@asyncapi/protobuf-schema-parser@npm:3.2.4" @@ -625,16 +466,7 @@ __metadata: languageName: node linkType: hard -"@asyncapi/specs@npm:^6.0.0-next-major-spec.9, @asyncapi/specs@npm:^6.5.0": - version: 6.5.0 - resolution: "@asyncapi/specs@npm:6.5.0" - dependencies: - "@types/json-schema": ^7.0.11 - checksum: 85c3719a9de8af1b0c5585e93be9967845a9c02fc3cc62dcce7f9ed5f31eb1bbab0b8488840da3a93da1e8e4dbb9a8e6a525b19fa6954f94c36b22971cab8788 - languageName: node - linkType: hard - -"@asyncapi/specs@npm:^6.7.1": +"@asyncapi/specs@npm:^6.0.0-next-major-spec.9, @asyncapi/specs@npm:^6.5.0, @asyncapi/specs@npm:^6.7.1": version: 6.7.1 resolution: "@asyncapi/specs@npm:6.7.1" dependencies: @@ -643,17 +475,7 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.0, @babel/code-frame@npm:^7.23.5, @babel/code-frame@npm:^7.8.3": - version: 7.23.5 - resolution: "@babel/code-frame@npm:7.23.5" - dependencies: - "@babel/highlight": ^7.23.4 - chalk: ^2.4.2 - checksum: d90981fdf56a2824a9b14d19a4c0e8db93633fd488c772624b4e83e0ceac6039a27cd298a247c3214faa952bf803ba23696172ae7e7235f3b97f43ba278c569a - languageName: node - linkType: hard - -"@babel/code-frame@npm:^7.24.1, @babel/code-frame@npm:^7.24.2": +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.0, @babel/code-frame@npm:^7.23.5, @babel/code-frame@npm:^7.24.1, @babel/code-frame@npm:^7.24.2, @babel/code-frame@npm:^7.8.3": version: 7.24.2 resolution: "@babel/code-frame@npm:7.24.2" dependencies: @@ -663,14 +485,7 @@ __metadata: languageName: node linkType: hard -"@babel/compat-data@npm:^7.22.20, @babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.22.9, @babel/compat-data@npm:^7.23.5": - version: 7.23.5 - resolution: "@babel/compat-data@npm:7.23.5" - checksum: 06ce244cda5763295a0ea924728c09bae57d35713b675175227278896946f922a63edf803c322f855a3878323d48d0255a2a3023409d2a123483c8a69ebb4744 - languageName: node - linkType: hard - -"@babel/compat-data@npm:^7.24.4": +"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.23.5, @babel/compat-data@npm:^7.24.4": version: 7.24.4 resolution: "@babel/compat-data@npm:7.24.4" checksum: 52ce371658dc7796c9447c9cb3b9c0659370d141b76997f21c5e0028cca4d026ca546b84bc8d157ce7ca30bd353d89f9238504eb8b7aefa9b1f178b4c100c2d4 @@ -701,30 +516,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.19.6, @babel/core@npm:^7.23.9": - version: 7.23.9 - resolution: "@babel/core@npm:7.23.9" - dependencies: - "@ampproject/remapping": ^2.2.0 - "@babel/code-frame": ^7.23.5 - "@babel/generator": ^7.23.6 - "@babel/helper-compilation-targets": ^7.23.6 - "@babel/helper-module-transforms": ^7.23.3 - "@babel/helpers": ^7.23.9 - "@babel/parser": ^7.23.9 - "@babel/template": ^7.23.9 - "@babel/traverse": ^7.23.9 - "@babel/types": ^7.23.9 - convert-source-map: ^2.0.0 - debug: ^4.1.0 - gensync: ^1.0.0-beta.2 - json5: ^2.2.3 - semver: ^6.3.1 - checksum: 634a511f74db52a5f5a283c1121f25e2227b006c095b84a02a40a9213842489cd82dc7d61cdc74e10b5bcd9bb0a4e28bab47635b54c7e2256d47ab57356e2a76 - languageName: node - linkType: hard - -"@babel/core@npm:^7.23.3": +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.19.6, @babel/core@npm:^7.23.3, @babel/core@npm:^7.23.9": version: 7.24.4 resolution: "@babel/core@npm:7.24.4" dependencies: @@ -747,19 +539,7 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.12.5, @babel/generator@npm:^7.23.6, @babel/generator@npm:^7.7.2": - version: 7.23.6 - resolution: "@babel/generator@npm:7.23.6" - dependencies: - "@babel/types": ^7.23.6 - "@jridgewell/gen-mapping": ^0.3.2 - "@jridgewell/trace-mapping": ^0.3.17 - jsesc: ^2.5.1 - checksum: 1a1a1c4eac210f174cd108d479464d053930a812798e09fee069377de39a893422df5b5b146199ead7239ae6d3a04697b45fc9ac6e38e0f6b76374390f91fc6c - languageName: node - linkType: hard - -"@babel/generator@npm:^7.23.3, @babel/generator@npm:^7.24.1, @babel/generator@npm:^7.24.4": +"@babel/generator@npm:^7.12.5, @babel/generator@npm:^7.23.3, @babel/generator@npm:^7.24.1, @babel/generator@npm:^7.24.4, @babel/generator@npm:^7.7.2": version: 7.24.4 resolution: "@babel/generator@npm:7.24.4" dependencies: @@ -780,7 +560,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-builder-binary-assignment-operator-visitor@npm:^7.22.15, @babel/helper-builder-binary-assignment-operator-visitor@npm:^7.22.5": +"@babel/helper-builder-binary-assignment-operator-visitor@npm:^7.22.15": version: 7.22.15 resolution: "@babel/helper-builder-binary-assignment-operator-visitor@npm:7.22.15" dependencies: @@ -789,7 +569,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.22.15, @babel/helper-compilation-targets@npm:^7.22.5, @babel/helper-compilation-targets@npm:^7.22.6, @babel/helper-compilation-targets@npm:^7.23.6": +"@babel/helper-compilation-targets@npm:^7.22.6, @babel/helper-compilation-targets@npm:^7.23.6": version: 7.23.6 resolution: "@babel/helper-compilation-targets@npm:7.23.6" dependencies: @@ -802,25 +582,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-create-class-features-plugin@npm:^7.22.11, @babel/helper-create-class-features-plugin@npm:^7.22.15, @babel/helper-create-class-features-plugin@npm:^7.22.5": - version: 7.22.15 - resolution: "@babel/helper-create-class-features-plugin@npm:7.22.15" - dependencies: - "@babel/helper-annotate-as-pure": ^7.22.5 - "@babel/helper-environment-visitor": ^7.22.5 - "@babel/helper-function-name": ^7.22.5 - "@babel/helper-member-expression-to-functions": ^7.22.15 - "@babel/helper-optimise-call-expression": ^7.22.5 - "@babel/helper-replace-supers": ^7.22.9 - "@babel/helper-skip-transparent-expression-wrappers": ^7.22.5 - "@babel/helper-split-export-declaration": ^7.22.6 - semver: ^6.3.1 - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 52c500d8d164abb3a360b1b7c4b8fff77bc4a5920d3a2b41ae6e1d30617b0dc0b972c1f5db35b1752007e04a748908b4a99bc872b73549ae837e87dcdde005a3 - languageName: node - linkType: hard - "@babel/helper-create-class-features-plugin@npm:^7.24.1, @babel/helper-create-class-features-plugin@npm:^7.24.4": version: 7.24.4 resolution: "@babel/helper-create-class-features-plugin@npm:7.24.4" @@ -853,21 +614,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-define-polyfill-provider@npm:^0.4.2": - version: 0.4.2 - resolution: "@babel/helper-define-polyfill-provider@npm:0.4.2" - dependencies: - "@babel/helper-compilation-targets": ^7.22.6 - "@babel/helper-plugin-utils": ^7.22.5 - debug: ^4.1.1 - lodash.debounce: ^4.0.8 - resolve: ^1.14.2 - peerDependencies: - "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 1f6dec0c5d0876d278fe15b71238eccc5f74c4e2efa2c78aaafa8bc2cc96336b8e68d94cd1a78497356c96e8b91b8c1f4452179820624d1702aee2f9832e6569 - languageName: node - linkType: hard - "@babel/helper-define-polyfill-provider@npm:^0.6.1": version: 0.6.1 resolution: "@babel/helper-define-polyfill-provider@npm:0.6.1" @@ -883,7 +629,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-environment-visitor@npm:^7.22.20, @babel/helper-environment-visitor@npm:^7.22.5": +"@babel/helper-environment-visitor@npm:^7.22.20": version: 7.22.20 resolution: "@babel/helper-environment-visitor@npm:7.22.20" checksum: d80ee98ff66f41e233f36ca1921774c37e88a803b2f7dca3db7c057a5fea0473804db9fb6729e5dbfd07f4bed722d60f7852035c2c739382e84c335661590b69 @@ -909,15 +655,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-member-expression-to-functions@npm:^7.22.15": - version: 7.22.15 - resolution: "@babel/helper-member-expression-to-functions@npm:7.22.15" - dependencies: - "@babel/types": ^7.22.15 - checksum: c7c5d01c402dd8902c2ec3093f203ed0fc3bc5f669328a084d2e663c4c06dd0415480ee8220c6f96ba9b2dc49545c0078f221fc3900ab1e65de69a12fe7b361f - languageName: node - linkType: hard - "@babel/helper-member-expression-to-functions@npm:^7.23.0": version: 7.23.0 resolution: "@babel/helper-member-expression-to-functions@npm:7.23.0" @@ -927,16 +664,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.10.4, @babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.22.15, @babel/helper-module-imports@npm:^7.22.5": - version: 7.22.15 - resolution: "@babel/helper-module-imports@npm:7.22.15" - dependencies: - "@babel/types": ^7.22.15 - checksum: ecd7e457df0a46f889228f943ef9b4a47d485d82e030676767e6a2fdcbdaa63594d8124d4b55fd160b41c201025aec01fc27580352b1c87a37c9c6f33d116702 - languageName: node - linkType: hard - -"@babel/helper-module-imports@npm:^7.24.1, @babel/helper-module-imports@npm:^7.24.3": +"@babel/helper-module-imports@npm:^7.10.4, @babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.22.15, @babel/helper-module-imports@npm:^7.24.1, @babel/helper-module-imports@npm:^7.24.3": version: 7.24.3 resolution: "@babel/helper-module-imports@npm:7.24.3" dependencies: @@ -945,7 +673,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.12.1, @babel/helper-module-transforms@npm:^7.22.5, @babel/helper-module-transforms@npm:^7.22.9, @babel/helper-module-transforms@npm:^7.23.3": +"@babel/helper-module-transforms@npm:^7.12.1, @babel/helper-module-transforms@npm:^7.23.3": version: 7.23.3 resolution: "@babel/helper-module-transforms@npm:7.23.3" dependencies: @@ -969,21 +697,14 @@ __metadata: languageName: node linkType: hard -"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.8.0, @babel/helper-plugin-utils@npm:^7.8.3": - version: 7.22.5 - resolution: "@babel/helper-plugin-utils@npm:7.22.5" - checksum: c0fc7227076b6041acd2f0e818145d2e8c41968cc52fb5ca70eed48e21b8fe6dd88a0a91cbddf4951e33647336eb5ae184747ca706817ca3bef5e9e905151ff5 - languageName: node - linkType: hard - -"@babel/helper-plugin-utils@npm:^7.24.0": +"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.24.0, @babel/helper-plugin-utils@npm:^7.8.0, @babel/helper-plugin-utils@npm:^7.8.3": version: 7.24.0 resolution: "@babel/helper-plugin-utils@npm:7.24.0" checksum: e2baa0eede34d2fa2265947042aa84d444aa48dc51e9feedea55b67fc1bc3ab051387e18b33ca7748285a6061390831ab82f8a2c767d08470b93500ec727e9b9 languageName: node linkType: hard -"@babel/helper-remap-async-to-generator@npm:^7.22.20, @babel/helper-remap-async-to-generator@npm:^7.22.5, @babel/helper-remap-async-to-generator@npm:^7.22.9": +"@babel/helper-remap-async-to-generator@npm:^7.22.20": version: 7.22.20 resolution: "@babel/helper-remap-async-to-generator@npm:7.22.20" dependencies: @@ -996,19 +717,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-replace-supers@npm:^7.22.5, @babel/helper-replace-supers@npm:^7.22.9": - version: 7.22.20 - resolution: "@babel/helper-replace-supers@npm:7.22.20" - dependencies: - "@babel/helper-environment-visitor": ^7.22.20 - "@babel/helper-member-expression-to-functions": ^7.22.15 - "@babel/helper-optimise-call-expression": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0 - checksum: a0008332e24daedea2e9498733e3c39b389d6d4512637e000f96f62b797e702ee24a407ccbcd7a236a551590a38f31282829a8ef35c50a3c0457d88218cae639 - languageName: node - linkType: hard - "@babel/helper-replace-supers@npm:^7.24.1": version: 7.24.1 resolution: "@babel/helper-replace-supers@npm:7.24.1" @@ -1056,14 +764,14 @@ __metadata: languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.22.20, @babel/helper-validator-identifier@npm:^7.22.5": +"@babel/helper-validator-identifier@npm:^7.22.20": version: 7.22.20 resolution: "@babel/helper-validator-identifier@npm:7.22.20" checksum: 136412784d9428266bcdd4d91c32bcf9ff0e8d25534a9d94b044f77fe76bc50f941a90319b05aafd1ec04f7d127cd57a179a3716009ff7f3412ef835ada95bdc languageName: node linkType: hard -"@babel/helper-validator-option@npm:^7.22.15, @babel/helper-validator-option@npm:^7.23.5": +"@babel/helper-validator-option@npm:^7.23.5": version: 7.23.5 resolution: "@babel/helper-validator-option@npm:7.23.5" checksum: 537cde2330a8aede223552510e8a13e9c1c8798afee3757995a7d4acae564124fe2bf7e7c3d90d62d3657434a74340a274b3b3b1c6f17e9a2be1f48af29cb09e @@ -1081,18 +789,7 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.12.5, @babel/helpers@npm:^7.23.9": - version: 7.23.9 - resolution: "@babel/helpers@npm:7.23.9" - dependencies: - "@babel/template": ^7.23.9 - "@babel/traverse": ^7.23.9 - "@babel/types": ^7.23.9 - checksum: 2678231192c0471dbc2fc403fb19456cc46b1afefcfebf6bc0f48b2e938fdb0fef2e0fe90c8c8ae1f021dae5012b700372e4b5d15867f1d7764616532e4a6324 - languageName: node - linkType: hard - -"@babel/helpers@npm:^7.24.4": +"@babel/helpers@npm:^7.12.5, @babel/helpers@npm:^7.24.4": version: 7.24.4 resolution: "@babel/helpers@npm:7.24.4" dependencies: @@ -1103,17 +800,6 @@ __metadata: languageName: node linkType: hard -"@babel/highlight@npm:^7.23.4": - version: 7.23.4 - resolution: "@babel/highlight@npm:7.23.4" - dependencies: - "@babel/helper-validator-identifier": ^7.22.20 - chalk: ^2.4.2 - js-tokens: ^4.0.0 - checksum: 643acecdc235f87d925979a979b539a5d7d1f31ae7db8d89047269082694122d11aa85351304c9c978ceeb6d250591ccadb06c366f358ccee08bb9c122476b89 - languageName: node - linkType: hard - "@babel/highlight@npm:^7.24.2": version: 7.24.2 resolution: "@babel/highlight@npm:7.24.2" @@ -1126,16 +812,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.12.7, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9": - version: 7.23.9 - resolution: "@babel/parser@npm:7.23.9" - bin: - parser: ./bin/babel-parser.js - checksum: e7cd4960ac8671774e13803349da88d512f9292d7baa952173260d3e8f15620a28a3701f14f709d769209022f9e7b79965256b8be204fc550cfe783cdcabe7c7 - languageName: node - linkType: hard - -"@babel/parser@npm:^7.24.0, @babel/parser@npm:^7.24.1, @babel/parser@npm:^7.24.4": +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.12.7, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.24.0, @babel/parser@npm:^7.24.1, @babel/parser@npm:^7.24.4": version: 7.24.4 resolution: "@babel/parser@npm:7.24.4" bin: @@ -1156,17 +833,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.22.15": - version: 7.22.15 - resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.22.15" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 8910ca21a7ec7c06f7b247d4b86c97c5aa15ef321518f44f6f490c5912fdf82c605aaa02b90892e375d82ccbedeadfdeadd922c1b836c9dd4c596871bf654753 - languageName: node - linkType: hard - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.24.1" @@ -1178,19 +844,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.22.15": - version: 7.22.15 - resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.22.15" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - "@babel/helper-skip-transparent-expression-wrappers": ^7.22.5 - "@babel/plugin-transform-optional-chaining": ^7.22.15 - peerDependencies: - "@babel/core": ^7.13.0 - checksum: fbefedc0da014c37f1a50a8094ce7dbbf2181ae93243f23d6ecba2499b5b20196c2124d6a4dfe3e9e0125798e80593103e456352a4beb4e5c6f7c75efb80fdac - languageName: node - linkType: hard - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.24.1" @@ -1291,17 +944,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-import-assertions@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-syntax-import-assertions@npm:7.22.5" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 2b8b5572db04a7bef1e6cd20debf447e4eef7cb012616f5eceb8fa3e23ce469b8f76ee74fd6d1e158ba17a8f58b0aec579d092fb67c5a30e83ccfbc5754916c1 - languageName: node - linkType: hard - "@babel/plugin-syntax-import-assertions@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-syntax-import-assertions@npm:7.24.1" @@ -1313,17 +955,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-import-attributes@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-syntax-import-attributes@npm:7.22.5" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 197b3c5ea2a9649347f033342cb222ab47f4645633695205c0250c6bf2af29e643753b8bb24a2db39948bef08e7c540babfd365591eb57fc110cb30b425ffc47 - languageName: node - linkType: hard - "@babel/plugin-syntax-import-attributes@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-syntax-import-attributes@npm:7.24.1" @@ -1357,18 +988,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-jsx@npm:^7.22.5, @babel/plugin-syntax-jsx@npm:^7.7.2": - version: 7.22.5 - resolution: "@babel/plugin-syntax-jsx@npm:7.22.5" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 8829d30c2617ab31393d99cec2978e41f014f4ac6f01a1cecf4c4dd8320c3ec12fdc3ce121126b2d8d32f6887e99ca1a0bad53dedb1e6ad165640b92b24980ce - languageName: node - linkType: hard - -"@babel/plugin-syntax-jsx@npm:^7.23.3, @babel/plugin-syntax-jsx@npm:^7.24.1": +"@babel/plugin-syntax-jsx@npm:^7.23.3, @babel/plugin-syntax-jsx@npm:^7.24.1, @babel/plugin-syntax-jsx@npm:^7.7.2": version: 7.24.1 resolution: "@babel/plugin-syntax-jsx@npm:7.24.1" dependencies: @@ -1467,18 +1087,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-typescript@npm:^7.22.5, @babel/plugin-syntax-typescript@npm:^7.7.2": - version: 7.22.5 - resolution: "@babel/plugin-syntax-typescript@npm:7.22.5" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 8ab7718fbb026d64da93681a57797d60326097fd7cb930380c8bffd9eb101689e90142c760a14b51e8e69c88a73ba3da956cb4520a3b0c65743aee5c71ef360a - languageName: node - linkType: hard - -"@babel/plugin-syntax-typescript@npm:^7.24.1": +"@babel/plugin-syntax-typescript@npm:^7.24.1, @babel/plugin-syntax-typescript@npm:^7.7.2": version: 7.24.1 resolution: "@babel/plugin-syntax-typescript@npm:7.24.1" dependencies: @@ -1501,17 +1110,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-arrow-functions@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-arrow-functions@npm:7.22.5" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 35abb6c57062802c7ce8bd96b2ef2883e3124370c688bbd67609f7d2453802fb73944df8808f893b6c67de978eb2bcf87bbfe325e46d6f39b5fcb09ece11d01a - languageName: node - linkType: hard - "@babel/plugin-transform-arrow-functions@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-arrow-functions@npm:7.24.1" @@ -1523,20 +1121,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-async-generator-functions@npm:^7.22.15": - version: 7.22.15 - resolution: "@babel/plugin-transform-async-generator-functions@npm:7.22.15" - dependencies: - "@babel/helper-environment-visitor": ^7.22.5 - "@babel/helper-plugin-utils": ^7.22.5 - "@babel/helper-remap-async-to-generator": ^7.22.9 - "@babel/plugin-syntax-async-generators": ^7.8.4 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: fad98786b446ce63bde0d14a221e2617eef5a7bbca62b49d96f16ab5e1694521234cfba6145b830fbf9af16d60a8a3dbf148e8694830bd91796fe333b0599e73 - languageName: node - linkType: hard - "@babel/plugin-transform-async-generator-functions@npm:^7.24.3": version: 7.24.3 resolution: "@babel/plugin-transform-async-generator-functions@npm:7.24.3" @@ -1551,19 +1135,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-async-to-generator@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-async-to-generator@npm:7.22.5" - dependencies: - "@babel/helper-module-imports": ^7.22.5 - "@babel/helper-plugin-utils": ^7.22.5 - "@babel/helper-remap-async-to-generator": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: b95f23f99dcb379a9f0a1c2a3bbea3f8dc0e1b16dc1ac8b484fe378370169290a7a63d520959a9ba1232837cf74a80e23f6facbe14fd42a3cda6d3c2d7168e62 - languageName: node - linkType: hard - "@babel/plugin-transform-async-to-generator@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-async-to-generator@npm:7.24.1" @@ -1577,17 +1148,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-block-scoped-functions@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.22.5" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 416b1341858e8ca4e524dee66044735956ced5f478b2c3b9bc11ec2285b0c25d7dbb96d79887169eb938084c95d0a89338c8b2fe70d473bd9dc92e5d9db1732c - languageName: node - linkType: hard - "@babel/plugin-transform-block-scoped-functions@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.24.1" @@ -1599,17 +1159,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-block-scoping@npm:^7.22.15": - version: 7.22.15 - resolution: "@babel/plugin-transform-block-scoping@npm:7.22.15" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: c7091dc000b854ce0c471588ca0704ef1ce78cff954584a9f21c1668fd0669e7c8d5396fb72fe49a2216d9b96a400d435f424f27e41a097ef6c855f9c57df195 - languageName: node - linkType: hard - "@babel/plugin-transform-block-scoping@npm:^7.24.4": version: 7.24.4 resolution: "@babel/plugin-transform-block-scoping@npm:7.24.4" @@ -1621,18 +1170,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-class-properties@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-class-properties@npm:7.22.5" - dependencies: - "@babel/helper-create-class-features-plugin": ^7.22.5 - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: b830152dfc2ff2f647f0abe76e6251babdfbef54d18c4b2c73a6bf76b1a00050a5d998dac80dc901a48514e95604324943a9dd39317073fe0928b559e0e0c579 - languageName: node - linkType: hard - "@babel/plugin-transform-class-properties@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-class-properties@npm:7.24.1" @@ -1645,19 +1182,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-class-static-block@npm:^7.22.11": - version: 7.22.11 - resolution: "@babel/plugin-transform-class-static-block@npm:7.22.11" - dependencies: - "@babel/helper-create-class-features-plugin": ^7.22.11 - "@babel/helper-plugin-utils": ^7.22.5 - "@babel/plugin-syntax-class-static-block": ^7.14.5 - peerDependencies: - "@babel/core": ^7.12.0 - checksum: 69f040506fad66f1c6918d288d0e0edbc5c8a07c8b4462c1184ad2f9f08995d68b057126c213871c0853ae0c72afc60ec87492049dfacb20902e32346a448bcb - languageName: node - linkType: hard - "@babel/plugin-transform-class-static-block@npm:^7.24.4": version: 7.24.4 resolution: "@babel/plugin-transform-class-static-block@npm:7.24.4" @@ -1671,25 +1195,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-classes@npm:^7.22.15": - version: 7.22.15 - resolution: "@babel/plugin-transform-classes@npm:7.22.15" - dependencies: - "@babel/helper-annotate-as-pure": ^7.22.5 - "@babel/helper-compilation-targets": ^7.22.15 - "@babel/helper-environment-visitor": ^7.22.5 - "@babel/helper-function-name": ^7.22.5 - "@babel/helper-optimise-call-expression": ^7.22.5 - "@babel/helper-plugin-utils": ^7.22.5 - "@babel/helper-replace-supers": ^7.22.9 - "@babel/helper-split-export-declaration": ^7.22.6 - globals: ^11.1.0 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: d3f4d0c107dd8a3557ea3575cc777fab27efa92958b41e4a9822f7499725c1f554beae58855de16ddec0a7b694e45f59a26cea8fbde4275563f72f09c6e039a0 - languageName: node - linkType: hard - "@babel/plugin-transform-classes@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-classes@npm:7.24.1" @@ -1708,18 +1213,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-computed-properties@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-computed-properties@npm:7.22.5" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - "@babel/template": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: c2a77a0f94ec71efbc569109ec14ea2aa925b333289272ced8b33c6108bdbb02caf01830ffc7e49486b62dec51911924d13f3a76f1149f40daace1898009e131 - languageName: node - linkType: hard - "@babel/plugin-transform-computed-properties@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-computed-properties@npm:7.24.1" @@ -1732,17 +1225,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-destructuring@npm:^7.22.15": - version: 7.22.15 - resolution: "@babel/plugin-transform-destructuring@npm:7.22.15" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 4bccb4765e5287f1d36119d930afb9941ea8f4f001bddb8febff716bac0e09dc58576624f3ec59470630513044dd342075fe11af16d8c1b234cb7406cffca9f0 - languageName: node - linkType: hard - "@babel/plugin-transform-destructuring@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-destructuring@npm:7.24.1" @@ -1754,18 +1236,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-dotall-regex@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-dotall-regex@npm:7.22.5" - dependencies: - "@babel/helper-create-regexp-features-plugin": ^7.22.5 - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 409b658d11e3082c8f69e9cdef2d96e4d6d11256f005772425fb230cc48fd05945edbfbcb709dab293a1a2f01f9c8a5bb7b4131e632b23264039d9f95864b453 - languageName: node - linkType: hard - "@babel/plugin-transform-dotall-regex@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-dotall-regex@npm:7.24.1" @@ -1778,17 +1248,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-duplicate-keys@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-duplicate-keys@npm:7.22.5" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: bb1280fbabaab6fab2ede585df34900712698210a3bd413f4df5bae6d8c24be36b496c92722ae676a7a67d060a4624f4d6c23b923485f906bfba8773c69f55b4 - languageName: node - linkType: hard - "@babel/plugin-transform-duplicate-keys@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-duplicate-keys@npm:7.24.1" @@ -1800,18 +1259,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-dynamic-import@npm:^7.22.11": - version: 7.22.11 - resolution: "@babel/plugin-transform-dynamic-import@npm:7.22.11" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - "@babel/plugin-syntax-dynamic-import": ^7.8.3 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 78fc9c532210bf9e8f231747f542318568ac360ee6c27e80853962c984283c73da3f8f8aebe83c2096090a435b356b092ed85de617a156cbe0729d847632be45 - languageName: node - linkType: hard - "@babel/plugin-transform-dynamic-import@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-dynamic-import@npm:7.24.1" @@ -1824,18 +1271,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-exponentiation-operator@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.22.5" - dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor": ^7.22.5 - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: f2d660c1b1d51ad5fec1cd5ad426a52187204068c4158f8c4aa977b31535c61b66898d532603eef21c15756827be8277f724c869b888d560f26d7fe848bb5eae - languageName: node - linkType: hard - "@babel/plugin-transform-exponentiation-operator@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.24.1" @@ -1848,18 +1283,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-export-namespace-from@npm:^7.22.11": - version: 7.22.11 - resolution: "@babel/plugin-transform-export-namespace-from@npm:7.22.11" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - "@babel/plugin-syntax-export-namespace-from": ^7.8.3 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 73af5883a321ed56a4bfd43c8a7de0164faebe619287706896fc6ee2f7a4e69042adaa1338c0b8b4bdb9f7e5fdceb016fb1d40694cb43ca3b8827429e8aac4bf - languageName: node - linkType: hard - "@babel/plugin-transform-export-namespace-from@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-export-namespace-from@npm:7.24.1" @@ -1872,17 +1295,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-for-of@npm:^7.22.15": - version: 7.22.15 - resolution: "@babel/plugin-transform-for-of@npm:7.22.15" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: f395ae7bce31e14961460f56cf751b5d6e37dd27d7df5b1f4e49fec1c11b6f9cf71991c7ffbe6549878591e87df0d66af798cf26edfa4bfa6b4c3dba1fb2f73a - languageName: node - linkType: hard - "@babel/plugin-transform-for-of@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-for-of@npm:7.24.1" @@ -1895,19 +1307,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-function-name@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-function-name@npm:7.22.5" - dependencies: - "@babel/helper-compilation-targets": ^7.22.5 - "@babel/helper-function-name": ^7.22.5 - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: cff3b876357999cb8ae30e439c3ec6b0491a53b0aa6f722920a4675a6dd5b53af97a833051df4b34791fe5b3dd326ccf769d5c8e45b322aa50ee11a660b17845 - languageName: node - linkType: hard - "@babel/plugin-transform-function-name@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-function-name@npm:7.24.1" @@ -1921,18 +1320,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-json-strings@npm:^7.22.11": - version: 7.22.11 - resolution: "@babel/plugin-transform-json-strings@npm:7.22.11" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - "@babel/plugin-syntax-json-strings": ^7.8.3 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 50665e5979e66358c50e90a26db53c55917f78175127ac2fa05c7888d156d418ffb930ec0a109353db0a7c5f57c756ce01bfc9825d24cbfd2b3ec453f2ed8cba - languageName: node - linkType: hard - "@babel/plugin-transform-json-strings@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-json-strings@npm:7.24.1" @@ -1945,17 +1332,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-literals@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-literals@npm:7.22.5" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: ec37cc2ffb32667af935ab32fe28f00920ec8a1eb999aa6dc6602f2bebd8ba205a558aeedcdccdebf334381d5c57106c61f52332045730393e73410892a9735b - languageName: node - linkType: hard - "@babel/plugin-transform-literals@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-literals@npm:7.24.1" @@ -1967,18 +1343,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-logical-assignment-operators@npm:^7.22.11": - version: 7.22.11 - resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.22.11" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - "@babel/plugin-syntax-logical-assignment-operators": ^7.10.4 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: c664e9798e85afa7f92f07b867682dee7392046181d82f5d21bae6f2ca26dfe9c8375cdc52b7483c3fc09a983c1989f60eff9fbc4f373b0c0a74090553d05739 - languageName: node - linkType: hard - "@babel/plugin-transform-logical-assignment-operators@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.24.1" @@ -1991,17 +1355,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-member-expression-literals@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-member-expression-literals@npm:7.22.5" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: ec4b0e07915ddd4fda0142fd104ee61015c208608a84cfa13643a95d18760b1dc1ceb6c6e0548898b8c49e5959a994e46367260176dbabc4467f729b21868504 - languageName: node - linkType: hard - "@babel/plugin-transform-member-expression-literals@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-member-expression-literals@npm:7.24.1" @@ -2013,18 +1366,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-amd@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-modules-amd@npm:7.22.5" - dependencies: - "@babel/helper-module-transforms": ^7.22.5 - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 7da4c4ebbbcf7d182abb59b2046b22d86eee340caf8a22a39ef6a727da2d8acfec1f714fcdcd5054110b280e4934f735e80a6848d192b6834c5d4459a014f04d - languageName: node - linkType: hard - "@babel/plugin-transform-modules-amd@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-modules-amd@npm:7.24.1" @@ -2037,20 +1378,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-commonjs@npm:^7.22.15, @babel/plugin-transform-modules-commonjs@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-modules-commonjs@npm:7.23.3" - dependencies: - "@babel/helper-module-transforms": ^7.23.3 - "@babel/helper-plugin-utils": ^7.22.5 - "@babel/helper-simple-access": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 720a231ceade4ae4d2632478db4e7fecf21987d444942b72d523487ac8d715ca97de6c8f415c71e939595e1a4776403e7dc24ed68fe9125ad4acf57753c9bff7 - languageName: node - linkType: hard - -"@babel/plugin-transform-modules-commonjs@npm:^7.24.1": +"@babel/plugin-transform-modules-commonjs@npm:^7.23.3, @babel/plugin-transform-modules-commonjs@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-modules-commonjs@npm:7.24.1" dependencies: @@ -2063,20 +1391,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-systemjs@npm:^7.22.11": - version: 7.22.11 - resolution: "@babel/plugin-transform-modules-systemjs@npm:7.22.11" - dependencies: - "@babel/helper-hoist-variables": ^7.22.5 - "@babel/helper-module-transforms": ^7.22.9 - "@babel/helper-plugin-utils": ^7.22.5 - "@babel/helper-validator-identifier": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: d0991e4bdc3352b6a9f4d12b6662e3645d892cd5c3c005ba5f14e65f1e218c6a8f7f4497e64a51d82a046e507aaa7db3143b800b0270dca1824cbd214ff3363d - languageName: node - linkType: hard - "@babel/plugin-transform-modules-systemjs@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-modules-systemjs@npm:7.24.1" @@ -2091,18 +1405,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-umd@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-modules-umd@npm:7.22.5" - dependencies: - "@babel/helper-module-transforms": ^7.22.5 - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 46622834c54c551b231963b867adbc80854881b3e516ff29984a8da989bd81665bd70e8cba6710345248e97166689310f544aee1a5773e262845a8f1b3e5b8b4 - languageName: node - linkType: hard - "@babel/plugin-transform-modules-umd@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-modules-umd@npm:7.24.1" @@ -2127,17 +1429,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-new-target@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-new-target@npm:7.22.5" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 6b72112773487a881a1d6ffa680afde08bad699252020e86122180ee7a88854d5da3f15d9bca3331cf2e025df045604494a8208a2e63b486266b07c14e2ffbf3 - languageName: node - linkType: hard - "@babel/plugin-transform-new-target@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-new-target@npm:7.24.1" @@ -2149,18 +1440,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.22.11": - version: 7.22.11 - resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.22.11" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 167babecc8b8fe70796a7b7d34af667ebbf43da166c21689502e5e8cc93180b7a85979c77c9f64b7cce431b36718bd0a6df9e5e0ffea4ae22afb22cfef886372 - languageName: node - linkType: hard - "@babel/plugin-transform-nullish-coalescing-operator@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.24.1" @@ -2173,18 +1452,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-numeric-separator@npm:^7.22.11": - version: 7.22.11 - resolution: "@babel/plugin-transform-numeric-separator@npm:7.22.11" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - "@babel/plugin-syntax-numeric-separator": ^7.10.4 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: af064d06a4a041767ec396a5f258103f64785df290e038bba9f0ef454e6c914f2ac45d862bbdad8fac2c7ad47fa4e95356f29053c60c100a0160b02a995fe2a3 - languageName: node - linkType: hard - "@babel/plugin-transform-numeric-separator@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-numeric-separator@npm:7.24.1" @@ -2197,21 +1464,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-object-rest-spread@npm:^7.22.15": - version: 7.22.15 - resolution: "@babel/plugin-transform-object-rest-spread@npm:7.22.15" - dependencies: - "@babel/compat-data": ^7.22.9 - "@babel/helper-compilation-targets": ^7.22.15 - "@babel/helper-plugin-utils": ^7.22.5 - "@babel/plugin-syntax-object-rest-spread": ^7.8.3 - "@babel/plugin-transform-parameters": ^7.22.15 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 62197a6f12289c1c1bd57f3bed9f0f765ca32390bfe91e0b5561dd94dd9770f4480c4162dec98da094bc0ba99d2c2ebba68de47c019454041b0b7a68ba2ec66d - languageName: node - linkType: hard - "@babel/plugin-transform-object-rest-spread@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-object-rest-spread@npm:7.24.1" @@ -2226,18 +1478,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-object-super@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-object-super@npm:7.22.5" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - "@babel/helper-replace-supers": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: b71887877d74cb64dbccb5c0324fa67e31171e6a5311991f626650e44a4083e5436a1eaa89da78c0474fb095d4ec322d63ee778b202d33aa2e4194e1ed8e62d7 - languageName: node - linkType: hard - "@babel/plugin-transform-object-super@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-object-super@npm:7.24.1" @@ -2250,18 +1490,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-optional-catch-binding@npm:^7.22.11": - version: 7.22.11 - resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.22.11" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - "@babel/plugin-syntax-optional-catch-binding": ^7.8.3 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: f17abd90e1de67c84d63afea29c8021c74abb2794d3a6eeafb0bbe7372d3db32aefca386e392116ec63884537a4a2815d090d26264d259bacc08f6e3ed05294c - languageName: node - linkType: hard - "@babel/plugin-transform-optional-catch-binding@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.24.1" @@ -2274,19 +1502,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-optional-chaining@npm:^7.22.15": - version: 7.22.15 - resolution: "@babel/plugin-transform-optional-chaining@npm:7.22.15" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - "@babel/helper-skip-transparent-expression-wrappers": ^7.22.5 - "@babel/plugin-syntax-optional-chaining": ^7.8.3 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 6b97abe0e50ca2dd8684fcef2c8d12607637e707aa9d513b7035f5e812efbde9305736b438d422103a7844e04124cad5efa4ff0e6226a57afa1210a1c7485c8e - languageName: node - linkType: hard - "@babel/plugin-transform-optional-chaining@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-optional-chaining@npm:7.24.1" @@ -2300,17 +1515,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-parameters@npm:^7.22.15": - version: 7.22.15 - resolution: "@babel/plugin-transform-parameters@npm:7.22.15" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 541188bb7d1876cad87687b5c7daf90f63d8208ae83df24acb1e2b05020ad1c78786b2723ca4054a83fcb74fb6509f30c4cacc5b538ee684224261ad5fb047c1 - languageName: node - linkType: hard - "@babel/plugin-transform-parameters@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-parameters@npm:7.24.1" @@ -2322,18 +1526,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-private-methods@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-private-methods@npm:7.22.5" - dependencies: - "@babel/helper-create-class-features-plugin": ^7.22.5 - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 321479b4fcb6d3b3ef622ab22fd24001e43d46e680e8e41324c033d5810c84646e470f81b44cbcbef5c22e99030784f7cac92f1829974da7a47a60a7139082c3 - languageName: node - linkType: hard - "@babel/plugin-transform-private-methods@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-private-methods@npm:7.24.1" @@ -2346,20 +1538,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-private-property-in-object@npm:^7.22.11": - version: 7.22.11 - resolution: "@babel/plugin-transform-private-property-in-object@npm:7.22.11" - dependencies: - "@babel/helper-annotate-as-pure": ^7.22.5 - "@babel/helper-create-class-features-plugin": ^7.22.11 - "@babel/helper-plugin-utils": ^7.22.5 - "@babel/plugin-syntax-private-property-in-object": ^7.14.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 4d029d84901e53c46dead7a46e2990a7bc62470f4e4ca58a0d063394f86652fd58fe4eea1eb941da3669cd536b559b9d058b342b59300026346b7a2a51badac8 - languageName: node - linkType: hard - "@babel/plugin-transform-private-property-in-object@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-private-property-in-object@npm:7.24.1" @@ -2374,17 +1552,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-property-literals@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-property-literals@npm:7.22.5" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 796176a3176106f77fcb8cd04eb34a8475ce82d6d03a88db089531b8f0453a2fb8b0c6ec9a52c27948bc0ea478becec449893741fc546dfc3930ab927e3f9f2e - languageName: node - linkType: hard - "@babel/plugin-transform-property-literals@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-property-literals@npm:7.24.1" @@ -2407,17 +1574,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-react-display-name@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-react-display-name@npm:7.22.5" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: a12bfd1e4e93055efca3ace3c34722571bda59d9740dca364d225d9c6e3ca874f134694d21715c42cc63d79efd46db9665bd4a022998767f9245f1e29d5d204d - languageName: node - linkType: hard - "@babel/plugin-transform-react-display-name@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-react-display-name@npm:7.24.1" @@ -2440,22 +1596,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-react-jsx@npm:^7.22.15, @babel/plugin-transform-react-jsx@npm:^7.22.5": - version: 7.22.15 - resolution: "@babel/plugin-transform-react-jsx@npm:7.22.15" - dependencies: - "@babel/helper-annotate-as-pure": ^7.22.5 - "@babel/helper-module-imports": ^7.22.15 - "@babel/helper-plugin-utils": ^7.22.5 - "@babel/plugin-syntax-jsx": ^7.22.5 - "@babel/types": ^7.22.15 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 3899054e89550c3a0ef041af7c47ee266e2e934f498ee80fefeda778a6aa177b48aa8b4d2a8bf5848de977fec564571699ab952d9fa089c4c19b45ddb121df09 - languageName: node - linkType: hard - -"@babel/plugin-transform-react-jsx@npm:^7.23.4": +"@babel/plugin-transform-react-jsx@npm:^7.22.5, @babel/plugin-transform-react-jsx@npm:^7.23.4": version: 7.23.4 resolution: "@babel/plugin-transform-react-jsx@npm:7.23.4" dependencies: @@ -2470,18 +1611,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-react-pure-annotations@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-react-pure-annotations@npm:7.22.5" - dependencies: - "@babel/helper-annotate-as-pure": ^7.22.5 - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 092021c4f404e267002099ec20b3f12dd730cb90b0d83c5feed3dc00dbe43b9c42c795a18e7c6c7d7bddea20c7dd56221b146aec81b37f2e7eb5137331c61120 - languageName: node - linkType: hard - "@babel/plugin-transform-react-pure-annotations@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-react-pure-annotations@npm:7.24.1" @@ -2494,18 +1623,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-regenerator@npm:^7.22.10": - version: 7.22.10 - resolution: "@babel/plugin-transform-regenerator@npm:7.22.10" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - regenerator-transform: ^0.15.2 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: e13678d62d6fa96f11cb8b863f00e8693491e7adc88bfca3f2820f80cbac8336e7dec3a596eee6a1c4663b7ececc3564f2cd7fb44ed6d4ce84ac2bb7f39ecc6e - languageName: node - linkType: hard - "@babel/plugin-transform-regenerator@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-regenerator@npm:7.24.1" @@ -2518,17 +1635,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-reserved-words@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-reserved-words@npm:7.22.5" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 3ffd7dbc425fe8132bfec118b9817572799cab1473113a635d25ab606c1f5a2341a636c04cf6b22df3813320365ed5a965b5eeb3192320a10e4cc2c137bd8bfc - languageName: node - linkType: hard - "@babel/plugin-transform-reserved-words@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-reserved-words@npm:7.24.1" @@ -2556,17 +1662,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-shorthand-properties@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-shorthand-properties@npm:7.22.5" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: a5ac902c56ea8effa99f681340ee61bac21094588f7aef0bc01dff98246651702e677552fa6d10e548c4ac22a3ffad047dd2f8c8f0540b68316c2c203e56818b - languageName: node - linkType: hard - "@babel/plugin-transform-shorthand-properties@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-shorthand-properties@npm:7.24.1" @@ -2578,18 +1673,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-spread@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-spread@npm:7.22.5" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - "@babel/helper-skip-transparent-expression-wrappers": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 5587f0deb60b3dfc9b274e269031cc45ec75facccf1933ea2ea71ced9fd3ce98ed91bb36d6cd26817c14474b90ed998c5078415f0eab531caf301496ce24c95c - languageName: node - linkType: hard - "@babel/plugin-transform-spread@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-spread@npm:7.24.1" @@ -2602,17 +1685,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-sticky-regex@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-sticky-regex@npm:7.22.5" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 63b2c575e3e7f96c32d52ed45ee098fb7d354b35c2223b8c8e76840b32cc529ee0c0ceb5742fd082e56e91e3d82842a367ce177e82b05039af3d602c9627a729 - languageName: node - linkType: hard - "@babel/plugin-transform-sticky-regex@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-sticky-regex@npm:7.24.1" @@ -2624,17 +1696,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-template-literals@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-template-literals@npm:7.22.5" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 27e9bb030654cb425381c69754be4abe6a7c75b45cd7f962cd8d604b841b2f0fb7b024f2efc1c25cc53f5b16d79d5e8cfc47cacbdaa983895b3aeefa3e7e24ff - languageName: node - linkType: hard - "@babel/plugin-transform-template-literals@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-template-literals@npm:7.24.1" @@ -2646,17 +1707,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-typeof-symbol@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-typeof-symbol@npm:7.22.5" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 82a53a63ffc3010b689ca9a54e5f53b2718b9f4b4a9818f36f9b7dba234f38a01876680553d2716a645a61920b5e6e4aaf8d4a0064add379b27ca0b403049512 - languageName: node - linkType: hard - "@babel/plugin-transform-typeof-symbol@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-typeof-symbol@npm:7.24.1" @@ -2668,20 +1718,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-typescript@npm:^7.22.15": - version: 7.22.15 - resolution: "@babel/plugin-transform-typescript@npm:7.22.15" - dependencies: - "@babel/helper-annotate-as-pure": ^7.22.5 - "@babel/helper-create-class-features-plugin": ^7.22.15 - "@babel/helper-plugin-utils": ^7.22.5 - "@babel/plugin-syntax-typescript": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: c5d96cdbf0e1512707aa1c1e3ac6b370a25fd9c545d26008ce44eb13a47bd7fd67a1eb799c98b5ccc82e33a345fda55c0055e1fe3ed97646ed405dd13020b226 - languageName: node - linkType: hard - "@babel/plugin-transform-typescript@npm:^7.24.1": version: 7.24.4 resolution: "@babel/plugin-transform-typescript@npm:7.24.4" @@ -2696,17 +1732,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-escapes@npm:^7.22.10": - version: 7.22.10 - resolution: "@babel/plugin-transform-unicode-escapes@npm:7.22.10" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 807f40ed1324c8cb107c45358f1903384ca3f0ef1d01c5a3c5c9b271c8d8eec66936a3dcc8d75ddfceea9421420368c2e77ae3adef0a50557e778dfe296bf382 - languageName: node - linkType: hard - "@babel/plugin-transform-unicode-escapes@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-unicode-escapes@npm:7.24.1" @@ -2718,18 +1743,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-property-regex@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-unicode-property-regex@npm:7.22.5" - dependencies: - "@babel/helper-create-regexp-features-plugin": ^7.22.5 - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 2495e5f663cb388e3d888b4ba3df419ac436a5012144ac170b622ddfc221f9ea9bdba839fa2bc0185cb776b578030666406452ec7791cbf0e7a3d4c88ae9574c - languageName: node - linkType: hard - "@babel/plugin-transform-unicode-property-regex@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-unicode-property-regex@npm:7.24.1" @@ -2742,39 +1755,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-regex@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-unicode-regex@npm:7.22.5" - dependencies: - "@babel/helper-create-regexp-features-plugin": ^7.22.5 - "@babel/helper-plugin-utils": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 6b5d1404c8c623b0ec9bd436c00d885a17d6a34f3f2597996343ddb9d94f6379705b21582dfd4cec2c47fd34068872e74ab6b9580116c0566b3f9447e2a7fa06 - languageName: node - linkType: hard - "@babel/plugin-transform-unicode-regex@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-unicode-regex@npm:7.24.1" dependencies: "@babel/helper-create-regexp-features-plugin": ^7.22.15 - "@babel/helper-plugin-utils": ^7.24.0 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 400a0927bdb1425b4c0dc68a61b5b2d7d17c7d9f0e07317a1a6a373c080ef94be1dd65fdc4ac9a78fcdb58f89fd128450c7bc0d5b8ca0ae7eca3fbd98e50acba - languageName: node - linkType: hard - -"@babel/plugin-transform-unicode-sets-regex@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-unicode-sets-regex@npm:7.22.5" - dependencies: - "@babel/helper-create-regexp-features-plugin": ^7.22.5 - "@babel/helper-plugin-utils": ^7.22.5 + "@babel/helper-plugin-utils": ^7.24.0 peerDependencies: - "@babel/core": ^7.0.0 - checksum: c042070f980b139547f8b0179efbc049ac5930abec7fc26ed7a41d89a048d8ab17d362200e204b6f71c3c20d6991a0e74415e1a412a49adc8131c2a40c04822e + "@babel/core": ^7.0.0-0 + checksum: 400a0927bdb1425b4c0dc68a61b5b2d7d17c7d9f0e07317a1a6a373c080ef94be1dd65fdc4ac9a78fcdb58f89fd128450c7bc0d5b8ca0ae7eca3fbd98e50acba languageName: node linkType: hard @@ -2790,97 +1779,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-env@npm:^7.12.1, @babel/preset-env@npm:^7.12.7, @babel/preset-env@npm:^7.19.4": - version: 7.22.20 - resolution: "@babel/preset-env@npm:7.22.20" - dependencies: - "@babel/compat-data": ^7.22.20 - "@babel/helper-compilation-targets": ^7.22.15 - "@babel/helper-plugin-utils": ^7.22.5 - "@babel/helper-validator-option": ^7.22.15 - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ^7.22.15 - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ^7.22.15 - "@babel/plugin-proposal-private-property-in-object": 7.21.0-placeholder-for-preset-env.2 - "@babel/plugin-syntax-async-generators": ^7.8.4 - "@babel/plugin-syntax-class-properties": ^7.12.13 - "@babel/plugin-syntax-class-static-block": ^7.14.5 - "@babel/plugin-syntax-dynamic-import": ^7.8.3 - "@babel/plugin-syntax-export-namespace-from": ^7.8.3 - "@babel/plugin-syntax-import-assertions": ^7.22.5 - "@babel/plugin-syntax-import-attributes": ^7.22.5 - "@babel/plugin-syntax-import-meta": ^7.10.4 - "@babel/plugin-syntax-json-strings": ^7.8.3 - "@babel/plugin-syntax-logical-assignment-operators": ^7.10.4 - "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3 - "@babel/plugin-syntax-numeric-separator": ^7.10.4 - "@babel/plugin-syntax-object-rest-spread": ^7.8.3 - "@babel/plugin-syntax-optional-catch-binding": ^7.8.3 - "@babel/plugin-syntax-optional-chaining": ^7.8.3 - "@babel/plugin-syntax-private-property-in-object": ^7.14.5 - "@babel/plugin-syntax-top-level-await": ^7.14.5 - "@babel/plugin-syntax-unicode-sets-regex": ^7.18.6 - "@babel/plugin-transform-arrow-functions": ^7.22.5 - "@babel/plugin-transform-async-generator-functions": ^7.22.15 - "@babel/plugin-transform-async-to-generator": ^7.22.5 - "@babel/plugin-transform-block-scoped-functions": ^7.22.5 - "@babel/plugin-transform-block-scoping": ^7.22.15 - "@babel/plugin-transform-class-properties": ^7.22.5 - "@babel/plugin-transform-class-static-block": ^7.22.11 - "@babel/plugin-transform-classes": ^7.22.15 - "@babel/plugin-transform-computed-properties": ^7.22.5 - "@babel/plugin-transform-destructuring": ^7.22.15 - "@babel/plugin-transform-dotall-regex": ^7.22.5 - "@babel/plugin-transform-duplicate-keys": ^7.22.5 - "@babel/plugin-transform-dynamic-import": ^7.22.11 - "@babel/plugin-transform-exponentiation-operator": ^7.22.5 - "@babel/plugin-transform-export-namespace-from": ^7.22.11 - "@babel/plugin-transform-for-of": ^7.22.15 - "@babel/plugin-transform-function-name": ^7.22.5 - "@babel/plugin-transform-json-strings": ^7.22.11 - "@babel/plugin-transform-literals": ^7.22.5 - "@babel/plugin-transform-logical-assignment-operators": ^7.22.11 - "@babel/plugin-transform-member-expression-literals": ^7.22.5 - "@babel/plugin-transform-modules-amd": ^7.22.5 - "@babel/plugin-transform-modules-commonjs": ^7.22.15 - "@babel/plugin-transform-modules-systemjs": ^7.22.11 - "@babel/plugin-transform-modules-umd": ^7.22.5 - "@babel/plugin-transform-named-capturing-groups-regex": ^7.22.5 - "@babel/plugin-transform-new-target": ^7.22.5 - "@babel/plugin-transform-nullish-coalescing-operator": ^7.22.11 - "@babel/plugin-transform-numeric-separator": ^7.22.11 - "@babel/plugin-transform-object-rest-spread": ^7.22.15 - "@babel/plugin-transform-object-super": ^7.22.5 - "@babel/plugin-transform-optional-catch-binding": ^7.22.11 - "@babel/plugin-transform-optional-chaining": ^7.22.15 - "@babel/plugin-transform-parameters": ^7.22.15 - "@babel/plugin-transform-private-methods": ^7.22.5 - "@babel/plugin-transform-private-property-in-object": ^7.22.11 - "@babel/plugin-transform-property-literals": ^7.22.5 - "@babel/plugin-transform-regenerator": ^7.22.10 - "@babel/plugin-transform-reserved-words": ^7.22.5 - "@babel/plugin-transform-shorthand-properties": ^7.22.5 - "@babel/plugin-transform-spread": ^7.22.5 - "@babel/plugin-transform-sticky-regex": ^7.22.5 - "@babel/plugin-transform-template-literals": ^7.22.5 - "@babel/plugin-transform-typeof-symbol": ^7.22.5 - "@babel/plugin-transform-unicode-escapes": ^7.22.10 - "@babel/plugin-transform-unicode-property-regex": ^7.22.5 - "@babel/plugin-transform-unicode-regex": ^7.22.5 - "@babel/plugin-transform-unicode-sets-regex": ^7.22.5 - "@babel/preset-modules": 0.1.6-no-external-plugins - "@babel/types": ^7.22.19 - babel-plugin-polyfill-corejs2: ^0.4.5 - babel-plugin-polyfill-corejs3: ^0.8.3 - babel-plugin-polyfill-regenerator: ^0.5.2 - core-js-compat: ^3.31.0 - semver: ^6.3.1 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 99357a5cb30f53bacdc0d1cd6dff0f052ea6c2d1ba874d969bba69897ef716e87283e84a59dc52fb49aa31fd1b6f55ed756c64c04f5678380700239f6030b881 - languageName: node - linkType: hard - -"@babel/preset-env@npm:^7.22.9": +"@babel/preset-env@npm:^7.12.1, @babel/preset-env@npm:^7.12.7, @babel/preset-env@npm:^7.19.4, @babel/preset-env@npm:^7.22.9": version: 7.24.4 resolution: "@babel/preset-env@npm:7.24.4" dependencies: @@ -2984,23 +1883,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-react@npm:^7.12.5, @babel/preset-react@npm:^7.12.7, @babel/preset-react@npm:^7.18.6": - version: 7.22.15 - resolution: "@babel/preset-react@npm:7.22.15" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - "@babel/helper-validator-option": ^7.22.15 - "@babel/plugin-transform-react-display-name": ^7.22.5 - "@babel/plugin-transform-react-jsx": ^7.22.15 - "@babel/plugin-transform-react-jsx-development": ^7.22.5 - "@babel/plugin-transform-react-pure-annotations": ^7.22.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: c3ef99dfa2e9f57d2e08603e883aa20f47630a826c8e413888a93ae6e0084b5016871e463829be125329d40a1ba0a89f7c43d77b6dab52083c225cb43e63d10e - languageName: node - linkType: hard - -"@babel/preset-react@npm:^7.22.5": +"@babel/preset-react@npm:^7.12.5, @babel/preset-react@npm:^7.12.7, @babel/preset-react@npm:^7.18.6, @babel/preset-react@npm:^7.22.5": version: 7.24.1 resolution: "@babel/preset-react@npm:7.24.1" dependencies: @@ -3016,22 +1899,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-typescript@npm:^7.18.6": - version: 7.22.15 - resolution: "@babel/preset-typescript@npm:7.22.15" - dependencies: - "@babel/helper-plugin-utils": ^7.22.5 - "@babel/helper-validator-option": ^7.22.15 - "@babel/plugin-syntax-jsx": ^7.22.5 - "@babel/plugin-transform-modules-commonjs": ^7.22.15 - "@babel/plugin-transform-typescript": ^7.22.15 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 02ac4d5c812a52357c8f517f81584725f06f385d54ccfda89dd082e0ed89a94bd9f4d9b05fa1cbdcf426e3489c1921f04c93c5acc5deea83407a64c22ad2feb4 - languageName: node - linkType: hard - -"@babel/preset-typescript@npm:^7.22.5": +"@babel/preset-typescript@npm:^7.18.6, @babel/preset-typescript@npm:^7.22.5": version: 7.24.1 resolution: "@babel/preset-typescript@npm:7.24.1" dependencies: @@ -3063,16 +1931,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.3, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.8.4": - version: 7.22.15 - resolution: "@babel/runtime@npm:7.22.15" - dependencies: - regenerator-runtime: ^0.14.0 - checksum: 793296df1e41599a935a3d77ec01eb6088410d3fd4dbe4e92f06c6b7bb2f8355024e6d78621a3a35f44e0e23b0b59107f23d585384df4f3123256a1e1492040e - languageName: node - linkType: hard - -"@babel/runtime@npm:^7.22.6": +"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.3, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.6, @babel/runtime@npm:^7.8.4": version: 7.24.4 resolution: "@babel/runtime@npm:7.24.4" dependencies: @@ -3081,18 +1940,7 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.12.7, @babel/template@npm:^7.22.15, @babel/template@npm:^7.22.5, @babel/template@npm:^7.23.9, @babel/template@npm:^7.3.3": - version: 7.23.9 - resolution: "@babel/template@npm:7.23.9" - dependencies: - "@babel/code-frame": ^7.23.5 - "@babel/parser": ^7.23.9 - "@babel/types": ^7.23.9 - checksum: 6e67414c0f7125d7ecaf20c11fab88085fa98a96c3ef10da0a61e962e04fdf3a18a496a66047005ddd1bb682a7cc7842d556d1db2f3f3f6ccfca97d5e445d342 - languageName: node - linkType: hard - -"@babel/template@npm:^7.24.0": +"@babel/template@npm:^7.12.7, @babel/template@npm:^7.22.15, @babel/template@npm:^7.24.0, @babel/template@npm:^7.3.3": version: 7.24.0 resolution: "@babel/template@npm:7.24.0" dependencies: @@ -3103,25 +1951,7 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.12.9, @babel/traverse@npm:^7.23.9": - version: 7.23.9 - resolution: "@babel/traverse@npm:7.23.9" - dependencies: - "@babel/code-frame": ^7.23.5 - "@babel/generator": ^7.23.6 - "@babel/helper-environment-visitor": ^7.22.20 - "@babel/helper-function-name": ^7.23.0 - "@babel/helper-hoist-variables": ^7.22.5 - "@babel/helper-split-export-declaration": ^7.22.6 - "@babel/parser": ^7.23.9 - "@babel/types": ^7.23.9 - debug: ^4.3.1 - globals: ^11.1.0 - checksum: a932f7aa850e158c00c97aad22f639d48c72805c687290f6a73e30c5c4957c07f5d28310c9bf59648e2980fe6c9d16adeb2ff92a9ca0f97fa75739c1328fc6c3 - languageName: node - linkType: hard - -"@babel/traverse@npm:^7.22.8, @babel/traverse@npm:^7.24.1": +"@babel/traverse@npm:^7.12.9, @babel/traverse@npm:^7.22.8, @babel/traverse@npm:^7.24.1": version: 7.24.1 resolution: "@babel/traverse@npm:7.24.1" dependencies: @@ -3139,18 +1969,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.6, @babel/types@npm:^7.12.7, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.6, @babel/types@npm:^7.23.9, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": - version: 7.23.9 - resolution: "@babel/types@npm:7.23.9" - dependencies: - "@babel/helper-string-parser": ^7.23.4 - "@babel/helper-validator-identifier": ^7.22.20 - to-fast-properties: ^2.0.0 - checksum: 0a9b008e9bfc89beb8c185e620fa0f8ed6c771f1e1b2e01e1596870969096fec7793898a1d64a035176abf1dd13e2668ee30bf699f2d92c210a8128f4b151e65 - languageName: node - linkType: hard - -"@babel/types@npm:^7.23.4, @babel/types@npm:^7.24.0": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.6, @babel/types@npm:^7.12.7, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.4, @babel/types@npm:^7.24.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": version: 7.24.0 resolution: "@babel/types@npm:7.24.0" dependencies: @@ -4184,18 +3003,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/gen-mapping@npm:^0.3.0, @jridgewell/gen-mapping@npm:^0.3.2": - version: 0.3.3 - resolution: "@jridgewell/gen-mapping@npm:0.3.3" - dependencies: - "@jridgewell/set-array": ^1.0.1 - "@jridgewell/sourcemap-codec": ^1.4.10 - "@jridgewell/trace-mapping": ^0.3.9 - checksum: 4a74944bd31f22354fc01c3da32e83c19e519e3bbadafa114f6da4522ea77dd0c2842607e923a591d60a76699d819a2fbb6f3552e277efdb9b58b081390b60ab - languageName: node - linkType: hard - -"@jridgewell/gen-mapping@npm:^0.3.5": +"@jridgewell/gen-mapping@npm:^0.3.0, @jridgewell/gen-mapping@npm:^0.3.5": version: 0.3.5 resolution: "@jridgewell/gen-mapping@npm:0.3.5" dependencies: @@ -4213,13 +3021,6 @@ __metadata: languageName: node linkType: hard -"@jridgewell/set-array@npm:^1.0.1": - version: 1.1.2 - resolution: "@jridgewell/set-array@npm:1.1.2" - checksum: 69a84d5980385f396ff60a175f7177af0b8da4ddb81824cb7016a9ef914eee9806c72b6b65942003c63f7983d4f39a5c6c27185bbca88eb4690b62075602e28e - languageName: node - linkType: hard - "@jridgewell/set-array@npm:^1.2.1": version: 1.2.1 resolution: "@jridgewell/set-array@npm:1.2.1" @@ -4254,17 +3055,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.17, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.9": - version: 0.3.19 - resolution: "@jridgewell/trace-mapping@npm:0.3.19" - dependencies: - "@jridgewell/resolve-uri": ^3.1.0 - "@jridgewell/sourcemap-codec": ^1.4.14 - checksum: 956a6f0f6fec060fb48c6bf1f5ec2064e13cd38c8be3873877d4b92b4a27ba58289a34071752671262a3e3c202abcc3fa2aac64d8447b4b0fa1ba3c9047f1c20 - languageName: node - linkType: hard - -"@jridgewell/trace-mapping@npm:^0.3.20, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": +"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.20, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25, @jridgewell/trace-mapping@npm:^0.3.9": version: 0.3.25 resolution: "@jridgewell/trace-mapping@npm:0.3.25" dependencies: @@ -6694,10 +5485,10 @@ __metadata: languageName: node linkType: hard -"@types/estree@npm:*, @types/estree@npm:^1.0.0": - version: 1.0.1 - resolution: "@types/estree@npm:1.0.1" - checksum: e9aa175eacb797216fafce4d41e8202c7a75555bc55232dee0f9903d7171f8f19f0ae7d5191bb1a88cb90e65468be508c0df850a9fb81b4433b293a5a749899d +"@types/estree@npm:*, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.5": + version: 1.0.5 + resolution: "@types/estree@npm:1.0.5" + checksum: dd8b5bed28e6213b7acd0fb665a84e693554d850b0df423ac8076cc3ad5823a6bc26b0251d080bdc545af83179ede51dd3f6fa78cad2c46ed1f29624ddf3e41a languageName: node linkType: hard @@ -6708,13 +5499,6 @@ __metadata: languageName: node linkType: hard -"@types/estree@npm:^1.0.5": - version: 1.0.5 - resolution: "@types/estree@npm:1.0.5" - checksum: dd8b5bed28e6213b7acd0fb665a84e693554d850b0df423ac8076cc3ad5823a6bc26b0251d080bdc545af83179ede51dd3f6fa78cad2c46ed1f29624ddf3e41a - languageName: node - linkType: hard - "@types/express-serve-static-core@npm:*, @types/express-serve-static-core@npm:^4.17.33": version: 4.17.36 resolution: "@types/express-serve-static-core@npm:4.17.36" @@ -6799,14 +5583,7 @@ __metadata: languageName: node linkType: hard -"@types/http-cache-semantics@npm:*": - version: 4.0.2 - resolution: "@types/http-cache-semantics@npm:4.0.2" - checksum: 513429786a45d8124f93cc7ea1454b692008190ef743e9fec75a6a3c998309782d216f1e67d7d497ffece9c9212310ae05a8c56e8955492ee400eacdd7620e61 - languageName: node - linkType: hard - -"@types/http-cache-semantics@npm:^4.0.2": +"@types/http-cache-semantics@npm:*, @types/http-cache-semantics@npm:^4.0.2": version: 4.0.4 resolution: "@types/http-cache-semantics@npm:4.0.4" checksum: 7f4dd832e618bc1e271be49717d7b4066d77c2d4eed5b81198eb987e532bb3e1c7e02f45d77918185bad936f884b700c10cebe06305f50400f382ab75055f9e8 @@ -7066,18 +5843,7 @@ __metadata: languageName: node linkType: hard -"@types/react-router-config@npm:*": - version: 5.0.7 - resolution: "@types/react-router-config@npm:5.0.7" - dependencies: - "@types/history": ^4.7.11 - "@types/react": "*" - "@types/react-router": ^5.1.0 - checksum: e7ecc3fc957a41a22d64c53529e801c434d8b3fb80d0b98e9fc614b2d34ede1b89ec32bbaf68ead8ec7e573a485ac6a5476142e6e659bbee0697599f206070a7 - languageName: node - linkType: hard - -"@types/react-router-config@npm:^5.0.7": +"@types/react-router-config@npm:*, @types/react-router-config@npm:^5.0.7": version: 5.0.11 resolution: "@types/react-router-config@npm:5.0.11" dependencies: @@ -7249,20 +6015,13 @@ __metadata: languageName: node linkType: hard -"@types/unist@npm:^2": +"@types/unist@npm:^2, @types/unist@npm:^2.0.0": version: 2.0.10 resolution: "@types/unist@npm:2.0.10" checksum: e2924e18dedf45f68a5c6ccd6015cd62f1643b1b43baac1854efa21ae9e70505db94290434a23da1137d9e31eb58e54ca175982005698ac37300a1c889f6c4aa languageName: node linkType: hard -"@types/unist@npm:^2.0.0": - version: 2.0.8 - resolution: "@types/unist@npm:2.0.8" - checksum: f4852d10a6752dc70df363917ef74453e5d2fd42824c0f6d09d19d530618e1402193977b1207366af4415aaec81d4e262c64d00345402020c4ca179216e553c7 - languageName: node - linkType: hard - "@types/urijs@npm:^1.19.19": version: 1.19.20 resolution: "@types/urijs@npm:1.19.20" @@ -7752,7 +6511,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.0.0": +"acorn@npm:^8.0.0, acorn@npm:^8.0.4, acorn@npm:^8.2.4, acorn@npm:^8.4.1, acorn@npm:^8.7.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": version: 8.11.3 resolution: "acorn@npm:8.11.3" bin: @@ -7761,15 +6520,6 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.0.4, acorn@npm:^8.2.4, acorn@npm:^8.4.1, acorn@npm:^8.7.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": - version: 8.10.0 - resolution: "acorn@npm:8.10.0" - bin: - acorn: bin/acorn - checksum: 538ba38af0cc9e5ef983aee196c4b8b4d87c0c94532334fa7e065b2c8a1f85863467bb774231aae91613fcda5e68740c15d97b1967ae3394d20faddddd8af61d - languageName: node - linkType: hard - "add-stream@npm:^1.0.0": version: 1.0.0 resolution: "add-stream@npm:1.0.0" @@ -7940,7 +6690,7 @@ __metadata: languageName: node linkType: hard -"algoliasearch@npm:^4.18.0": +"algoliasearch@npm:^4.18.0, algoliasearch@npm:^4.19.1": version: 4.23.3 resolution: "algoliasearch@npm:4.23.3" dependencies: @@ -7963,28 +6713,6 @@ __metadata: languageName: node linkType: hard -"algoliasearch@npm:^4.19.1": - version: 4.20.0 - resolution: "algoliasearch@npm:4.20.0" - dependencies: - "@algolia/cache-browser-local-storage": 4.20.0 - "@algolia/cache-common": 4.20.0 - "@algolia/cache-in-memory": 4.20.0 - "@algolia/client-account": 4.20.0 - "@algolia/client-analytics": 4.20.0 - "@algolia/client-common": 4.20.0 - "@algolia/client-personalization": 4.20.0 - "@algolia/client-search": 4.20.0 - "@algolia/logger-common": 4.20.0 - "@algolia/logger-console": 4.20.0 - "@algolia/requester-browser-xhr": 4.20.0 - "@algolia/requester-common": 4.20.0 - "@algolia/requester-node-http": 4.20.0 - "@algolia/transporter": 4.20.0 - checksum: 078954944452f57d2e3b47c6ed4905caf797814324a4d5068a8b6685d434a885977a3e607714c5fb6eb29c7c3e717b3ee9cb01c8b2320e2c7bd73bcd8d42e70f - languageName: node - linkType: hard - "amqplib@npm:^0.10.3": version: 0.10.3 resolution: "amqplib@npm:0.10.3" @@ -8371,25 +7099,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"autoprefixer@npm:^10.4.12": - version: 10.4.15 - resolution: "autoprefixer@npm:10.4.15" - dependencies: - browserslist: ^4.21.10 - caniuse-lite: ^1.0.30001520 - fraction.js: ^4.2.0 - normalize-range: ^0.1.2 - picocolors: ^1.0.0 - postcss-value-parser: ^4.2.0 - peerDependencies: - postcss: ^8.1.0 - bin: - autoprefixer: bin/autoprefixer - checksum: d490b14fb098c043e109fc13cd23628f146af99a493d35b9df3a26f8ec0b4dd8937c5601cdbaeb465b98ea31d3ea05aa7184711d4d93dfb52358d073dcb67032 - languageName: node - linkType: hard - -"autoprefixer@npm:^10.4.14": +"autoprefixer@npm:^10.4.12, autoprefixer@npm:^10.4.14": version: 10.4.19 resolution: "autoprefixer@npm:10.4.19" dependencies: @@ -8518,19 +7228,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"babel-plugin-polyfill-corejs2@npm:^0.4.5": - version: 0.4.5 - resolution: "babel-plugin-polyfill-corejs2@npm:0.4.5" - dependencies: - "@babel/compat-data": ^7.22.6 - "@babel/helper-define-polyfill-provider": ^0.4.2 - semver: ^6.3.1 - peerDependencies: - "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 33a8e06aa54e2858d211c743d179f0487b03222f9ca1bfd7c4865bca243fca942a3358cb75f6bb894ed476cbddede834811fbd6903ff589f055821146f053e1a - languageName: node - linkType: hard - "babel-plugin-polyfill-corejs3@npm:^0.10.1, babel-plugin-polyfill-corejs3@npm:^0.10.4": version: 0.10.4 resolution: "babel-plugin-polyfill-corejs3@npm:0.10.4" @@ -8543,29 +7240,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"babel-plugin-polyfill-corejs3@npm:^0.8.3": - version: 0.8.3 - resolution: "babel-plugin-polyfill-corejs3@npm:0.8.3" - dependencies: - "@babel/helper-define-polyfill-provider": ^0.4.2 - core-js-compat: ^3.31.0 - peerDependencies: - "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: dcbb30e551702a82cfd4d2c375da2c317658e55f95e9edcda93b9bbfdcc8fb6e5344efcb144e04d3406859e7682afce7974c60ededd9f12072a48a83dd22a0da - languageName: node - linkType: hard - -"babel-plugin-polyfill-regenerator@npm:^0.5.2": - version: 0.5.2 - resolution: "babel-plugin-polyfill-regenerator@npm:0.5.2" - dependencies: - "@babel/helper-define-polyfill-provider": ^0.4.2 - peerDependencies: - "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: d962200f604016a9a09bc9b4aaf60a3db7af876bb65bcefaeac04d44ac9d9ec4037cf24ce117760cc141d7046b6394c7eb0320ba9665cb4a2ee64df2be187c93 - languageName: node - linkType: hard - "babel-plugin-polyfill-regenerator@npm:^0.6.1": version: 0.6.1 resolution: "babel-plugin-polyfill-regenerator@npm:0.6.1" @@ -8874,21 +7548,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"browserslist@npm:^4.0.0, browserslist@npm:^4.18.1, browserslist@npm:^4.21.10, browserslist@npm:^4.21.4, browserslist@npm:^4.22.2": - version: 4.22.3 - resolution: "browserslist@npm:4.22.3" - dependencies: - caniuse-lite: ^1.0.30001580 - electron-to-chromium: ^1.4.648 - node-releases: ^2.0.14 - update-browserslist-db: ^1.0.13 - bin: - browserslist: cli.js - checksum: e62b17348e92143fe58181b02a6a97c4a98bd812d1dc9274673a54f73eec53dbed1c855ebf73e318ee00ee039f23c9a6d0e7629d24f3baef08c7a5b469742d57 - languageName: node - linkType: hard - -"browserslist@npm:^4.23.0": +"browserslist@npm:^4.0.0, browserslist@npm:^4.18.1, browserslist@npm:^4.21.10, browserslist@npm:^4.21.4, browserslist@npm:^4.22.2, browserslist@npm:^4.23.0": version: 4.23.0 resolution: "browserslist@npm:4.23.0" dependencies: @@ -9207,14 +7867,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001520, caniuse-lite@npm:^1.0.30001580": - version: 1.0.30001582 - resolution: "caniuse-lite@npm:1.0.30001582" - checksum: 2fc420cb6e6080a9808781ff81a2f0d37d63897c8c981d477001be18e55c8ec33422cf966e49efdca61d4a5335d16a4d6a09d5bd6da22a8396d0dcd350b9b9da - languageName: node - linkType: hard - -"caniuse-lite@npm:^1.0.30001587, caniuse-lite@npm:^1.0.30001599": +"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001587, caniuse-lite@npm:^1.0.30001599": version: 1.0.30001612 resolution: "caniuse-lite@npm:1.0.30001612" checksum: 2b6ab6a19c72bdf8dccac824944e828a2a1fae52c6dfeb2d64ccecfd60d0466d2e5a392e996da2150d92850188a5034666dceed34a38d978177f6934e0bf106d @@ -9431,16 +8084,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"clean-css@npm:^5.2.2": - version: 5.3.2 - resolution: "clean-css@npm:5.3.2" - dependencies: - source-map: ~0.6.0 - checksum: 8787b281acc9878f309b5f835d410085deedfd4e126472666773040a6a8a72f472a1d24185947d23b87b1c419bf2c5ed429395d5c5ff8279c98b05d8011e9758 - languageName: node - linkType: hard - -"clean-css@npm:^5.3.2, clean-css@npm:~5.3.2": +"clean-css@npm:^5.2.2, clean-css@npm:^5.3.2, clean-css@npm:~5.3.2": version: 5.3.3 resolution: "clean-css@npm:5.3.3" dependencies: @@ -10172,16 +8816,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"core-js-compat@npm:^3.31.0": - version: 3.32.2 - resolution: "core-js-compat@npm:3.32.2" - dependencies: - browserslist: ^4.21.10 - checksum: efca146ad71a542e6f196db5ba5aed617e48c615bdf1fbb065471b3267f833ac545bd5fc5ad0642c3d3974b955f0684ff0863d7471d7050ee0284e0a1313942e - languageName: node - linkType: hard - -"core-js-compat@npm:^3.36.1": +"core-js-compat@npm:^3.31.0, core-js-compat@npm:^3.36.1": version: 3.37.0 resolution: "core-js-compat@npm:3.37.0" dependencies: @@ -11726,13 +10361,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"electron-to-chromium@npm:^1.4.648": - version: 1.4.653 - resolution: "electron-to-chromium@npm:1.4.653" - checksum: 5e1fb48e749811f4384cd7a9940585a124f46d2a9f5bdfd2d7e79685d55db448433da303bd0fb6c3665ed052299843b5661319e10119821defbc3be8ff8eb060 - languageName: node - linkType: hard - "electron-to-chromium@npm:^1.4.668": version: 1.4.745 resolution: "electron-to-chromium@npm:1.4.745" @@ -13229,13 +11857,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"fraction.js@npm:^4.2.0": - version: 4.3.6 - resolution: "fraction.js@npm:4.3.6" - checksum: e96ae77e64ebfd442d3a5a01a3f0637b0663fc2440bcf2841b3ad9341ba24c81fb2e3e7142e43ef7d088558c6b3f8609df135b201adc7a1c674aea6a71384162 - languageName: node - linkType: hard - "fraction.js@npm:^4.3.7": version: 4.3.7 resolution: "fraction.js@npm:4.3.7" @@ -13280,18 +11901,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"fs-extra@npm:^11.1.0": - version: 11.1.1 - resolution: "fs-extra@npm:11.1.1" - dependencies: - graceful-fs: ^4.2.0 - jsonfile: ^6.0.1 - universalify: ^2.0.0 - checksum: fb883c68245b2d777fbc1f2082c9efb084eaa2bbf9fddaa366130d196c03608eebef7fb490541276429ee1ca99f317e2d73e96f5ca0999eefedf5a624ae1edfd - languageName: node - linkType: hard - -"fs-extra@npm:^11.1.1": +"fs-extra@npm:^11.1.0, fs-extra@npm:^11.1.1": version: 11.2.0 resolution: "fs-extra@npm:11.2.0" dependencies: @@ -18858,7 +17468,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"nanoid@npm:^3.3.6, nanoid@npm:^3.3.7": +"nanoid@npm:^3.3.7": version: 3.3.7 resolution: "nanoid@npm:3.3.7" bin: @@ -20443,6 +19053,33 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"parserapiv3@npm:@asyncapi/parser@^3.0.7": + version: 3.0.7 + resolution: "@asyncapi/parser@npm:3.0.7" + dependencies: + "@asyncapi/specs": ^6.5.0 + "@openapi-contrib/openapi-schema-to-json-schema": ~3.2.0 + "@stoplight/json": ^3.20.2 + "@stoplight/json-ref-readers": ^1.2.2 + "@stoplight/json-ref-resolver": ^3.1.5 + "@stoplight/spectral-core": ^1.16.1 + "@stoplight/spectral-functions": ^1.7.2 + "@stoplight/spectral-parsers": ^1.0.2 + "@stoplight/spectral-ref-resolver": ^1.0.3 + "@stoplight/types": ^13.12.0 + "@types/json-schema": ^7.0.11 + "@types/urijs": ^1.19.19 + ajv: ^8.11.0 + ajv-errors: ^3.0.0 + ajv-formats: ^2.1.1 + avsc: ^5.7.5 + js-yaml: ^4.1.0 + jsonpath-plus: ^7.2.0 + node-fetch: 2.6.7 + checksum: 8297f415c43c0e539cdeaaa5ef48c920f24e9af2efae8f7d14c8f1768238bce02075f7f649d29f953eeb9634e8d6692d08e831e3c2fab11bb6c8a98ccbd8f67a + languageName: node + linkType: hard + "parseurl@npm:~1.3.2, parseurl@npm:~1.3.3": version: 1.3.3 resolution: "parseurl@npm:1.3.3" @@ -21183,18 +19820,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"postcss@npm:^8.4.17, postcss@npm:^8.4.21": - version: 8.4.30 - resolution: "postcss@npm:8.4.30" - dependencies: - nanoid: ^3.3.6 - picocolors: ^1.0.0 - source-map-js: ^1.0.2 - checksum: 6c810c10c9bd3e03ca016e0b6b6756261e640aba1a9a7b1200b55502bc34b9165e38f590aef3493afc2f30ab55cdfcd43fd0f8408d69a77318ddbcf2a8ad164b - languageName: node - linkType: hard - -"postcss@npm:^8.4.26, postcss@npm:^8.4.33": +"postcss@npm:^8.4.17, postcss@npm:^8.4.21, postcss@npm:^8.4.26, postcss@npm:^8.4.33": version: 8.4.38 resolution: "postcss@npm:8.4.38" dependencies: @@ -23391,13 +22017,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"source-map-js@npm:^1.0.2": - version: 1.0.2 - resolution: "source-map-js@npm:1.0.2" - checksum: c049a7fc4deb9a7e9b481ae3d424cc793cb4845daa690bc5a05d428bf41bf231ced49b4cf0c9e77f9d42fdb3d20d6187619fc586605f5eabe995a316da8d377c - languageName: node - linkType: hard - "source-map-js@npm:^1.2.0": version: 1.2.0 resolution: "source-map-js@npm:1.2.0" @@ -24221,21 +22840,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"terser@npm:^5.10.0": - version: 5.19.4 - resolution: "terser@npm:5.19.4" - dependencies: - "@jridgewell/source-map": ^0.3.3 - acorn: ^8.8.2 - commander: ^2.20.0 - source-map-support: ~0.5.20 - bin: - terser: bin/terser - checksum: 09273ce7d3fbe8fea0ec2603ad1c06cc304838bdac42bbfe77835b0b0b6c4a894054575ca518fe16c95d5c401574a8c703f4fde97da45f1c972ea568e6ecafda - languageName: node - linkType: hard - -"terser@npm:^5.15.1, terser@npm:^5.26.0": +"terser@npm:^5.10.0, terser@npm:^5.15.1, terser@npm:^5.26.0": version: 5.30.3 resolution: "terser@npm:5.30.3" dependencies: @@ -24299,19 +22904,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"threadedclass@npm:^1.2.1": - version: 1.2.1 - resolution: "threadedclass@npm:1.2.1" - dependencies: - callsites: ^3.1.0 - eventemitter3: ^4.0.4 - is-running: ^2.1.0 - tslib: ^1.13.0 - checksum: 2f9cab0df9ed21865f6f874ff7e352bbc32c9970bb48f132ae9bd71d9203655089ad065fa8438bdb786e381c3e0284b43bf7f281ad607962ab365781ea9aef6f - languageName: node - linkType: hard - -"threadedclass@npm:^1.2.2": +"threadedclass@npm:^1.2.1, threadedclass@npm:^1.2.2": version: 1.2.2 resolution: "threadedclass@npm:1.2.2" dependencies: From 16ca1522a89b000afbec66c761c40f0966479bc8 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Fri, 28 Jun 2024 07:11:11 +0200 Subject: [PATCH 382/479] fix: improve logging of error in handleUpdatedPackageInfoForRundown is ingestRundown is missing --- packages/job-worker/src/ingest/lock.ts | 7 ++++--- packages/job-worker/src/ingest/packageInfo.ts | 11 ++++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/job-worker/src/ingest/lock.ts b/packages/job-worker/src/ingest/lock.ts index d5be901bf9..a37a6f0494 100644 --- a/packages/job-worker/src/ingest/lock.ts +++ b/packages/job-worker/src/ingest/lock.ts @@ -37,6 +37,7 @@ export interface CommitIngestData { } export enum UpdateIngestRundownAction { + REJECT = 'reject', DELETE = 'delete', } @@ -79,9 +80,9 @@ export async function runIngestJob( const updatedIngestRundown = updateCacheFcn(clone(oldIngestRundown)) let newIngestRundown: LocalIngestRundown | undefined switch (updatedIngestRundown) { - // case UpdateIngestRundownAction.REJECT: - // // Reject change - // return + case UpdateIngestRundownAction.REJECT: + // Reject change + return case UpdateIngestRundownAction.DELETE: ingestObjCache.delete() newIngestRundown = undefined diff --git a/packages/job-worker/src/ingest/packageInfo.ts b/packages/job-worker/src/ingest/packageInfo.ts index 616b5c0add..3dcc618a61 100644 --- a/packages/job-worker/src/ingest/packageInfo.ts +++ b/packages/job-worker/src/ingest/packageInfo.ts @@ -4,7 +4,7 @@ import { ExpectedPackagesRegenerateProps, PackageInfosUpdatedProps } from '@sofi import { logger } from '../logging' import { JobContext } from '../jobs' import { regenerateSegmentsFromIngestData } from './generationSegment' -import { runIngestJob, runWithRundownLock } from './lock' +import { UpdateIngestRundownAction, runIngestJob, runWithRundownLock } from './lock' import { CacheForIngest } from './cache' import { updateExpectedPackagesOnRundown } from './expectedPackages' @@ -41,11 +41,16 @@ export async function handleUpdatedPackageInfoForRundown( context, data, (ingestRundown) => { - if (!ingestRundown) throw new Error('onUpdatedPackageInfoForRundown called but ingestData is undefined') + if (!ingestRundown) { + logger.error( + `onUpdatedPackageInfoForRundown called but ingestRundown is undefined (rundownExternalId: "${data.rundownExternalId}")` + ) + return UpdateIngestRundownAction.REJECT + } return ingestRundown // don't mutate any ingest data }, async (context, cache, ingestRundown) => { - if (!ingestRundown) throw new Error('onUpdatedPackageInfoForRundown called but ingestData is undefined') + if (!ingestRundown) throw new Error('onUpdatedPackageInfoForRundown called but ingestRundown is undefined') /** All segments that need updating */ const segmentsToUpdate = new Set() From 2aa28c7f0523ec98ecab2ea122278f2daef6dce3 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Fri, 28 Jun 2024 07:12:35 +0200 Subject: [PATCH 383/479] fix: ignore PackageInfo updates for orphaned rundowns (because handleUpdatedPackageInfoForRundown() will error if there is no ingestRundown) --- meteor/server/api/ingest/packageInfo.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/meteor/server/api/ingest/packageInfo.ts b/meteor/server/api/ingest/packageInfo.ts index 50559cd61a..ea9f946ad4 100644 --- a/meteor/server/api/ingest/packageInfo.ts +++ b/meteor/server/api/ingest/packageInfo.ts @@ -77,6 +77,14 @@ async function onUpdatedPackageInfoForRundown( ) return } + if (tmpRundown.orphaned) { + logger.debug( + `onUpdatedPackageInfoForRundown: Ignoring Rundown "${rundownId}", because it is orphaned ("${ + tmpRundown.orphaned + }"), for packages "${packageIds.join(', ')}"` + ) + return + } await runIngestOperation(tmpRundown.studioId, IngestJobs.PackageInfosUpdated, { rundownExternalId: tmpRundown.externalId, From 13264bb5a48056d895f9bd206437554b3b2499fd Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 28 Jun 2024 15:10:56 +0100 Subject: [PATCH 384/479] fix: `Cannot replace infinite PieceInstance` during normal operation SOFIE-3305 Pieces are deleted by setting them to `null`, so a `.has` check will match deleted pieces which is not desired --- .../model/implementation/PlayoutPartInstanceModelImpl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts index a45047a858..ec82ccab1f 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts @@ -384,7 +384,7 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { } for (const pieceInstance of pieceInstances) { - if (this.pieceInstancesImpl.has(pieceInstance._id)) + if (this.pieceInstancesImpl.get(pieceInstance._id)) throw new Error( `Cannot replace infinite PieceInstance "${pieceInstance._id}" as it replaces a non-infinite` ) From e2ecc7eb48b9ad6c0d95b299d954abac577e70a7 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Tue, 2 Jul 2024 08:07:04 +0200 Subject: [PATCH 385/479] fix: make stringifyError handle UserError better --- packages/corelib/src/__tests__/error.spec.ts | 29 +++++++++++++++++++ packages/shared-lib/src/lib/stringifyError.ts | 13 +++++---- 2 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 packages/corelib/src/__tests__/error.spec.ts diff --git a/packages/corelib/src/__tests__/error.spec.ts b/packages/corelib/src/__tests__/error.spec.ts new file mode 100644 index 0000000000..1116728509 --- /dev/null +++ b/packages/corelib/src/__tests__/error.spec.ts @@ -0,0 +1,29 @@ +import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' +import { UserError, UserErrorMessage } from '../error' + +describe('UserError', () => { + test('stringifyError', () => { + const rawError = new Error('raw') + rawError.stack = 'mock stack' + const userError = UserError.from(rawError, UserErrorMessage.PartNotFound, { key: 'translatable message' }) + + expect(stringifyError(userError)).toEqual( + 'UserError: ' + + JSON.stringify({ + rawError: 'Error: raw, mock stack', + message: { + key: 'The selected part does not exist', + args: { + key: 'translatable message', + }, + }, + key: 25, + errorCode: 500, + }) + ) + + // serialized and restored + const restored = JSON.parse(userError.toString()) + expect(stringifyError(restored)).toEqual('raw, mock stack') + }) +}) diff --git a/packages/shared-lib/src/lib/stringifyError.ts b/packages/shared-lib/src/lib/stringifyError.ts index 69a5cd7270..abd1ea3d68 100644 --- a/packages/shared-lib/src/lib/stringifyError.ts +++ b/packages/shared-lib/src/lib/stringifyError.ts @@ -11,10 +11,13 @@ export function stringifyError(error: unknown, noStack = false): string { // Has a custom toString() method str = `${(error as any).toString()}` } else { - str = '' - if ((error as Error).message) str += `${(error as Error).message} ` // Is an Error - if ((error as any).reason) str += `${(error as any).reason} ` // Is a Meteor.Error - if ((error as any).details) str += `${(error as any).details} ` + const strings: string[] = [] + if (typeof (error as any).rawError === 'string') strings.push(`${(error as any).rawError}`) // Is an UserError + if (typeof (error as Error).message === 'string') strings.push(`${(error as Error).message}`) // Is an Error + if (typeof (error as any).reason === 'string') strings.push(`${(error as any).reason}`) // Is a Meteor.Error + if (typeof (error as any).details === 'string') strings.push(` ${(error as any).details}`) + + str = strings.join(', ') } if (!str) { @@ -34,7 +37,7 @@ export function stringifyError(error: unknown, noStack = false): string { } if (!noStack) { - if (error && typeof error === 'object' && (error as any).stack) { + if (error && typeof error === 'object' && typeof (error as any).stack === 'string') { str += ', ' + (error as any).stack } } From 02330735fd4f3b03f9bff84e4c41fb9199a4623e Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Tue, 2 Jul 2024 09:08:43 +0200 Subject: [PATCH 386/479] fix: further improve stringifyError --- packages/shared-lib/src/lib/stringifyError.ts | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/shared-lib/src/lib/stringifyError.ts b/packages/shared-lib/src/lib/stringifyError.ts index abd1ea3d68..f97659813f 100644 --- a/packages/shared-lib/src/lib/stringifyError.ts +++ b/packages/shared-lib/src/lib/stringifyError.ts @@ -11,13 +11,13 @@ export function stringifyError(error: unknown, noStack = false): string { // Has a custom toString() method str = `${(error as any).toString()}` } else { - const strings: string[] = [] - if (typeof (error as any).rawError === 'string') strings.push(`${(error as any).rawError}`) // Is an UserError - if (typeof (error as Error).message === 'string') strings.push(`${(error as Error).message}`) // Is an Error - if (typeof (error as any).reason === 'string') strings.push(`${(error as any).reason}`) // Is a Meteor.Error - if (typeof (error as any).details === 'string') strings.push(` ${(error as any).details}`) - - str = strings.join(', ') + const strings: (string | undefined)[] = [ + stringify((error as any).rawError), // UserError + stringify((error as Error).message), // Error + stringify((error as any).reason), // Meteor.Error + stringify((error as any).details), + ] + str = strings.filter(Boolean).join(', ') } if (!str) { @@ -46,3 +46,15 @@ export function stringifyError(error: unknown, noStack = false): string { return str } + +function stringify(v: any): string | undefined { + // Tries to stringify objects if they have a toString() that returns something sensible + if (v === undefined) return undefined + if (v === null) return 'null' + + if (typeof v === 'object') { + const str = `${v}` + if (str !== '[object Object]') return str + return undefined + } else return `${v}` +} From 8b252122c3265c629dd5c3647a412eaf464d61d1 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Tue, 9 Jul 2024 13:32:50 +0200 Subject: [PATCH 387/479] chore: don't run sonarCloud analysis for external PRs Because they will fail due to SonarCloud not supporting running on external PRs anyway. --- .github/workflows/sonar.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sonar.yaml b/.github/workflows/sonar.yaml index 29a206dd6c..71ecc86846 100644 --- a/.github/workflows/sonar.yaml +++ b/.github/workflows/sonar.yaml @@ -12,7 +12,7 @@ jobs: sonarcloud: name: SonarCloud runs-on: ubuntu-latest - if: ${{ github.repository_owner == 'nrkno' }} + if: ${{ github.repository_owner == 'nrkno' && !github.event.pull_request.head.repo.fork }} steps: - uses: actions/checkout@v4 From fc71626b46c3dfc0599a9e28b35d8764b5f41104 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Thu, 11 Jul 2024 14:42:15 +0200 Subject: [PATCH 388/479] fix: add PM properties to HTTP accessor --- .../ui/Settings/Studio/PackageManager.tsx | 30 +++++++++++++++++++ .../shared-lib/src/package-manager/package.ts | 8 ++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/meteor/client/ui/Settings/Studio/PackageManager.tsx b/meteor/client/ui/Settings/Studio/PackageManager.tsx index d56ec1cb95..55e0c679e2 100644 --- a/meteor/client/ui/Settings/Studio/PackageManager.tsx +++ b/meteor/client/ui/Settings/Studio/PackageManager.tsx @@ -488,6 +488,36 @@ export const StudioPackageManagerSettings = withTranslation()( {t('Base url to the resource (example: http://myserver/folder)')} + +
{msg.success ? 'Success' : msg.success === false ? 'Error: ' + msg.errorMessage : null} {prettyPrintJsonString(msg.args)} + + {props.renderButtons(msg)}
{t('Client IP')} {t('Action')} {t('Method')}{t('Status')}{t('Status')} {t('Parameters')}
- + {props.renderButtons(msg)}
+ {t('Amount of documents exceeds the limt of 10 000 entries.')} +
) @@ -199,6 +149,7 @@ function UserActionsList(props: Readonly) { function UserActivity(): JSX.Element { const { t } = useTranslation() + // TODO: This needs to be set to the correct values on Component boot-up const [dateFrom, setDateFrom] = useState
From 7485d2b78db95e15c9fb2a77d3015053ba35d907 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 28 Aug 2024 16:13:33 +0100 Subject: [PATCH 439/479] fix: use same timelineHash when writing to db and fasttrack SOFIE-3420 SOFIE-3427 --- .../model/implementation/PlayoutModelImpl.ts | 7 ++++++- .../job-worker/src/playout/timeline/generate.ts | 14 ++------------ .../src/studio/model/StudioPlayoutModel.ts | 5 ++++- .../src/studio/model/StudioPlayoutModelImpl.ts | 7 ++++++- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts index 9db7eb59f3..f6d23ce52d 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts @@ -667,7 +667,10 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou this.#playlistHasChanged = true } - setTimeline(timelineObjs: TimelineObjGeneric[], generationVersions: TimelineCompleteGenerationVersions): void { + setTimeline( + timelineObjs: TimelineObjGeneric[], + generationVersions: TimelineCompleteGenerationVersions + ): ReadonlyDeep { this.timelineImpl = { _id: this.context.studioId, timelineHash: getRandomId(), // randomized on every timeline change @@ -676,6 +679,8 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou generationVersions: generationVersions, } this.#timelineHasChanged = true + + return this.timelineImpl } setExpectedPackagesForStudioBaseline(packages: ExpectedPackageDBFromStudioBaselineObjects[]): void { diff --git a/packages/job-worker/src/playout/timeline/generate.ts b/packages/job-worker/src/playout/timeline/generate.ts index ae055d97a1..9487b87553 100644 --- a/packages/job-worker/src/playout/timeline/generate.ts +++ b/packages/job-worker/src/playout/timeline/generate.ts @@ -11,8 +11,6 @@ import { import { deserializeTimelineBlob, OnGenerateTimelineObjExt, - serializeTimelineBlob, - TimelineComplete, TimelineCompleteGenerationVersions, TimelineEnableExt, TimelineObjGeneric, @@ -21,7 +19,7 @@ import { } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { RundownBaselineObj } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineObj' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' -import { applyToArray, clone, getRandomId, literal, normalizeArray, omit } from '@sofie-automation/corelib/dist/lib' +import { applyToArray, clone, literal, normalizeArray, omit } from '@sofie-automation/corelib/dist/lib' import { PlayoutModel } from '../model/PlayoutModel' import { logger } from '../../logging' import { getCurrentTime, getSystemVersion } from '../../lib' @@ -238,15 +236,7 @@ export function saveTimeline( timelineObjs: TimelineObjGeneric[], generationVersions: TimelineCompleteGenerationVersions ): void { - const newTimeline: TimelineComplete = { - _id: context.studio._id, - timelineHash: getRandomId(), // randomized on every timeline change - generated: getCurrentTime(), - timelineBlob: serializeTimelineBlob(timelineObjs), - generationVersions: generationVersions, - } - - studioPlayoutModel.setTimeline(timelineObjs, generationVersions) + const newTimeline = studioPlayoutModel.setTimeline(timelineObjs, generationVersions) // Also do a fast-track for the timeline to be published faster: context.hackPublishTimelineToFastTrack(newTimeline) diff --git a/packages/job-worker/src/studio/model/StudioPlayoutModel.ts b/packages/job-worker/src/studio/model/StudioPlayoutModel.ts index 03d687b728..a4b22ed20a 100644 --- a/packages/job-worker/src/studio/model/StudioPlayoutModel.ts +++ b/packages/job-worker/src/studio/model/StudioPlayoutModel.ts @@ -45,7 +45,10 @@ export interface StudioPlayoutModelBase extends StudioPlayoutModelBaseReadonly { * @param timelineObjs Timeline objects to be run in the Studio * @param generationVersions Details about the versions where these objects were generated */ - setTimeline(timelineObjs: TimelineObjGeneric[], generationVersions: TimelineCompleteGenerationVersions): void + setTimeline( + timelineObjs: TimelineObjGeneric[], + generationVersions: TimelineCompleteGenerationVersions + ): ReadonlyDeep } /** diff --git a/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts b/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts index 65430a2081..06e9689f93 100644 --- a/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts +++ b/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts @@ -84,7 +84,10 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel { this.#baselineHelper.setExpectedPlayoutItems(playoutItems) } - setTimeline(timelineObjs: TimelineObjGeneric[], generationVersions: TimelineCompleteGenerationVersions): void { + setTimeline( + timelineObjs: TimelineObjGeneric[], + generationVersions: TimelineCompleteGenerationVersions + ): ReadonlyDeep { this.#timeline = { _id: this.context.studioId, timelineHash: getRandomId(), @@ -93,6 +96,8 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel { generationVersions: generationVersions, } this.#timelineHasChanged = true + + return this.#timeline } /** From 3a2429cffc45bb301fbac05e1cfd3d16c4e692fe Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 28 Aug 2024 17:45:27 +0100 Subject: [PATCH 440/479] chore: update developer guide, to install correct version of meteor tools --- DEVELOPER.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEVELOPER.md b/DEVELOPER.md index 9fb8c33709..a58ed5d207 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -19,7 +19,7 @@ Follow these instructions to start up Sofie Core in development mode. (For produ ### Prerequisites - Install [Node.js](https://nodejs.org) 14 (using [nvm](https://github.com/nvm-sh/nvm) or [nvm-windows](https://github.com/coreybutler/nvm-windows) is the recommended way to install Node.js) -- Install [Meteor](https://www.meteor.com/install) (`npm install --global meteor`) +- Install [Meteor](https://www.meteor.com/install) (`npm install --global meteor@2`) - Install [Node.js](https://nodejs.org) 18 (using the same method you used above, you can uninstall node 14 if needed) - Install an older version of corepack (`npm install --global corepack@0.15.3`) - Enable [corepack](https://nodejs.org/api/corepack.html#corepack) (`corepack enable`) as administrator/root. If `corepack` is not found, you may need to install it first with `npm install --global corepack` From 9ace41deb43d1a5bf571063f7e5a9dc7a3773d63 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 29 Aug 2024 09:57:26 +0100 Subject: [PATCH 441/479] chore: update test snapshots --- .../__snapshots__/playout.test.ts.snap | 4 +- .../__snapshots__/timeline.test.ts.snap | 38 +++++++++---------- .../src/playout/__tests__/timeline.test.ts | 4 +- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/job-worker/src/playout/__tests__/__snapshots__/playout.test.ts.snap b/packages/job-worker/src/playout/__tests__/__snapshots__/playout.test.ts.snap index ecb20c7178..570be55e51 100644 --- a/packages/job-worker/src/playout/__tests__/__snapshots__/playout.test.ts.snap +++ b/packages/job-worker/src/playout/__tests__/__snapshots__/playout.test.ts.snap @@ -12,7 +12,7 @@ exports[`Playout API Basic rundown control 1`] = ` "studio": "asdf", }, "timelineBlob": "[{"id":"playlist_randomId9000_status","objectType":"rundown","enable":{"while":1},"layer":"rundown_status","content":{"deviceType":"ABSTRACT"},"classes":["rundown_active"],"priority":0},{"id":"part_group_randomId9000_part0_0_randomId9003","objectType":"rundown","enable":{"start":"now"},"priority":5,"layer":"","content":{"deviceType":"ABSTRACT","type":"group"},"children":[],"isGroup":true,"metaData":{"isPieceTimeline":true}},{"id":"part_group_firstobject_randomId9000_part0_0_randomId9003","objectType":"rundown","enable":{"start":0},"layer":"group_first_object","content":{"deviceType":"ABSTRACT","type":"callback","callBack":"partPlaybackStarted","callBackData":{"rundownPlaylistId":"playlist_randomId9000","partInstanceId":"randomId9000_part0_0_randomId9003"},"callBackStopped":"partPlaybackStopped"},"inGroup":"part_group_randomId9000_part0_0_randomId9003","classes":[],"priority":0},{"id":"piece_group_control_randomId9000_part0_0_randomId9003_randomId9000_piece001","objectType":"rundown","enable":{"start":0},"layer":"vt0","priority":5,"content":{"deviceType":"ABSTRACT","type":"callback","callBack":"piecePlaybackStarted","callBackData":{"rundownPlaylistId":"playlist_randomId9000","partInstanceId":"randomId9000_part0_0_randomId9003","pieceInstanceId":"randomId9000_part0_0_randomId9003_randomId9000_piece001","dynamicallyInserted":false},"callBackStopped":"piecePlaybackStopped"},"classes":["current_part"],"inGroup":"part_group_randomId9000_part0_0_randomId9003","metaData":{"isPieceTimeline":true,"triggerPieceInstanceId":"randomId9000_part0_0_randomId9003_randomId9000_piece001"}},{"id":"piece_group_randomId9000_part0_0_randomId9003_randomId9000_piece001","content":{"deviceType":"ABSTRACT","type":"group"},"children":[],"inGroup":"part_group_randomId9000_part0_0_randomId9003","isGroup":true,"objectType":"rundown","enable":{"start":"#piece_group_control_randomId9000_part0_0_randomId9003_randomId9000_piece001.start - 0","end":"#piece_group_control_randomId9000_part0_0_randomId9003_randomId9000_piece001.end + 0"},"layer":"","metaData":{"isPieceTimeline":true,"pieceInstanceGroupId":"randomId9000_part0_0_randomId9003_randomId9000_piece001"},"priority":0}]", - "timelineHash": "randomId9008", + "timelineHash": "randomId9006", }, ] `; @@ -57,7 +57,7 @@ exports[`Playout API Basic rundown control 3`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9010", + "timelineHash": "randomId9007", }, ] `; diff --git a/packages/job-worker/src/playout/__tests__/__snapshots__/timeline.test.ts.snap b/packages/job-worker/src/playout/__tests__/__snapshots__/timeline.test.ts.snap index ef48638e9a..69c95348f7 100644 --- a/packages/job-worker/src/playout/__tests__/__snapshots__/timeline.test.ts.snap +++ b/packages/job-worker/src/playout/__tests__/__snapshots__/timeline.test.ts.snap @@ -12,7 +12,7 @@ exports[`Timeline Adlib pieces Current part with preroll 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9022", + "timelineHash": "randomId9013", }, ] `; @@ -29,7 +29,7 @@ exports[`Timeline Adlib pieces Current part with preroll and adlib preroll 1`] = "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9022", + "timelineHash": "randomId9013", }, ] `; @@ -46,7 +46,7 @@ exports[`Timeline Basic rundown 1`] = ` "studio": "asdf", }, "timelineBlob": "[{"id":"playlist_randomId9000_status","objectType":"rundown","enable":{"while":1},"layer":"rundown_status","content":{"deviceType":"ABSTRACT"},"classes":["rundown_active"],"priority":0},{"id":"part_group_randomId9000_part0_0_randomId9003","objectType":"rundown","enable":{"start":"now"},"priority":5,"layer":"","content":{"deviceType":"ABSTRACT","type":"group"},"children":[],"isGroup":true,"metaData":{"isPieceTimeline":true}},{"id":"part_group_firstobject_randomId9000_part0_0_randomId9003","objectType":"rundown","enable":{"start":0},"layer":"group_first_object","content":{"deviceType":"ABSTRACT","type":"callback","callBack":"partPlaybackStarted","callBackData":{"rundownPlaylistId":"playlist_randomId9000","partInstanceId":"randomId9000_part0_0_randomId9003"},"callBackStopped":"partPlaybackStopped"},"inGroup":"part_group_randomId9000_part0_0_randomId9003","classes":[],"priority":0},{"id":"piece_group_control_randomId9000_part0_0_randomId9003_randomId9000_piece001","objectType":"rundown","enable":{"start":0},"layer":"vt0","priority":5,"content":{"deviceType":"ABSTRACT","type":"callback","callBack":"piecePlaybackStarted","callBackData":{"rundownPlaylistId":"playlist_randomId9000","partInstanceId":"randomId9000_part0_0_randomId9003","pieceInstanceId":"randomId9000_part0_0_randomId9003_randomId9000_piece001","dynamicallyInserted":false},"callBackStopped":"piecePlaybackStopped"},"classes":["current_part"],"inGroup":"part_group_randomId9000_part0_0_randomId9003","metaData":{"isPieceTimeline":true,"triggerPieceInstanceId":"randomId9000_part0_0_randomId9003_randomId9000_piece001"}},{"id":"piece_group_randomId9000_part0_0_randomId9003_randomId9000_piece001","content":{"deviceType":"ABSTRACT","type":"group"},"children":[],"inGroup":"part_group_randomId9000_part0_0_randomId9003","isGroup":true,"objectType":"rundown","enable":{"start":"#piece_group_control_randomId9000_part0_0_randomId9003_randomId9000_piece001.start - 0","end":"#piece_group_control_randomId9000_part0_0_randomId9003_randomId9000_piece001.end + 0"},"layer":"","metaData":{"isPieceTimeline":true,"pieceInstanceGroupId":"randomId9000_part0_0_randomId9003_randomId9000_piece001"},"priority":0}]", - "timelineHash": "randomId9010", + "timelineHash": "randomId9007", }, ] `; @@ -63,7 +63,7 @@ exports[`Timeline Basic rundown 2`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9012", + "timelineHash": "randomId9008", }, ] `; @@ -80,7 +80,7 @@ exports[`Timeline In transitions Basic inTransition 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9014", + "timelineHash": "randomId9009", }, ] `; @@ -97,7 +97,7 @@ exports[`Timeline In transitions Basic inTransition with contentDelay + preroll "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9014", + "timelineHash": "randomId9009", }, ] `; @@ -114,7 +114,7 @@ exports[`Timeline In transitions Basic inTransition with contentDelay 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9014", + "timelineHash": "randomId9009", }, ] `; @@ -131,7 +131,7 @@ exports[`Timeline In transitions Basic inTransition with planned pieces 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9014", + "timelineHash": "randomId9009", }, ] `; @@ -148,7 +148,7 @@ exports[`Timeline In transitions Preroll 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9014", + "timelineHash": "randomId9009", }, ] `; @@ -165,7 +165,7 @@ exports[`Timeline In transitions inTransition disabled 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9014", + "timelineHash": "randomId9009", }, ] `; @@ -182,7 +182,7 @@ exports[`Timeline In transitions inTransition is disabled during hold 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9016", + "timelineHash": "randomId9010", }, ] `; @@ -199,7 +199,7 @@ exports[`Timeline In transitions inTransition with existing infinites 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9015", + "timelineHash": "randomId9010", }, ] `; @@ -216,7 +216,7 @@ exports[`Timeline In transitions inTransition with new infinite 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9015", + "timelineHash": "randomId9010", }, ] `; @@ -233,7 +233,7 @@ exports[`Timeline Infinite Pieces Infinite Piece has stable timing across timeli "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9016", + "timelineHash": "randomId9010", }, ] `; @@ -250,7 +250,7 @@ exports[`Timeline Out transitions Basic outTransition 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9014", + "timelineHash": "randomId9009", }, ] `; @@ -267,7 +267,7 @@ exports[`Timeline Out transitions outTransition + inTransition 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9014", + "timelineHash": "randomId9009", }, ] `; @@ -284,7 +284,7 @@ exports[`Timeline Out transitions outTransition + preroll (2) 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9014", + "timelineHash": "randomId9009", }, ] `; @@ -301,7 +301,7 @@ exports[`Timeline Out transitions outTransition + preroll 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9014", + "timelineHash": "randomId9009", }, ] `; @@ -318,7 +318,7 @@ exports[`Timeline Out transitions outTransition is disabled during hold 1`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9016", + "timelineHash": "randomId9010", }, ] `; diff --git a/packages/job-worker/src/playout/__tests__/timeline.test.ts b/packages/job-worker/src/playout/__tests__/timeline.test.ts index c5132deebe..5a8a99d142 100644 --- a/packages/job-worker/src/playout/__tests__/timeline.test.ts +++ b/packages/job-worker/src/playout/__tests__/timeline.test.ts @@ -1240,7 +1240,7 @@ describe('Timeline', () => { }) ) - const adlibbedPieceId = 'randomId9010' + const adlibbedPieceId = 'randomId9007' // The adlib should be starting at 'now' await checkTimings({ @@ -1406,7 +1406,7 @@ describe('Timeline', () => { }) ) - const adlibbedPieceId = 'randomId9010' + const adlibbedPieceId = 'randomId9007' // The adlib should be starting at 'now' await checkTimings({ From fdb4a256e3c119a5f8410d555706c0fcd44be34c Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Thu, 29 Aug 2024 13:57:35 +0200 Subject: [PATCH 442/479] fix(SegmentList): not-hidden infinite Pieces are not be visible if they don't have a column assigned --- .../ui/SegmentList/LinePartTimeline.tsx | 37 +++++++++---------- .../docs/user-guide/features/sofie-views.mdx | 2 +- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/meteor/client/ui/SegmentList/LinePartTimeline.tsx b/meteor/client/ui/SegmentList/LinePartTimeline.tsx index 19b9d902dd..3534c5afeb 100644 --- a/meteor/client/ui/SegmentList/LinePartTimeline.tsx +++ b/meteor/client/ui/SegmentList/LinePartTimeline.tsx @@ -35,14 +35,9 @@ const supportedSourceLayerTypes = new Set( ) ) -function findMainPiece(pieces: PieceExtended[], original?: boolean) { +function findMainPiece(pieces: PieceExtended[]) { return findPieceExtendedToShowFromOrderedResolvedInstances( - pieces.filter( - (piece) => - piece.outputLayer?.isPGM && - piece.sourceLayer?.onPresenterScreen && - (!original || !piece.instance.dynamicallyInserted) - ), + pieces.filter((piece) => piece.outputLayer?.isPGM && piece.sourceLayer?.onPresenterScreen), supportedSourceLayerTypes ) } @@ -55,17 +50,20 @@ function findTransitionPiece(pieces: PieceExtended[]) { }) } -function findTimedGraphics(pieces: PieceExtended[]) { - return pieces.slice().filter((piece) => { - if ( - piece.sourceLayer?.type === SourceLayerType.LOWER_THIRD && - !piece.sourceLayer?.isHidden && - piece.instance.piece.lifespan === PieceLifespan.WithinPart && - piece.instance.piece.enable.duration - ) { - return true - } - }) +function findTimelineGraphics(pieces: PieceExtended[]) { + return pieces + .slice() + .filter((piece) => { + if ( + piece.sourceLayer?.type === SourceLayerType.LOWER_THIRD && + !piece.sourceLayer?.isHidden && + ((piece.instance.piece.lifespan === PieceLifespan.WithinPart && piece.instance.piece.enable.duration) || + !piece.sourceLayer?.onListViewColumn) + ) { + return true + } + }) + .sort((a, b) => (a.sourceLayer?._rank ?? 0) - (b.sourceLayer?._rank ?? 0)) } export const LinePartTimeline: React.FC = function LinePartTimeline({ @@ -80,9 +78,8 @@ export const LinePartTimeline: React.FC = function LinePartTimeline({ // const [highlight] = useState(false) const mainPiece = useMemo(() => findMainPiece(part.pieces), [part.pieces]) - // const mainDisplayPiece = useMemo(() => findMainPiece(part.pieces), [part.pieces]) const transitionPiece = useMemo(() => findTransitionPiece(part.pieces), [part.pieces]) - const timedGraphics = useMemo(() => findTimedGraphics(part.pieces), [part.pieces]) + const timedGraphics = useMemo(() => findTimelineGraphics(part.pieces), [part.pieces]) const timings = part.instance.partPlayoutTimings const toPartDelay = (timings?.toPartDelay ?? 0) - ((timings?.fromPartRemaining ?? 0) - (timings?.toPartDelay ?? 0)) diff --git a/packages/documentation/docs/user-guide/features/sofie-views.mdx b/packages/documentation/docs/user-guide/features/sofie-views.mdx index 1a4ccfdea6..d4e0cebd4b 100644 --- a/packages/documentation/docs/user-guide/features/sofie-views.mdx +++ b/packages/documentation/docs/user-guide/features/sofie-views.mdx @@ -70,7 +70,7 @@ Another mode available to display a Segment is the List View. In this mode, each ![List View Mode](/img/docs/main/list_view.png) -In this mode, the focus is on the "main" Piece of the Part. Additional _Lower-Third_ content that is not spanning the entire Part (is not infinite) will be displayed on top of the main Piece. All other content can be displayed to the right of the mini-timeline as a set of indicators, one per every Layer. Clicking on those indicators will show a pop-up with the Pieces so that they can be investigated using _hover-scrub_. Indicators can be also shown for Ad-Libs assigned to a Part, for easier discovery by the User. Which Layers should be shown in the columns can be decided in the [Settings ● Layers](../configuration/settings-view.md#show-style) area. A special, larger indicator is reserved for the Script piece, which can be useful to display so-called _out-words_. +In this mode, the focus is on the "main" Piece of the Part. Additional _Lower-Third_ Pieces will be displayed on top of the main Piece. Infinite _Lower-Third_ Pieces and all other content can be displayed to the right of the mini-timeline as a set of indicators, one per every Layer. Clicking on those indicators will show a pop-up with the Pieces so that they can be investigated using _hover-scrub_. Indicators can be also shown for Ad-Libs assigned to a Part, for easier discovery by the User. Which Layers should be shown in the columns can be decided in the [Settings ● Layers](../configuration/settings-view.md#show-style) area. A special, larger indicator is reserved for the Script piece, which can be useful to display so-called _out-words_. If a Part has an _in-transition_ Piece, it will be displayed to the left of the Part's Take Point. From 0a2ef7176f24ad6d2c2a6ed21548336ef0828d5e Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Fri, 30 Aug 2024 07:43:05 +0200 Subject: [PATCH 443/479] fix: GUI: userlog: don't expand(collapse when user is selecting text --- meteor/client/lib/collapseJSON.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/meteor/client/lib/collapseJSON.tsx b/meteor/client/lib/collapseJSON.tsx index 085da42ee9..25519fef38 100644 --- a/meteor/client/lib/collapseJSON.tsx +++ b/meteor/client/lib/collapseJSON.tsx @@ -77,7 +77,17 @@ export function CollapseJSON({ json }: { json: string }): JSX.Element { ) return ( -
 setExpanded(!expanded)} className="collapse-json__block">
+		
 {
+				// Don't expand when user is selecting text:
+				const selection = window.getSelection()
+				if (selection?.type != 'Range') {
+					setExpanded(!expanded)
+				}
+			}}
+			className="collapse-json__block"
+		>
 			{displayContents}
 		
) From 3f9a896ba3474b4012a269cdba544c5dda06dc8c Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 30 Aug 2024 09:30:29 +0100 Subject: [PATCH 444/479] fix: avoid cursor jumping when editing action trigger 'trigger mode' SOFIE-3429 (#1253) --- meteor/client/lib/Components/TextInput.tsx | 46 +++++++++- meteor/client/lib/EditAttribute.tsx | 87 +++---------------- .../actionEditors/AdLibActionEditor.tsx | 24 ++--- 3 files changed, 72 insertions(+), 85 deletions(-) diff --git a/meteor/client/lib/Components/TextInput.tsx b/meteor/client/lib/Components/TextInput.tsx index c260e7b4f6..76320be406 100644 --- a/meteor/client/lib/Components/TextInput.tsx +++ b/meteor/client/lib/Components/TextInput.tsx @@ -1,11 +1,21 @@ -import React, { useCallback, useState } from 'react' +import React, { useCallback, useMemo, useState } from 'react' import ClassNames from 'classnames' +import { DropdownInputOption } from './DropdownInput' +import { getRandomString } from '@sofie-automation/corelib/dist/lib' +export type TextInputSuggestion = DropdownInputOption +export interface TextInputSuggestionGroup { + name: string + options: TextInputSuggestion[] +} interface ITextInputControlProps { classNames?: string modifiedClassName?: string disabled?: boolean placeholder?: string + spellCheck?: boolean + + suggestions?: Array /** Call handleUpdate on every change, before focus is lost */ updateOnKey?: boolean @@ -19,6 +29,8 @@ export function TextInputControl({ value, disabled, placeholder, + spellCheck, + suggestions, handleUpdate, updateOnKey, }: Readonly): JSX.Element { @@ -60,7 +72,9 @@ export function TextInputControl({ [handleUpdate] ) - return ( + const fieldId = useMemo(() => getRandomString(), []) + + const textInput = ( ) + + if (!suggestions) { + return textInput + } else { + return ( +
+ {textInput} + + + {suggestions.map((o, j) => + 'options' in o ? ( + + {o.options.map((v, i) => ( + + ))} + + ) : ( + + ) + )} + +
+ ) + } } diff --git a/meteor/client/lib/EditAttribute.tsx b/meteor/client/lib/EditAttribute.tsx index 0252431e41..6775fb096b 100644 --- a/meteor/client/lib/EditAttribute.tsx +++ b/meteor/client/lib/EditAttribute.tsx @@ -6,12 +6,12 @@ import { MultiSelect, MultiSelectEvent, MultiSelectOptions } from './multiSelect import ClassNames from 'classnames' import { ColorPickerEvent, ColorPicker } from './colorPicker' import { IconPicker, IconPickerEvent } from './iconPicker' -import { assertNever, getRandomString } from '../../lib/lib' +import { assertNever } from '../../lib/lib' import { MongoCollection } from '../../lib/collections/lib' import { CheckboxControl } from './Components/Checkbox' import { TextInputControl } from './Components/TextInput' import { IntInputControl } from './Components/IntInput' -import { DropdownInputControl, getDropdownInputOptions } from './Components/DropdownInput' +import { DropdownInputControl, DropdownInputOption, getDropdownInputOptions } from './Components/DropdownInput' import { FloatInputControl } from './Components/FloatInput' import { joinLines, MultiLineTextInputControl, splitValueIntoLines } from './Components/MultiLineTextInput' import { JsonTextInputControl, tryParseJson } from './Components/JsonTextInput' @@ -469,81 +469,22 @@ const EditAttributeDropdown = wrapEditAttribute( ) const EditAttributeDropdownText = wrapEditAttribute( class EditAttributeDropdownText extends EditAttributeBase { - private _id: string - - constructor(props: any) { - super(props) - - this.handleChangeDropdown = this.handleChangeDropdown.bind(this) - this.handleChangeText = this.handleChangeText.bind(this) - this.handleBlurText = this.handleBlurText.bind(this) - this.handleEscape = this.handleEscape.bind(this) - - this._id = getRandomString() - } - handleChangeDropdown(event: React.ChangeEvent) { - // because event.target.value is always a string, use the original value instead - const option = _.find(this.getOptions(), (o) => { - return o.value + '' === event.target.value + '' - }) - - const value = option ? option.value : event.target.value - - this.handleUpdate(this.props.optionsAreNumbers ? Number(value) : value) - } - handleChangeText(event: React.ChangeEvent) { - this.handleChangeDropdown(event) - } - handleBlurText(event: React.FocusEvent) { - this.handleUpdate(event.target.value) - } - handleEscape(e: React.KeyboardEvent) { - if (e.key === 'Escape') { - this.handleDiscard() - } - } - getOptions() { + private getOptions(): DropdownInputOption[] { return getDropdownInputOptions(this.props.options) } render(): JSX.Element { return ( -
- - - - {this.getOptions().map((o, j) => - Array.isArray(o.value) ? ( - - {o.value.map((v, i) => ( - - ))} - - ) : ( - - ) - )} - -
+ ) } } diff --git a/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/actionEditors/AdLibActionEditor.tsx b/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/actionEditors/AdLibActionEditor.tsx index d1f81d4c28..0770108862 100644 --- a/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/actionEditors/AdLibActionEditor.tsx +++ b/meteor/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/actionEditors/AdLibActionEditor.tsx @@ -5,6 +5,7 @@ import { PlayoutActions, SomeAction } from '@sofie-automation/blueprints-integra import { EditAttribute } from '../../../../../../../lib/EditAttribute' import { useTracker } from '../../../../../../../lib/ReactMeteorData/ReactMeteorData' import { AdLibActions, RundownBaselineAdLibActions } from '../../../../../../../collections' +import { TextInputControl, TextInputSuggestion } from '../../../../../../../lib/Components/TextInput' export function AdLibActionEditor({ action, @@ -14,9 +15,9 @@ export function AdLibActionEditor({ onChange: (newVal: Partial) => void }>): JSX.Element | null { const { t } = useTranslation() - const allTriggerModes = useTracker( + const allTriggerModes = useTracker( () => { - return _.chain([ + const triggerModes = _.chain([ ...RundownBaselineAdLibActions.find().map((action) => action.triggerModes?.map((triggerMode) => triggerMode.data) ), @@ -25,7 +26,10 @@ export function AdLibActionEditor({ .flatten() .compact() .uniq() + .sort() .value() + + return triggerModes.map((triggerMode, i): TextInputSuggestion => ({ name: triggerMode, value: triggerMode, i })) }, [], [] @@ -55,13 +59,11 @@ export function AdLibActionEditor({ {action.arguments && (
- { + onChange({ ...action, arguments: { @@ -69,7 +71,9 @@ export function AdLibActionEditor({ triggerMode: newVal, }, }) - }} + } + spellCheck={false} + suggestions={allTriggerModes} />
)} From cf88c75d985615e414188ffb757d09570cb0123b Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Fri, 30 Aug 2024 10:14:49 +0000 Subject: [PATCH 445/479] chore: update TSR to nightly --- meteor/yarn.lock | 10 +++++----- packages/playout-gateway/package.json | 2 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 22 +++++++++++----------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 0ca5be7f22..062f32ac6c 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1418,7 +1418,7 @@ __metadata: resolution: "@sofie-automation/shared-lib@portal:../packages/shared-lib::locator=automation-core%40workspace%3A." dependencies: "@mos-connection/model": ^4.1.1 - timeline-state-resolver-types: 9.1.0 + timeline-state-resolver-types: 9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: node @@ -11748,12 +11748,12 @@ __metadata: languageName: node linkType: hard -"timeline-state-resolver-types@npm:9.1.0": - version: 9.1.0 - resolution: "timeline-state-resolver-types@npm:9.1.0" +"timeline-state-resolver-types@npm:9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0": + version: 9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0 + resolution: "timeline-state-resolver-types@npm:9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0" dependencies: tslib: ^2.6.2 - checksum: dab48013908d588a4a9357aeb824117316ce1666d972575c4fc89a0f70647bee4aacdfddb73bce5db7f7bdc5920f9e31b3669b47d6e97cf627e41f9f205e3f03 + checksum: 7d57f25139d33a1bd62cd46b7d1a8c6ab9d5a12f51ba5dc115ae5a40dc751bef8100bedc1b81bde500315231b4918e25198171a4520ee82fd785f74c7e040be9 languageName: node linkType: hard diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index f526298e3d..8dc8cebe6b 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -60,7 +60,7 @@ "@sofie-automation/shared-lib": "1.51.0-in-testing.0", "debug": "^4.3.4", "influx": "^5.9.3", - "timeline-state-resolver": "9.1.0", + "timeline-state-resolver": "9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0", "tslib": "^2.6.2", "underscore": "^1.13.6", "winston": "^3.11.0" diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 04d198d1b2..8a85f53953 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -39,7 +39,7 @@ ], "dependencies": { "@mos-connection/model": "^4.1.1", - "timeline-state-resolver-types": "9.1.0", + "timeline-state-resolver-types": "9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0", "tslib": "^2.6.2", "type-fest": "^3.13.1" }, diff --git a/packages/yarn.lock b/packages/yarn.lock index 34523db9c0..6f2f8adae7 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -4711,7 +4711,7 @@ __metadata: resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: "@mos-connection/model": ^4.1.1 - timeline-state-resolver-types: 9.1.0 + timeline-state-resolver-types: 9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: unknown @@ -19419,7 +19419,7 @@ asn1@evs-broadcast/node-asn1: "@sofie-automation/shared-lib": 1.51.0-in-testing.0 debug: ^4.3.4 influx: ^5.9.3 - timeline-state-resolver: 9.1.0 + timeline-state-resolver: 9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0 tslib: ^2.6.2 underscore: ^1.13.6 winston: ^3.11.0 @@ -23015,18 +23015,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"timeline-state-resolver-types@npm:9.1.0": - version: 9.1.0 - resolution: "timeline-state-resolver-types@npm:9.1.0" +"timeline-state-resolver-types@npm:9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0": + version: 9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0 + resolution: "timeline-state-resolver-types@npm:9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0" dependencies: tslib: ^2.6.2 - checksum: dab48013908d588a4a9357aeb824117316ce1666d972575c4fc89a0f70647bee4aacdfddb73bce5db7f7bdc5920f9e31b3669b47d6e97cf627e41f9f205e3f03 + checksum: 7d57f25139d33a1bd62cd46b7d1a8c6ab9d5a12f51ba5dc115ae5a40dc751bef8100bedc1b81bde500315231b4918e25198171a4520ee82fd785f74c7e040be9 languageName: node linkType: hard -"timeline-state-resolver@npm:9.1.0": - version: 9.1.0 - resolution: "timeline-state-resolver@npm:9.1.0" +"timeline-state-resolver@npm:9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0": + version: 9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0 + resolution: "timeline-state-resolver@npm:9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0" dependencies: "@tv2media/v-connection": ^7.3.2 atem-connection: 3.5.0 @@ -23051,7 +23051,7 @@ asn1@evs-broadcast/node-asn1: sprintf-js: ^1.1.3 superfly-timeline: ^9.0.0 threadedclass: ^1.2.1 - timeline-state-resolver-types: 9.1.0 + timeline-state-resolver-types: 9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0 tslib: ^2.6.2 tv-automation-quantel-gateway-client: ^3.1.7 type-fest: ^3.13.1 @@ -23059,7 +23059,7 @@ asn1@evs-broadcast/node-asn1: utf-8-validate: ^5.0.10 ws: ^7.5.10 xml-js: ^1.6.11 - checksum: 015f0c0b120958aea181d16cf26a60504942fd56eee8328c754a74e553f90c5c18d793e57224cec557b9f1c4f3d4a0d93672520e90eda98f97aa3b00f5f01c5f + checksum: 098e1e14c2a89b1a5c4bab88a16a90b2e32e8583bee902638be139ca962b59a694027dcb80600e855e5abc646f44832c1e3429c326d087474d22665368b242ff languageName: node linkType: hard From 2fb87bbdc1254c1e164d9fa6ec547cd71f9c49ab Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Fri, 30 Aug 2024 13:44:34 +0200 Subject: [PATCH 446/479] fix: only include previous PieceInstances in active when the previous partInstane hasn't stopped yet (SOFIE-3428) (#1255) Co-authored-by: Julian Waller --- .../src/collections/pieceInstancesHandler.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts index 12913f6bd7..74bff309e2 100644 --- a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts +++ b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts @@ -130,7 +130,14 @@ export class PieceInstancesHandler ) : [] - const active = [...inPreviousPartInstance, ...inCurrentPartInstance] + const active = [...inCurrentPartInstance] + // Only include the pieces from the previous part if the part is still considered to be playing + if ( + this._partInstances?.previous?.timings && + (this._partInstances.previous.timings.plannedStoppedPlayback ?? 0) > Date.now() + ) { + active.push(...inPreviousPartInstance) + } let hasAnythingChanged = false if (!_.isEqual(this._collectionData.active, active)) { From 7671510fcd8b00e5e23bdf32850d6abc60a8db92 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 30 Aug 2024 16:36:16 +0100 Subject: [PATCH 447/479] chore: add timeouts to sonar and trivy ci --- .github/workflows/sonar.yaml | 1 + .github/workflows/trivy.yml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/sonar.yaml b/.github/workflows/sonar.yaml index 71ecc86846..f67d9cab0f 100644 --- a/.github/workflows/sonar.yaml +++ b/.github/workflows/sonar.yaml @@ -13,6 +13,7 @@ jobs: name: SonarCloud runs-on: ubuntu-latest if: ${{ github.repository_owner == 'nrkno' && !github.event.pull_request.head.repo.fork }} + timeout-minutes: 15 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index e97416ce8e..f6973fa2c9 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -11,6 +11,8 @@ jobs: strategy: matrix: image: ["server-core", "playout-gateway", "mos-gateway"] + timeout-minutes: 15 + steps: - name: Run Trivy vulnerability scanner (json) uses: aquasecurity/trivy-action@0.24.0 From 681146a47124e68b414b2a804057f71f7c3efe9a Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 4 Sep 2024 12:32:51 +0200 Subject: [PATCH 448/479] fix(SystemStatus/UserLog): improve style layout to compress the information more --- meteor/client/lib/collapseJSON.tsx | 7 +++-- meteor/client/styles/collapseJSON.scss | 14 ++++++++-- meteor/client/styles/systemStatus.scss | 37 +++++++++++++++++--------- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/meteor/client/lib/collapseJSON.tsx b/meteor/client/lib/collapseJSON.tsx index 25519fef38..c5aeca7b0c 100644 --- a/meteor/client/lib/collapseJSON.tsx +++ b/meteor/client/lib/collapseJSON.tsx @@ -1,3 +1,4 @@ +import classNames from 'classnames' import React, { useMemo, useState } from 'react' /** @@ -41,7 +42,7 @@ export function CollapseJSON({ json }: { json: string }): JSX.Element { }, [originalString]) if (originalString.length < 100 && indexOf5thLine === null) { - return
{originalString}
+ return
{originalString}
} const displayContents = expanded ? ( @@ -79,6 +80,9 @@ export function CollapseJSON({ json }: { json: string }): JSX.Element { return (
 {
 				// Don't expand when user is selecting text:
 				const selection = window.getSelection()
@@ -86,7 +90,6 @@ export function CollapseJSON({ json }: { json: string }): JSX.Element {
 					setExpanded(!expanded)
 				}
 			}}
-			className="collapse-json__block"
 		>
 			{displayContents}
 		
diff --git a/meteor/client/styles/collapseJSON.scss b/meteor/client/styles/collapseJSON.scss index e7975db3df..c5abd4603b 100644 --- a/meteor/client/styles/collapseJSON.scss +++ b/meteor/client/styles/collapseJSON.scss @@ -1,14 +1,24 @@ .collapse-json__block { - cursor: pointer; + text-wrap: wrap; + white-space: pre-wrap; + word-break: break-all; + + &.expanding { + cursor: pointer; + } } .collapse-json__collapser { - display: inline-block; + display: block; background: #eee; border-radius: 3px; line-height: 1.3em; padding: 0 0.3em; + margin-top: 0.2em; border: none; + float: right; + clear: both; + user-select: none; } .collapse-json__collapser:focus { diff --git a/meteor/client/styles/systemStatus.scss b/meteor/client/styles/systemStatus.scss index 64a5e4de29..536f5acaec 100644 --- a/meteor/client/styles/systemStatus.scss +++ b/meteor/client/styles/systemStatus.scss @@ -217,30 +217,46 @@ min-width: 12em; } .user-action-log__userId { - min-width: 5em; + min-width: 2em; } .user-action-log__executionTime { min-width: 5em; - td { - white-space: nowrap; + > table { + width: 100%; + + > tbody > tr:first-child td { + border-top: none; + } + + td { + white-space: nowrap; + + &:first-child { + width: 4.5em; + } + } } } .user-action-log__clientAddress { - min-width: 7em; + min-width: 8em; } .user-action-log__context { min-width: 10em; } .user-action-log__method { - min-width: 23em; + min-width: 17em; } .user-action-log__status { - min-width: 23em; + min-width: 6em; + } + .user-action-log__args { + min-width: 30em; } td.user-action-log__args { position: relative; - font-family: 'Consolas', 'Courier New', Courier, monospace; + font-family: 'Consolas', 'SF Mono', SFMono-Regular, 'DejaVu Sans Mono', Menlo, ui-monospace, 'Courier New', Courier, + monospace; color: #555; white-space: pre; overflow: hidden; @@ -248,11 +264,8 @@ font-size: 0.8em; line-height: 1.7em; - &:hover { - // white-space: normal; - overflow: visible; - text-overflow: unset; - word-break: break-all; + pre { + margin: 0; } } } From b76ecf289fe7dbc080688886e8c352082678408f Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 4 Sep 2024 13:07:33 +0200 Subject: [PATCH 449/479] feat(CollapseJSON): add a Copy button --- meteor/client/lib/collapseJSON.tsx | 65 +++++++++++++++++--------- meteor/client/styles/collapseJSON.scss | 34 ++++++++++---- 2 files changed, 68 insertions(+), 31 deletions(-) diff --git a/meteor/client/lib/collapseJSON.tsx b/meteor/client/lib/collapseJSON.tsx index c5aeca7b0c..806531e30b 100644 --- a/meteor/client/lib/collapseJSON.tsx +++ b/meteor/client/lib/collapseJSON.tsx @@ -41,6 +41,12 @@ export function CollapseJSON({ json }: { json: string }): JSX.Element { return indexOf5thLine }, [originalString]) + function copyContents() { + if (!navigator.clipboard) return + + navigator.clipboard.writeText(json).catch((e) => console.error('Unable to copy JSON contents to clipboard', e)) + } + if (originalString.length < 100 && indexOf5thLine === null) { return
{originalString}
} @@ -48,32 +54,47 @@ export function CollapseJSON({ json }: { json: string }): JSX.Element { const displayContents = expanded ? ( <> {originalString} - +
+ + +
) : ( <> {originalString.substring(0, Math.min(indexOf5thLine || 100, 100))} - +
+ +
) diff --git a/meteor/client/styles/collapseJSON.scss b/meteor/client/styles/collapseJSON.scss index c5abd4603b..3d90e0a02c 100644 --- a/meteor/client/styles/collapseJSON.scss +++ b/meteor/client/styles/collapseJSON.scss @@ -8,20 +8,36 @@ } } -.collapse-json__collapser { +.collapse-json__tools { display: block; + float: right; + clear: both; +} + +.collapse-json__tools > button { background: #eee; border-radius: 3px; - line-height: 1.3em; - padding: 0 0.3em; - margin-top: 0.2em; + line-height: 1rem; + padding: 0 0.3rem; + margin-top: 0.2rem; border: none; - float: right; - clear: both; user-select: none; + margin-left: 0.2rem; + vertical-align: top; + + &:focus { + outline: none; + box-shadow: 0 0 0 2px #00feff; + } + + &:active { + background-color: #ddd; + } } -.collapse-json__collapser:focus { - outline: none; - box-shadow: 0 0 0 2px #00feff; +.collapse-json__copy { + font-family: Roboto, arial, sans-serif; + font-weight: 500; + font-size: 0.8em; + color: #888; } From 746e0d2c822b95927172e76cbd6a4436d1a99192 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Thu, 5 Sep 2024 07:24:45 +0200 Subject: [PATCH 450/479] fix: update TSR dependency (to fix timeline bug) see https://github.com/SuperFlyTV/supertimeline/pull/102 --- meteor/yarn.lock | 10 ++++---- packages/playout-gateway/package.json | 2 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 35 +++++++++++++++++---------- 4 files changed, 29 insertions(+), 20 deletions(-) diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 062f32ac6c..bc3847ce80 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1418,7 +1418,7 @@ __metadata: resolution: "@sofie-automation/shared-lib@portal:../packages/shared-lib::locator=automation-core%40workspace%3A." dependencies: "@mos-connection/model": ^4.1.1 - timeline-state-resolver-types: 9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0 + timeline-state-resolver-types: 9.1.1-nightly-release51-20240904-105552-81af9f7a1.0 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: node @@ -11748,12 +11748,12 @@ __metadata: languageName: node linkType: hard -"timeline-state-resolver-types@npm:9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0": - version: 9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0 - resolution: "timeline-state-resolver-types@npm:9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0" +"timeline-state-resolver-types@npm:9.1.1-nightly-release51-20240904-105552-81af9f7a1.0": + version: 9.1.1-nightly-release51-20240904-105552-81af9f7a1.0 + resolution: "timeline-state-resolver-types@npm:9.1.1-nightly-release51-20240904-105552-81af9f7a1.0" dependencies: tslib: ^2.6.2 - checksum: 7d57f25139d33a1bd62cd46b7d1a8c6ab9d5a12f51ba5dc115ae5a40dc751bef8100bedc1b81bde500315231b4918e25198171a4520ee82fd785f74c7e040be9 + checksum: bfaa86f746076667dbab259c083da5fc3917a1a3b3908937bf6122f404e1bfeaf2af46e206072da893316fbb3c7b8b586b593b8c2f65a677980dbaa330be4a32 languageName: node linkType: hard diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index 8dc8cebe6b..82a481f632 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -60,7 +60,7 @@ "@sofie-automation/shared-lib": "1.51.0-in-testing.0", "debug": "^4.3.4", "influx": "^5.9.3", - "timeline-state-resolver": "9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0", + "timeline-state-resolver": "9.1.1-nightly-release51-20240904-105552-81af9f7a1.0", "tslib": "^2.6.2", "underscore": "^1.13.6", "winston": "^3.11.0" diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 8a85f53953..0ec5f10b79 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -39,7 +39,7 @@ ], "dependencies": { "@mos-connection/model": "^4.1.1", - "timeline-state-resolver-types": "9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0", + "timeline-state-resolver-types": "9.1.1-nightly-release51-20240904-105552-81af9f7a1.0", "tslib": "^2.6.2", "type-fest": "^3.13.1" }, diff --git a/packages/yarn.lock b/packages/yarn.lock index 6f2f8adae7..3f3d38165e 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -4711,7 +4711,7 @@ __metadata: resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: "@mos-connection/model": ^4.1.1 - timeline-state-resolver-types: 9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0 + timeline-state-resolver-types: 9.1.1-nightly-release51-20240904-105552-81af9f7a1.0 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: unknown @@ -19419,7 +19419,7 @@ asn1@evs-broadcast/node-asn1: "@sofie-automation/shared-lib": 1.51.0-in-testing.0 debug: ^4.3.4 influx: ^5.9.3 - timeline-state-resolver: 9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0 + timeline-state-resolver: 9.1.1-nightly-release51-20240904-105552-81af9f7a1.0 tslib: ^2.6.2 underscore: ^1.13.6 winston: ^3.11.0 @@ -22649,7 +22649,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"superfly-timeline@npm:9.0.0, superfly-timeline@npm:^9.0.0": +"superfly-timeline@npm:9.0.0": version: 9.0.0 resolution: "superfly-timeline@npm:9.0.0" dependencies: @@ -22658,6 +22658,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"superfly-timeline@npm:^9.0.1": + version: 9.0.1 + resolution: "superfly-timeline@npm:9.0.1" + dependencies: + tslib: ^2.6.0 + checksum: 4267eed691fe9ce9f89bf17c8aed1a98206938dd6d850c64b083e4fd3a3dc5329801c76c757450c9520375bad100ce512cc6d6a3e4a997bdfa14a4e7d65f09f2 + languageName: node + linkType: hard + "supports-color@npm:^5.3.0, supports-color@npm:^5.5.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" @@ -23015,18 +23024,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"timeline-state-resolver-types@npm:9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0": - version: 9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0 - resolution: "timeline-state-resolver-types@npm:9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0" +"timeline-state-resolver-types@npm:9.1.1-nightly-release51-20240904-105552-81af9f7a1.0": + version: 9.1.1-nightly-release51-20240904-105552-81af9f7a1.0 + resolution: "timeline-state-resolver-types@npm:9.1.1-nightly-release51-20240904-105552-81af9f7a1.0" dependencies: tslib: ^2.6.2 - checksum: 7d57f25139d33a1bd62cd46b7d1a8c6ab9d5a12f51ba5dc115ae5a40dc751bef8100bedc1b81bde500315231b4918e25198171a4520ee82fd785f74c7e040be9 + checksum: bfaa86f746076667dbab259c083da5fc3917a1a3b3908937bf6122f404e1bfeaf2af46e206072da893316fbb3c7b8b586b593b8c2f65a677980dbaa330be4a32 languageName: node linkType: hard -"timeline-state-resolver@npm:9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0": - version: 9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0 - resolution: "timeline-state-resolver@npm:9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0" +"timeline-state-resolver@npm:9.1.1-nightly-release51-20240904-105552-81af9f7a1.0": + version: 9.1.1-nightly-release51-20240904-105552-81af9f7a1.0 + resolution: "timeline-state-resolver@npm:9.1.1-nightly-release51-20240904-105552-81af9f7a1.0" dependencies: "@tv2media/v-connection": ^7.3.2 atem-connection: 3.5.0 @@ -23049,9 +23058,9 @@ asn1@evs-broadcast/node-asn1: p-timeout: ^3.2.0 simple-oauth2: ^5.0.0 sprintf-js: ^1.1.3 - superfly-timeline: ^9.0.0 + superfly-timeline: ^9.0.1 threadedclass: ^1.2.1 - timeline-state-resolver-types: 9.1.1-nightly-release51-20240830-093530-7d1db9cd4.0 + timeline-state-resolver-types: 9.1.1-nightly-release51-20240904-105552-81af9f7a1.0 tslib: ^2.6.2 tv-automation-quantel-gateway-client: ^3.1.7 type-fest: ^3.13.1 @@ -23059,7 +23068,7 @@ asn1@evs-broadcast/node-asn1: utf-8-validate: ^5.0.10 ws: ^7.5.10 xml-js: ^1.6.11 - checksum: 098e1e14c2a89b1a5c4bab88a16a90b2e32e8583bee902638be139ca962b59a694027dcb80600e855e5abc646f44832c1e3429c326d087474d22665368b242ff + checksum: 1be4c906881041d99410790eff0986503fb45adeae8f9eeb58811e8ac2f80038fc41eef7fa26a52c0f531922aa801dbbf5b5066d2866bf2f098bca5ca82b4c01 languageName: node linkType: hard From 749da26fc892f0f1b0073ce56b80eaf7dc647325 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Fri, 6 Sep 2024 10:36:23 +0200 Subject: [PATCH 451/479] chore: fix issue with multiple versions --- meteor/package.json | 2 +- meteor/yarn.lock | 12 ++++++------ packages/job-worker/package.json | 2 +- packages/yarn.lock | 13 ++----------- 4 files changed, 10 insertions(+), 19 deletions(-) diff --git a/meteor/package.json b/meteor/package.json index be483a0def..c49f51be5e 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -100,7 +100,7 @@ "react-router-dom": "^5.3.4", "react-timer-hoc": "^2.3.0", "semver": "^7.5.4", - "superfly-timeline": "9.0.0", + "superfly-timeline": "9.0.1", "threadedclass": "^1.2.2", "timecode": "0.0.4", "type-fest": "^3.13.1", diff --git a/meteor/yarn.lock b/meteor/yarn.lock index bc3847ce80..650b25a393 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1405,7 +1405,7 @@ __metadata: node-fetch: ^2.7.0 p-lazy: ^3.1.0 p-timeout: ^4.1.0 - superfly-timeline: 9.0.0 + superfly-timeline: 9.0.1 threadedclass: ^1.2.2 tslib: ^2.6.2 type-fest: ^3.13.1 @@ -2794,7 +2794,7 @@ __metadata: semver: ^7.5.4 sinon: ^14.0.2 standard-version: ^9.5.0 - superfly-timeline: 9.0.0 + superfly-timeline: 9.0.1 threadedclass: ^1.2.2 timecode: 0.0.4 ts-jest: ^29.1.2 @@ -11570,12 +11570,12 @@ __metadata: languageName: node linkType: hard -"superfly-timeline@npm:9.0.0": - version: 9.0.0 - resolution: "superfly-timeline@npm:9.0.0" +"superfly-timeline@npm:9.0.1": + version: 9.0.1 + resolution: "superfly-timeline@npm:9.0.1" dependencies: tslib: ^2.6.0 - checksum: bb56fc6d6884f2956cdc1f18edceff48bc693c2329b009170e4d399ce70b8123ef38ddada0f0479eb359690eae39e7e578a9ec4ba326d6e4704653f74cde1a6d + checksum: 4267eed691fe9ce9f89bf17c8aed1a98206938dd6d850c64b083e4fd3a3dc5329801c76c757450c9520375bad100ce512cc6d6a3e4a997bdfa14a4e7d65f09f2 languageName: node linkType: hard diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index f9779400b8..0d6754623b 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -52,7 +52,7 @@ "node-fetch": "^2.7.0", "p-lazy": "^3.1.0", "p-timeout": "^4.1.0", - "superfly-timeline": "9.0.0", + "superfly-timeline": "9.0.1", "threadedclass": "^1.2.2", "tslib": "^2.6.2", "type-fest": "^3.13.1", diff --git a/packages/yarn.lock b/packages/yarn.lock index 3f3d38165e..7929bd4e0f 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -4672,7 +4672,7 @@ __metadata: node-fetch: ^2.7.0 p-lazy: ^3.1.0 p-timeout: ^4.1.0 - superfly-timeline: 9.0.0 + superfly-timeline: 9.0.1 threadedclass: ^1.2.2 tslib: ^2.6.2 type-fest: ^3.13.1 @@ -22649,16 +22649,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"superfly-timeline@npm:9.0.0": - version: 9.0.0 - resolution: "superfly-timeline@npm:9.0.0" - dependencies: - tslib: ^2.6.0 - checksum: bb56fc6d6884f2956cdc1f18edceff48bc693c2329b009170e4d399ce70b8123ef38ddada0f0479eb359690eae39e7e578a9ec4ba326d6e4704653f74cde1a6d - languageName: node - linkType: hard - -"superfly-timeline@npm:^9.0.1": +"superfly-timeline@npm:9.0.1, superfly-timeline@npm:^9.0.1": version: 9.0.1 resolution: "superfly-timeline@npm:9.0.1" dependencies: From 153d100fb659546201a654af5c566b513951df88 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Fri, 6 Sep 2024 10:45:47 +0200 Subject: [PATCH 452/479] fix(LinePartTimeline): make rules for findMainPiece consistent, make infinite graphics Pieces display correctly --- .../LinePartSecondaryPiece/LinePartSecondaryPiece.tsx | 2 +- meteor/client/ui/SegmentList/LinePartTimeline.tsx | 5 ++++- .../StoryboardPartThumbnail/StoryboardPartThumbnail.tsx | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/meteor/client/ui/SegmentList/LinePartSecondaryPiece/LinePartSecondaryPiece.tsx b/meteor/client/ui/SegmentList/LinePartSecondaryPiece/LinePartSecondaryPiece.tsx index f61bf48fb4..b62914bfe8 100644 --- a/meteor/client/ui/SegmentList/LinePartSecondaryPiece/LinePartSecondaryPiece.tsx +++ b/meteor/client/ui/SegmentList/LinePartSecondaryPiece/LinePartSecondaryPiece.tsx @@ -38,7 +38,7 @@ export const LinePartSecondaryPiece: React.FC = React.memo(function Line const typeClass = piece?.sourceLayer?.type ? RundownUtils.getSourceLayerClassName(piece?.sourceLayer?.type) : '' const pieceStyle = useMemo(() => { - const width = timeInBase(piece.renderedDuration ?? partDuration, timelineBase, timelineBase) + const width = timeInBase(piece.renderedDuration ?? Math.max(timelineBase, partDuration), timelineBase, timelineBase) const left = timeInBase(piece.renderedInPoint ?? 0, timelineBase, timelineBase) const overflow = Math.max(0, left + width - 100) return { diff --git a/meteor/client/ui/SegmentList/LinePartTimeline.tsx b/meteor/client/ui/SegmentList/LinePartTimeline.tsx index 3534c5afeb..5d95308a54 100644 --- a/meteor/client/ui/SegmentList/LinePartTimeline.tsx +++ b/meteor/client/ui/SegmentList/LinePartTimeline.tsx @@ -14,9 +14,12 @@ import { PieceUi } from '../SegmentContainer/withResolvedSegment' import StudioContext from '../RundownView/StudioContext' import { InvalidPartCover } from '../SegmentTimeline/Parts/InvalidPartCover' import { getPartInstanceTimingId } from '../../lib/rundownTiming' +import { getShowHiddenSourceLayers } from '../../lib/localStorage' const TIMELINE_DEFAULT_BASE = 30 * 1000 +const showHiddenSourceLayers = getShowHiddenSourceLayers() + interface IProps { part: PartExtended isLive: boolean @@ -56,7 +59,7 @@ function findTimelineGraphics(pieces: PieceExtended[]) { .filter((piece) => { if ( piece.sourceLayer?.type === SourceLayerType.LOWER_THIRD && - !piece.sourceLayer?.isHidden && + (showHiddenSourceLayers || !piece.sourceLayer?.isHidden) && ((piece.instance.piece.lifespan === PieceLifespan.WithinPart && piece.instance.piece.enable.duration) || !piece.sourceLayer?.onListViewColumn) ) { diff --git a/meteor/client/ui/SegmentStoryboard/StoryboardPartThumbnail/StoryboardPartThumbnail.tsx b/meteor/client/ui/SegmentStoryboard/StoryboardPartThumbnail/StoryboardPartThumbnail.tsx index 6701da2633..5c310fbca8 100644 --- a/meteor/client/ui/SegmentStoryboard/StoryboardPartThumbnail/StoryboardPartThumbnail.tsx +++ b/meteor/client/ui/SegmentStoryboard/StoryboardPartThumbnail/StoryboardPartThumbnail.tsx @@ -21,7 +21,7 @@ const supportedSourceLayerTypes = new Set( function findMainPiece(pieces: PieceExtended[]) { return findPieceExtendedToShowFromOrderedResolvedInstances( - pieces.filter((piece) => piece.outputLayer?.isPGM), + pieces.filter((piece) => piece.outputLayer?.isPGM && piece.sourceLayer?.onPresenterScreen), supportedSourceLayerTypes ) } From 3374f2ba0693fe0748bc3acc8a86045a3dba6862 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Fri, 6 Sep 2024 11:00:55 +0200 Subject: [PATCH 453/479] chore: 1.51.0-in-testing.0 --- meteor/package.json | 2 +- meteor/yarn.lock | 12 +++--- packages/blueprints-integration/CHANGELOG.md | 22 +++++++++++ packages/blueprints-integration/package.json | 4 +- packages/corelib/package.json | 6 +-- packages/documentation/package.json | 2 +- packages/job-worker/package.json | 8 ++-- packages/lerna.json | 2 +- packages/live-status-gateway/package.json | 10 ++--- packages/mos-gateway/CHANGELOG.md | 16 ++++++++ packages/mos-gateway/package.json | 6 +-- packages/openapi/package.json | 2 +- packages/playout-gateway/CHANGELOG.md | 24 ++++++++++++ packages/playout-gateway/package.json | 6 +-- packages/server-core-integration/CHANGELOG.md | 16 ++++++++ packages/server-core-integration/package.json | 4 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 38 +++++++++---------- 18 files changed, 130 insertions(+), 52 deletions(-) diff --git a/meteor/package.json b/meteor/package.json index c49f51be5e..644f905f31 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -1,6 +1,6 @@ { "name": "automation-core", - "version": "1.51.0-in-testing.0", + "version": "1.51.0-in-testing.1", "private": true, "engines": { "node": ">=14.19.1" diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 650b25a393..34f0f856df 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1321,7 +1321,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/blueprints-integration@portal:../packages/blueprints-integration::locator=automation-core%40workspace%3A." dependencies: - "@sofie-automation/shared-lib": 1.51.0-in-testing.0 + "@sofie-automation/shared-lib": 1.51.0-in-testing.1 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: node @@ -1362,8 +1362,8 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/corelib@portal:../packages/corelib::locator=automation-core%40workspace%3A." dependencies: - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.0 - "@sofie-automation/shared-lib": 1.51.0-in-testing.0 + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.1 + "@sofie-automation/shared-lib": 1.51.0-in-testing.1 fast-clone: ^1.5.13 i18next: ^21.10.0 influx: ^5.9.3 @@ -1394,9 +1394,9 @@ __metadata: resolution: "@sofie-automation/job-worker@portal:../packages/job-worker::locator=automation-core%40workspace%3A." dependencies: "@slack/webhook": ^6.1.0 - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.0 - "@sofie-automation/corelib": 1.51.0-in-testing.0 - "@sofie-automation/shared-lib": 1.51.0-in-testing.0 + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.1 + "@sofie-automation/corelib": 1.51.0-in-testing.1 + "@sofie-automation/shared-lib": 1.51.0-in-testing.1 amqplib: ^0.10.3 deepmerge: ^4.3.1 elastic-apm-node: ^3.51.0 diff --git a/packages/blueprints-integration/CHANGELOG.md b/packages/blueprints-integration/CHANGELOG.md index e9fe08dc42..9f8c48bcb7 100644 --- a/packages/blueprints-integration/CHANGELOG.md +++ b/packages/blueprints-integration/CHANGELOG.md @@ -3,6 +3,28 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.51.0-in-testing.1](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.0...v1.51.0-in-testing.1) (2024-09-06) + + +### Features + +* stringify piece NoraContent payload SOFIE-3398 ([#1248](https://github.com/nrkno/sofie-core/issues/1248)) ([0613f74](https://github.com/nrkno/sofie-core/commit/0613f740c1e2f740d7d9c39bc72178e301f5f72f)) + + + + + +# [1.51.0-in-testing.1](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.0...v1.51.0-in-testing.1) (2024-09-06) + + +### Features + +* stringify piece NoraContent payload SOFIE-3398 ([#1248](https://github.com/nrkno/sofie-core/issues/1248)) ([0613f74](https://github.com/nrkno/sofie-core/commit/0613f740c1e2f740d7d9c39bc72178e301f5f72f)) + + + + + # [1.51.0-in-testing.0](https://github.com/nrkno/sofie-core/compare/v1.50.4...v1.51.0-in-testing.0) (2024-08-19) ## [1.50.5-LSG-updates](https://github.com/nrkno/sofie-core/compare/v1.50.4-LSG-updates...v1.50.5-LSG-updates) (2024-08-08) diff --git a/packages/blueprints-integration/package.json b/packages/blueprints-integration/package.json index 7135a1abce..fcefd2ec03 100644 --- a/packages/blueprints-integration/package.json +++ b/packages/blueprints-integration/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/blueprints-integration", - "version": "1.51.0-in-testing.0", + "version": "1.51.0-in-testing.1", "description": "Library to define the interaction between core and the blueprints.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -38,7 +38,7 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/shared-lib": "1.51.0-in-testing.0", + "@sofie-automation/shared-lib": "1.51.0-in-testing.1", "tslib": "^2.6.2", "type-fest": "^3.13.1" }, diff --git a/packages/corelib/package.json b/packages/corelib/package.json index 086bc24b7b..0a66bf5919 100644 --- a/packages/corelib/package.json +++ b/packages/corelib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/corelib", - "version": "1.51.0-in-testing.0", + "version": "1.51.0-in-testing.1", "private": true, "description": "Internal library for some types shared by core and workers", "main": "dist/index.js", @@ -39,8 +39,8 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/blueprints-integration": "1.51.0-in-testing.0", - "@sofie-automation/shared-lib": "1.51.0-in-testing.0", + "@sofie-automation/blueprints-integration": "1.51.0-in-testing.1", + "@sofie-automation/shared-lib": "1.51.0-in-testing.1", "fast-clone": "^1.5.13", "i18next": "^21.10.0", "influx": "^5.9.3", diff --git a/packages/documentation/package.json b/packages/documentation/package.json index ae06aefb4c..b986086916 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -1,6 +1,6 @@ { "name": "sofie-documentation", - "version": "1.51.0-in-testing.0", + "version": "1.51.0-in-testing.1", "private": true, "scripts": { "docusaurus": "docusaurus", diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index 0d6754623b..febfae4014 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/job-worker", - "version": "1.51.0-in-testing.0", + "version": "1.51.0-in-testing.1", "description": "Worker for things", "main": "dist/index.js", "license": "MIT", @@ -41,9 +41,9 @@ ], "dependencies": { "@slack/webhook": "^6.1.0", - "@sofie-automation/blueprints-integration": "1.51.0-in-testing.0", - "@sofie-automation/corelib": "1.51.0-in-testing.0", - "@sofie-automation/shared-lib": "1.51.0-in-testing.0", + "@sofie-automation/blueprints-integration": "1.51.0-in-testing.1", + "@sofie-automation/corelib": "1.51.0-in-testing.1", + "@sofie-automation/shared-lib": "1.51.0-in-testing.1", "amqplib": "^0.10.3", "deepmerge": "^4.3.1", "elastic-apm-node": "^3.51.0", diff --git a/packages/lerna.json b/packages/lerna.json index aec3a9d3e7..16d12180a4 100644 --- a/packages/lerna.json +++ b/packages/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.51.0-in-testing.0", + "version": "1.51.0-in-testing.1", "npmClient": "yarn", "useWorkspaces": true } diff --git a/packages/live-status-gateway/package.json b/packages/live-status-gateway/package.json index b2de220d7e..e543275a0d 100644 --- a/packages/live-status-gateway/package.json +++ b/packages/live-status-gateway/package.json @@ -1,6 +1,6 @@ { "name": "live-status-gateway", - "version": "1.51.0-in-testing.0", + "version": "1.51.0-in-testing.1", "private": true, "description": "Provides state from Sofie over sockets", "license": "MIT", @@ -53,10 +53,10 @@ "production" ], "dependencies": { - "@sofie-automation/blueprints-integration": "1.51.0-in-testing.0", - "@sofie-automation/corelib": "1.51.0-in-testing.0", - "@sofie-automation/server-core-integration": "1.51.0-in-testing.0", - "@sofie-automation/shared-lib": "1.51.0-in-testing.0", + "@sofie-automation/blueprints-integration": "1.51.0-in-testing.1", + "@sofie-automation/corelib": "1.51.0-in-testing.1", + "@sofie-automation/server-core-integration": "1.51.0-in-testing.1", + "@sofie-automation/shared-lib": "1.51.0-in-testing.1", "debug": "^4.3.4", "fast-clone": "^1.5.13", "influx": "^5.9.3", diff --git a/packages/mos-gateway/CHANGELOG.md b/packages/mos-gateway/CHANGELOG.md index a3ae51a67f..413dac4df1 100644 --- a/packages/mos-gateway/CHANGELOG.md +++ b/packages/mos-gateway/CHANGELOG.md @@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.51.0-in-testing.1](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.0...v1.51.0-in-testing.1) (2024-09-06) + +**Note:** Version bump only for package mos-gateway + + + + + +# [1.51.0-in-testing.1](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.0...v1.51.0-in-testing.1) (2024-09-06) + +**Note:** Version bump only for package mos-gateway + + + + + # [1.51.0-in-testing.0](https://github.com/nrkno/sofie-core/compare/v1.50.4...v1.51.0-in-testing.0) (2024-08-19) ### Features diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index a2daa397aa..7359ef1c65 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -1,6 +1,6 @@ { "name": "mos-gateway", - "version": "1.51.0-in-testing.0", + "version": "1.51.0-in-testing.1", "private": true, "description": "MOS-Gateway for the Sofie project", "license": "MIT", @@ -66,8 +66,8 @@ ], "dependencies": { "@mos-connection/connector": "4.1.1", - "@sofie-automation/server-core-integration": "1.51.0-in-testing.0", - "@sofie-automation/shared-lib": "1.51.0-in-testing.0", + "@sofie-automation/server-core-integration": "1.51.0-in-testing.1", + "@sofie-automation/shared-lib": "1.51.0-in-testing.1", "tslib": "^2.6.2", "type-fest": "^3.13.1", "underscore": "^1.13.6", diff --git a/packages/openapi/package.json b/packages/openapi/package.json index 8550a44622..9056750040 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/openapi", - "version": "1.51.0-in-testing.0", + "version": "1.51.0-in-testing.1", "private": true, "license": "MIT", "repository": { diff --git a/packages/playout-gateway/CHANGELOG.md b/packages/playout-gateway/CHANGELOG.md index 8ebb3e627e..98ccb17af6 100644 --- a/packages/playout-gateway/CHANGELOG.md +++ b/packages/playout-gateway/CHANGELOG.md @@ -3,6 +3,30 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.51.0-in-testing.1](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.0...v1.51.0-in-testing.1) (2024-09-06) + + +### Bug Fixes + +* playout-gateway not passing datastore to tsr at startup ([82f6fb2](https://github.com/nrkno/sofie-core/commit/82f6fb2720992581f26ff18d161e787784195c95)) +* update TSR dependency (to fix timeline bug) ([746e0d2](https://github.com/nrkno/sofie-core/commit/746e0d2c822b95927172e76cbd6a4436d1a99192)) + + + + + +# [1.51.0-in-testing.1](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.0...v1.51.0-in-testing.1) (2024-09-06) + + +### Bug Fixes + +* playout-gateway not passing datastore to tsr at startup ([82f6fb2](https://github.com/nrkno/sofie-core/commit/82f6fb2720992581f26ff18d161e787784195c95)) +* update TSR dependency (to fix timeline bug) ([746e0d2](https://github.com/nrkno/sofie-core/commit/746e0d2c822b95927172e76cbd6a4436d1a99192)) + + + + + # [1.51.0-in-testing.0](https://github.com/nrkno/sofie-core/compare/v1.50.4...v1.51.0-in-testing.0) (2024-08-19) diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index 82a481f632..afb2b41a70 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -1,6 +1,6 @@ { "name": "playout-gateway", - "version": "1.51.0-in-testing.0", + "version": "1.51.0-in-testing.1", "private": true, "description": "Connect to Core, play stuff", "license": "MIT", @@ -56,8 +56,8 @@ "production" ], "dependencies": { - "@sofie-automation/server-core-integration": "1.51.0-in-testing.0", - "@sofie-automation/shared-lib": "1.51.0-in-testing.0", + "@sofie-automation/server-core-integration": "1.51.0-in-testing.1", + "@sofie-automation/shared-lib": "1.51.0-in-testing.1", "debug": "^4.3.4", "influx": "^5.9.3", "timeline-state-resolver": "9.1.1-nightly-release51-20240904-105552-81af9f7a1.0", diff --git a/packages/server-core-integration/CHANGELOG.md b/packages/server-core-integration/CHANGELOG.md index 5173eccf92..a439067c5e 100644 --- a/packages/server-core-integration/CHANGELOG.md +++ b/packages/server-core-integration/CHANGELOG.md @@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.51.0-in-testing.1](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.0...v1.51.0-in-testing.1) (2024-09-06) + +**Note:** Version bump only for package @sofie-automation/server-core-integration + + + + + +# [1.51.0-in-testing.1](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.0...v1.51.0-in-testing.1) (2024-09-06) + +**Note:** Version bump only for package @sofie-automation/server-core-integration + + + + + # [1.51.0-in-testing.0](https://github.com/nrkno/sofie-core/compare/v1.50.4...v1.51.0-in-testing.0) (2024-08-19) ## [1.50.5-LSG-updates](https://github.com/nrkno/sofie-core/compare/v1.50.4-LSG-updates...v1.50.5-LSG-updates) (2024-08-08) diff --git a/packages/server-core-integration/package.json b/packages/server-core-integration/package.json index 008cd6efc9..6060ea3d15 100644 --- a/packages/server-core-integration/package.json +++ b/packages/server-core-integration/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/server-core-integration", - "version": "1.51.0-in-testing.0", + "version": "1.51.0-in-testing.1", "description": "Library for connecting to Core", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -70,7 +70,7 @@ "production" ], "dependencies": { - "@sofie-automation/shared-lib": "1.51.0-in-testing.0", + "@sofie-automation/shared-lib": "1.51.0-in-testing.1", "ejson": "^2.2.3", "eventemitter3": "^4.0.7", "faye-websocket": "^0.11.4", diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 0ec5f10b79..94af8959fc 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/shared-lib", - "version": "1.51.0-in-testing.0", + "version": "1.51.0-in-testing.1", "description": "Library for types & values shared by core, workers and gateways", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/packages/yarn.lock b/packages/yarn.lock index 7929bd4e0f..6ffb7e8192 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -4583,11 +4583,11 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/blueprints-integration@1.51.0-in-testing.0, @sofie-automation/blueprints-integration@workspace:blueprints-integration": +"@sofie-automation/blueprints-integration@1.51.0-in-testing.1, @sofie-automation/blueprints-integration@workspace:blueprints-integration": version: 0.0.0-use.local resolution: "@sofie-automation/blueprints-integration@workspace:blueprints-integration" dependencies: - "@sofie-automation/shared-lib": 1.51.0-in-testing.0 + "@sofie-automation/shared-lib": 1.51.0-in-testing.1 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: unknown @@ -4624,12 +4624,12 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/corelib@1.51.0-in-testing.0, @sofie-automation/corelib@workspace:corelib": +"@sofie-automation/corelib@1.51.0-in-testing.1, @sofie-automation/corelib@workspace:corelib": version: 0.0.0-use.local resolution: "@sofie-automation/corelib@workspace:corelib" dependencies: - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.0 - "@sofie-automation/shared-lib": 1.51.0-in-testing.0 + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.1 + "@sofie-automation/shared-lib": 1.51.0-in-testing.1 fast-clone: ^1.5.13 i18next: ^21.10.0 influx: ^5.9.3 @@ -4660,9 +4660,9 @@ __metadata: resolution: "@sofie-automation/job-worker@workspace:job-worker" dependencies: "@slack/webhook": ^6.1.0 - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.0 - "@sofie-automation/corelib": 1.51.0-in-testing.0 - "@sofie-automation/shared-lib": 1.51.0-in-testing.0 + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.1 + "@sofie-automation/corelib": 1.51.0-in-testing.1 + "@sofie-automation/shared-lib": 1.51.0-in-testing.1 amqplib: ^0.10.3 deepmerge: ^4.3.1 elastic-apm-node: ^3.51.0 @@ -4692,11 +4692,11 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/server-core-integration@1.51.0-in-testing.0, @sofie-automation/server-core-integration@workspace:server-core-integration": +"@sofie-automation/server-core-integration@1.51.0-in-testing.1, @sofie-automation/server-core-integration@workspace:server-core-integration": version: 0.0.0-use.local resolution: "@sofie-automation/server-core-integration@workspace:server-core-integration" dependencies: - "@sofie-automation/shared-lib": 1.51.0-in-testing.0 + "@sofie-automation/shared-lib": 1.51.0-in-testing.1 ejson: ^2.2.3 eventemitter3: ^4.0.7 faye-websocket: ^0.11.4 @@ -4706,7 +4706,7 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/shared-lib@1.51.0-in-testing.0, @sofie-automation/shared-lib@workspace:shared-lib": +"@sofie-automation/shared-lib@1.51.0-in-testing.1, @sofie-automation/shared-lib@workspace:shared-lib": version: 0.0.0-use.local resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: @@ -15352,10 +15352,10 @@ asn1@evs-broadcast/node-asn1: "@asyncapi/generator": ^1.17.25 "@asyncapi/html-template": ^2.3.9 "@asyncapi/nodejs-ws-template": ^0.9.36 - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.0 - "@sofie-automation/corelib": 1.51.0-in-testing.0 - "@sofie-automation/server-core-integration": 1.51.0-in-testing.0 - "@sofie-automation/shared-lib": 1.51.0-in-testing.0 + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.1 + "@sofie-automation/corelib": 1.51.0-in-testing.1 + "@sofie-automation/server-core-integration": 1.51.0-in-testing.1 + "@sofie-automation/shared-lib": 1.51.0-in-testing.1 debug: ^4.3.4 fast-clone: ^1.5.13 influx: ^5.9.3 @@ -17428,8 +17428,8 @@ asn1@evs-broadcast/node-asn1: resolution: "mos-gateway@workspace:mos-gateway" dependencies: "@mos-connection/connector": 4.1.1 - "@sofie-automation/server-core-integration": 1.51.0-in-testing.0 - "@sofie-automation/shared-lib": 1.51.0-in-testing.0 + "@sofie-automation/server-core-integration": 1.51.0-in-testing.1 + "@sofie-automation/shared-lib": 1.51.0-in-testing.1 tslib: ^2.6.2 type-fest: ^3.13.1 underscore: ^1.13.6 @@ -19415,8 +19415,8 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "playout-gateway@workspace:playout-gateway" dependencies: - "@sofie-automation/server-core-integration": 1.51.0-in-testing.0 - "@sofie-automation/shared-lib": 1.51.0-in-testing.0 + "@sofie-automation/server-core-integration": 1.51.0-in-testing.1 + "@sofie-automation/shared-lib": 1.51.0-in-testing.1 debug: ^4.3.4 influx: ^5.9.3 timeline-state-resolver: 9.1.1-nightly-release51-20240904-105552-81af9f7a1.0 From e36b5aa204aeb86195350a37b59262647ce9991d Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Mon, 9 Sep 2024 13:10:44 +0200 Subject: [PATCH 454/479] chore: update timeline-state-resolver --- meteor/yarn.lock | 10 +++++----- packages/playout-gateway/package.json | 2 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 22 +++++++++++----------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 34f0f856df..f589906d4c 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1418,7 +1418,7 @@ __metadata: resolution: "@sofie-automation/shared-lib@portal:../packages/shared-lib::locator=automation-core%40workspace%3A." dependencies: "@mos-connection/model": ^4.1.1 - timeline-state-resolver-types: 9.1.1-nightly-release51-20240904-105552-81af9f7a1.0 + timeline-state-resolver-types: 9.2.0-alpha.0 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: node @@ -11748,12 +11748,12 @@ __metadata: languageName: node linkType: hard -"timeline-state-resolver-types@npm:9.1.1-nightly-release51-20240904-105552-81af9f7a1.0": - version: 9.1.1-nightly-release51-20240904-105552-81af9f7a1.0 - resolution: "timeline-state-resolver-types@npm:9.1.1-nightly-release51-20240904-105552-81af9f7a1.0" +"timeline-state-resolver-types@npm:9.2.0-alpha.0": + version: 9.2.0-alpha.0 + resolution: "timeline-state-resolver-types@npm:9.2.0-alpha.0" dependencies: tslib: ^2.6.2 - checksum: bfaa86f746076667dbab259c083da5fc3917a1a3b3908937bf6122f404e1bfeaf2af46e206072da893316fbb3c7b8b586b593b8c2f65a677980dbaa330be4a32 + checksum: 88004b58606489102eb39601926bc56ca6ea2142e70339e9d0b8a7a848d7347a4b213340da3ebf5cf13edad804b14b1a155191b2123b805b96f8f221488e015a languageName: node linkType: hard diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index afb2b41a70..aefe811407 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -60,7 +60,7 @@ "@sofie-automation/shared-lib": "1.51.0-in-testing.1", "debug": "^4.3.4", "influx": "^5.9.3", - "timeline-state-resolver": "9.1.1-nightly-release51-20240904-105552-81af9f7a1.0", + "timeline-state-resolver": "9.2.0-alpha.0", "tslib": "^2.6.2", "underscore": "^1.13.6", "winston": "^3.11.0" diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 94af8959fc..4373cec47d 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -39,7 +39,7 @@ ], "dependencies": { "@mos-connection/model": "^4.1.1", - "timeline-state-resolver-types": "9.1.1-nightly-release51-20240904-105552-81af9f7a1.0", + "timeline-state-resolver-types": "9.2.0-alpha.0", "tslib": "^2.6.2", "type-fest": "^3.13.1" }, diff --git a/packages/yarn.lock b/packages/yarn.lock index 6ffb7e8192..96b65cbcee 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -4711,7 +4711,7 @@ __metadata: resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: "@mos-connection/model": ^4.1.1 - timeline-state-resolver-types: 9.1.1-nightly-release51-20240904-105552-81af9f7a1.0 + timeline-state-resolver-types: 9.2.0-alpha.0 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: unknown @@ -19419,7 +19419,7 @@ asn1@evs-broadcast/node-asn1: "@sofie-automation/shared-lib": 1.51.0-in-testing.1 debug: ^4.3.4 influx: ^5.9.3 - timeline-state-resolver: 9.1.1-nightly-release51-20240904-105552-81af9f7a1.0 + timeline-state-resolver: 9.2.0-alpha.0 tslib: ^2.6.2 underscore: ^1.13.6 winston: ^3.11.0 @@ -23015,18 +23015,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"timeline-state-resolver-types@npm:9.1.1-nightly-release51-20240904-105552-81af9f7a1.0": - version: 9.1.1-nightly-release51-20240904-105552-81af9f7a1.0 - resolution: "timeline-state-resolver-types@npm:9.1.1-nightly-release51-20240904-105552-81af9f7a1.0" +"timeline-state-resolver-types@npm:9.2.0-alpha.0": + version: 9.2.0-alpha.0 + resolution: "timeline-state-resolver-types@npm:9.2.0-alpha.0" dependencies: tslib: ^2.6.2 - checksum: bfaa86f746076667dbab259c083da5fc3917a1a3b3908937bf6122f404e1bfeaf2af46e206072da893316fbb3c7b8b586b593b8c2f65a677980dbaa330be4a32 + checksum: 88004b58606489102eb39601926bc56ca6ea2142e70339e9d0b8a7a848d7347a4b213340da3ebf5cf13edad804b14b1a155191b2123b805b96f8f221488e015a languageName: node linkType: hard -"timeline-state-resolver@npm:9.1.1-nightly-release51-20240904-105552-81af9f7a1.0": - version: 9.1.1-nightly-release51-20240904-105552-81af9f7a1.0 - resolution: "timeline-state-resolver@npm:9.1.1-nightly-release51-20240904-105552-81af9f7a1.0" +"timeline-state-resolver@npm:9.2.0-alpha.0": + version: 9.2.0-alpha.0 + resolution: "timeline-state-resolver@npm:9.2.0-alpha.0" dependencies: "@tv2media/v-connection": ^7.3.2 atem-connection: 3.5.0 @@ -23051,7 +23051,7 @@ asn1@evs-broadcast/node-asn1: sprintf-js: ^1.1.3 superfly-timeline: ^9.0.1 threadedclass: ^1.2.1 - timeline-state-resolver-types: 9.1.1-nightly-release51-20240904-105552-81af9f7a1.0 + timeline-state-resolver-types: 9.2.0-alpha.0 tslib: ^2.6.2 tv-automation-quantel-gateway-client: ^3.1.7 type-fest: ^3.13.1 @@ -23059,7 +23059,7 @@ asn1@evs-broadcast/node-asn1: utf-8-validate: ^5.0.10 ws: ^7.5.10 xml-js: ^1.6.11 - checksum: 1be4c906881041d99410790eff0986503fb45adeae8f9eeb58811e8ac2f80038fc41eef7fa26a52c0f531922aa801dbbf5b5066d2866bf2f098bca5ca82b4c01 + checksum: b358df9d0f758484084bf549e241a39dd78a9e6e4e78b303c18cd716854882eeb44ad987f34fb47f2904cfc66b6c35ff9590ce9708a87c2699eed33250ebcd58 languageName: node linkType: hard From c532e6a7a7e2d7cc37b7e816e4d42bcc4ea299b2 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 11 Sep 2024 14:20:48 +0100 Subject: [PATCH 455/479] chore: remove local patch of `@asyncapi/generator-react-sdk` --- ...ator-react-sdk-npm-1.0.20-af18b2f42b.patch | 14 ---------- packages/package.json | 6 +---- packages/yarn.lock | 26 +++---------------- 3 files changed, 5 insertions(+), 41 deletions(-) delete mode 100644 packages/.yarn/patches/@asyncapi-generator-react-sdk-npm-1.0.20-af18b2f42b.patch diff --git a/packages/.yarn/patches/@asyncapi-generator-react-sdk-npm-1.0.20-af18b2f42b.patch b/packages/.yarn/patches/@asyncapi-generator-react-sdk-npm-1.0.20-af18b2f42b.patch deleted file mode 100644 index 8d71b66241..0000000000 --- a/packages/.yarn/patches/@asyncapi-generator-react-sdk-npm-1.0.20-af18b2f42b.patch +++ /dev/null @@ -1,14 +0,0 @@ -diff --git a/lib/transpiler/transpiler.js b/lib/transpiler/transpiler.js -index 3c26e5852787f0246065f6206ba0901ee32b16a7..370266a29f2c618ae48c50f4acc7462ac6cc0b4f 100644 ---- a/lib/transpiler/transpiler.js -+++ b/lib/transpiler/transpiler.js -@@ -90,7 +90,8 @@ function transpileFiles(directory, outputDir, options) { - dir: outputDir, - exports: "auto", - paths: { -- 'react/jsx-runtime': 'react/cjs/react-jsx-runtime.production.min', -+ // Make sure it finds the correct version of react https://github.com/asyncapi/generator-react-sdk/pull/242 -+ 'react/jsx-runtime': require.resolve('react/cjs/react-jsx-runtime.production.min'), - }, - sanitizeFileName: false, - })]; diff --git a/packages/package.json b/packages/package.json index ac9e9c160b..c379c53211 100644 --- a/packages/package.json +++ b/packages/package.json @@ -64,9 +64,5 @@ "typescript": "~4.9.5" }, "name": "packages", - "packageManager": "yarn@3.5.0", - "resolutions": { - "@asyncapi/generator-react-sdk@^1.0.20": "patch:@asyncapi/generator-react-sdk@npm%3A1.0.20#./.yarn/patches/@asyncapi-generator-react-sdk-npm-1.0.20-af18b2f42b.patch", - "@asyncapi/generator-react-sdk@^1.0.18": "patch:@asyncapi/generator-react-sdk@npm%3A1.0.20#./.yarn/patches/@asyncapi-generator-react-sdk-npm-1.0.20-af18b2f42b.patch" - } + "packageManager": "yarn@3.5.0" } diff --git a/packages/yarn.lock b/packages/yarn.lock index 96b65cbcee..599aefa9da 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -277,27 +277,9 @@ __metadata: languageName: node linkType: hard -"@asyncapi/generator-react-sdk@npm:1.0.20": - version: 1.0.20 - resolution: "@asyncapi/generator-react-sdk@npm:1.0.20" - dependencies: - "@asyncapi/parser": ^3.1.0 - "@babel/core": 7.12.9 - "@babel/preset-env": ^7.12.7 - "@babel/preset-react": ^7.12.7 - "@rollup/plugin-babel": ^5.2.1 - babel-plugin-source-map-support: ^2.1.3 - prop-types: ^15.7.2 - react: ^17.0.1 - rollup: ^2.60.1 - source-map-support: ^0.5.19 - checksum: 59473d68935b939027c09da821fac794034020880dc3451c1486c63a6c486e7807abc604d97271dfcf15d4d0f060f8f119e5877c6b81b143661af77f8ad3f545 - languageName: node - linkType: hard - -"@asyncapi/generator-react-sdk@patch:@asyncapi/generator-react-sdk@npm%3A1.0.20#./.yarn/patches/@asyncapi-generator-react-sdk-npm-1.0.20-af18b2f42b.patch::locator=packages%40workspace%3A.": - version: 1.0.20 - resolution: "@asyncapi/generator-react-sdk@patch:@asyncapi/generator-react-sdk@npm%3A1.0.20#./.yarn/patches/@asyncapi-generator-react-sdk-npm-1.0.20-af18b2f42b.patch::version=1.0.20&hash=4ff360&locator=packages%40workspace%3A." +"@asyncapi/generator-react-sdk@npm:^1.0.18, @asyncapi/generator-react-sdk@npm:^1.0.20": + version: 1.1.2 + resolution: "@asyncapi/generator-react-sdk@npm:1.1.2" dependencies: "@asyncapi/parser": ^3.1.0 "@babel/core": 7.12.9 @@ -309,7 +291,7 @@ __metadata: react: ^17.0.1 rollup: ^2.60.1 source-map-support: ^0.5.19 - checksum: dc34d29fd0e043ed225adf920f0899b4885425504a9aae507317cc4d9d4edff98adbadb3618fb813a52e147b4084eca1ac40618427fb6c4c71c5943e1f053c0e + checksum: 431ff8bfc7e131797af3d4377130742be49ec50ac4095458faf3851fdcfd75e0622e5cd8be3d8a2029c0bf0a8f134b2765873d78d9fda278475cea191bcf2cd0 languageName: node linkType: hard From 8f17c0aa2fcd4ed7e4f0e34f1fe7e9af565b8d99 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Tue, 17 Sep 2024 16:53:38 +0200 Subject: [PATCH 456/479] chore(SnapshotsView): improve margins between upload buttons --- meteor/client/ui/Settings/SnapshotsView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meteor/client/ui/Settings/SnapshotsView.tsx b/meteor/client/ui/Settings/SnapshotsView.tsx index 42d7bb4c62..e8eabe8e3f 100644 --- a/meteor/client/ui/Settings/SnapshotsView.tsx +++ b/meteor/client/ui/Settings/SnapshotsView.tsx @@ -324,7 +324,7 @@ const SnapshotsViewContent = withTranslation()( this.onUploadFile(e, true)} key={this.state.uploadFileKey2} > From 4a3a2e779c144b1c9e88c187cce2e5c80d34626d Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 18 Sep 2024 12:18:50 +0200 Subject: [PATCH 457/479] fix(NoraFloatingInspector): prevent Segment crash when trying to show a Piece with an invalid Nora `previewPayload` --- .../L3rdFloatingInspector.tsx | 5 +- .../NoraFloatingInspector.tsx | 60 ++++++++++--------- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/meteor/client/ui/FloatingInspectors/L3rdFloatingInspector.tsx b/meteor/client/ui/FloatingInspectors/L3rdFloatingInspector.tsx index 19d90fd208..ef14c8d0a1 100644 --- a/meteor/client/ui/FloatingInspectors/L3rdFloatingInspector.tsx +++ b/meteor/client/ui/FloatingInspectors/L3rdFloatingInspector.tsx @@ -84,7 +84,10 @@ export const L3rdFloatingInspector: React.FunctionComponent = ({ const { style: floatingInspectorStyle } = useInspectorPosition(position, ref, showMiniInspector) - return noraContent && noraContent.previewPayload && noraContent.previewRenderer ? ( + const hasValidPreviewPayload = + noraContent?.previewPayload && noraContent?.previewPayload && noraContent.previewPayload.toString() !== '{}' + + return hasValidPreviewPayload && noraContent.previewRenderer ? ( showMiniInspector ? ( ) : null diff --git a/meteor/client/ui/FloatingInspectors/NoraFloatingInspector.tsx b/meteor/client/ui/FloatingInspectors/NoraFloatingInspector.tsx index 503ccf284d..8bb6d2f7db 100644 --- a/meteor/client/ui/FloatingInspectors/NoraFloatingInspector.tsx +++ b/meteor/client/ui/FloatingInspectors/NoraFloatingInspector.tsx @@ -1,4 +1,4 @@ -import { JSONBlobParse, NoraContent } from '@sofie-automation/blueprints-integration' +import { JSONBlobParse, NoraContent, NoraPayload } from '@sofie-automation/blueprints-integration' import React, { useEffect, useImperativeHandle } from 'react' import _ from 'underscore' import { getNoraContentSteps } from '../SegmentContainer/PieceMultistepChevron' @@ -86,35 +86,39 @@ export class NoraPreviewRenderer extends React.Component<{}, IStateHeader> { } private postNoraEvent(contentWindow: Window, noraContent: NoraContent) { - const payload = JSONBlobParse(noraContent.previewPayload) - - contentWindow.postMessage( - { - event: 'nora', - contentToShow: { - manifest: payload.manifest, - template: { - event: 'preview', - name: payload.template.name, - channel: 'gfx1', - layer: payload.template.layer, - system: 'html', + try { + const payload = JSONBlobParse(noraContent.previewPayload) + + contentWindow.postMessage( + { + event: 'nora', + contentToShow: { + manifest: payload.manifest, + template: { + event: 'preview', + name: payload.template.name, + channel: 'gfx1', + layer: payload.template.layer, + system: 'html', + }, + content: { + ...payload.content, + _valid: false, + }, + timing: { + duration: '00:05', + in: 'auto', + out: 'auto', + timeIn: '00:00', + }, + step: payload.step, }, - content: { - ...payload.content, - _valid: false, - }, - timing: { - duration: '00:05', - in: 'auto', - out: 'auto', - timeIn: '00:00', - }, - step: payload.step, }, - }, - '*' - ) + '*' + ) + } catch (e) { + console.error(`Error in NoraPreviewRenderer.postNoraEvent: ${e}`, e) + } } private _show() { From a4250efe8d7214270d427b862aadcc1ce10715f2 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 18 Sep 2024 13:07:47 +0100 Subject: [PATCH 458/479] chore: fix type error --- .../__tests__/PlayoutPartInstanceModelImpl.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/job-worker/src/playout/model/implementation/__tests__/PlayoutPartInstanceModelImpl.spec.ts b/packages/job-worker/src/playout/model/implementation/__tests__/PlayoutPartInstanceModelImpl.spec.ts index e90a967e90..fa39b7d5e4 100644 --- a/packages/job-worker/src/playout/model/implementation/__tests__/PlayoutPartInstanceModelImpl.spec.ts +++ b/packages/job-worker/src/playout/model/implementation/__tests__/PlayoutPartInstanceModelImpl.spec.ts @@ -25,7 +25,7 @@ describe('PlayoutPartInstanceModelImpl', () => { externalId: '', title: '', - expectedDurationWithPreroll: undefined, + expectedDurationWithTransition: undefined, }, } } From a16d9777a301a6d7d69ea00be02b70c53cb9bdcc Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Mon, 23 Sep 2024 12:33:17 +0200 Subject: [PATCH 459/479] fix(BucketPanel): Bucket AdLibs don't trigger when created before Rundown activation (SOFIE-3478) --- meteor/client/lib/RenderLimiter.tsx | 21 ------------------- meteor/client/ui/Shelf/BucketPanel.tsx | 2 +- meteor/client/ui/Shelf/DashboardPanel.tsx | 2 +- .../ui/Shelf/Inspector/ShelfInspector.tsx | 3 +-- .../ui/Shelf/TimelineDashboardPanel.tsx | 2 +- 5 files changed, 4 insertions(+), 26 deletions(-) delete mode 100644 meteor/client/lib/RenderLimiter.tsx diff --git a/meteor/client/lib/RenderLimiter.tsx b/meteor/client/lib/RenderLimiter.tsx deleted file mode 100644 index b95ed1ce36..0000000000 --- a/meteor/client/lib/RenderLimiter.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import * as React from 'react' - -type IWrappedComponent = new (props: IProps, state: IState) => React.Component - -export function withRenderLimiter( - shouldComponentUpdate: (currentProps: IProps, nextProps: IProps) => boolean -): ( - WrappedComponent: IWrappedComponent -) => new (props: IProps, context: any) => React.Component { - return (WrappedComponent) => { - return class WithRenderLimiterHOCComponent extends React.Component { - shouldComponentUpdate(nextProps: IProps, _nextState: IState): boolean { - return shouldComponentUpdate(this.props, nextProps) - } - - render(): JSX.Element { - return - } - } - } -} diff --git a/meteor/client/ui/Shelf/BucketPanel.tsx b/meteor/client/ui/Shelf/BucketPanel.tsx index be75b575e9..141bc82806 100644 --- a/meteor/client/ui/Shelf/BucketPanel.tsx +++ b/meteor/client/ui/Shelf/BucketPanel.tsx @@ -380,7 +380,7 @@ export const BucketPanel = React.memo( ) }, (props: IBucketPanelProps, nextProps: IBucketPanelProps) => { - return !_.isEqual(props, nextProps) + return _.isEqual(props, nextProps) } ) diff --git a/meteor/client/ui/Shelf/DashboardPanel.tsx b/meteor/client/ui/Shelf/DashboardPanel.tsx index 89f6589070..363125a038 100644 --- a/meteor/client/ui/Shelf/DashboardPanel.tsx +++ b/meteor/client/ui/Shelf/DashboardPanel.tsx @@ -627,6 +627,6 @@ export const DashboardPanel = React.memo( ) }, (props: IAdLibPanelProps, nextProps: IAdLibPanelProps) => { - return !_.isEqual(props, nextProps) + return _.isEqual(props, nextProps) } ) diff --git a/meteor/client/ui/Shelf/Inspector/ShelfInspector.tsx b/meteor/client/ui/Shelf/Inspector/ShelfInspector.tsx index bc34c096ed..f82cc2f133 100644 --- a/meteor/client/ui/Shelf/Inspector/ShelfInspector.tsx +++ b/meteor/client/ui/Shelf/Inspector/ShelfInspector.tsx @@ -39,7 +39,6 @@ export const ShelfInspector = React.memo( ) }, (prevProps, nextProps) => { - if (_.isEqual(nextProps, prevProps)) return false - return true + return _.isEqual(nextProps, prevProps) } ) diff --git a/meteor/client/ui/Shelf/TimelineDashboardPanel.tsx b/meteor/client/ui/Shelf/TimelineDashboardPanel.tsx index d4441b29a9..2dec1bdbb5 100644 --- a/meteor/client/ui/Shelf/TimelineDashboardPanel.tsx +++ b/meteor/client/ui/Shelf/TimelineDashboardPanel.tsx @@ -32,7 +32,7 @@ export const TimelineDashboardPanel = React.memo( return }, (props: IAdLibPanelProps, nextProps: IAdLibPanelProps) => { - return !_.isEqual(props, nextProps) + return _.isEqual(props, nextProps) } ) From 3e873ee2a3bbfe28c6a3ef759eef350fdbb326eb Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Tue, 24 Sep 2024 15:18:35 +0200 Subject: [PATCH 460/479] chore(release): 1.51.0-in-testing.2 --- meteor/CHANGELOG.md | 11 ++++++ meteor/package.json | 2 +- meteor/yarn.lock | 12 +++--- packages/blueprints-integration/CHANGELOG.md | 8 ++++ packages/blueprints-integration/package.json | 4 +- packages/corelib/package.json | 6 +-- packages/documentation/package.json | 2 +- packages/job-worker/package.json | 8 ++-- packages/lerna.json | 4 +- packages/live-status-gateway/package.json | 10 ++--- packages/mos-gateway/CHANGELOG.md | 8 ++++ packages/mos-gateway/package.json | 6 +-- packages/openapi/package.json | 2 +- packages/playout-gateway/CHANGELOG.md | 8 ++++ packages/playout-gateway/package.json | 6 +-- packages/server-core-integration/CHANGELOG.md | 8 ++++ packages/server-core-integration/package.json | 4 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 38 +++++++++---------- 19 files changed, 96 insertions(+), 53 deletions(-) diff --git a/meteor/CHANGELOG.md b/meteor/CHANGELOG.md index df0291d5b7..31b3937078 100644 --- a/meteor/CHANGELOG.md +++ b/meteor/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.51.0-in-testing.2](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.1...v1.51.0-in-testing.2) (2024-09-24) + + +### Bug Fixes + +* allow replacement in replaceInfinitesFromPreviousPlayhead ([ebb154d](https://github.com/nrkno/sofie-core/commit/ebb154d6b59369588da400d8d921a00e41b84dc8)) +* **BucketPanel:** Bucket AdLibs don't trigger when created before Rundown activation (SOFIE-3478) ([a16d977](https://github.com/nrkno/sofie-core/commit/a16d9777a301a6d7d69ea00be02b70c53cb9bdcc)) +* **LinePartTimeline:** make rules for findMainPiece consistent, make infinite graphics Pieces display correctly ([153d100](https://github.com/nrkno/sofie-core/commit/153d100fb659546201a654af5c566b513951df88)) +* **NoraFloatingInspector:** prevent Segment crash when trying to show a Piece with an invalid Nora `previewPayload` ([4a3a2e7](https://github.com/nrkno/sofie-core/commit/4a3a2e779c144b1c9e88c187cce2e5c80d34626d)) +* resolve an issue with prompter moving when Parts become PartInstances and the prompter position is juuuust right ([a670a73](https://github.com/nrkno/sofie-core/commit/a670a73fa6bfb8331921a2bedd9c927952cfffcf)) + ## [1.51.0-in-testing.0](https://github.com/nrkno/sofie-core/compare/v1.50.4...v1.51.0-in-testing.0) (2024-08-19) diff --git a/meteor/package.json b/meteor/package.json index 644f905f31..34d3e2fe35 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -1,6 +1,6 @@ { "name": "automation-core", - "version": "1.51.0-in-testing.1", + "version": "1.51.0-in-testing.2", "private": true, "engines": { "node": ">=14.19.1" diff --git a/meteor/yarn.lock b/meteor/yarn.lock index f589906d4c..e628496da6 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1321,7 +1321,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/blueprints-integration@portal:../packages/blueprints-integration::locator=automation-core%40workspace%3A." dependencies: - "@sofie-automation/shared-lib": 1.51.0-in-testing.1 + "@sofie-automation/shared-lib": 1.51.0-in-testing.2 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: node @@ -1362,8 +1362,8 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/corelib@portal:../packages/corelib::locator=automation-core%40workspace%3A." dependencies: - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.1 - "@sofie-automation/shared-lib": 1.51.0-in-testing.1 + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.2 + "@sofie-automation/shared-lib": 1.51.0-in-testing.2 fast-clone: ^1.5.13 i18next: ^21.10.0 influx: ^5.9.3 @@ -1394,9 +1394,9 @@ __metadata: resolution: "@sofie-automation/job-worker@portal:../packages/job-worker::locator=automation-core%40workspace%3A." dependencies: "@slack/webhook": ^6.1.0 - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.1 - "@sofie-automation/corelib": 1.51.0-in-testing.1 - "@sofie-automation/shared-lib": 1.51.0-in-testing.1 + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.2 + "@sofie-automation/corelib": 1.51.0-in-testing.2 + "@sofie-automation/shared-lib": 1.51.0-in-testing.2 amqplib: ^0.10.3 deepmerge: ^4.3.1 elastic-apm-node: ^3.51.0 diff --git a/packages/blueprints-integration/CHANGELOG.md b/packages/blueprints-integration/CHANGELOG.md index 7647e8c41b..5115571a60 100644 --- a/packages/blueprints-integration/CHANGELOG.md +++ b/packages/blueprints-integration/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.51.0-in-testing.2](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.1...v1.51.0-in-testing.2) (2024-09-24) + +**Note:** Version bump only for package @sofie-automation/blueprints-integration + + + + + # [1.51.0-in-testing.1](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.0...v1.51.0-in-testing.1) (2024-09-06) diff --git a/packages/blueprints-integration/package.json b/packages/blueprints-integration/package.json index fcefd2ec03..b848b51f74 100644 --- a/packages/blueprints-integration/package.json +++ b/packages/blueprints-integration/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/blueprints-integration", - "version": "1.51.0-in-testing.1", + "version": "1.51.0-in-testing.2", "description": "Library to define the interaction between core and the blueprints.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -38,7 +38,7 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/shared-lib": "1.51.0-in-testing.1", + "@sofie-automation/shared-lib": "1.51.0-in-testing.2", "tslib": "^2.6.2", "type-fest": "^3.13.1" }, diff --git a/packages/corelib/package.json b/packages/corelib/package.json index 0a66bf5919..5b59b27ecc 100644 --- a/packages/corelib/package.json +++ b/packages/corelib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/corelib", - "version": "1.51.0-in-testing.1", + "version": "1.51.0-in-testing.2", "private": true, "description": "Internal library for some types shared by core and workers", "main": "dist/index.js", @@ -39,8 +39,8 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/blueprints-integration": "1.51.0-in-testing.1", - "@sofie-automation/shared-lib": "1.51.0-in-testing.1", + "@sofie-automation/blueprints-integration": "1.51.0-in-testing.2", + "@sofie-automation/shared-lib": "1.51.0-in-testing.2", "fast-clone": "^1.5.13", "i18next": "^21.10.0", "influx": "^5.9.3", diff --git a/packages/documentation/package.json b/packages/documentation/package.json index b986086916..4a3a75ec4b 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -1,6 +1,6 @@ { "name": "sofie-documentation", - "version": "1.51.0-in-testing.1", + "version": "1.51.0-in-testing.2", "private": true, "scripts": { "docusaurus": "docusaurus", diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index febfae4014..d4b2e4b4b3 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/job-worker", - "version": "1.51.0-in-testing.1", + "version": "1.51.0-in-testing.2", "description": "Worker for things", "main": "dist/index.js", "license": "MIT", @@ -41,9 +41,9 @@ ], "dependencies": { "@slack/webhook": "^6.1.0", - "@sofie-automation/blueprints-integration": "1.51.0-in-testing.1", - "@sofie-automation/corelib": "1.51.0-in-testing.1", - "@sofie-automation/shared-lib": "1.51.0-in-testing.1", + "@sofie-automation/blueprints-integration": "1.51.0-in-testing.2", + "@sofie-automation/corelib": "1.51.0-in-testing.2", + "@sofie-automation/shared-lib": "1.51.0-in-testing.2", "amqplib": "^0.10.3", "deepmerge": "^4.3.1", "elastic-apm-node": "^3.51.0", diff --git a/packages/lerna.json b/packages/lerna.json index ab06bef67f..20b18579eb 100644 --- a/packages/lerna.json +++ b/packages/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.51.0-in-testing.1", + "version": "1.51.0-in-testing.2", "npmClient": "yarn", "useWorkspaces": true -} \ No newline at end of file +} diff --git a/packages/live-status-gateway/package.json b/packages/live-status-gateway/package.json index e543275a0d..2e8a241f0e 100644 --- a/packages/live-status-gateway/package.json +++ b/packages/live-status-gateway/package.json @@ -1,6 +1,6 @@ { "name": "live-status-gateway", - "version": "1.51.0-in-testing.1", + "version": "1.51.0-in-testing.2", "private": true, "description": "Provides state from Sofie over sockets", "license": "MIT", @@ -53,10 +53,10 @@ "production" ], "dependencies": { - "@sofie-automation/blueprints-integration": "1.51.0-in-testing.1", - "@sofie-automation/corelib": "1.51.0-in-testing.1", - "@sofie-automation/server-core-integration": "1.51.0-in-testing.1", - "@sofie-automation/shared-lib": "1.51.0-in-testing.1", + "@sofie-automation/blueprints-integration": "1.51.0-in-testing.2", + "@sofie-automation/corelib": "1.51.0-in-testing.2", + "@sofie-automation/server-core-integration": "1.51.0-in-testing.2", + "@sofie-automation/shared-lib": "1.51.0-in-testing.2", "debug": "^4.3.4", "fast-clone": "^1.5.13", "influx": "^5.9.3", diff --git a/packages/mos-gateway/CHANGELOG.md b/packages/mos-gateway/CHANGELOG.md index bd46ea33b2..af8af2bb76 100644 --- a/packages/mos-gateway/CHANGELOG.md +++ b/packages/mos-gateway/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.51.0-in-testing.2](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.1...v1.51.0-in-testing.2) (2024-09-24) + +**Note:** Version bump only for package mos-gateway + + + + + # [1.51.0-in-testing.1](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.0...v1.51.0-in-testing.1) (2024-09-06) diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index 7359ef1c65..64d347f02c 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -1,6 +1,6 @@ { "name": "mos-gateway", - "version": "1.51.0-in-testing.1", + "version": "1.51.0-in-testing.2", "private": true, "description": "MOS-Gateway for the Sofie project", "license": "MIT", @@ -66,8 +66,8 @@ ], "dependencies": { "@mos-connection/connector": "4.1.1", - "@sofie-automation/server-core-integration": "1.51.0-in-testing.1", - "@sofie-automation/shared-lib": "1.51.0-in-testing.1", + "@sofie-automation/server-core-integration": "1.51.0-in-testing.2", + "@sofie-automation/shared-lib": "1.51.0-in-testing.2", "tslib": "^2.6.2", "type-fest": "^3.13.1", "underscore": "^1.13.6", diff --git a/packages/openapi/package.json b/packages/openapi/package.json index dd44d0a6b6..7b4bab25c5 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/openapi", - "version": "1.51.0-in-testing.1", + "version": "1.51.0-in-testing.2", "license": "MIT", "repository": { "type": "git", diff --git a/packages/playout-gateway/CHANGELOG.md b/packages/playout-gateway/CHANGELOG.md index d68d91d33d..4f45f143ee 100644 --- a/packages/playout-gateway/CHANGELOG.md +++ b/packages/playout-gateway/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.51.0-in-testing.2](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.1...v1.51.0-in-testing.2) (2024-09-24) + +**Note:** Version bump only for package playout-gateway + + + + + # [1.51.0-in-testing.1](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.0...v1.51.0-in-testing.1) (2024-09-06) diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index aefe811407..dcc7c39d5b 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -1,6 +1,6 @@ { "name": "playout-gateway", - "version": "1.51.0-in-testing.1", + "version": "1.51.0-in-testing.2", "private": true, "description": "Connect to Core, play stuff", "license": "MIT", @@ -56,8 +56,8 @@ "production" ], "dependencies": { - "@sofie-automation/server-core-integration": "1.51.0-in-testing.1", - "@sofie-automation/shared-lib": "1.51.0-in-testing.1", + "@sofie-automation/server-core-integration": "1.51.0-in-testing.2", + "@sofie-automation/shared-lib": "1.51.0-in-testing.2", "debug": "^4.3.4", "influx": "^5.9.3", "timeline-state-resolver": "9.2.0-alpha.0", diff --git a/packages/server-core-integration/CHANGELOG.md b/packages/server-core-integration/CHANGELOG.md index b0f83d985d..94c6662cee 100644 --- a/packages/server-core-integration/CHANGELOG.md +++ b/packages/server-core-integration/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.51.0-in-testing.2](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.1...v1.51.0-in-testing.2) (2024-09-24) + +**Note:** Version bump only for package @sofie-automation/server-core-integration + + + + + # [1.51.0-in-testing.1](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.0...v1.51.0-in-testing.1) (2024-09-06) **Note:** Version bump only for package @sofie-automation/server-core-integration diff --git a/packages/server-core-integration/package.json b/packages/server-core-integration/package.json index 6060ea3d15..e000abe983 100644 --- a/packages/server-core-integration/package.json +++ b/packages/server-core-integration/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/server-core-integration", - "version": "1.51.0-in-testing.1", + "version": "1.51.0-in-testing.2", "description": "Library for connecting to Core", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -70,7 +70,7 @@ "production" ], "dependencies": { - "@sofie-automation/shared-lib": "1.51.0-in-testing.1", + "@sofie-automation/shared-lib": "1.51.0-in-testing.2", "ejson": "^2.2.3", "eventemitter3": "^4.0.7", "faye-websocket": "^0.11.4", diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 4373cec47d..6681e141cf 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/shared-lib", - "version": "1.51.0-in-testing.1", + "version": "1.51.0-in-testing.2", "description": "Library for types & values shared by core, workers and gateways", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/packages/yarn.lock b/packages/yarn.lock index 599aefa9da..a9171fc4be 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -4565,11 +4565,11 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/blueprints-integration@1.51.0-in-testing.1, @sofie-automation/blueprints-integration@workspace:blueprints-integration": +"@sofie-automation/blueprints-integration@1.51.0-in-testing.2, @sofie-automation/blueprints-integration@workspace:blueprints-integration": version: 0.0.0-use.local resolution: "@sofie-automation/blueprints-integration@workspace:blueprints-integration" dependencies: - "@sofie-automation/shared-lib": 1.51.0-in-testing.1 + "@sofie-automation/shared-lib": 1.51.0-in-testing.2 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: unknown @@ -4606,12 +4606,12 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/corelib@1.51.0-in-testing.1, @sofie-automation/corelib@workspace:corelib": +"@sofie-automation/corelib@1.51.0-in-testing.2, @sofie-automation/corelib@workspace:corelib": version: 0.0.0-use.local resolution: "@sofie-automation/corelib@workspace:corelib" dependencies: - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.1 - "@sofie-automation/shared-lib": 1.51.0-in-testing.1 + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.2 + "@sofie-automation/shared-lib": 1.51.0-in-testing.2 fast-clone: ^1.5.13 i18next: ^21.10.0 influx: ^5.9.3 @@ -4642,9 +4642,9 @@ __metadata: resolution: "@sofie-automation/job-worker@workspace:job-worker" dependencies: "@slack/webhook": ^6.1.0 - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.1 - "@sofie-automation/corelib": 1.51.0-in-testing.1 - "@sofie-automation/shared-lib": 1.51.0-in-testing.1 + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.2 + "@sofie-automation/corelib": 1.51.0-in-testing.2 + "@sofie-automation/shared-lib": 1.51.0-in-testing.2 amqplib: ^0.10.3 deepmerge: ^4.3.1 elastic-apm-node: ^3.51.0 @@ -4674,11 +4674,11 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/server-core-integration@1.51.0-in-testing.1, @sofie-automation/server-core-integration@workspace:server-core-integration": +"@sofie-automation/server-core-integration@1.51.0-in-testing.2, @sofie-automation/server-core-integration@workspace:server-core-integration": version: 0.0.0-use.local resolution: "@sofie-automation/server-core-integration@workspace:server-core-integration" dependencies: - "@sofie-automation/shared-lib": 1.51.0-in-testing.1 + "@sofie-automation/shared-lib": 1.51.0-in-testing.2 ejson: ^2.2.3 eventemitter3: ^4.0.7 faye-websocket: ^0.11.4 @@ -4688,7 +4688,7 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/shared-lib@1.51.0-in-testing.1, @sofie-automation/shared-lib@workspace:shared-lib": +"@sofie-automation/shared-lib@1.51.0-in-testing.2, @sofie-automation/shared-lib@workspace:shared-lib": version: 0.0.0-use.local resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: @@ -15334,10 +15334,10 @@ asn1@evs-broadcast/node-asn1: "@asyncapi/generator": ^1.17.25 "@asyncapi/html-template": ^2.3.9 "@asyncapi/nodejs-ws-template": ^0.9.36 - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.1 - "@sofie-automation/corelib": 1.51.0-in-testing.1 - "@sofie-automation/server-core-integration": 1.51.0-in-testing.1 - "@sofie-automation/shared-lib": 1.51.0-in-testing.1 + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.2 + "@sofie-automation/corelib": 1.51.0-in-testing.2 + "@sofie-automation/server-core-integration": 1.51.0-in-testing.2 + "@sofie-automation/shared-lib": 1.51.0-in-testing.2 debug: ^4.3.4 fast-clone: ^1.5.13 influx: ^5.9.3 @@ -17410,8 +17410,8 @@ asn1@evs-broadcast/node-asn1: resolution: "mos-gateway@workspace:mos-gateway" dependencies: "@mos-connection/connector": 4.1.1 - "@sofie-automation/server-core-integration": 1.51.0-in-testing.1 - "@sofie-automation/shared-lib": 1.51.0-in-testing.1 + "@sofie-automation/server-core-integration": 1.51.0-in-testing.2 + "@sofie-automation/shared-lib": 1.51.0-in-testing.2 tslib: ^2.6.2 type-fest: ^3.13.1 underscore: ^1.13.6 @@ -19397,8 +19397,8 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "playout-gateway@workspace:playout-gateway" dependencies: - "@sofie-automation/server-core-integration": 1.51.0-in-testing.1 - "@sofie-automation/shared-lib": 1.51.0-in-testing.1 + "@sofie-automation/server-core-integration": 1.51.0-in-testing.2 + "@sofie-automation/shared-lib": 1.51.0-in-testing.2 debug: ^4.3.4 influx: ^5.9.3 timeline-state-resolver: 9.2.0-alpha.0 From 94d425a0c90192345288b056915d3b78fdf3fd27 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Wed, 25 Sep 2024 07:40:18 +0200 Subject: [PATCH 461/479] chore: revert changes in PR #1182 due to failing in CI --- .github/workflows/node.yaml | 33 ++++++++++++++------------- .github/workflows/prerelease-libs.yml | 31 +++++++++++++------------ 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index 90038c9cdf..52ec4146bb 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -664,7 +664,7 @@ jobs: else # make dependencies of `determine-npm-tag` available yarn install --mode=skip-build - + cd packages PACKAGE_NAME="@sofie-automation/shared-lib" PUBLISHED_VERSION=$(yarn npm info --json $PACKAGE_NAME | jq -c '.version' -r) @@ -682,21 +682,22 @@ jobs: yarn build env: CI: true - - name: Generate OpenAPI client library - if: ${{ steps.do-publish.outputs.tag }} - uses: hatamiarash7/openapi-generator@v0.3.0 - with: - generator: typescript-fetch - openapi-file: ./packages/openapi/api/actions.yaml - output-dir: ./packages/openapi/client/ts - command-args: -p supportsES6=true - - name: Build OpenAPI client library - if: ${{ steps.do-publish.outputs.tag }} - run: | - cd packages/openapi - yarn build:main - env: - CI: true + # Temporarily disabled due to failing in CI: + # - name: Generate OpenAPI client library + # if: ${{ steps.do-publish.outputs.tag }} + # uses: hatamiarash7/openapi-generator@v0.3.0 + # with: + # generator: typescript-fetch + # openapi-file: ./packages/openapi/api/actions.yaml + # output-dir: ./packages/openapi/client/ts + # command-args: -p supportsES6=true + # - name: Build OpenAPI client library + # if: ${{ steps.do-publish.outputs.tag }} + # run: | + # cd packages/openapi + # yarn build:main + # env: + # CI: true - name: Modify dependencies to use npm packages run: node scripts/prepublish.js - name: Publish to NPM diff --git a/.github/workflows/prerelease-libs.yml b/.github/workflows/prerelease-libs.yml index bbd864e32c..1c5854e605 100644 --- a/.github/workflows/prerelease-libs.yml +++ b/.github/workflows/prerelease-libs.yml @@ -131,21 +131,22 @@ jobs: yarn build env: CI: true - - name: Generate OpenAPI client library - if: ${{ steps.do-publish.outputs.tag }} - uses: hatamiarash7/openapi-generator@v0.3.0 - with: - generator: typescript-fetch - openapi-file: ./packages/openapi/api/actions.yaml - output-dir: ./packages/openapi/client/ts - command-args: -p supportsES6=true - - name: Build OpenAPI client library - if: ${{ steps.do-publish.outputs.tag }} - run: | - cd packages/openapi - yarn build:main - env: - CI: true + # Temporarily disabled due to failing in CI: + # - name: Generate OpenAPI client library + # if: ${{ steps.do-publish.outputs.tag }} + # uses: hatamiarash7/openapi-generator@v0.3.0 + # with: + # generator: typescript-fetch + # openapi-file: ./packages/openapi/api/actions.yaml + # output-dir: ./packages/openapi/client/ts + # command-args: -p supportsES6=true + # - name: Build OpenAPI client library + # if: ${{ steps.do-publish.outputs.tag }} + # run: | + # cd packages/openapi + # yarn build:main + # env: + # CI: true - name: Modify dependencies to use npm packages run: node scripts/prepublish.js - name: Publish to NPM From 79d9b352e952455b734652975f651b96a798fcd4 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Wed, 25 Sep 2024 07:53:28 +0200 Subject: [PATCH 462/479] chore(release): 1.51.0-in-testing.3 --- meteor/CHANGELOG.md | 2 + meteor/package.json | 2 +- meteor/yarn.lock | 12 +++--- packages/blueprints-integration/CHANGELOG.md | 8 ++++ packages/blueprints-integration/package.json | 4 +- packages/corelib/package.json | 6 +-- packages/documentation/package.json | 2 +- packages/job-worker/package.json | 8 ++-- packages/lerna.json | 2 +- packages/live-status-gateway/package.json | 10 ++--- packages/mos-gateway/CHANGELOG.md | 8 ++++ packages/mos-gateway/package.json | 6 +-- packages/openapi/package.json | 2 +- packages/playout-gateway/CHANGELOG.md | 8 ++++ packages/playout-gateway/package.json | 6 +-- packages/server-core-integration/CHANGELOG.md | 8 ++++ packages/server-core-integration/package.json | 4 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 38 +++++++++---------- 19 files changed, 86 insertions(+), 52 deletions(-) diff --git a/meteor/CHANGELOG.md b/meteor/CHANGELOG.md index 31b3937078..7a08165001 100644 --- a/meteor/CHANGELOG.md +++ b/meteor/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.51.0-in-testing.3](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.2...v1.51.0-in-testing.3) (2024-09-25) + ## [1.51.0-in-testing.2](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.1...v1.51.0-in-testing.2) (2024-09-24) diff --git a/meteor/package.json b/meteor/package.json index 34d3e2fe35..c8a40aefa7 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -1,6 +1,6 @@ { "name": "automation-core", - "version": "1.51.0-in-testing.2", + "version": "1.51.0-in-testing.3", "private": true, "engines": { "node": ">=14.19.1" diff --git a/meteor/yarn.lock b/meteor/yarn.lock index e628496da6..e0667868a7 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1321,7 +1321,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/blueprints-integration@portal:../packages/blueprints-integration::locator=automation-core%40workspace%3A." dependencies: - "@sofie-automation/shared-lib": 1.51.0-in-testing.2 + "@sofie-automation/shared-lib": 1.51.0-in-testing.3 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: node @@ -1362,8 +1362,8 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/corelib@portal:../packages/corelib::locator=automation-core%40workspace%3A." dependencies: - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.2 - "@sofie-automation/shared-lib": 1.51.0-in-testing.2 + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.3 + "@sofie-automation/shared-lib": 1.51.0-in-testing.3 fast-clone: ^1.5.13 i18next: ^21.10.0 influx: ^5.9.3 @@ -1394,9 +1394,9 @@ __metadata: resolution: "@sofie-automation/job-worker@portal:../packages/job-worker::locator=automation-core%40workspace%3A." dependencies: "@slack/webhook": ^6.1.0 - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.2 - "@sofie-automation/corelib": 1.51.0-in-testing.2 - "@sofie-automation/shared-lib": 1.51.0-in-testing.2 + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.3 + "@sofie-automation/corelib": 1.51.0-in-testing.3 + "@sofie-automation/shared-lib": 1.51.0-in-testing.3 amqplib: ^0.10.3 deepmerge: ^4.3.1 elastic-apm-node: ^3.51.0 diff --git a/packages/blueprints-integration/CHANGELOG.md b/packages/blueprints-integration/CHANGELOG.md index 5115571a60..7f7d19e7ac 100644 --- a/packages/blueprints-integration/CHANGELOG.md +++ b/packages/blueprints-integration/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.51.0-in-testing.3](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.2...v1.51.0-in-testing.3) (2024-09-25) + +**Note:** Version bump only for package @sofie-automation/blueprints-integration + + + + + # [1.51.0-in-testing.2](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.1...v1.51.0-in-testing.2) (2024-09-24) **Note:** Version bump only for package @sofie-automation/blueprints-integration diff --git a/packages/blueprints-integration/package.json b/packages/blueprints-integration/package.json index b848b51f74..bed9b93615 100644 --- a/packages/blueprints-integration/package.json +++ b/packages/blueprints-integration/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/blueprints-integration", - "version": "1.51.0-in-testing.2", + "version": "1.51.0-in-testing.3", "description": "Library to define the interaction between core and the blueprints.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -38,7 +38,7 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/shared-lib": "1.51.0-in-testing.2", + "@sofie-automation/shared-lib": "1.51.0-in-testing.3", "tslib": "^2.6.2", "type-fest": "^3.13.1" }, diff --git a/packages/corelib/package.json b/packages/corelib/package.json index 5b59b27ecc..f10e62e012 100644 --- a/packages/corelib/package.json +++ b/packages/corelib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/corelib", - "version": "1.51.0-in-testing.2", + "version": "1.51.0-in-testing.3", "private": true, "description": "Internal library for some types shared by core and workers", "main": "dist/index.js", @@ -39,8 +39,8 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/blueprints-integration": "1.51.0-in-testing.2", - "@sofie-automation/shared-lib": "1.51.0-in-testing.2", + "@sofie-automation/blueprints-integration": "1.51.0-in-testing.3", + "@sofie-automation/shared-lib": "1.51.0-in-testing.3", "fast-clone": "^1.5.13", "i18next": "^21.10.0", "influx": "^5.9.3", diff --git a/packages/documentation/package.json b/packages/documentation/package.json index 4a3a75ec4b..402e20b927 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -1,6 +1,6 @@ { "name": "sofie-documentation", - "version": "1.51.0-in-testing.2", + "version": "1.51.0-in-testing.3", "private": true, "scripts": { "docusaurus": "docusaurus", diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index d4b2e4b4b3..dbae10b7ca 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/job-worker", - "version": "1.51.0-in-testing.2", + "version": "1.51.0-in-testing.3", "description": "Worker for things", "main": "dist/index.js", "license": "MIT", @@ -41,9 +41,9 @@ ], "dependencies": { "@slack/webhook": "^6.1.0", - "@sofie-automation/blueprints-integration": "1.51.0-in-testing.2", - "@sofie-automation/corelib": "1.51.0-in-testing.2", - "@sofie-automation/shared-lib": "1.51.0-in-testing.2", + "@sofie-automation/blueprints-integration": "1.51.0-in-testing.3", + "@sofie-automation/corelib": "1.51.0-in-testing.3", + "@sofie-automation/shared-lib": "1.51.0-in-testing.3", "amqplib": "^0.10.3", "deepmerge": "^4.3.1", "elastic-apm-node": "^3.51.0", diff --git a/packages/lerna.json b/packages/lerna.json index 20b18579eb..99d49ac919 100644 --- a/packages/lerna.json +++ b/packages/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.51.0-in-testing.2", + "version": "1.51.0-in-testing.3", "npmClient": "yarn", "useWorkspaces": true } diff --git a/packages/live-status-gateway/package.json b/packages/live-status-gateway/package.json index 2e8a241f0e..36ea25d94d 100644 --- a/packages/live-status-gateway/package.json +++ b/packages/live-status-gateway/package.json @@ -1,6 +1,6 @@ { "name": "live-status-gateway", - "version": "1.51.0-in-testing.2", + "version": "1.51.0-in-testing.3", "private": true, "description": "Provides state from Sofie over sockets", "license": "MIT", @@ -53,10 +53,10 @@ "production" ], "dependencies": { - "@sofie-automation/blueprints-integration": "1.51.0-in-testing.2", - "@sofie-automation/corelib": "1.51.0-in-testing.2", - "@sofie-automation/server-core-integration": "1.51.0-in-testing.2", - "@sofie-automation/shared-lib": "1.51.0-in-testing.2", + "@sofie-automation/blueprints-integration": "1.51.0-in-testing.3", + "@sofie-automation/corelib": "1.51.0-in-testing.3", + "@sofie-automation/server-core-integration": "1.51.0-in-testing.3", + "@sofie-automation/shared-lib": "1.51.0-in-testing.3", "debug": "^4.3.4", "fast-clone": "^1.5.13", "influx": "^5.9.3", diff --git a/packages/mos-gateway/CHANGELOG.md b/packages/mos-gateway/CHANGELOG.md index af8af2bb76..956e4bea81 100644 --- a/packages/mos-gateway/CHANGELOG.md +++ b/packages/mos-gateway/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.51.0-in-testing.3](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.2...v1.51.0-in-testing.3) (2024-09-25) + +**Note:** Version bump only for package mos-gateway + + + + + # [1.51.0-in-testing.2](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.1...v1.51.0-in-testing.2) (2024-09-24) **Note:** Version bump only for package mos-gateway diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index 64d347f02c..4aa3d551dd 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -1,6 +1,6 @@ { "name": "mos-gateway", - "version": "1.51.0-in-testing.2", + "version": "1.51.0-in-testing.3", "private": true, "description": "MOS-Gateway for the Sofie project", "license": "MIT", @@ -66,8 +66,8 @@ ], "dependencies": { "@mos-connection/connector": "4.1.1", - "@sofie-automation/server-core-integration": "1.51.0-in-testing.2", - "@sofie-automation/shared-lib": "1.51.0-in-testing.2", + "@sofie-automation/server-core-integration": "1.51.0-in-testing.3", + "@sofie-automation/shared-lib": "1.51.0-in-testing.3", "tslib": "^2.6.2", "type-fest": "^3.13.1", "underscore": "^1.13.6", diff --git a/packages/openapi/package.json b/packages/openapi/package.json index 7b4bab25c5..e885dc38f5 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/openapi", - "version": "1.51.0-in-testing.2", + "version": "1.51.0-in-testing.3", "license": "MIT", "repository": { "type": "git", diff --git a/packages/playout-gateway/CHANGELOG.md b/packages/playout-gateway/CHANGELOG.md index 4f45f143ee..9dae36de7a 100644 --- a/packages/playout-gateway/CHANGELOG.md +++ b/packages/playout-gateway/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.51.0-in-testing.3](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.2...v1.51.0-in-testing.3) (2024-09-25) + +**Note:** Version bump only for package playout-gateway + + + + + # [1.51.0-in-testing.2](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.1...v1.51.0-in-testing.2) (2024-09-24) **Note:** Version bump only for package playout-gateway diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index dcc7c39d5b..71c56cac17 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -1,6 +1,6 @@ { "name": "playout-gateway", - "version": "1.51.0-in-testing.2", + "version": "1.51.0-in-testing.3", "private": true, "description": "Connect to Core, play stuff", "license": "MIT", @@ -56,8 +56,8 @@ "production" ], "dependencies": { - "@sofie-automation/server-core-integration": "1.51.0-in-testing.2", - "@sofie-automation/shared-lib": "1.51.0-in-testing.2", + "@sofie-automation/server-core-integration": "1.51.0-in-testing.3", + "@sofie-automation/shared-lib": "1.51.0-in-testing.3", "debug": "^4.3.4", "influx": "^5.9.3", "timeline-state-resolver": "9.2.0-alpha.0", diff --git a/packages/server-core-integration/CHANGELOG.md b/packages/server-core-integration/CHANGELOG.md index 94c6662cee..46ff10a210 100644 --- a/packages/server-core-integration/CHANGELOG.md +++ b/packages/server-core-integration/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.51.0-in-testing.3](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.2...v1.51.0-in-testing.3) (2024-09-25) + +**Note:** Version bump only for package @sofie-automation/server-core-integration + + + + + # [1.51.0-in-testing.2](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.1...v1.51.0-in-testing.2) (2024-09-24) **Note:** Version bump only for package @sofie-automation/server-core-integration diff --git a/packages/server-core-integration/package.json b/packages/server-core-integration/package.json index e000abe983..90c8dc9d62 100644 --- a/packages/server-core-integration/package.json +++ b/packages/server-core-integration/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/server-core-integration", - "version": "1.51.0-in-testing.2", + "version": "1.51.0-in-testing.3", "description": "Library for connecting to Core", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -70,7 +70,7 @@ "production" ], "dependencies": { - "@sofie-automation/shared-lib": "1.51.0-in-testing.2", + "@sofie-automation/shared-lib": "1.51.0-in-testing.3", "ejson": "^2.2.3", "eventemitter3": "^4.0.7", "faye-websocket": "^0.11.4", diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 6681e141cf..b88ca025f9 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/shared-lib", - "version": "1.51.0-in-testing.2", + "version": "1.51.0-in-testing.3", "description": "Library for types & values shared by core, workers and gateways", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/packages/yarn.lock b/packages/yarn.lock index a9171fc4be..4627a0435e 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -4565,11 +4565,11 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/blueprints-integration@1.51.0-in-testing.2, @sofie-automation/blueprints-integration@workspace:blueprints-integration": +"@sofie-automation/blueprints-integration@1.51.0-in-testing.3, @sofie-automation/blueprints-integration@workspace:blueprints-integration": version: 0.0.0-use.local resolution: "@sofie-automation/blueprints-integration@workspace:blueprints-integration" dependencies: - "@sofie-automation/shared-lib": 1.51.0-in-testing.2 + "@sofie-automation/shared-lib": 1.51.0-in-testing.3 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: unknown @@ -4606,12 +4606,12 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/corelib@1.51.0-in-testing.2, @sofie-automation/corelib@workspace:corelib": +"@sofie-automation/corelib@1.51.0-in-testing.3, @sofie-automation/corelib@workspace:corelib": version: 0.0.0-use.local resolution: "@sofie-automation/corelib@workspace:corelib" dependencies: - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.2 - "@sofie-automation/shared-lib": 1.51.0-in-testing.2 + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.3 + "@sofie-automation/shared-lib": 1.51.0-in-testing.3 fast-clone: ^1.5.13 i18next: ^21.10.0 influx: ^5.9.3 @@ -4642,9 +4642,9 @@ __metadata: resolution: "@sofie-automation/job-worker@workspace:job-worker" dependencies: "@slack/webhook": ^6.1.0 - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.2 - "@sofie-automation/corelib": 1.51.0-in-testing.2 - "@sofie-automation/shared-lib": 1.51.0-in-testing.2 + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.3 + "@sofie-automation/corelib": 1.51.0-in-testing.3 + "@sofie-automation/shared-lib": 1.51.0-in-testing.3 amqplib: ^0.10.3 deepmerge: ^4.3.1 elastic-apm-node: ^3.51.0 @@ -4674,11 +4674,11 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/server-core-integration@1.51.0-in-testing.2, @sofie-automation/server-core-integration@workspace:server-core-integration": +"@sofie-automation/server-core-integration@1.51.0-in-testing.3, @sofie-automation/server-core-integration@workspace:server-core-integration": version: 0.0.0-use.local resolution: "@sofie-automation/server-core-integration@workspace:server-core-integration" dependencies: - "@sofie-automation/shared-lib": 1.51.0-in-testing.2 + "@sofie-automation/shared-lib": 1.51.0-in-testing.3 ejson: ^2.2.3 eventemitter3: ^4.0.7 faye-websocket: ^0.11.4 @@ -4688,7 +4688,7 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/shared-lib@1.51.0-in-testing.2, @sofie-automation/shared-lib@workspace:shared-lib": +"@sofie-automation/shared-lib@1.51.0-in-testing.3, @sofie-automation/shared-lib@workspace:shared-lib": version: 0.0.0-use.local resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: @@ -15334,10 +15334,10 @@ asn1@evs-broadcast/node-asn1: "@asyncapi/generator": ^1.17.25 "@asyncapi/html-template": ^2.3.9 "@asyncapi/nodejs-ws-template": ^0.9.36 - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.2 - "@sofie-automation/corelib": 1.51.0-in-testing.2 - "@sofie-automation/server-core-integration": 1.51.0-in-testing.2 - "@sofie-automation/shared-lib": 1.51.0-in-testing.2 + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.3 + "@sofie-automation/corelib": 1.51.0-in-testing.3 + "@sofie-automation/server-core-integration": 1.51.0-in-testing.3 + "@sofie-automation/shared-lib": 1.51.0-in-testing.3 debug: ^4.3.4 fast-clone: ^1.5.13 influx: ^5.9.3 @@ -17410,8 +17410,8 @@ asn1@evs-broadcast/node-asn1: resolution: "mos-gateway@workspace:mos-gateway" dependencies: "@mos-connection/connector": 4.1.1 - "@sofie-automation/server-core-integration": 1.51.0-in-testing.2 - "@sofie-automation/shared-lib": 1.51.0-in-testing.2 + "@sofie-automation/server-core-integration": 1.51.0-in-testing.3 + "@sofie-automation/shared-lib": 1.51.0-in-testing.3 tslib: ^2.6.2 type-fest: ^3.13.1 underscore: ^1.13.6 @@ -19397,8 +19397,8 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "playout-gateway@workspace:playout-gateway" dependencies: - "@sofie-automation/server-core-integration": 1.51.0-in-testing.2 - "@sofie-automation/shared-lib": 1.51.0-in-testing.2 + "@sofie-automation/server-core-integration": 1.51.0-in-testing.3 + "@sofie-automation/shared-lib": 1.51.0-in-testing.3 debug: ^4.3.4 influx: ^5.9.3 timeline-state-resolver: 9.2.0-alpha.0 From 7cf6470bcae95bf19eebb81ef1b7bac0ee271a06 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Wed, 25 Sep 2024 10:23:08 +0200 Subject: [PATCH 463/479] re-enable build step for openapi for test and publishing (#1265) * chore(ci): Add build step for openapi for test and publishing * chore: fix ci * chore(ci): openapi: re-add gendocs and genserver to CI --- .github/workflows/node.yaml | 33 ++++++++++++--------------- .github/workflows/prerelease-libs.yml | 24 +++++++------------ 2 files changed, 22 insertions(+), 35 deletions(-) diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index 52ec4146bb..e04f4aec23 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -569,13 +569,17 @@ jobs: yarn env: CI: true - - name: Run generator + - name: Build OpenAPI client library + run: | + cd packages/openapi + yarn build + env: + CI: true + - name: Generate OpenAPI docs and server run: | cd packages/openapi - yarn gendocs yarn genserver - yarn genclient:ts env: CI: true @@ -682,22 +686,13 @@ jobs: yarn build env: CI: true - # Temporarily disabled due to failing in CI: - # - name: Generate OpenAPI client library - # if: ${{ steps.do-publish.outputs.tag }} - # uses: hatamiarash7/openapi-generator@v0.3.0 - # with: - # generator: typescript-fetch - # openapi-file: ./packages/openapi/api/actions.yaml - # output-dir: ./packages/openapi/client/ts - # command-args: -p supportsES6=true - # - name: Build OpenAPI client library - # if: ${{ steps.do-publish.outputs.tag }} - # run: | - # cd packages/openapi - # yarn build:main - # env: - # CI: true + - name: Build OpenAPI client library + if: ${{ steps.do-publish.outputs.tag }} + run: | + cd packages/openapi + yarn build + env: + CI: true - name: Modify dependencies to use npm packages run: node scripts/prepublish.js - name: Publish to NPM diff --git a/.github/workflows/prerelease-libs.yml b/.github/workflows/prerelease-libs.yml index 1c5854e605..af155a8cce 100644 --- a/.github/workflows/prerelease-libs.yml +++ b/.github/workflows/prerelease-libs.yml @@ -131,22 +131,14 @@ jobs: yarn build env: CI: true - # Temporarily disabled due to failing in CI: - # - name: Generate OpenAPI client library - # if: ${{ steps.do-publish.outputs.tag }} - # uses: hatamiarash7/openapi-generator@v0.3.0 - # with: - # generator: typescript-fetch - # openapi-file: ./packages/openapi/api/actions.yaml - # output-dir: ./packages/openapi/client/ts - # command-args: -p supportsES6=true - # - name: Build OpenAPI client library - # if: ${{ steps.do-publish.outputs.tag }} - # run: | - # cd packages/openapi - # yarn build:main - # env: - # CI: true + + - name: Build OpenAPI client library + if: ${{ steps.do-publish.outputs.publish }} + run: | + cd packages/openapi + yarn build + env: + CI: true - name: Modify dependencies to use npm packages run: node scripts/prepublish.js - name: Publish to NPM From 31d94a8adf4b250d8df4129fa026f5df569c4f46 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 25 Sep 2024 12:00:28 +0200 Subject: [PATCH 464/479] fix(BucketPieceButton): doesn't show media status of Bucket Adlibs --- meteor/client/ui/Shelf/BucketPieceButton.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meteor/client/ui/Shelf/BucketPieceButton.tsx b/meteor/client/ui/Shelf/BucketPieceButton.tsx index 299d4e0981..80319dae20 100644 --- a/meteor/client/ui/Shelf/BucketPieceButton.tsx +++ b/meteor/client/ui/Shelf/BucketPieceButton.tsx @@ -12,7 +12,7 @@ import { } from 'react-dnd' import { DragDropItemTypes } from '../DragDropItemTypes' import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' -import { useContentStatusForAdlibPiece } from '../SegmentTimeline/withMediaObjectStatus' +import { useContentStatusForItem } from '../SegmentTimeline/withMediaObjectStatus' import { BucketAdLibActionUi, BucketAdLibItem } from './RundownViewBuckets' import { IBlueprintActionTriggerMode } from '@sofie-automation/blueprints-integration' import { BucketId, PieceId } from '@sofie-automation/corelib/dist/dataModel/Ids' @@ -60,7 +60,7 @@ class BucketPieceButtonBase extends DashboardPieceButtonBase & BucketPieceButtonBaseProps ): JSX.Element { - const contentStatus = useContentStatusForAdlibPiece(props.piece) + const contentStatus = useContentStatusForItem(props.piece) const [, connectDropTarget] = useDrop({ accept: DragDropItemTypes.BUCKET_ADLIB_PIECE, From 7fe60eab0a39907575ad05eabd2c1d45f01d0d99 Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Wed, 25 Sep 2024 12:39:35 +0000 Subject: [PATCH 465/479] fix: currentPart timeline dur respects postroll in autonext --- packages/job-worker/src/playout/timeline/piece.ts | 2 +- packages/job-worker/src/playout/timeline/rundown.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/job-worker/src/playout/timeline/piece.ts b/packages/job-worker/src/playout/timeline/piece.ts index 9e011a3348..861946f01d 100644 --- a/packages/job-worker/src/playout/timeline/piece.ts +++ b/packages/job-worker/src/playout/timeline/piece.ts @@ -106,7 +106,7 @@ export function getPieceEnableInsidePart( if (partTimings.toPartPostroll) { if (!pieceEnable.duration) { // make sure that the control object is shortened correctly - pieceEnable.duration = `#${partGroupId} - ${partTimings.toPartPostroll}` + pieceEnable.end = `#${partGroupId} - ${partTimings.toPartPostroll}` } } diff --git a/packages/job-worker/src/playout/timeline/rundown.ts b/packages/job-worker/src/playout/timeline/rundown.ts index d8ee36ab7d..27299011a1 100644 --- a/packages/job-worker/src/playout/timeline/rundown.ts +++ b/packages/job-worker/src/playout/timeline/rundown.ts @@ -146,7 +146,8 @@ export function buildTimelineObjsForRundown( // If there is a valid autonext out of the current part, then calculate the duration currentPartEnable.duration = partInstancesInfo.current.partInstance.part.expectedDuration + - partInstancesInfo.current.calculatedTimings.toPartDelay + partInstancesInfo.current.calculatedTimings.toPartDelay + + partInstancesInfo.current.calculatedTimings.toPartPostroll // autonext should have the postroll added to it to not confuse the timeline } const currentPartGroup = createPartGroup(partInstancesInfo.current.partInstance, currentPartEnable) From ced0e11fbe895ae3a0c00e501d5d8dd447f84a33 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 26 Sep 2024 17:35:01 +0100 Subject: [PATCH 466/479] fix: clear pieces with fixed duration --- packages/job-worker/src/playout/adlibUtils.ts | 108 ++++++++++-------- 1 file changed, 58 insertions(+), 50 deletions(-) diff --git a/packages/job-worker/src/playout/adlibUtils.ts b/packages/job-worker/src/playout/adlibUtils.ts index 1aea41cc52..b883369644 100644 --- a/packages/job-worker/src/playout/adlibUtils.ts +++ b/packages/job-worker/src/playout/adlibUtils.ts @@ -274,61 +274,69 @@ export function innerStopPieces( for (const resolvedPieceInstance of resolvedPieces) { const pieceInstance = resolvedPieceInstance.instance - if ( - !pieceInstance.userDuration && - !pieceInstance.piece.virtual && - filter(pieceInstance) && - resolvedPieceInstance.resolvedStart !== undefined && - resolvedPieceInstance.resolvedStart <= relativeStopAt && - !pieceInstance.plannedStoppedPlayback - ) { - switch (pieceInstance.piece.lifespan) { - case PieceLifespan.WithinPart: - case PieceLifespan.OutOnSegmentChange: - case PieceLifespan.OutOnRundownChange: { - logger.info(`Blueprint action: Cropping PieceInstance "${pieceInstance._id}" to ${stopAt}`) - - const pieceInstanceModel = playoutModel.findPieceInstance(pieceInstance._id) - if (pieceInstanceModel) { - const newDuration: Required['userDuration'] = playoutModel.isMultiGatewayMode - ? { - endRelativeToNow: offsetRelativeToNow, - } - : { - endRelativeToPart: relativeStopAt, - } - - pieceInstanceModel.pieceInstance.setDuration(newDuration) - - stoppedInstances.push(pieceInstance._id) - } else { - logger.warn( - `Blueprint action: Failed to crop PieceInstance "${pieceInstance._id}", it was not found` - ) - } - - break - } - case PieceLifespan.OutOnSegmentEnd: - case PieceLifespan.OutOnRundownEnd: - case PieceLifespan.OutOnShowStyleEnd: { - logger.info( - `Blueprint action: Cropping PieceInstance "${pieceInstance._id}" to ${stopAt} with a virtual` - ) - currentPartInstance.insertVirtualPiece( - relativeStopAt, - pieceInstance.piece.lifespan, - pieceInstance.piece.sourceLayerId, - pieceInstance.piece.outputLayerId - ) + // Virtual pieces aren't allowed a timed end + if (pieceInstance.piece.virtual) continue + + // Check if piece has already had an end defined + if (pieceInstance.userDuration) continue + + // Caller can filter out pieces + if (!filter(pieceInstance)) continue + + // Check if piece has started yet + if (resolvedPieceInstance.resolvedStart == undefined || resolvedPieceInstance.resolvedStart > relativeStopAt) + continue + + // If there end time of the piece is already known, make sure it is in the future + if (pieceInstance.plannedStoppedPlayback && pieceInstance.plannedStoppedPlayback <= stopAt) continue + + switch (pieceInstance.piece.lifespan) { + case PieceLifespan.WithinPart: + case PieceLifespan.OutOnSegmentChange: + case PieceLifespan.OutOnRundownChange: { + logger.info(`Blueprint action: Cropping PieceInstance "${pieceInstance._id}" to ${stopAt}`) + + const pieceInstanceModel = playoutModel.findPieceInstance(pieceInstance._id) + if (pieceInstanceModel) { + const newDuration: Required['userDuration'] = playoutModel.isMultiGatewayMode + ? { + endRelativeToNow: offsetRelativeToNow, + } + : { + endRelativeToPart: relativeStopAt, + } + + pieceInstanceModel.pieceInstance.setDuration(newDuration) stoppedInstances.push(pieceInstance._id) - break + } else { + logger.warn( + `Blueprint action: Failed to crop PieceInstance "${pieceInstance._id}", it was not found` + ) } - default: - assertNever(pieceInstance.piece.lifespan) + + break + } + case PieceLifespan.OutOnSegmentEnd: + case PieceLifespan.OutOnRundownEnd: + case PieceLifespan.OutOnShowStyleEnd: { + logger.info( + `Blueprint action: Cropping PieceInstance "${pieceInstance._id}" to ${stopAt} with a virtual` + ) + + currentPartInstance.insertVirtualPiece( + relativeStopAt, + pieceInstance.piece.lifespan, + pieceInstance.piece.sourceLayerId, + pieceInstance.piece.outputLayerId + ) + + stoppedInstances.push(pieceInstance._id) + break } + default: + assertNever(pieceInstance.piece.lifespan) } } From 82ba095a317a5ce8fd703e16198c835e771c39fc Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 2 Oct 2024 13:27:30 +0200 Subject: [PATCH 467/479] fix(LoadPlayoutModel): rundowns are not sorted in order, which can cause unusual effects --- packages/corelib/src/playout/playlist.ts | 20 +++++++++++++++++++ .../model/implementation/LoadPlayoutModel.ts | 3 ++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/corelib/src/playout/playlist.ts b/packages/corelib/src/playout/playlist.ts index dd53fa50f6..622ce56851 100644 --- a/packages/corelib/src/playout/playlist.ts +++ b/packages/corelib/src/playout/playlist.ts @@ -1,3 +1,4 @@ +import { DBRundown } from '../dataModel/Rundown' import { DBSegment } from '../dataModel/Segment' import { DBPart } from '../dataModel/Part' import { DBPartInstance } from '../dataModel/PartInstance' @@ -89,3 +90,22 @@ export function sortRundownIDsInPlaylist( return [...sortedVerifiedExisting, ...missingIds] } + +export function sortRundownsWithinPlaylist( + sortedPossibleIds: ReadonlyDeep, + unsortedRundowns: DBRundown[] +): DBRundown[] { + return unsortedRundowns.slice().sort((a, b) => { + const indexA = sortedPossibleIds.indexOf(a._id) + const indexB = sortedPossibleIds.indexOf(b._id) + if (indexA === -1 && indexB === -1) { + return a._id.toString().localeCompare(b._id.toString()) + } else if (indexA === -1) { + return -1 + } else if (indexB === -1) { + return 1 + } + + return indexA - indexB + }) +} diff --git a/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts b/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts index 769c911a7e..64d0bcde55 100644 --- a/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts +++ b/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts @@ -22,6 +22,7 @@ import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/Perip import { PlayoutModel, PlayoutModelPreInit } from '../PlayoutModel' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { RundownBaselineObj } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineObj' +import { sortRundownsWithinPlaylist } from '@sofie-automation/corelib/dist/playout/playlist' /** * Load a PlayoutModelPreInit for the given RundownPlaylist @@ -59,7 +60,7 @@ export async function loadPlayoutModelPreInit( peripheralDevices: PeripheralDevices, playlist: Playlist, - rundowns: Rundowns, + rundowns: sortRundownsWithinPlaylist(Playlist.rundownIdsInOrder, Rundowns), getRundown: (id: RundownId) => Rundowns.find((rd) => rd._id === id), } From ea0b907ea9b1f2b8056d79b7784c850957385fee Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Wed, 2 Oct 2024 08:51:35 +0000 Subject: [PATCH 468/479] fix(ui): postroll timing from previous part --- packages/corelib/src/playout/timings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/corelib/src/playout/timings.ts b/packages/corelib/src/playout/timings.ts index e79e60fde9..abdd6edd93 100644 --- a/packages/corelib/src/playout/timings.ts +++ b/packages/corelib/src/playout/timings.ts @@ -159,7 +159,7 @@ export function getPartTimingsOrDefaults( function calculateExpectedDurationWithTransition(rawDuration: number, timings: PartCalculatedTimings): number { // toPartDelay needs to be subtracted, because it is added to `fromPartRemaining` when the `fromPartRemaining` value is calculated. - return Math.max(0, rawDuration - (timings.fromPartRemaining - timings.toPartDelay)) + return Math.max(0, rawDuration - (timings.fromPartRemaining - timings.toPartDelay - timings.fromPartPostroll)) } export type CalculateExpectedDurationPart = Pick From e72c5ae2a7ba17f7846ea7fc1932a16e9a414f24 Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Wed, 2 Oct 2024 08:51:53 +0000 Subject: [PATCH 469/479] fix(ui): autonext timing --- meteor/client/lib/rundownTiming.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/meteor/client/lib/rundownTiming.ts b/meteor/client/lib/rundownTiming.ts index 874915b6fc..43f9f8396e 100644 --- a/meteor/client/lib/rundownTiming.ts +++ b/meteor/client/lib/rundownTiming.ts @@ -219,7 +219,11 @@ export class RundownTimingCalculator { totalRundownDuration += calculatePartInstanceExpectedDurationWithTransition(partInstance) || 0 } - const lastStartedPlayback = partInstance.timings?.plannedStartedPlayback + // note: lastStartedPlayback that lies in the future means it hasn't started yet (like from autonext) + const lastStartedPlayback = + (partInstance.timings?.plannedStartedPlayback ?? 0) <= now + ? partInstance.timings?.plannedStartedPlayback + : undefined const playOffset = partInstance.timings?.playOffset || 0 let partDuration = 0 From 7ab01b3ceb7de53aecfca2588f1b60dd87b21419 Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Wed, 2 Oct 2024 13:11:50 +0000 Subject: [PATCH 470/479] chore: add unit tests for rundownTiming --- .../lib/__tests__/rundownTiming.test.ts | 283 ++++++++++++++++++ 1 file changed, 283 insertions(+) diff --git a/meteor/client/lib/__tests__/rundownTiming.test.ts b/meteor/client/lib/__tests__/rundownTiming.test.ts index a2b258d32a..b9e9e963d5 100644 --- a/meteor/client/lib/__tests__/rundownTiming.test.ts +++ b/meteor/client/lib/__tests__/rundownTiming.test.ts @@ -1262,6 +1262,289 @@ describe('rundown Timing Calculator', () => { ) }) + it('Handles part with autonext', () => { + const timing = new RundownTimingCalculator() + const playlist: DBRundownPlaylist = makeMockPlaylist() + playlist.timing = { + type: 'forward-time' as any, + expectedStart: 0, + expectedDuration: 40000, + } + const rundownId1 = 'rundown1' + const segmentId1 = 'segment1' + const segmentId2 = 'segment2' + const segmentsMap: Map = new Map() + segmentsMap.set(protectString(segmentId1), makeMockSegment(segmentId1, 0, rundownId1)) + segmentsMap.set(protectString(segmentId2), makeMockSegment(segmentId2, 0, rundownId1)) + const parts: DBPart[] = [] + parts.push( + makeMockPart('part1', 0, rundownId1, segmentId1, { + budgetDuration: 2000, + expectedDuration: 1000, + }) + ) + parts.push( + makeMockPart('part2', 0, rundownId1, segmentId1, { + budgetDuration: 3000, + expectedDuration: 1000, + }) + ) + parts.push( + makeMockPart('part3', 0, rundownId1, segmentId2, { + budgetDuration: 3000, + expectedDuration: 1000, + }) + ) + parts.push(makeMockPart('part4', 0, rundownId1, segmentId2, { expectedDuration: 1000 })) + // set autonext and create partInstances + parts[0].autoNext = true + const partInstance1 = wrapPartToTemporaryInstance(protectString(''), parts[0]) + partInstance1.isTemporary = false + partInstance1.timings = { + plannedStartedPlayback: 0, + } + const partInstance2 = wrapPartToTemporaryInstance(protectString(''), parts[1]) + partInstance2.isTemporary = false + partInstance2.timings = { + plannedStartedPlayback: 1000, // start after part1's expectedDuration + } + const partInstances = [partInstance1, partInstance2, ...convertPartsToPartInstances([parts[2], parts[3]])] + const partInstancesMap: Map = new Map() + const rundown = makeMockRundown(rundownId1, playlist) + const rundowns = [rundown] + // at t = 0 + const result = timing.updateDurations( + 0, + false, + playlist, + rundowns, + undefined, + partInstances, + partInstancesMap, + segmentsMap, + DEFAULT_DURATION, + [] + ) + expect(result).toEqual( + literal({ + currentPartInstanceId: null, + isLowResolution: false, + asDisplayedPlaylistDuration: 4000, + asPlayedPlaylistDuration: 8000, + currentPartWillAutoNext: false, + currentTime: 0, + rundownExpectedDurations: { + [rundownId1]: 4000, + }, + rundownAsPlayedDurations: { + [rundownId1]: 8000, + }, + partCountdown: { + part1: 0, + part2: 1000, + part3: 5000, + part4: 6000, + }, + partDisplayDurations: { + part1_tmp_instance: 1000, + part2_tmp_instance: 1000, + part3: 1000, + part4: 1000, + }, + partDisplayStartsAt: { + part1_tmp_instance: 0, + part2_tmp_instance: 1000, + part3: 2000, + part4: 3000, + }, + partDurations: { + part1_tmp_instance: 1000, + part2_tmp_instance: 1000, + part3: 1000, + part4: 1000, + }, + partExpectedDurations: { + part1_tmp_instance: 1000, + part2_tmp_instance: 1000, + part3: 1000, + part4: 1000, + }, + partPlayed: { + part1_tmp_instance: 0, + part2_tmp_instance: 0, + part3: 0, + part4: 0, + }, + partStartsAt: { + part1_tmp_instance: 0, + part2_tmp_instance: 1000, + part3: 2000, + part4: 3000, + }, + remainingPlaylistDuration: 8000, + totalPlaylistDuration: 8000, + breakIsLastRundown: undefined, + remainingTimeOnCurrentPart: undefined, + rundownsBeforeNextBreak: undefined, + segmentBudgetDurations: { + [segmentId1]: 5000, + [segmentId2]: 3000, + }, + segmentStartedPlayback: {}, + }) + ) + }) + + it('Handles part with postroll', () => { + const timing = new RundownTimingCalculator() + const playlist: DBRundownPlaylist = makeMockPlaylist() + playlist.timing = { + type: 'forward-time' as any, + expectedStart: 0, + expectedDuration: 40000, + } + const rundownId1 = 'rundown1' + const segmentId1 = 'segment1' + const segmentId2 = 'segment2' + const segmentsMap: Map = new Map() + segmentsMap.set(protectString(segmentId1), makeMockSegment(segmentId1, 0, rundownId1)) + segmentsMap.set(protectString(segmentId2), makeMockSegment(segmentId2, 0, rundownId1)) + const parts: DBPart[] = [] + parts.push( + makeMockPart('part1', 0, rundownId1, segmentId1, { + budgetDuration: 2000, + expectedDuration: 2000, + }) + ) + parts.push( + makeMockPart('part2', 0, rundownId1, segmentId1, { + budgetDuration: 3000, + expectedDuration: 2000, + }) + ) + parts.push( + makeMockPart('part3', 0, rundownId1, segmentId2, { + budgetDuration: 3000, + expectedDuration: 1000, + }) + ) + parts.push(makeMockPart('part4', 0, rundownId1, segmentId2, { expectedDuration: 1000 })) + // set autonext and create partInstances + parts[0].autoNext = true + const partInstance1 = wrapPartToTemporaryInstance(protectString(''), parts[0]) + partInstance1.isTemporary = false + partInstance1.timings = { + plannedStartedPlayback: 0, + reportedStartedPlayback: 0, + reportedStoppedPlayback: 2000, + } + partInstance1.partPlayoutTimings = { + inTransitionStart: 0, + toPartDelay: 0, + toPartPostroll: 500, + fromPartRemaining: 0, + fromPartPostroll: 0, + } + const partInstance2 = wrapPartToTemporaryInstance(protectString(''), parts[1]) + partInstance2.isTemporary = false + partInstance2.timings = { + plannedStartedPlayback: 2000, // start after part1's expectedDuration + reportedStartedPlayback: 2000, + } + partInstance2.partPlayoutTimings = { + inTransitionStart: 0, + toPartDelay: 0, + toPartPostroll: 0, + fromPartRemaining: 500, + fromPartPostroll: 500, + } + const partInstances = [partInstance1, partInstance2, ...convertPartsToPartInstances([parts[2], parts[3]])] + const partInstancesMap: Map = new Map() + const rundown = makeMockRundown(rundownId1, playlist) + const rundowns = [rundown] + // at t = 0 + const result = timing.updateDurations( + 3000, + false, + playlist, + rundowns, + undefined, + partInstances, + partInstancesMap, + segmentsMap, + DEFAULT_DURATION, + [] + ) + expect(result).toEqual( + literal({ + currentPartInstanceId: null, + isLowResolution: false, + asDisplayedPlaylistDuration: 6000, + asPlayedPlaylistDuration: 8000, + currentPartWillAutoNext: false, + currentTime: 3000, + rundownExpectedDurations: { + [rundownId1]: 6000, + }, + rundownAsPlayedDurations: { + [rundownId1]: 8000, + }, + partCountdown: { + part1: 4000, + part2: 6000, + part3: 6000, + part4: 7000, + }, + partDisplayDurations: { + part1_tmp_instance: 2000, + part2_tmp_instance: 2000, + part3: 1000, + part4: 1000, + }, + partDisplayStartsAt: { + part1_tmp_instance: 0, + part2_tmp_instance: 2000, + part3: 4000, + part4: 5000, + }, + partDurations: { + part1_tmp_instance: 2000, + part2_tmp_instance: 2000, + part3: 1000, + part4: 1000, + }, + partExpectedDurations: { + part1_tmp_instance: 2000, + part2_tmp_instance: 2000, + part3: 1000, + part4: 1000, + }, + partPlayed: { + part1_tmp_instance: 0, + part2_tmp_instance: 1000, + part3: 0, + part4: 0, + }, + partStartsAt: { + part1_tmp_instance: 0, + part2_tmp_instance: 2000, + part3: 4000, + part4: 5000, + }, + remainingPlaylistDuration: 8000, + totalPlaylistDuration: 8000, + breakIsLastRundown: undefined, + remainingTimeOnCurrentPart: undefined, + rundownsBeforeNextBreak: undefined, + segmentBudgetDurations: { + [segmentId1]: 5000, + [segmentId2]: 3000, + }, + segmentStartedPlayback: {}, + }) + ) + }) + it('Back-time: Can find the next expectedStart rundown anchor when it is in a future segment', () => { const timing = new RundownTimingCalculator() const playlist: DBRundownPlaylist = makeMockPlaylist() From 89df068525dadc37e31294a1bfe1b1f11e05e210 Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Wed, 2 Oct 2024 13:15:07 +0000 Subject: [PATCH 471/479] chore: update comment --- packages/corelib/src/playout/timings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/corelib/src/playout/timings.ts b/packages/corelib/src/playout/timings.ts index abdd6edd93..77209d90e7 100644 --- a/packages/corelib/src/playout/timings.ts +++ b/packages/corelib/src/playout/timings.ts @@ -158,7 +158,7 @@ export function getPartTimingsOrDefaults( } function calculateExpectedDurationWithTransition(rawDuration: number, timings: PartCalculatedTimings): number { - // toPartDelay needs to be subtracted, because it is added to `fromPartRemaining` when the `fromPartRemaining` value is calculated. + // toPartDelay and fromPartPostroll needs to be subtracted, because it is added to `fromPartRemaining` when the `fromPartRemaining` value is calculated. return Math.max(0, rawDuration - (timings.fromPartRemaining - timings.toPartDelay - timings.fromPartPostroll)) } From 3c0d3d5322508546af4fb6e5960239d8b6beb6fe Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 2 Oct 2024 16:31:31 +0200 Subject: [PATCH 472/479] chore: add tests --- packages/corelib/src/playout/playlist.ts | 4 +- .../model/implementation/LoadPlayoutModel.ts | 1 + .../__tests__/LoadPlayoutModel.spec.ts | 97 +++++++++++++++++++ 3 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 packages/job-worker/src/playout/model/implementation/__tests__/LoadPlayoutModel.spec.ts diff --git a/packages/corelib/src/playout/playlist.ts b/packages/corelib/src/playout/playlist.ts index 622ce56851..28c780e389 100644 --- a/packages/corelib/src/playout/playlist.ts +++ b/packages/corelib/src/playout/playlist.ts @@ -101,9 +101,9 @@ export function sortRundownsWithinPlaylist( if (indexA === -1 && indexB === -1) { return a._id.toString().localeCompare(b._id.toString()) } else if (indexA === -1) { - return -1 - } else if (indexB === -1) { return 1 + } else if (indexB === -1) { + return -1 } return indexA - indexB diff --git a/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts b/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts index 64d0bcde55..ddd7d280db 100644 --- a/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts +++ b/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts @@ -61,6 +61,7 @@ export async function loadPlayoutModelPreInit( playlist: Playlist, rundowns: sortRundownsWithinPlaylist(Playlist.rundownIdsInOrder, Rundowns), + // rundowns: Rundowns, getRundown: (id: RundownId) => Rundowns.find((rd) => rd._id === id), } diff --git a/packages/job-worker/src/playout/model/implementation/__tests__/LoadPlayoutModel.spec.ts b/packages/job-worker/src/playout/model/implementation/__tests__/LoadPlayoutModel.spec.ts new file mode 100644 index 0000000000..76f136429e --- /dev/null +++ b/packages/job-worker/src/playout/model/implementation/__tests__/LoadPlayoutModel.spec.ts @@ -0,0 +1,97 @@ +import { + setupDefaultRundown, + setupDefaultRundownPlaylist, + setupMockShowStyleCompound, +} from '../../../../__mocks__/presetCollections' +import { MockJobContext, setupDefaultJobEnvironment } from '../../../../__mocks__/context' +import { ProcessedShowStyleCompound } from '../../../../jobs' +import { ReadonlyDeep } from 'type-fest' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { loadPlayoutModelPreInit } from '../LoadPlayoutModel' +import { runWithPlaylistLock } from '../../../../playout/lock' + +describe('LoadPlayoutModel', () => { + let context: MockJobContext + let showStyleCompound: ReadonlyDeep + + beforeAll(async () => { + context = setupDefaultJobEnvironment() + + showStyleCompound = await setupMockShowStyleCompound(context) + }) + + describe('loadPlayoutModelPreInit', () => { + afterEach(async () => + Promise.all([ + context.mockCollections.RundownBaselineAdLibPieces.remove({}), + context.mockCollections.RundownBaselineAdLibActions.remove({}), + context.mockCollections.RundownBaselineObjects.remove({}), + context.mockCollections.AdLibActions.remove({}), + context.mockCollections.AdLibPieces.remove({}), + context.mockCollections.Pieces.remove({}), + context.mockCollections.Parts.remove({}), + context.mockCollections.Segments.remove({}), + context.mockCollections.Rundowns.remove({}), + context.mockCollections.RundownPlaylists.remove({}), + ]) + ) + + test('Rundowns are in order specified in RundownPlaylist', async () => { + // Set up a playlist: + const { rundownId: rundownId00, playlistId: playlistId0 } = await setupDefaultRundownPlaylist( + context, + showStyleCompound, + protectString('rundown00') + ) + const rundownId01 = protectString('rundown01') + await setupDefaultRundown(context, showStyleCompound, playlistId0, rundownId01) + const rundownId02 = protectString('rundown02') + await setupDefaultRundown(context, showStyleCompound, playlistId0, rundownId02) + + const playlist0 = await context.mockCollections.RundownPlaylists.findOne(playlistId0) + expect(playlist0).toBeTruthy() + + if (!playlist0) throw new Error(`Playlist "${playlistId0}" not found!`) + + const rundownIdsInOrder = [rundownId01, rundownId02, rundownId00] + + await context.mockCollections.RundownPlaylists.update(playlistId0, { + rundownIdsInOrder, + }) + + await runWithPlaylistLock(context, playlistId0, async (lock) => { + const model = await loadPlayoutModelPreInit(context, lock, playlist0) + expect(model.rundowns.map((r) => r._id)).toMatchObject([rundownId01, rundownId02, rundownId00]) + }) + }) + + test('Rundowns not ordered in RundownPlaylist are at the end', async () => { + // Set up a playlist: + const { rundownId: rundownId00, playlistId: playlistId0 } = await setupDefaultRundownPlaylist( + context, + showStyleCompound, + protectString('rundown00') + ) + const rundownId01 = protectString('rundown01') + await setupDefaultRundown(context, showStyleCompound, playlistId0, rundownId01) + const rundownId02 = protectString('rundown02') + await setupDefaultRundown(context, showStyleCompound, playlistId0, rundownId02) + + const playlist0 = await context.mockCollections.RundownPlaylists.findOne(playlistId0) + expect(playlist0).toBeTruthy() + + if (!playlist0) throw new Error(`Playlist "${playlistId0}" not found!`) + + const rundownIdsInOrder = [rundownId01] + + await context.mockCollections.RundownPlaylists.update(playlistId0, { + rundownIdsInOrder, + }) + + await runWithPlaylistLock(context, playlistId0, async (lock) => { + const model = await loadPlayoutModelPreInit(context, lock, playlist0) + expect(model.rundowns.map((r) => r._id)).toMatchObject([rundownId01, rundownId00, rundownId02]) + }) + }) + }) +}) From 913f988856611481679c9f65b87ae68a74e7ceaf Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 2 Oct 2024 16:40:16 +0200 Subject: [PATCH 473/479] chore: remove commented line --- .../src/playout/model/implementation/LoadPlayoutModel.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts b/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts index ddd7d280db..64d0bcde55 100644 --- a/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts +++ b/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts @@ -61,7 +61,6 @@ export async function loadPlayoutModelPreInit( playlist: Playlist, rundowns: sortRundownsWithinPlaylist(Playlist.rundownIdsInOrder, Rundowns), - // rundowns: Rundowns, getRundown: (id: RundownId) => Rundowns.find((rd) => rd._id === id), } From b5eefc2bd44811240f15abb90e7c884a508eda1d Mon Sep 17 00:00:00 2001 From: olzzon Date: Fri, 27 Sep 2024 10:00:50 +0200 Subject: [PATCH 474/479] fix: In kiosk mode, rundown page gets stalled if rundown is removed while on the page. --- meteor/client/styles/rundownView.scss | 15 +++++++++++++++ meteor/client/ui/RundownView.tsx | 18 ++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/meteor/client/styles/rundownView.scss b/meteor/client/styles/rundownView.scss index 6b18b445a1..6be933b1d3 100644 --- a/meteor/client/styles/rundownView.scss +++ b/meteor/client/styles/rundownView.scss @@ -3256,6 +3256,21 @@ svg.icon { left: 50%; transform: translate(-150%, -50%); } + + > .rundown-view__label { + position: absolute; + top: 60%; + left: 1%; + right: 0; + text-align: center; + font-size: 3em; + transform: translateY(-50%); + + > p { + margin: 20px auto; + max-width: 1200px; + } + } } .rundown-view { diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx index a6ce8517e3..c9bb164956 100644 --- a/meteor/client/ui/RundownView.tsx +++ b/meteor/client/ui/RundownView.tsx @@ -3257,9 +3257,27 @@ const RundownViewContent = translateWithTracker +
+

+ ( + + )} + /> +

+
+
) From 225677784104219b16ddedd8a427d7a3677e1d97 Mon Sep 17 00:00:00 2001 From: olzzon Date: Mon, 30 Sep 2024 12:08:36 +0200 Subject: [PATCH 475/479] fix: go to renderDataMissing() is rundown is not found - and revert previous implentation --- .../lib/ReactMeteorData/ReactMeteorData.tsx | 4 ++-- meteor/client/styles/rundownView.scss | 15 --------------- meteor/client/ui/RundownView.tsx | 18 ------------------ 3 files changed, 2 insertions(+), 35 deletions(-) diff --git a/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx b/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx index 133178f2b8..667e2652e3 100644 --- a/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx +++ b/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx @@ -402,6 +402,7 @@ export function useSubscription( /** * A Meteor Subscription hook that allows using React Functional Components and the Hooks API with Meteor subscriptions. * Subscriptions will be torn down 1000ms after unmounting the component. + * If the subscription is not enabled, the subscription will not be created, and the ready state will always be true. * * @export * @param {PubSub} sub The subscription to be subscribed to @@ -418,7 +419,6 @@ export function useSubscriptionIfEnabled( useEffect(() => { if (!enable) { - setReady(false) return } @@ -432,7 +432,7 @@ export function useSubscriptionIfEnabled( } }, [sub, enable, stringifyObjects(args)]) - return ready + return !enable || ready } /** diff --git a/meteor/client/styles/rundownView.scss b/meteor/client/styles/rundownView.scss index 6be933b1d3..6b18b445a1 100644 --- a/meteor/client/styles/rundownView.scss +++ b/meteor/client/styles/rundownView.scss @@ -3256,21 +3256,6 @@ svg.icon { left: 50%; transform: translate(-150%, -50%); } - - > .rundown-view__label { - position: absolute; - top: 60%; - left: 1%; - right: 0; - text-align: center; - font-size: 3em; - transform: translateY(-50%); - - > p { - margin: 20px auto; - max-width: 1200px; - } - } } .rundown-view { diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx index c9bb164956..a6ce8517e3 100644 --- a/meteor/client/ui/RundownView.tsx +++ b/meteor/client/ui/RundownView.tsx @@ -3257,27 +3257,9 @@ const RundownViewContent = translateWithTracker -
-

- ( - - )} - /> -

-
-
) From 9304c7147df66925856f3d02aa43e920780378aa Mon Sep 17 00:00:00 2001 From: olzzon Date: Mon, 30 Sep 2024 12:13:09 +0200 Subject: [PATCH 476/479] fix: re-implement setReady to false in useSubscriptionIfEnabled() --- meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx b/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx index 667e2652e3..df3ddd7d30 100644 --- a/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx +++ b/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx @@ -419,6 +419,7 @@ export function useSubscriptionIfEnabled( useEffect(() => { if (!enable) { + setReady(false) return } From ece0ca2fca7822a0bd3ae944af0849f8d1bf237b Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Mon, 7 Oct 2024 11:20:04 +0200 Subject: [PATCH 477/479] chore: update TSR --- packages/playout-gateway/package.json | 4 ++-- packages/shared-lib/package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index 71c56cac17..a25e6e5fd1 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -60,7 +60,7 @@ "@sofie-automation/shared-lib": "1.51.0-in-testing.3", "debug": "^4.3.4", "influx": "^5.9.3", - "timeline-state-resolver": "9.2.0-alpha.0", + "timeline-state-resolver": "9.2.0", "tslib": "^2.6.2", "underscore": "^1.13.6", "winston": "^3.11.0" @@ -74,4 +74,4 @@ ] }, "packageManager": "yarn@3.5.0" -} +} \ No newline at end of file diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index b88ca025f9..fcd49b8325 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -39,7 +39,7 @@ ], "dependencies": { "@mos-connection/model": "^4.1.1", - "timeline-state-resolver-types": "9.2.0-alpha.0", + "timeline-state-resolver-types": "9.2.0", "tslib": "^2.6.2", "type-fest": "^3.13.1" }, @@ -53,4 +53,4 @@ ] }, "packageManager": "yarn@3.5.0" -} +} \ No newline at end of file From 82074b3c5c4c81771e58da099de36e56c4ebc1b4 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Mon, 7 Oct 2024 12:27:54 +0200 Subject: [PATCH 478/479] chore: update deps --- meteor/yarn.lock | 10 +++++----- packages/playout-gateway/package.json | 2 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 22 +++++++++++----------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/meteor/yarn.lock b/meteor/yarn.lock index e0667868a7..36b45b054a 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1418,7 +1418,7 @@ __metadata: resolution: "@sofie-automation/shared-lib@portal:../packages/shared-lib::locator=automation-core%40workspace%3A." dependencies: "@mos-connection/model": ^4.1.1 - timeline-state-resolver-types: 9.2.0-alpha.0 + timeline-state-resolver-types: 9.2.0 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: node @@ -11748,12 +11748,12 @@ __metadata: languageName: node linkType: hard -"timeline-state-resolver-types@npm:9.2.0-alpha.0": - version: 9.2.0-alpha.0 - resolution: "timeline-state-resolver-types@npm:9.2.0-alpha.0" +"timeline-state-resolver-types@npm:9.2.0": + version: 9.2.0 + resolution: "timeline-state-resolver-types@npm:9.2.0" dependencies: tslib: ^2.6.2 - checksum: 88004b58606489102eb39601926bc56ca6ea2142e70339e9d0b8a7a848d7347a4b213340da3ebf5cf13edad804b14b1a155191b2123b805b96f8f221488e015a + checksum: aa51cff0d18b16c613705583f5a1c087e44df59b4b5a7e0fe2f31e52f492686a326bb3a63493b828b166dc8196d58d1470e2adcb730439e8c9ef522e796aea4e languageName: node linkType: hard diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index a25e6e5fd1..9ad9e6859e 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -74,4 +74,4 @@ ] }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index fcd49b8325..45698c5d51 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -53,4 +53,4 @@ ] }, "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} diff --git a/packages/yarn.lock b/packages/yarn.lock index 4627a0435e..8753a42441 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -4693,7 +4693,7 @@ __metadata: resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: "@mos-connection/model": ^4.1.1 - timeline-state-resolver-types: 9.2.0-alpha.0 + timeline-state-resolver-types: 9.2.0 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: unknown @@ -19401,7 +19401,7 @@ asn1@evs-broadcast/node-asn1: "@sofie-automation/shared-lib": 1.51.0-in-testing.3 debug: ^4.3.4 influx: ^5.9.3 - timeline-state-resolver: 9.2.0-alpha.0 + timeline-state-resolver: 9.2.0 tslib: ^2.6.2 underscore: ^1.13.6 winston: ^3.11.0 @@ -22997,18 +22997,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"timeline-state-resolver-types@npm:9.2.0-alpha.0": - version: 9.2.0-alpha.0 - resolution: "timeline-state-resolver-types@npm:9.2.0-alpha.0" +"timeline-state-resolver-types@npm:9.2.0": + version: 9.2.0 + resolution: "timeline-state-resolver-types@npm:9.2.0" dependencies: tslib: ^2.6.2 - checksum: 88004b58606489102eb39601926bc56ca6ea2142e70339e9d0b8a7a848d7347a4b213340da3ebf5cf13edad804b14b1a155191b2123b805b96f8f221488e015a + checksum: aa51cff0d18b16c613705583f5a1c087e44df59b4b5a7e0fe2f31e52f492686a326bb3a63493b828b166dc8196d58d1470e2adcb730439e8c9ef522e796aea4e languageName: node linkType: hard -"timeline-state-resolver@npm:9.2.0-alpha.0": - version: 9.2.0-alpha.0 - resolution: "timeline-state-resolver@npm:9.2.0-alpha.0" +"timeline-state-resolver@npm:9.2.0": + version: 9.2.0 + resolution: "timeline-state-resolver@npm:9.2.0" dependencies: "@tv2media/v-connection": ^7.3.2 atem-connection: 3.5.0 @@ -23033,7 +23033,7 @@ asn1@evs-broadcast/node-asn1: sprintf-js: ^1.1.3 superfly-timeline: ^9.0.1 threadedclass: ^1.2.1 - timeline-state-resolver-types: 9.2.0-alpha.0 + timeline-state-resolver-types: 9.2.0 tslib: ^2.6.2 tv-automation-quantel-gateway-client: ^3.1.7 type-fest: ^3.13.1 @@ -23041,7 +23041,7 @@ asn1@evs-broadcast/node-asn1: utf-8-validate: ^5.0.10 ws: ^7.5.10 xml-js: ^1.6.11 - checksum: b358df9d0f758484084bf549e241a39dd78a9e6e4e78b303c18cd716854882eeb44ad987f34fb47f2904cfc66b6c35ff9590ce9708a87c2699eed33250ebcd58 + checksum: 823203ccef5efe7f0bcd58fff52f9dec288ffc224457e3ef42e384f1071793dc4e14d14ac0fab74d9388693050548c203a4db6a200dc2749551a737e7e11e18f languageName: node linkType: hard From 3ef299fff257ac053d3a46feef5e50853861c038 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Mon, 7 Oct 2024 12:35:32 +0200 Subject: [PATCH 479/479] chore(release): 1.51.0 --- meteor/package.json | 2 +- meteor/yarn.lock | 12 +++--- packages/blueprints-integration/CHANGELOG.md | 16 ++++++++ packages/blueprints-integration/package.json | 4 +- packages/corelib/package.json | 6 +-- packages/documentation/package.json | 2 +- packages/job-worker/package.json | 8 ++-- packages/lerna.json | 2 +- packages/live-status-gateway/package.json | 10 ++--- packages/mos-gateway/CHANGELOG.md | 16 ++++++++ packages/mos-gateway/package.json | 6 +-- packages/openapi/package.json | 2 +- packages/playout-gateway/CHANGELOG.md | 16 ++++++++ packages/playout-gateway/package.json | 6 +-- packages/server-core-integration/CHANGELOG.md | 16 ++++++++ packages/server-core-integration/package.json | 4 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 38 +++++++++---------- 18 files changed, 116 insertions(+), 52 deletions(-) diff --git a/meteor/package.json b/meteor/package.json index c8a40aefa7..7c5f4a0174 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -1,6 +1,6 @@ { "name": "automation-core", - "version": "1.51.0-in-testing.3", + "version": "1.51.0", "private": true, "engines": { "node": ">=14.19.1" diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 36b45b054a..6703e7570c 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1321,7 +1321,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/blueprints-integration@portal:../packages/blueprints-integration::locator=automation-core%40workspace%3A." dependencies: - "@sofie-automation/shared-lib": 1.51.0-in-testing.3 + "@sofie-automation/shared-lib": 1.51.0 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: node @@ -1362,8 +1362,8 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/corelib@portal:../packages/corelib::locator=automation-core%40workspace%3A." dependencies: - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.3 - "@sofie-automation/shared-lib": 1.51.0-in-testing.3 + "@sofie-automation/blueprints-integration": 1.51.0 + "@sofie-automation/shared-lib": 1.51.0 fast-clone: ^1.5.13 i18next: ^21.10.0 influx: ^5.9.3 @@ -1394,9 +1394,9 @@ __metadata: resolution: "@sofie-automation/job-worker@portal:../packages/job-worker::locator=automation-core%40workspace%3A." dependencies: "@slack/webhook": ^6.1.0 - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.3 - "@sofie-automation/corelib": 1.51.0-in-testing.3 - "@sofie-automation/shared-lib": 1.51.0-in-testing.3 + "@sofie-automation/blueprints-integration": 1.51.0 + "@sofie-automation/corelib": 1.51.0 + "@sofie-automation/shared-lib": 1.51.0 amqplib: ^0.10.3 deepmerge: ^4.3.1 elastic-apm-node: ^3.51.0 diff --git a/packages/blueprints-integration/CHANGELOG.md b/packages/blueprints-integration/CHANGELOG.md index 7f7d19e7ac..55ec9e0ed0 100644 --- a/packages/blueprints-integration/CHANGELOG.md +++ b/packages/blueprints-integration/CHANGELOG.md @@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.51.0](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.3...v1.51.0) (2024-10-07) + +**Note:** Version bump only for package @sofie-automation/blueprints-integration + + + + + +# [1.51.0](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.3...v1.51.0) (2024-10-07) + +**Note:** Version bump only for package @sofie-automation/blueprints-integration + + + + + # [1.51.0-in-testing.3](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.2...v1.51.0-in-testing.3) (2024-09-25) **Note:** Version bump only for package @sofie-automation/blueprints-integration diff --git a/packages/blueprints-integration/package.json b/packages/blueprints-integration/package.json index bed9b93615..930198f9cf 100644 --- a/packages/blueprints-integration/package.json +++ b/packages/blueprints-integration/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/blueprints-integration", - "version": "1.51.0-in-testing.3", + "version": "1.51.0", "description": "Library to define the interaction between core and the blueprints.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -38,7 +38,7 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/shared-lib": "1.51.0-in-testing.3", + "@sofie-automation/shared-lib": "1.51.0", "tslib": "^2.6.2", "type-fest": "^3.13.1" }, diff --git a/packages/corelib/package.json b/packages/corelib/package.json index f10e62e012..8f3e8f208a 100644 --- a/packages/corelib/package.json +++ b/packages/corelib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/corelib", - "version": "1.51.0-in-testing.3", + "version": "1.51.0", "private": true, "description": "Internal library for some types shared by core and workers", "main": "dist/index.js", @@ -39,8 +39,8 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/blueprints-integration": "1.51.0-in-testing.3", - "@sofie-automation/shared-lib": "1.51.0-in-testing.3", + "@sofie-automation/blueprints-integration": "1.51.0", + "@sofie-automation/shared-lib": "1.51.0", "fast-clone": "^1.5.13", "i18next": "^21.10.0", "influx": "^5.9.3", diff --git a/packages/documentation/package.json b/packages/documentation/package.json index 402e20b927..b7e29f1811 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -1,6 +1,6 @@ { "name": "sofie-documentation", - "version": "1.51.0-in-testing.3", + "version": "1.51.0", "private": true, "scripts": { "docusaurus": "docusaurus", diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index dbae10b7ca..fc5c6d4ee8 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/job-worker", - "version": "1.51.0-in-testing.3", + "version": "1.51.0", "description": "Worker for things", "main": "dist/index.js", "license": "MIT", @@ -41,9 +41,9 @@ ], "dependencies": { "@slack/webhook": "^6.1.0", - "@sofie-automation/blueprints-integration": "1.51.0-in-testing.3", - "@sofie-automation/corelib": "1.51.0-in-testing.3", - "@sofie-automation/shared-lib": "1.51.0-in-testing.3", + "@sofie-automation/blueprints-integration": "1.51.0", + "@sofie-automation/corelib": "1.51.0", + "@sofie-automation/shared-lib": "1.51.0", "amqplib": "^0.10.3", "deepmerge": "^4.3.1", "elastic-apm-node": "^3.51.0", diff --git a/packages/lerna.json b/packages/lerna.json index 99d49ac919..3dc563f246 100644 --- a/packages/lerna.json +++ b/packages/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.51.0-in-testing.3", + "version": "1.51.0", "npmClient": "yarn", "useWorkspaces": true } diff --git a/packages/live-status-gateway/package.json b/packages/live-status-gateway/package.json index 36ea25d94d..09169a8c79 100644 --- a/packages/live-status-gateway/package.json +++ b/packages/live-status-gateway/package.json @@ -1,6 +1,6 @@ { "name": "live-status-gateway", - "version": "1.51.0-in-testing.3", + "version": "1.51.0", "private": true, "description": "Provides state from Sofie over sockets", "license": "MIT", @@ -53,10 +53,10 @@ "production" ], "dependencies": { - "@sofie-automation/blueprints-integration": "1.51.0-in-testing.3", - "@sofie-automation/corelib": "1.51.0-in-testing.3", - "@sofie-automation/server-core-integration": "1.51.0-in-testing.3", - "@sofie-automation/shared-lib": "1.51.0-in-testing.3", + "@sofie-automation/blueprints-integration": "1.51.0", + "@sofie-automation/corelib": "1.51.0", + "@sofie-automation/server-core-integration": "1.51.0", + "@sofie-automation/shared-lib": "1.51.0", "debug": "^4.3.4", "fast-clone": "^1.5.13", "influx": "^5.9.3", diff --git a/packages/mos-gateway/CHANGELOG.md b/packages/mos-gateway/CHANGELOG.md index 956e4bea81..e5f4ca3e25 100644 --- a/packages/mos-gateway/CHANGELOG.md +++ b/packages/mos-gateway/CHANGELOG.md @@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.51.0](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.3...v1.51.0) (2024-10-07) + +**Note:** Version bump only for package mos-gateway + + + + + +# [1.51.0](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.3...v1.51.0) (2024-10-07) + +**Note:** Version bump only for package mos-gateway + + + + + # [1.51.0-in-testing.3](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.2...v1.51.0-in-testing.3) (2024-09-25) **Note:** Version bump only for package mos-gateway diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index 4aa3d551dd..8f596d779e 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -1,6 +1,6 @@ { "name": "mos-gateway", - "version": "1.51.0-in-testing.3", + "version": "1.51.0", "private": true, "description": "MOS-Gateway for the Sofie project", "license": "MIT", @@ -66,8 +66,8 @@ ], "dependencies": { "@mos-connection/connector": "4.1.1", - "@sofie-automation/server-core-integration": "1.51.0-in-testing.3", - "@sofie-automation/shared-lib": "1.51.0-in-testing.3", + "@sofie-automation/server-core-integration": "1.51.0", + "@sofie-automation/shared-lib": "1.51.0", "tslib": "^2.6.2", "type-fest": "^3.13.1", "underscore": "^1.13.6", diff --git a/packages/openapi/package.json b/packages/openapi/package.json index e885dc38f5..8c3d4ed241 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/openapi", - "version": "1.51.0-in-testing.3", + "version": "1.51.0", "license": "MIT", "repository": { "type": "git", diff --git a/packages/playout-gateway/CHANGELOG.md b/packages/playout-gateway/CHANGELOG.md index 9dae36de7a..957ccfeabf 100644 --- a/packages/playout-gateway/CHANGELOG.md +++ b/packages/playout-gateway/CHANGELOG.md @@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.51.0](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.3...v1.51.0) (2024-10-07) + +**Note:** Version bump only for package playout-gateway + + + + + +# [1.51.0](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.3...v1.51.0) (2024-10-07) + +**Note:** Version bump only for package playout-gateway + + + + + # [1.51.0-in-testing.3](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.2...v1.51.0-in-testing.3) (2024-09-25) **Note:** Version bump only for package playout-gateway diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index 9ad9e6859e..d0d347cbdd 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -1,6 +1,6 @@ { "name": "playout-gateway", - "version": "1.51.0-in-testing.3", + "version": "1.51.0", "private": true, "description": "Connect to Core, play stuff", "license": "MIT", @@ -56,8 +56,8 @@ "production" ], "dependencies": { - "@sofie-automation/server-core-integration": "1.51.0-in-testing.3", - "@sofie-automation/shared-lib": "1.51.0-in-testing.3", + "@sofie-automation/server-core-integration": "1.51.0", + "@sofie-automation/shared-lib": "1.51.0", "debug": "^4.3.4", "influx": "^5.9.3", "timeline-state-resolver": "9.2.0", diff --git a/packages/server-core-integration/CHANGELOG.md b/packages/server-core-integration/CHANGELOG.md index 46ff10a210..ee716eef17 100644 --- a/packages/server-core-integration/CHANGELOG.md +++ b/packages/server-core-integration/CHANGELOG.md @@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.51.0](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.3...v1.51.0) (2024-10-07) + +**Note:** Version bump only for package @sofie-automation/server-core-integration + + + + + +# [1.51.0](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.3...v1.51.0) (2024-10-07) + +**Note:** Version bump only for package @sofie-automation/server-core-integration + + + + + # [1.51.0-in-testing.3](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.2...v1.51.0-in-testing.3) (2024-09-25) **Note:** Version bump only for package @sofie-automation/server-core-integration diff --git a/packages/server-core-integration/package.json b/packages/server-core-integration/package.json index 90c8dc9d62..5680bb015a 100644 --- a/packages/server-core-integration/package.json +++ b/packages/server-core-integration/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/server-core-integration", - "version": "1.51.0-in-testing.3", + "version": "1.51.0", "description": "Library for connecting to Core", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -70,7 +70,7 @@ "production" ], "dependencies": { - "@sofie-automation/shared-lib": "1.51.0-in-testing.3", + "@sofie-automation/shared-lib": "1.51.0", "ejson": "^2.2.3", "eventemitter3": "^4.0.7", "faye-websocket": "^0.11.4", diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 45698c5d51..efe3f7f5db 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/shared-lib", - "version": "1.51.0-in-testing.3", + "version": "1.51.0", "description": "Library for types & values shared by core, workers and gateways", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/packages/yarn.lock b/packages/yarn.lock index 8753a42441..f6f9c36bd9 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -4565,11 +4565,11 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/blueprints-integration@1.51.0-in-testing.3, @sofie-automation/blueprints-integration@workspace:blueprints-integration": +"@sofie-automation/blueprints-integration@1.51.0, @sofie-automation/blueprints-integration@workspace:blueprints-integration": version: 0.0.0-use.local resolution: "@sofie-automation/blueprints-integration@workspace:blueprints-integration" dependencies: - "@sofie-automation/shared-lib": 1.51.0-in-testing.3 + "@sofie-automation/shared-lib": 1.51.0 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: unknown @@ -4606,12 +4606,12 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/corelib@1.51.0-in-testing.3, @sofie-automation/corelib@workspace:corelib": +"@sofie-automation/corelib@1.51.0, @sofie-automation/corelib@workspace:corelib": version: 0.0.0-use.local resolution: "@sofie-automation/corelib@workspace:corelib" dependencies: - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.3 - "@sofie-automation/shared-lib": 1.51.0-in-testing.3 + "@sofie-automation/blueprints-integration": 1.51.0 + "@sofie-automation/shared-lib": 1.51.0 fast-clone: ^1.5.13 i18next: ^21.10.0 influx: ^5.9.3 @@ -4642,9 +4642,9 @@ __metadata: resolution: "@sofie-automation/job-worker@workspace:job-worker" dependencies: "@slack/webhook": ^6.1.0 - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.3 - "@sofie-automation/corelib": 1.51.0-in-testing.3 - "@sofie-automation/shared-lib": 1.51.0-in-testing.3 + "@sofie-automation/blueprints-integration": 1.51.0 + "@sofie-automation/corelib": 1.51.0 + "@sofie-automation/shared-lib": 1.51.0 amqplib: ^0.10.3 deepmerge: ^4.3.1 elastic-apm-node: ^3.51.0 @@ -4674,11 +4674,11 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/server-core-integration@1.51.0-in-testing.3, @sofie-automation/server-core-integration@workspace:server-core-integration": +"@sofie-automation/server-core-integration@1.51.0, @sofie-automation/server-core-integration@workspace:server-core-integration": version: 0.0.0-use.local resolution: "@sofie-automation/server-core-integration@workspace:server-core-integration" dependencies: - "@sofie-automation/shared-lib": 1.51.0-in-testing.3 + "@sofie-automation/shared-lib": 1.51.0 ejson: ^2.2.3 eventemitter3: ^4.0.7 faye-websocket: ^0.11.4 @@ -4688,7 +4688,7 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/shared-lib@1.51.0-in-testing.3, @sofie-automation/shared-lib@workspace:shared-lib": +"@sofie-automation/shared-lib@1.51.0, @sofie-automation/shared-lib@workspace:shared-lib": version: 0.0.0-use.local resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: @@ -15334,10 +15334,10 @@ asn1@evs-broadcast/node-asn1: "@asyncapi/generator": ^1.17.25 "@asyncapi/html-template": ^2.3.9 "@asyncapi/nodejs-ws-template": ^0.9.36 - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.3 - "@sofie-automation/corelib": 1.51.0-in-testing.3 - "@sofie-automation/server-core-integration": 1.51.0-in-testing.3 - "@sofie-automation/shared-lib": 1.51.0-in-testing.3 + "@sofie-automation/blueprints-integration": 1.51.0 + "@sofie-automation/corelib": 1.51.0 + "@sofie-automation/server-core-integration": 1.51.0 + "@sofie-automation/shared-lib": 1.51.0 debug: ^4.3.4 fast-clone: ^1.5.13 influx: ^5.9.3 @@ -17410,8 +17410,8 @@ asn1@evs-broadcast/node-asn1: resolution: "mos-gateway@workspace:mos-gateway" dependencies: "@mos-connection/connector": 4.1.1 - "@sofie-automation/server-core-integration": 1.51.0-in-testing.3 - "@sofie-automation/shared-lib": 1.51.0-in-testing.3 + "@sofie-automation/server-core-integration": 1.51.0 + "@sofie-automation/shared-lib": 1.51.0 tslib: ^2.6.2 type-fest: ^3.13.1 underscore: ^1.13.6 @@ -19397,8 +19397,8 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "playout-gateway@workspace:playout-gateway" dependencies: - "@sofie-automation/server-core-integration": 1.51.0-in-testing.3 - "@sofie-automation/shared-lib": 1.51.0-in-testing.3 + "@sofie-automation/server-core-integration": 1.51.0 + "@sofie-automation/shared-lib": 1.51.0 debug: ^4.3.4 influx: ^5.9.3 timeline-state-resolver: 9.2.0