diff --git a/apps/package-manager/packages/generic/src/packageManager.ts b/apps/package-manager/packages/generic/src/packageManager.ts index f89f492b..c397d1cc 100644 --- a/apps/package-manager/packages/generic/src/packageManager.ts +++ b/apps/package-manager/packages/generic/src/packageManager.ts @@ -32,13 +32,28 @@ export class PackageManagerHandler { private expectedPackageCache: { [id: string]: ExpectedPackageWrap } = {} private packageContainersCache: PackageContainers = {} - private toReportExpectationStatus: { [id: string]: ExpectedPackageStatusAPI.WorkStatus } = {} - private sendUpdateExpectationStatusTimeouts: { [id: string]: NodeJS.Timeout } = {} - - private toReportPackageStatus: { [id: string]: ExpectedPackageStatusAPI.PackageContainerPackageStatus } = {} - private sendUpdatePackageContainerPackageStatusTimeouts: { [id: string]: NodeJS.Timeout } = {} + private reportedWorkStatuses: { [id: string]: ExpectedPackageStatusAPI.WorkStatus } = {} + private toReportExpectationStatus: { + [id: string]: { + workStatus: ExpectedPackageStatusAPI.WorkStatus | null + /** If the status is new and needs to be reported to Core */ + isUpdated: boolean + } + } = {} + private sendUpdateExpectationStatusTimeouts: NodeJS.Timeout | undefined + + private reportedPackageStatuses: { [id: string]: ExpectedPackageStatusAPI.PackageContainerPackageStatus } = {} + private toReportPackageStatus: { + [key: string]: { + containerId: string + packageId: string + packageStatus: ExpectedPackageStatusAPI.PackageContainerPackageStatus | null + /** If the status is new and needs to be reported to Core */ + isUpdated: boolean + } + } = {} + private sendUpdatePackageContainerPackageStatusTimeouts: NodeJS.Timeout | undefined - private reportedStatuses: { [id: string]: ExpectedPackageStatusAPI.WorkStatus } = {} private externalData: { packageContainers: PackageContainers; expectedPackages: ExpectedPackageWrap[] } = { packageContainers: {}, expectedPackages: [], @@ -365,13 +380,12 @@ export class PackageManagerHandler { ): void { if (!expectaction) { if (this.toReportExpectationStatus[expectationId]) { - delete this.toReportExpectationStatus[expectationId] - this.triggerSendUpdateExpectationStatus(expectationId) + this.triggerSendUpdateExpectationStatus(expectationId, null) } } else { if (!expectaction.statusReport.sendReport) return // Don't report the status - const packageStatus: ExpectedPackageStatusAPI.WorkStatus = { + const workStatus: ExpectedPackageStatusAPI.WorkStatus = { // Default properties: ...{ status: 'N/A', @@ -385,9 +399,9 @@ export class PackageManagerHandler { ...statusInfo, fromPackages: expectaction.fromPackages.map((fromPackage) => { - const prevPromPackage = this.toReportExpectationStatus[expectationId]?.fromPackages.find( - (p) => p.id === fromPackage.id - ) + const prevPromPackage = this.toReportExpectationStatus[ + expectationId + ]?.workStatus?.fromPackages.find((p) => p.id === fromPackage.id) return { id: fromPackage.id, expectedContentVersionHash: fromPackage.expectedContentVersionHash, @@ -396,80 +410,84 @@ export class PackageManagerHandler { }), } - this.toReportExpectationStatus[expectationId] = packageStatus - this.triggerSendUpdateExpectationStatus(expectationId) + this.triggerSendUpdateExpectationStatus(expectationId, workStatus) } } - private triggerSendUpdateExpectationStatus(expectationId: string) { - if (!this.sendUpdateExpectationStatusTimeouts[expectationId]) { - this.sendUpdateExpectationStatusTimeouts[expectationId] = setTimeout(() => { - delete this.sendUpdateExpectationStatusTimeouts[expectationId] - this.sendUpdateExpectationStatus(expectationId) - }, 300) + private triggerSendUpdateExpectationStatus( + expectationId: string, + workStatus: ExpectedPackageStatusAPI.WorkStatus | null + ) { + this.toReportExpectationStatus[expectationId] = { + workStatus: workStatus, + isUpdated: true, + } + + if (!this.sendUpdateExpectationStatusTimeouts) { + this.sendUpdateExpectationStatusTimeouts = setTimeout(() => { + delete this.sendUpdateExpectationStatusTimeouts + this.sendUpdateExpectationStatus() + }, 500) } } - private sendUpdateExpectationStatus(expectationId: string) { - const toReportStatus = this.toReportExpectationStatus[expectationId] + private sendUpdateExpectationStatus() { + const changesTosend: UpdateExpectedPackageWorkStatusesChanges = [] + + for (const [expectationId, o] of Object.entries(this.toReportExpectationStatus)) { + if (o.isUpdated) { + if (!o.workStatus) { + if (this.reportedWorkStatuses[expectationId]) { + // Removed + changesTosend.push({ + id: expectationId, + type: 'delete', + }) + delete this.reportedWorkStatuses[expectationId] + } + } else { + const lastReportedStatus = this.reportedWorkStatuses[expectationId] + + if (!lastReportedStatus) { + // Inserted + changesTosend.push({ + id: expectationId, + type: 'insert', + status: o.workStatus, + }) + } else { + // Updated + const mod: Partial = {} + for (const key of Object.keys(o.workStatus) as (keyof ExpectedPackageStatusAPI.WorkStatus)[]) { + if (o.workStatus[key] !== lastReportedStatus[key]) { + mod[key] = o.workStatus[key] as any + } + } + if (!_.isEmpty(mod)) { + changesTosend.push({ + id: expectationId, + type: 'update', + status: mod, + }) + } + } + this.reportedWorkStatuses[expectationId] = o.workStatus + } - if (!toReportStatus && this.reportedStatuses[expectationId]) { + o.isUpdated = false + } + } + + if (changesTosend.length) { this._coreHandler.core - .callMethod(PeripheralDeviceAPI.methods.removeExpectedPackageWorkStatus, [expectationId]) + .callMethod(PeripheralDeviceAPI.methods.updateExpectedPackageWorkStatuses, [changesTosend]) .catch((err) => { - this.logger.error('Error when calling method removeExpectedPackageStatus:') + this.logger.error('Error when calling method updateExpectedPackageWorkStatuses:') this.logger.error(err) }) - delete this.reportedStatuses[expectationId] - } else { - // const expWrap = this.expectedPackageCache[expectaction.statusReport.packageId] - // if (!expWrap) return // If the expectedPackage isn't found, we shouldn't send any updates - - const lastReportedStatus = this.reportedStatuses[expectationId] - - if (!lastReportedStatus) { - this._coreHandler.core - .callMethod(PeripheralDeviceAPI.methods.insertExpectedPackageWorkStatus, [ - expectationId, - toReportStatus, - ]) - .catch((err) => { - this.logger.error('Error when calling method insertExpectedPackageStatus:') - this.logger.error(err) - }) - } else { - const mod: Partial = {} - for (const key of Object.keys(toReportStatus)) { - // @ts-expect-error no index signature found - if (toReportStatus[key] !== lastReportedStatus[key]) { - // @ts-expect-error no index signature found - mod[key] = toReportStatus[key] - } - } - if (!_.isEmpty(mod)) { - // Send partial update: - this._coreHandler.core - .callMethod(PeripheralDeviceAPI.methods.updateExpectedPackageWorkStatus, [expectationId, mod]) - .then((okResult) => { - if (!okResult) { - // Retry with sending full update - return this._coreHandler.core.callMethod( - PeripheralDeviceAPI.methods.insertExpectedPackageWorkStatus, - [expectationId, toReportStatus] - ) - } - return Promise.resolve() - }) - .catch((err) => { - this.logger.error('Error when calling method updateExpectedPackageStatus:') - this.logger.error(err) - }) - } - } - this.reportedStatuses[expectationId] = { - ...toReportStatus, - } } } private async cleanReportedExpectations() { + // Clean out all reported statuses, this is an easy way to sync a clean state with core + this.reportedWorkStatuses = {} await this._coreHandler.core.callMethod( PeripheralDeviceAPI.methods.removeAllExpectedPackageWorkStatusOfDevice, [] @@ -482,7 +500,7 @@ export class PackageManagerHandler { ): void { const packageContainerPackageId = `${containerId}_${packageId}` if (!packageStatus) { - delete this.toReportPackageStatus[packageContainerPackageId] + this.triggerSendUpdatePackageContainerPackageStatus(containerId, packageId, null) } else { const o: ExpectedPackageStatusAPI.PackageContainerPackageStatus = { // Default properties: @@ -491,41 +509,74 @@ export class PackageManagerHandler { progress: 0, statusReason: '', }, - // Previous properties: - ...(((this.toReportPackageStatus[packageContainerPackageId] || {}) as any) as Record), // Intentionally cast to Any, to make typings in const packageStatus more strict - // Updated porperties: + // pre-existing properties: + ...(((this.toReportPackageStatus[packageContainerPackageId] || {}) as any) as Record), // Intentionally cast to Any, to make typings in the outer spread-assignment more strict + // Updated properties: ...packageStatus, } - this.toReportPackageStatus[packageContainerPackageId] = o + this.triggerSendUpdatePackageContainerPackageStatus(containerId, packageId, o) } - this.triggerSendUpdatePackageContainerPackageStatus(containerId, packageId) } - private triggerSendUpdatePackageContainerPackageStatus(containerId: string, packageId: string): void { - const packageContainerPackageId = `${containerId}_${packageId}` - if (!this.sendUpdatePackageContainerPackageStatusTimeouts[packageContainerPackageId]) { - this.sendUpdatePackageContainerPackageStatusTimeouts[packageContainerPackageId] = setTimeout(() => { - delete this.sendUpdatePackageContainerPackageStatusTimeouts[packageContainerPackageId] - this.sendUpdatePackageContainerPackageStatus(containerId, packageId) + private triggerSendUpdatePackageContainerPackageStatus( + containerId: string, + packageId: string, + packageStatus: ExpectedPackageStatusAPI.PackageContainerPackageStatus | null + ): void { + const key = `${containerId}_${packageId}` + + this.toReportPackageStatus[key] = { + containerId, + packageId, + packageStatus, + isUpdated: true, + } + + if (!this.sendUpdatePackageContainerPackageStatusTimeouts) { + this.sendUpdatePackageContainerPackageStatusTimeouts = setTimeout(() => { + delete this.sendUpdatePackageContainerPackageStatusTimeouts + this.sendUpdatePackageContainerPackageStatus() }, 300) } } - public sendUpdatePackageContainerPackageStatus(containerId: string, packageId: string): void { - const packageContainerPackageId = `${containerId}_${packageId}` + public sendUpdatePackageContainerPackageStatus(): void { + const changesTosend: UpdatePackageContainerPackageStatusesChanges = [] + + for (const [key, o] of Object.entries(this.toReportPackageStatus)) { + if (o.isUpdated) { + if (!o.packageStatus) { + if (this.reportedPackageStatuses[key]) { + // Removed + changesTosend.push({ + containerId: o.containerId, + packageId: o.packageId, + type: 'delete', + }) + delete this.reportedPackageStatuses[key] + } + } else { + // Inserted / Updated + changesTosend.push({ + containerId: o.containerId, + packageId: o.packageId, + type: 'update', + status: o.packageStatus, + }) + this.reportedPackageStatuses[key] = o.packageStatus + } - const toReportPackageStatus: ExpectedPackageStatusAPI.PackageContainerPackageStatus | null = - this.toReportPackageStatus[packageContainerPackageId] || null - - this._coreHandler.core - .callMethod(PeripheralDeviceAPI.methods.updatePackageContainerPackageStatus, [ - containerId, - packageId, - toReportPackageStatus, - ]) - .catch((err) => { - this.logger.error('Error when calling method removeExpectedPackageStatus:') - this.logger.error(err) - }) + o.isUpdated = false + } + } + + if (changesTosend.length) { + this._coreHandler.core + .callMethod(PeripheralDeviceAPI.methods.updatePackageContainerPackageStatuses, [changesTosend]) + .catch((err) => { + this.logger.error('Error when calling method updatePackageContainerPackageStatuses:') + this.logger.error(err) + }) + } } public updatePackageContainerStatus( containerId: string, @@ -540,17 +591,17 @@ export class PackageManagerHandler { ): Promise { switch (message.type) { case 'fetchPackageInfoMetadata': - return await this._coreHandler.core.callMethod( + return this._coreHandler.core.callMethod( PeripheralDeviceAPI.methods.fetchPackageInfoMetadata, message.arguments ) case 'updatePackageInfo': - return await this._coreHandler.core.callMethod( + return this._coreHandler.core.callMethod( PeripheralDeviceAPI.methods.updatePackageInfo, message.arguments ) case 'removePackageInfo': - return await this._coreHandler.core.callMethod( + return this._coreHandler.core.callMethod( PeripheralDeviceAPI.methods.removePackageInfo, message.arguments ) @@ -559,7 +610,7 @@ export class PackageManagerHandler { break default: - // @ts-expect-error message.type is never + // @ts-expect-error message is never throw new Error(`Unsupported message type "${message.type}"`) } } @@ -657,3 +708,35 @@ export interface ActiveRundown { export interface PackageManagerSettings { delayRemoval: number } + +/** Note: This is based on the Core method updateExpectedPackageWorkStatuses. */ +type UpdateExpectedPackageWorkStatusesChanges = ( + | { + id: string + type: 'delete' + } + | { + id: string + type: 'insert' + status: ExpectedPackageStatusAPI.WorkStatus + } + | { + id: string + type: 'update' + status: Partial + } +)[] +/** Note: This is based on the Core method updatePackageContainerPackageStatuses. */ +type UpdatePackageContainerPackageStatusesChanges = ( + | { + containerId: string + packageId: string + type: 'delete' + } + | { + containerId: string + packageId: string + type: 'update' + status: ExpectedPackageStatusAPI.PackageContainerPackageStatus + } +)[] diff --git a/package.json b/package.json index a0245cd8..b8a8f987 100644 --- a/package.json +++ b/package.json @@ -55,8 +55,8 @@ "node": ">=12.3.0" }, "dependencies": { - "@sofie-automation/blueprints-integration": "1.34.0-nightly-test-mserk5-20210507-123656-f937ba8.0", - "@sofie-automation/server-core-integration": "1.34.0-nightly-test-mserk5-20210507-123656-f937ba8.0" + "@sofie-automation/blueprints-integration": "1.35.0-nightly-feat-package-management-20210521-082825-31727c2.0", + "@sofie-automation/server-core-integration": "1.35.0-nightly-feat-package-management-20210521-082825-31727c2.0" }, "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", "husky": { diff --git a/yarn.lock b/yarn.lock index ae2df9eb..a7dba6ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1478,15 +1478,15 @@ tslib "^2.0.3" underscore "1.12.0" -"@sofie-automation/blueprints-integration@1.34.0-nightly-test-mserk5-20210507-123656-f937ba8.0": - version "1.34.0-nightly-test-mserk5-20210507-123656-f937ba8.0" - resolved "https://registry.yarnpkg.com/@sofie-automation/blueprints-integration/-/blueprints-integration-1.34.0-nightly-test-mserk5-20210507-123656-f937ba8.0.tgz#9a4a528559e4dad175f9bb4f369d3bb1887aadbe" - integrity sha512-CMhfQiEMMIZRTymHdkqfVgabA0ElXfvJvhApyEAFZ7LQbf7+EzT248hyxi2u1iT5ZeFoa3LASWQIY4yBvcIK8w== +"@sofie-automation/blueprints-integration@1.35.0-nightly-feat-package-management-20210521-082825-31727c2.0": + version "1.35.0-nightly-feat-package-management-20210521-082825-31727c2.0" + resolved "https://registry.yarnpkg.com/@sofie-automation/blueprints-integration/-/blueprints-integration-1.35.0-nightly-feat-package-management-20210521-082825-31727c2.0.tgz#6e9279af9dac23e88df9e76bb4b41cb9cc04c446" + integrity sha512-4SsMbHcEzUVk24G7maJQXg2i9F8rdfGn/rV0Pn95Gih4OlJxefne4T5WKS6kOY5A3C8ZGDNRolHR/WtrbcOHLA== dependencies: moment "2.29.1" - timeline-state-resolver-types "5.7.0-nightly-release33-20210421-102534-d3150d7e3.0" + timeline-state-resolver-types "5.9.0-nightly-release34-20210511-085833-30ad952a1.0" tslib "^2.1.0" - underscore "1.12.1" + underscore "1.13.1" "@sofie-automation/code-standard-preset@^0.2.1", "@sofie-automation/code-standard-preset@^0.2.2": version "0.2.2" @@ -1507,10 +1507,10 @@ read-pkg-up "^7.0.1" shelljs "^0.8.4" -"@sofie-automation/server-core-integration@1.34.0-nightly-test-mserk5-20210507-123656-f937ba8.0": - version "1.34.0-nightly-test-mserk5-20210507-123656-f937ba8.0" - resolved "https://registry.yarnpkg.com/@sofie-automation/server-core-integration/-/server-core-integration-1.34.0-nightly-test-mserk5-20210507-123656-f937ba8.0.tgz#7e8cf63085ad2265d24d657f89596f2350d2b3fa" - integrity sha512-1VWmOlAHcXJ5K3MX/s1UFK1rPWzka/WcFHI0RF5imcu3ypN1bRvbYjRfmY2PCBvGbO7AJ8q5cbRs2Fd84fDZEw== +"@sofie-automation/server-core-integration@1.35.0-nightly-feat-package-management-20210521-082825-31727c2.0": + version "1.35.0-nightly-feat-package-management-20210521-082825-31727c2.0" + resolved "https://registry.yarnpkg.com/@sofie-automation/server-core-integration/-/server-core-integration-1.35.0-nightly-feat-package-management-20210521-082825-31727c2.0.tgz#dc115837aa303bb1c7741140c1a6de5c453f32b5" + integrity sha512-yy3wTxfLef+821BVshwozVBJEwOIZhUpQqFJDcQ9AVZDUzJEl97c+q++V/FvJ2SPbT/5FqFwVp3uzlnVUIWzIA== dependencies: data-store "3.1.0" ejson "^2.2.0" @@ -9648,10 +9648,10 @@ timeline-state-resolver-types@5.5.1: dependencies: tslib "^1.13.0" -timeline-state-resolver-types@5.7.0-nightly-release33-20210421-102534-d3150d7e3.0: - version "5.7.0-nightly-release33-20210421-102534-d3150d7e3.0" - resolved "https://registry.yarnpkg.com/timeline-state-resolver-types/-/timeline-state-resolver-types-5.7.0-nightly-release33-20210421-102534-d3150d7e3.0.tgz#569d8436cb89996c32d354acf63ade5e91a6140c" - integrity sha512-0duh41MtYAnDaFoQ2JibTzGtmz+c/ydoAECmpdnpo0GVMb96viz7eLnTkMTWMMlwJBYJbuzPtvmkgh/ZWaZRvw== +timeline-state-resolver-types@5.9.0-nightly-release34-20210511-085833-30ad952a1.0: + version "5.9.0-nightly-release34-20210511-085833-30ad952a1.0" + resolved "https://registry.yarnpkg.com/timeline-state-resolver-types/-/timeline-state-resolver-types-5.9.0-nightly-release34-20210511-085833-30ad952a1.0.tgz#6edada5ea4199b290645c9ced2a4051ed68985ea" + integrity sha512-3M9CptINGuerTEJGriuez4PqFqyD3Cf2ZZ9lI0cv4mnXOTLt3kwbp4a0uc2HWLBgzPsYPM9JEz2Y20XFSJC4TQ== dependencies: tslib "^1.13.0" @@ -9961,7 +9961,12 @@ underscore@1.12.0, underscore@^1.12.0: resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.0.tgz#4814940551fc80587cef7840d1ebb0f16453be97" integrity sha512-21rQzss/XPMjolTiIezSu3JAjgagXKROtNrYFEOWK109qY1Uv2tVjPTZ1ci2HgvQDA16gHYSthQIJfB+XId/rQ== -underscore@1.12.1, underscore@^1.12.1: +underscore@1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.1.tgz#0c1c6bd2df54b6b69f2314066d65b6cde6fcf9d1" + integrity sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g== + +underscore@^1.12.1: version "1.12.1" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.1.tgz#7bb8cc9b3d397e201cf8553336d262544ead829e" integrity sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==