diff --git a/meteor/server/publications/lib/__tests__/debounce.test.ts b/meteor/server/publications/lib/__tests__/debounce.test.ts deleted file mode 100644 index f611c80b80..0000000000 --- a/meteor/server/publications/lib/__tests__/debounce.test.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { sleep } from '@sofie-automation/shared-lib/dist/lib/lib' -import { PromiseDebounce } from '../debounce' - -describe('PromiseDebounce', () => { - beforeEach(() => { - jest.useFakeTimers() - }) - - it('trigger', async () => { - const fn = jest.fn() - const debounce = new PromiseDebounce(fn, 10) - - // No promise returned - expect(debounce.trigger()).toBe(undefined) - // Not called yet - expect(fn).toHaveBeenCalledTimes(0) - - // Wait for a bit - await jest.advanceTimersByTimeAsync(6) - expect(fn).toHaveBeenCalledTimes(0) - - // Wait a bit more - await jest.advanceTimersByTimeAsync(6) - expect(fn).toHaveBeenCalledTimes(1) - - // No more calls - fn.mockClear() - await jest.advanceTimersByTimeAsync(50) - expect(fn).toHaveBeenCalledTimes(0) - }) - - it('call', async () => { - const fn = jest.fn() - const debounce = new PromiseDebounce(fn, 10) - - const ps = debounce.call() - expect(ps).not.toBe(undefined) - // Not called yet - expect(fn).toHaveBeenCalledTimes(0) - - // Wait for a bit - await jest.advanceTimersByTimeAsync(6) - expect(fn).toHaveBeenCalledTimes(0) - - // Wait a bit more - await jest.advanceTimersByTimeAsync(6) - expect(fn).toHaveBeenCalledTimes(1) - - // Should resolve without any more timer ticking - await expect(ps).resolves.toBe(undefined) - - // No more calls - fn.mockClear() - await jest.advanceTimersByTimeAsync(50) - expect(fn).toHaveBeenCalledTimes(0) - }) - - it('cancelWaiting - trigger', async () => { - const fn = jest.fn() - const debounce = new PromiseDebounce(fn, 10) - - // No promise returned - expect(debounce.trigger()).toBe(undefined) - // Not called yet - expect(fn).toHaveBeenCalledTimes(0) - - // Wait for a bit - await jest.advanceTimersByTimeAsync(6) - expect(fn).toHaveBeenCalledTimes(0) - - // Cancel waiting - debounce.cancelWaiting() - - // Wait until the timer should have fired - await jest.advanceTimersByTimeAsync(50) - expect(fn).toHaveBeenCalledTimes(0) - }) - - it('cancelWaiting - call', async () => { - const fn = jest.fn() - const debounce = new PromiseDebounce(fn, 10) - - const ps = debounce.call() - ps.catch(() => null) // Add an error handler - expect(ps).not.toBe(undefined) - // Not called yet - expect(fn).toHaveBeenCalledTimes(0) - - // Wait for a bit - await jest.advanceTimersByTimeAsync(6) - expect(fn).toHaveBeenCalledTimes(0) - - // Cancel waiting - debounce.cancelWaiting() - - // Wait until the timer should have fired - await jest.advanceTimersByTimeAsync(50) - expect(fn).toHaveBeenCalledTimes(0) - - // Should have rejected - await expect(ps).rejects.toThrow('Cancelled') - }) - - it('cancelWaiting - call with error', async () => { - const fn = jest.fn() - const debounce = new PromiseDebounce(fn, 10) - - const ps = debounce.call() - ps.catch(() => null) // Add an error handler - expect(ps).not.toBe(undefined) - // Not called yet - expect(fn).toHaveBeenCalledTimes(0) - - // Wait for a bit - await jest.advanceTimersByTimeAsync(6) - expect(fn).toHaveBeenCalledTimes(0) - - // Cancel waiting - debounce.cancelWaiting(new Error('Custom error')) - - // Wait until the timer should have fired - await jest.advanceTimersByTimeAsync(50) - expect(fn).toHaveBeenCalledTimes(0) - - // Should have rejected - await expect(ps).rejects.toThrow('Custom error') - }) - - it('trigger - multiple', async () => { - const fn = jest.fn() - const debounce = new PromiseDebounce(fn, 10) - - // No promise returned - expect(debounce.trigger(1)).toBe(undefined) - // Not called yet - expect(fn).toHaveBeenCalledTimes(0) - - // Wait for a bit - await jest.advanceTimersByTimeAsync(6) - expect(fn).toHaveBeenCalledTimes(0) - - // Trigger again - expect(debounce.trigger(3)).toBe(undefined) - expect(debounce.trigger(5)).toBe(undefined) - - // Wait until the timer should have fired - await jest.advanceTimersByTimeAsync(50) - expect(fn).toHaveBeenCalledTimes(1) - expect(fn).toHaveBeenCalledWith(5) - }) - - it('trigger - during slow execution', async () => { - const fn = jest.fn(async () => sleep(100)) - const debounce = new PromiseDebounce(fn, 10) - - // No promise returned - expect(debounce.trigger(1)).toBe(undefined) - // Not called yet - expect(fn).toHaveBeenCalledTimes(0) - - // Wait for it to start executing - await jest.advanceTimersByTimeAsync(50) - expect(fn).toHaveBeenCalledTimes(1) - expect(fn).toHaveBeenCalledWith(1) - - // Trigger again - fn.mockClear() - expect(debounce.trigger(3)).toBe(undefined) - await jest.advanceTimersByTimeAsync(20) - expect(debounce.trigger(5)).toBe(undefined) - - // Wait until the second timer timer should - await jest.advanceTimersByTimeAsync(100) - expect(fn).toHaveBeenCalledTimes(1) - expect(fn).toHaveBeenCalledWith(5) - }) - - it('call - return value', async () => { - const fn = jest.fn(async (val) => { - await sleep(100) - return val - }) - const debounce = new PromiseDebounce(fn, 10) - - const ps1 = debounce.call(1) - expect(ps1).not.toBe(undefined) - // Not called yet - expect(fn).toHaveBeenCalledTimes(0) - - // Wait for it to start executing - await jest.advanceTimersByTimeAsync(50) - expect(fn).toHaveBeenCalledTimes(1) - expect(fn).toHaveBeenCalledWith(1) - - // Trigger again - fn.mockClear() - const ps3 = debounce.call(3) - await jest.advanceTimersByTimeAsync(20) - const ps5 = debounce.call(5) - - // Wait until the second timer timer should - await jest.advanceTimersByTimeAsync(150) - expect(fn).toHaveBeenCalledTimes(1) - expect(fn).toHaveBeenCalledWith(5) - - await expect(ps1).resolves.toBe(1) - await expect(ps3).resolves.toBe(5) - await expect(ps5).resolves.toBe(5) - }) - - it('call - throw error', async () => { - const fn = jest.fn(async (val) => { - await sleep(100) - throw new Error(`Bad value: ${val}`) - }) - const debounce = new PromiseDebounce(fn, 10) - - const ps1 = debounce.call(1) - ps1.catch(() => null) // Add an error handler - expect(ps1).not.toBe(undefined) - // Not called yet - expect(fn).toHaveBeenCalledTimes(0) - - // Wait for it to start executing - await jest.advanceTimersByTimeAsync(50) - expect(fn).toHaveBeenCalledTimes(1) - expect(fn).toHaveBeenCalledWith(1) - - // Trigger again - fn.mockClear() - const ps3 = debounce.call(3) - ps3.catch(() => null) // Add an error handler - await jest.advanceTimersByTimeAsync(20) - const ps5 = debounce.call(5) - ps5.catch(() => null) // Add an error handler - - // Wait until the second timer timer should - await jest.advanceTimersByTimeAsync(150) - expect(fn).toHaveBeenCalledTimes(1) - expect(fn).toHaveBeenCalledWith(5) - - await expect(ps1).rejects.toThrow('Bad value: 1') - await expect(ps3).rejects.toThrow('Bad value: 5') - await expect(ps5).rejects.toThrow('Bad value: 5') - }) - - it('canelWaiting - during slow execution', async () => { - const fn = jest.fn(async () => sleep(100)) - const debounce = new PromiseDebounce(fn, 10) - - // No promise returned - expect(debounce.trigger(1)).toBe(undefined) - // Not called yet - expect(fn).toHaveBeenCalledTimes(0) - - // Wait for it to start executing - await jest.advanceTimersByTimeAsync(50) - expect(fn).toHaveBeenCalledTimes(1) - expect(fn).toHaveBeenCalledWith(1) - - // Trigger again - fn.mockClear() - expect(debounce.trigger(3)).toBe(undefined) - await jest.advanceTimersByTimeAsync(20) - expect(debounce.trigger(5)).toBe(undefined) - - debounce.cancelWaiting() - - // Wait until the second timer timer should - await jest.advanceTimersByTimeAsync(100) - expect(fn).toHaveBeenCalledTimes(0) - }) -}) diff --git a/meteor/server/publications/lib/debounce.ts b/meteor/server/publications/lib/debounce.ts deleted file mode 100644 index 5af796d9ab..0000000000 --- a/meteor/server/publications/lib/debounce.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { Meteor } from 'meteor/meteor' - -/** - * Based on https://github.com/sindresorhus/p-debounce - * With additional features: - * - `cancelWaiting` method - * - ensures only one execution in progress at a time - */ -export class PromiseDebounce { - readonly #fn: (...args: TArgs) => Promise - readonly #wait: number - - /** If an execution timeout has passed while */ - #pendingArgs: TArgs | null = null - #timeout: number | undefined - - #isExecuting = false - #waitingListeners: Listener[] = [] - - constructor(fn: (...args: TArgs) => Promise, wait: number) { - this.#fn = fn - this.#wait = wait - } - - /** - * Trigger an execution, and get the result. - * @returns A promise that resolves with the result of the function - */ - call = async (...args: TArgs): Promise => { - return new Promise((resolve, reject) => { - const listener: Listener = { resolve, reject } - this.#waitingListeners.push(listener) - - // Trigger an execution - this.trigger(...args) - }) - } - - /** - * Trigger an execution, but don't report the result. - */ - trigger = (...args: TArgs): void => { - // If an execution is 'imminent', don't do anything - if (this.#pendingArgs) { - this.#pendingArgs = args - return - } - - // Clear an existing timeout - if (this.#timeout) Meteor.clearTimeout(this.#timeout) - - // Start a new one - this.#timeout = Meteor.setTimeout(() => { - this.#timeout = undefined - - this.executeFn(args) - }, this.#wait) - } - - private executeFn(args: TArgs): void { - // If an execution is still in progress, mark as pending and stop - if (this.#isExecuting) { - this.#pendingArgs = args - return - } - - // We have the clear to begin executing - this.#isExecuting = true - this.#pendingArgs = null - - // Collect up the listeners for this execution - const listeners = this.#waitingListeners - this.#waitingListeners = [] - - Promise.resolve() - .then(async () => { - const result = await this.#fn(...args) - for (const listener of listeners) { - listener.resolve(result) - } - }) - .catch((error) => { - for (const listener of listeners) { - listener.reject(error) - } - }) - .finally(() => { - this.#isExecuting = false - - // If there is a pending execution, run that soon - if (this.#pendingArgs) { - const args = this.#pendingArgs - Meteor.setTimeout(() => this.executeFn(args), 0) - } - }) - } - - /** - * Cancel any waiting execution - */ - cancelWaiting = (error?: Error): void => { - this.#pendingArgs = null - - if (this.#timeout) { - Meteor.clearTimeout(this.#timeout) - this.#timeout = undefined - } - - // Inform any listeners - if (this.#waitingListeners.length > 0) { - const listeners = this.#waitingListeners - this.#waitingListeners = [] - - error = error ?? new Error('Cancelled') - - // Inform the listeners in the next tick - Meteor.defer(() => { - for (const listener of listeners) { - listener.reject(error) - } - }) - } - } -} - -interface Listener { - resolve: (value: TResult) => void - reject: (reason?: any) => void -} diff --git a/packages/blueprints-integration/src/api/studio.ts b/packages/blueprints-integration/src/api/studio.ts index 3818645f2b..32031f9d65 100644 --- a/packages/blueprints-integration/src/api/studio.ts +++ b/packages/blueprints-integration/src/api/studio.ts @@ -89,17 +89,6 @@ export interface StudioBlueprintManifest TProcessedConfig - /** - * Process an ingest operation, to apply changes to the sofie interpretation of the ingest data - */ - processIngestData?: ( - context: IProcessIngestDataContext, - mutableIngestRundown: MutableIngestRundown, - nrcsIngestRundown: IngestRundown, - previousNrcsIngestRundown: IngestRundown | undefined, - changes: NrcsIngestChangeDetails | UserOperationChange - ) => Promise - /** * Optional method to validate the blueprint config passed to this blueprint according to the API schema. * Returns a list of messages to the caller that are used for logging or to throw if errors have been found. @@ -117,6 +106,17 @@ export interface StudioBlueprintManifest object + + /** + * Process an ingest operation, to apply changes to the sofie interpretation of the ingest data + */ + processIngestData?: ( + context: IProcessIngestDataContext, + mutableIngestRundown: MutableIngestRundown, + nrcsIngestRundown: IngestRundown, + previousNrcsIngestRundown: IngestRundown | undefined, + changes: NrcsIngestChangeDetails | UserOperationChange + ) => Promise } export interface BlueprintResultStudioBaseline { diff --git a/packages/job-worker/src/playout/quickLoopMarkers.ts b/packages/job-worker/src/playout/quickLoopMarkers.ts index d29dfe6d0f..3803f59f93 100644 --- a/packages/job-worker/src/playout/quickLoopMarkers.ts +++ b/packages/job-worker/src/playout/quickLoopMarkers.ts @@ -8,8 +8,8 @@ import { setNextPart } from './setNext' import { resetPartInstancesWithPieceInstances } from './lib' import { QuickLoopMarker, QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { PlayoutModel } from './model/PlayoutModel' import { clone } from 'underscore' +import { PlayoutModel } from './model/PlayoutModel' export async function handleSetQuickLoopMarker(context: JobContext, data: SetQuickLoopMarkerProps): Promise { return runJobWithPlayoutModel( diff --git a/packages/live-status-gateway/api/schemas/activePlaylist.yaml b/packages/live-status-gateway/api/schemas/activePlaylist.yaml index 390399cf1f..2624e4e684 100644 --- a/packages/live-status-gateway/api/schemas/activePlaylist.yaml +++ b/packages/live-status-gateway/api/schemas/activePlaylist.yaml @@ -29,23 +29,6 @@ $defs: $ref: '#/$defs/part' publicData: description: Optional arbitrary data - quickLoop: - description: Information about the current quickLoop, if any - type: object - properties: - locked: - description: Whether the user is allowed to make alterations to the Start/End markers - type: boolean - running: - description: Whether the loop has two valid markers and is currently running - type: boolean - start: - description: The start of the loop - $ref: '#/$defs/quickLoopMarker' - end: - description: The end of the loop - $ref: '#/$defs/quickLoopMarker' - required: [locked, running] timing: description: Timing information about the active playlist type: object 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 5676ba8c05..b9b1af95b3 100644 --- a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts @@ -50,7 +50,6 @@ describe('ActivePlaylistTopic', () => { currentSegment: null, rundownIds: unprotectStringArray(playlist.rundownIdsInOrder), publicData: undefined, - quickLoop: undefined, timing: { timingMode: PlaylistTimingType.None, }, @@ -92,15 +91,14 @@ describe('ActivePlaylistTopic', () => { expectedDuration: 10000, publicData: { b: 'c' }, } - const currentPartInstance = { - _id: currentPartInstanceId, - part: part1, - timings: { plannedStartedPlayback: 1600000060000 }, - segmentId: segment1id, - } const testPartInstances: PartialDeep = { - current: currentPartInstance, - firstInSegmentPlayout: currentPartInstance, + current: { + _id: currentPartInstanceId, + part: part1, + timings: { plannedStartedPlayback: 1600000060000 }, + segmentId: segment1id, + }, + firstInSegmentPlayout: {}, inCurrentSegment: [ literal>({ _id: protectString(currentPartInstanceId), @@ -115,7 +113,6 @@ describe('ActivePlaylistTopic', () => { await topic.update(SegmentHandler.name, { _id: segment1id, - segmentTiming: { budgetDuration: 12300, countdownType: CountdownType.SEGMENT_BUDGET_DURATION }, } as DBSegment) topic.addSubscriber(mockSubscriber) @@ -138,14 +135,11 @@ describe('ActivePlaylistTopic', () => { id: 'SEGMENT_1', timing: { expectedDurationMs: 10000, - budgetDurationMs: 12300, - projectedEndTime: 1600000072300, - countdownType: 'segment_budget_duration', + projectedEndTime: 1600000070000, }, }, rundownIds: unprotectStringArray(playlist.rundownIdsInOrder), publicData: { a: 'b' }, - quickLoop: undefined, timing: { timingMode: PlaylistTimingType.None, }, diff --git a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts index 4d54bf1d43..b557a4e1d9 100644 --- a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts +++ b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts @@ -21,9 +21,9 @@ import { SelectedPieceInstances, PieceInstancesHandler, PieceInstanceMin } from import { PieceStatus, toPieceStatus } from './helpers/pieceStatus' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { SegmentHandler } from '../collections/segmentHandler' +import { PlaylistTimingType } from '@sofie-automation/blueprints-integration' import { SegmentsHandler } from '../collections/segmentsHandler' import { normalizeArray } from '@sofie-automation/corelib/dist/lib' -import { PlaylistTimingType } from '@sofie-automation/blueprints-integration' const THROTTLE_PERIOD_MS = 100