Skip to content

Commit

Permalink
Refactor API and implement DI (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
axelboberg authored Feb 5, 2025
1 parent 834dee4 commit 05f7518
Show file tree
Hide file tree
Showing 40 changed files with 2,059 additions and 1,635 deletions.
494 changes: 245 additions & 249 deletions api/browser/client.js

Large diffs are not rendered by default.

100 changes: 45 additions & 55 deletions api/browser/transport.js
Original file line number Diff line number Diff line change
@@ -1,67 +1,57 @@
// SPDX-FileCopyrightText: 2022 Sveriges Television AB
// SPDX-FileCopyrightText: 2025 Sveriges Television AB
//
// SPDX-License-Identifier: MIT

const handlers = []
const DIController = require('../../shared/DIController')

let sendQueue = []
class Transport {
#handlers = []
#queue = []

/**
* Replay the send queue and
* sequentially call send for
* every message
*/
function replayQueue () {
const tmpQueue = sendQueue
sendQueue = []
/**
* Replay the send queue and
* sequentially call send for
* every message
*/
replayQueue () {
const tmpQueue = this.#queue
this.#queue = []

for (const message of tmpQueue) {
this.send(message)
for (const message of tmpQueue) {
this.send(message)
}
}
}

/**
* Send a message with the
* transport, for messages
* leaving the api
*
* Unless overridden
* messages will be queued
*
* @param { Object } message
*/
function send (message) {
sendQueue.push(message)
}
/**
* The entrypoint for messages
* coming into the api
*
* This should be called by the application
* when a new message is to be handled
*
* @param { Object } msg
*/
receive (msg) {
this.#handlers.forEach(handler => handler(msg))
}

/**
* Add a handler to be called when
* this transport receives a message
* @param { (Any) -> Void } handler
*/
function onMessage (handler) {
handlers.push(handler)
}
onMessage (handler) {
this.#handlers.push(handler)
}

/**
* The entrypoint for messages
* coming into the api
*
* This should be called by the application
* when a new message is to be handled
*
* @param { Object } message
*/
function receive (message) {
handlers.forEach(handler => handler(message))
/**
* Send a message with the
* transport, for messages
* leaving the api
*
* Unless overridden
* messages will be queued
*
* @param { Object } msg
*/
send (msg) {
this.#queue.push(msg)
}
}

/**
* @type { import('../transport').Communicator }
*/
module.exports = {
send,
receive,
replayQueue,
onMessage
}
DIController.main.register('Transport', Transport)
9 changes: 4 additions & 5 deletions api/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@
//
// SPDX-License-Identifier: MIT

const client = (function () {
;(function () {
if (module.parent) {
return require('./node/client')
require('./node/client')
return
}
if (typeof window !== 'undefined') {
return require('./browser/client')
require('./browser/client')
}
})()

module.exports = client
244 changes: 128 additions & 116 deletions api/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@
//
// SPDX-License-Identifier: MIT

const transport = require('./transport')
const random = require('./random')

const handlers = new Map()
const DIController = require('../shared/DIController')

const NoLocalHandlerError = require('./error/NoLocalHandlerError')
const InvalidArgumentError = require('./error/InvalidArgumentError')
Expand All @@ -31,128 +30,141 @@ function handlerFactory (fn, returns) {
}
}

/*
Handle incoming messages and
call the registered handler
with the provided arguments
This code is also responsible
for executing any transactions
if the handler provides a
return value
*/
transport.onMessage(async message => {
const handler = handlers.get(message.command)
if (!handler) return

const args = message.args || []

if (!handler.returns) {
handler.call(...args)
} else {
const transaction = args.shift()
try {
const res = await handler.call(...args)
executeRawCommand(transaction, res)
} catch (err) {
executeRawCommand(transaction, undefined, {
message: err.message,
cause: err.cause,
stack: err.stack,
name: err.name
})
}
class Commands {
#props

#handlers = new Map()

constructor (props) {
this.#props = props
this.#setup()
}
})

/**
* Execute a command
* @param { String } command A command to execute
* @param { ...any } args Any arguments to pass to the handler, must be serializable
* @returns { Promise.<any> }
*/
function executeCommand (command, ...args) {
return new Promise((resolve, reject) => {
const transactionId = random.string(12)
const transaction = `transaction:${transactionId}:${command}`

registerCommand(transaction, (res, err) => {
removeCommand(transaction)

if (err) {
const error = new Error(err.message)
error.stack = err.stack
error.cause = err.cause
error.name = err.name
return reject(error)
#setup () {
/*
Handle incoming messages and
call the registered handler
with the provided arguments
This code is also responsible
for executing any transactions
if the handler provides a
return value
*/
this.#props.Transport.onMessage(async message => {
const handler = this.#handlers.get(message.command)
if (!handler) return

const args = message.args || []

if (!handler.returns) {
handler.call(...args)
} else {
const transaction = args.shift()
try {
const res = await handler.call(...args)
this.executeRawCommand(transaction, res)
} catch (err) {
this.executeRawCommand(transaction, undefined, {
message: err.message,
cause: err.cause,
stack: err.stack,
name: err.name
})
}
}
resolve(res)
}, false)

executeRawCommand(command, transaction, ...args)
})
}
exports.executeCommand = executeCommand
})
}

/**
* Execute a command without
* creating a transaction
*
* No return values are available
* from commands called with this
* function, use executeCommand if
* return values are required
* @param { String } command The command to execute
* @param { ...any } args Arguments to pass to the command
*/
function executeRawCommand (command, ...args) {
transport.send({ command, args: [...args] })
}
exports.executeRawCommand = executeRawCommand
/**
* Execute a command
* @param { String } command A command to execute
* @param { ...any } args Any arguments to pass to the handler, must be serializable
* @returns { Promise.<any> }
*/
executeCommand (command, ...args) {
return new Promise((resolve, reject) => {
const transactionId = random.string(12)
const transaction = `transaction:${transactionId}:${command}`

this.registerCommand(transaction, (res, err) => {
this.removeCommand(transaction)

if (err) {
const error = new Error(err.message)
error.stack = err.stack
error.cause = err.cause
error.name = err.name
return reject(error)
}
resolve(res)
}, false)

this.executeRawCommand(command, transaction, ...args)
})
}

/**
* Register a command
* with a handler
* @param { String } command The command to register, should be scoped
* @param {
* (...Any) => Promise.<Any> | (...Any) => Void
* } handler A handler to invoke whenever
* the command is run
* @param { Boolean } returns Indicate whether or not
* the handler returns a value,
* defaults to true
*/
function registerCommand (command, handler, returns = true) {
if (typeof handler !== 'function') {
throw new InvalidArgumentError('Parameter \'handler\' must be a function')
/**
* Execute a command without
* creating a transaction
*
* No return values are available
* from commands called with this
* function, use executeCommand if
* return values are required
* @param { String } command The command to execute
* @param { ...any } args Arguments to pass to the command
*/
executeRawCommand (command, ...args) {
this.#props.Transport.send({ command, args: [...args] })
}

handlers.set(command, handlerFactory(handler, returns))
transport.send({
command: 'commands.registerCommand',
args: [command]
})
}
exports.registerCommand = registerCommand
/**
* Register a command
* with a handler
* @param { String } command The command to register, should be scoped
* @param {
* (...Any) => Promise.<Any> | (...Any) => Void
* } handler A handler to invoke whenever
* the command is run
* @param { Boolean } returns Indicate whether or not
* the handler returns a value,
* defaults to true
*/
registerCommand (command, handler, returns = true) {
if (typeof handler !== 'function') {
throw new InvalidArgumentError('Parameter \'handler\' must be a function')
}

/**
* Remove a command
* and remove its handler
* @param { String } command The command to remove
*/
function removeCommand (command) {
/*
A plugin can only remove
its own commands
*/
if (!handlers.has(command)) {
throw new NoLocalHandlerError('Command cannot be removeed as it wasn\'t created by this plugin')
this.#handlers.set(command, handlerFactory(handler, returns))
this.#props.Transport.send({
command: 'commands.registerCommand',
args: [command]
})
}

handlers.delete(command)
transport.send({
command: 'commands.removeCommand',
args: [command]
})
/**
* Remove a command
* and remove its handler
* @param { String } command The command to remove
*/
removeCommand (command) {
/*
A plugin can only remove
its own commands
*/
if (!this.#handlers.has(command)) {
throw new NoLocalHandlerError('Command cannot be removeed as it wasn\'t created by this plugin')
}

this.#handlers.delete(command)
this.#props.Transport.send({
command: 'commands.removeCommand',
args: [command]
})
}
}
exports.removeCommand = removeCommand

DIController.main.register('Commands', Commands, [
'Transport'
])
Loading

0 comments on commit 05f7518

Please sign in to comment.