Skip to content

Commit

Permalink
feat: Continued backend implementation. Data flow from Sofie Core to …
Browse files Browse the repository at this point in the history
…web client
  • Loading branch information
nytamin committed Nov 15, 2023
1 parent 3563d86 commit 3e81a6e
Show file tree
Hide file tree
Showing 23 changed files with 1,296 additions and 28 deletions.
10 changes: 9 additions & 1 deletion packages/apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"node": ">=20.9.0"
},
"devDependencies": {
"@types/lodash.isequal": "^4.5.8",
"@types/yargs": "^17.0.24",
"nodemon": "^3.0.1"
},
"dependencies": {
Expand All @@ -27,12 +29,18 @@
"@feathersjs/socketio": "^5.0.11",
"@feathersjs/socketio-client": "^5.0.11",
"@koa/router": "^12.0.1",
"@sofie-automation/blueprints-integration": "1.50.0-nightly-release50-20230627-124025-83abcdc.0",
"@sofie-automation/server-core-integration": "1.50.0-nightly-release50-20230627-124025-83abcdc.0",
"@sofie-automation/shared-lib": "1.50.0-nightly-release50-20230627-124025-83abcdc.0",
"@sofie-prompter-editor/shared-lib": "0.0.0",
"@sofie-prompter-editor/shared-model": "0.0.0",
"eventemitter3": "^5.0.1",
"koa": "^2.14.2",
"koa-bodyparser": "^4.4.1",
"winston": "^3.11.0"
"lodash.isequal": "^4.5.0",
"mobx": "^6.11.0",
"winston": "^3.11.0",
"yargs": "^17.7.2"
},
"lint-staged": {
"*.{js,css,json,md,scss}": [
Expand Down
10 changes: 7 additions & 3 deletions packages/apps/backend/src/api-server/ApiServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { LoggerInstance } from '../lib/logger.js'
import { PublishChannels } from './PublishChannels.js'
import { PlaylistFeathersService, PlaylistService } from './services/PlaylistService.js'
import { ExampleFeathersService, ExampleService } from './services/ExampleService.js'
import { Store } from '../data-stores/Store.js'

export type ApiServerEvents = {
connection: []
Expand All @@ -18,8 +19,11 @@ export class ApiServer extends EventEmitter<ApiServerEvents> {
public readonly playlist: PlaylistFeathersService
public readonly example: ExampleFeathersService

constructor(log: LoggerInstance, port: number) {
private log: LoggerInstance
constructor(log: LoggerInstance, port: number, private store: Store) {
super()
this.log = log.category('ApiServer')

this.app.use(serveStatic('src'))

this.app.use(
Expand All @@ -33,7 +37,7 @@ export class ApiServer extends EventEmitter<ApiServerEvents> {
this.app.configure(rest())
this.app.configure(socketio({ cors: { origin: '*' } })) // TODO: cors

this.playlist = PlaylistService.setupService(this.app)
this.playlist = PlaylistService.setupService(this.app, this.store, this.log)
this.example = ExampleService.setupService(this.app)

this.app.on('connection', (connection) => {
Expand All @@ -56,7 +60,7 @@ export class ApiServer extends EventEmitter<ApiServerEvents> {
this.app
.listen(port)
.then(() => {
log.info('Feathers server listening on localhost:' + port)
this.log.info('Feathers server listening on localhost:' + port)
resolve()
})
.catch(reject)
Expand Down
4 changes: 4 additions & 0 deletions packages/apps/backend/src/api-server/PublishChannels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ export const PublishChannels = {
ExampleCategory: (category: string): string => {
return `example-category/${category}`
},

AllPlaylists: (): string => {
return `playlists`
},
}
46 changes: 42 additions & 4 deletions packages/apps/backend/src/api-server/services/PlaylistService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import { ServiceTypes, Services, PlaylistServiceDefinition as Definition } from
export { PlaylistServiceDefinition } from '@sofie-prompter-editor/shared-model'
import { PublishChannels } from '../PublishChannels.js'
import { CustomFeathersService } from './lib.js'
import { Store } from '../../data-stores/Store.js'
import { Lambda, observe } from 'mobx'
import { LoggerInstance } from '../../lib/logger.js'

export type PlaylistFeathersService = CustomFeathersService<Definition.Service, Definition.Events>

/** The methods exposed by this class are exposed in the API */
export class PlaylistService extends EventEmitter<Definition.Events> implements Definition.Service {
static setupService(app: Application<ServiceTypes, any>): PlaylistFeathersService {
app.use(Services.Playlist, new PlaylistService(), {
static setupService(app: Application<ServiceTypes, any>, store: Store, log: LoggerInstance): PlaylistFeathersService {
app.use(Services.Playlist, new PlaylistService(app, store, log.category('PlaylistService')), {
methods: Definition.ALL_METHODS,
serviceEvents: Definition.ALL_EVENTS,
})
Expand All @@ -23,13 +26,44 @@ export class PlaylistService extends EventEmitter<Definition.Events> implements
service.publish('tmpPong', (_data, _context) => {
return app.channel(PublishChannels.Everyone())
})

service.publish('created', (_data, _context) => {
return app.channel(PublishChannels.AllPlaylists())
})
service.publish('updated', (_data, _context) => {
return app.channel(PublishChannels.AllPlaylists())
})
service.publish('removed', (_data, _context) => {
return app.channel(PublishChannels.AllPlaylists())
})
}
constructor() {

private observers: Lambda[] = []
constructor(private app: Application<ServiceTypes, any>, private store: Store, private log: LoggerInstance) {
super()

this.observers.push(
observe(this.store.playlists.playlists, (change) => {
this.log.info('observed change', change)
if (change.type === 'add') {
this.emit('created', change.newValue)
} else if (change.type === 'update') {
this.emit('updated', change.newValue)
} else if (change.type === 'delete') {
this.emit('removed', change.oldValue._id)
}
})
)
}
destroy() {
// dispose of observers:
for (const obs of this.observers) {
obs()
}
}

public async find(_params?: Params & { paginate?: PaginationParams }): Promise<Data[]> {
throw new Error('Not implemented')
return Array.from(this.store.playlists.playlists.values())
}
public async get(_id: Id, _params?: Params): Promise<Data> {
throw new Error('Not implemented')
Expand All @@ -53,6 +87,10 @@ export class PlaylistService extends EventEmitter<Definition.Events> implements
// throw new Error('Not implemented')
// }

public async subscribeToPlaylists(_: unknown, params: Params): Promise<void> {
if (!params.connection) throw new Error('No connection!')
this.app.channel(PublishChannels.AllPlaylists()).join(params.connection)
}
public async tmpPing(_payload: string): Promise<string> {
console.log('got a ping!')
setTimeout(() => {
Expand Down
38 changes: 38 additions & 0 deletions packages/apps/backend/src/data-stores/PlaylistStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { action, makeAutoObservable, observable } from 'mobx'
import isEqual from 'lodash.isequal'
import { RundownPlaylist, RundownPlaylistId } from 'packages/shared/model/dist'

export class PlaylistStore {
ready: boolean = false
public readonly playlists = observable.map<RundownPlaylistId, RundownPlaylist>()

constructor() {
makeAutoObservable(this, {
create: action,
update: action,
remove: action,
})
}

create(playlist: RundownPlaylist) {
this._updateIfChanged(playlist)
}
update(playlist: RundownPlaylist) {
this._updateIfChanged(playlist)
}
remove(playlistId: RundownPlaylistId) {
console.log('a')
this._deleteIfChanged(playlistId)
}

private _updateIfChanged(playlist: RundownPlaylist) {
if (!isEqual(this.playlists.get(playlist._id), playlist)) {
this.playlists.set(playlist._id, playlist)
}
}
private _deleteIfChanged(playlistId: RundownPlaylistId) {
if (this.playlists.has(playlistId)) {
this.playlists.delete(playlistId)
}
}
}
5 changes: 5 additions & 0 deletions packages/apps/backend/src/data-stores/Store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { PlaylistStore } from './PlaylistStore.js'

export class Store {
public playlists = new PlaylistStore()
}
42 changes: 35 additions & 7 deletions packages/apps/backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,51 @@
import { DEFAULT_DEV_API_PORT } from '@sofie-prompter-editor/shared-lib'
import { ApiServer } from './api-server/ApiServer.js'
import { initializeLogger, setupLogger } from './lib/logger.js'
import { LoggerInstance, initializeLogger, isLogLevel, setLogLevel, setupLogger } from './lib/logger.js'
import { SofieCoreConnection } from './sofie-core-connection/SofieCoreConnection.js'
import { Store } from './data-stores/Store.js'
import { getConfigOptions } from './lib/config.js'
import { ProcessHandler } from './lib/ProcessHandler.js'

const loggerConfig = {}

initializeLogger(loggerConfig)
const logger = setupLogger(loggerConfig, 'backend')
const log: LoggerInstance = setupLogger(loggerConfig, 'backend')

async function init() {
logger.info('Initializing backend...')
const options = await getConfigOptions()
if (options.logLevel && isLogLevel(options.logLevel)) setLogLevel(options.logLevel)

const httpAPI = new ApiServer(logger, DEFAULT_DEV_API_PORT)
log.info('Options:')
log.info(JSON.stringify(options))

log.info('Initializing backend...')

const processHandler = new ProcessHandler(log)
processHandler.init(options)

const store = new Store()

const httpAPI = new ApiServer(log, DEFAULT_DEV_API_PORT, store)

httpAPI.on('connection', () => {
logger.info('new connection!')
log.info('new connection!')
})

await httpAPI.initialized

logger.info('Backend initialized')
if (!options.noCore) {
const coreConnection = new SofieCoreConnection(log, options, processHandler, store)
coreConnection.on('connected', () => {
log.info('Connected to Core')
})
coreConnection.on('disconnected', () => {
log.warn('Disconnected from Core!')
})
await coreConnection.initialized
} else {
log.info('NOT connecting to Core (noCore=true)')
}

log.info('Backend initialized')
}
init().catch(logger.error)
init().catch(log.error)
33 changes: 33 additions & 0 deletions packages/apps/backend/src/lib/ProcessHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import fs from 'fs'
import { LoggerInstance } from './logger'
import { ConfigOptions } from './config'

// export function setupProcess(config: ProcessConfig): void {}

export class ProcessHandler {
public certificates: Buffer[] = []
private log: LoggerInstance

constructor(log: LoggerInstance) {
this.log = log.category('ProcessHandler')
}
init(processConfig: ConfigOptions): void {
if (processConfig.unsafeSSL) {
this.log.info('Disabling NODE_TLS_REJECT_UNAUTHORIZED, be sure to ONLY DO THIS ON A LOCAL NETWORK!')
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0'
} else {
// var rootCas = SSLRootCAs.create()
}
if (processConfig.certificates.length) {
this.log.info(`Loading certificates...`)
for (const certificate of processConfig.certificates) {
try {
this.certificates.push(fs.readFileSync(certificate))
this.log.info(`Using certificate "${certificate}"`)
} catch (error) {
this.log.error(`Error loading certificate "${certificate}"`, error)
}
}
}
}
}
81 changes: 81 additions & 0 deletions packages/apps/backend/src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Options } from 'yargs'
import yargs from 'yargs/yargs'

/*
* This file contains various CLI argument definitions, used by the various processes that together constitutes the Package Manager
*/

/** Generic CLI-argument-definitions for any process */
const optionsDefinition = defineArguments({
logLevel: { type: 'string', describe: 'Set default log level. (Might be overwritten by Sofie Core)' },

unsafeSSL: {
type: 'boolean',
default: process.env.UNSAFE_SSL === '1',
describe: 'Set to true to allow all SSL certificates (only use this in a safe, local environment)',
},
certificates: { type: 'string', describe: 'SSL Certificates' },

noCore: {
type: 'boolean',
default: process.env.NO_CORE === '1',
describe: 'If true, Package Manager wont try to connect to Sofie Core',
},
coreHost: {
type: 'string',
default: process.env.CORE_HOST || '127.0.0.1',
describe: 'The IP-address/hostName to Sofie Core',
},
corePort: {
type: 'number',
default: parseInt(process.env.CORE_PORT || '', 10) || 3000,
describe: 'The port number of Sofie core (usually 80, 443 or 3000)',
},

deviceId: {
type: 'string',
default: process.env.DEVICE_ID || '',
describe: '(Optional) Unique device id of this device',
},
deviceToken: {
type: 'string',
default: process.env.DEVICE_TOKEN || '',
describe: '(Optional) access token of this device.',
},
})

export interface ConfigOptions {
logLevel: string | undefined
/** Will cause the Node app to blindly accept all certificates. Not recommenced unless in local, controlled networks. */
unsafeSSL: boolean
/** Paths to certificates to load, for SSL-connections */
certificates: string[]

noCore: boolean
coreHost: string
corePort: number
deviceId: string
deviceToken: string
}
export async function getConfigOptions(): Promise<ConfigOptions> {
const argv = await Promise.resolve(yargs(process.argv.slice(2)).options(optionsDefinition).argv)

return {
logLevel: argv.logLevel,
unsafeSSL: argv.unsafeSSL,
certificates: ((argv.certificates || process.env.CERTIFICATES || '').split(';') || []).filter(Boolean),

noCore: argv.noCore,
coreHost: argv.coreHost,
corePort: argv.corePort,
deviceId: argv.deviceId,
deviceToken: argv.deviceToken,
}
}

// ---------------------------------------------------------------------------------

/** Helper function, to get strict typings for the yargs-Options. */
function defineArguments<O extends { [key: string]: Options }>(opts: O): O {
return opts
}
Loading

0 comments on commit 3e81a6e

Please sign in to comment.