soundworks
plugin for runtime scripting. The plugin allows to define entry point in the application that enable the end user to modify the behavior of the distributed application at runtime, following an end-user programming strategy.
- Installation
- Example
- Usage
- Notes
- Credits
- License
npm install @soundworks/plugin-scripting --save
A working example can be found in the https://github.com/collective-soundworks/soundworks-examples repository.
// index.js
import { Server } from '@soundworks/core/server';
import pluginScriptingFactory from '@soundworks/plugin-scripting/server';
const server = new Server();
server.pluginManager.register('scripting', pluginScriptingFactory, {
// default to `.data/scripts`
directory: 'scripts',
}, []);
// MyExperience.js
import { AbstractExperience } from '@soundworks/core/server';
class MyExperience extends AbstractExperience {
constructor(server, clientType) {
super(server, clientType);
// require plugin in the experience
this.scripting = this.require('scripting');
}
}
// index.js
import { Client } from '@soundworks/core/client';
import pluginScriptingFactory from '@soundworks/plugin-scripting/client';
const client = new Client();
client.pluginManager.register('scripting', pluginScriptingFactory, {}, []);
// MyExperience.js
import { Experience } from '@soundworks/core/client';
class MyExperience extends Experience {
constructor(client) {
super(client);
// require plugin in the experience
this.scripting = this.require('scripting');
}
}
All the plugin scripting API presented below is similar server-side and client-side.
const scriptName = 'my-script';
// optional default value, defaults to:
// `function ${camelCase(scriptName)}() {}`
const defaultValue = `// ${scriptName}
function(audioContext) {
// write your code here...
}
`;
await this.scripting.create(scriptName, defaultValue);
await this.scripting.delete(scriptName);
const script = await this.scripting.attach(scriptName);
// observe creation and deletion of scripts on the network
this.scripting.observe(() => {
const list = this.scripting.getList();
console.log(list);
});
// getting the current list of scripts
const list = this.scripting.getList();
The scripts are internally transpiled using babel to enable usage of modern JS features in old browsers (we currently aim to support iOS >= 9.3).
const script = await this.scripting.attach('some-script');
// arguments passed to the script are at discretion of the developer this
// will define which part the application the end-user as access to.
script.execute(...args);
const script = await this.scripting.attach('some-script');
// As this method principally aims to provide a way of creating
// an editor, the code retrieved if the original code, not the transpiled one
const code = script.getValue();
// Similarly the value of the script can be set from the content of an editor
script.setValue(code);
// re-execute the script when its value has been updated
script.subscribe(updates => {
// execute the script only if no type errors found
// by default the error will be displayed in the console
if (!updates.error) {
script.execute(...args);
}
});
// later...
script.setValue(code);
// the given callback is also called when the script is deleted
script.onDetach(() => {
// do some cleaning...
});
await script.detach();
To provide the most possible entry points to scripting, the script files stored in the server are automatically watched by the server. This allows to update the application at runtime directly from your favorite editor.
The API provided by the plugin is by default very simple. However it makes possible to simply create more advanced behaviors and lifecycle. For example, the application can define a contract where the script acts as a factory function that returns an object consumed by the application, allowing the script to maintain its own local state and variables.
In such case, using a clean and commented default value (cf. Creating a script) can be important to help the end-user to understand and follow the API contract with the application.
// my-script.js
function createAudioEngine(audioContext, audioBuffers) {
let intervalId;
// create some audio graph
const bus = audioContext.createGain();
bus.connect(audioContext.destination);
function playBuffer() {
const src = audioContext.createBufferSource();
src.buffer = audioBuffers[Math.floor(Math.random() * audioBuffers.length)];
src.start(audioContext.currentTime);
}
return {
start() {
intervalId = setInterval(playBuffer, 1);
},
stop() {
clearInterval(intervalId);
bus.disconnect();
},
};
}
Such script could be consumed as following in the application code:
// application code
const script = await this.scripting.attach('my-script');
const engine = script.execute(audioContext);
script.onDetach(() => engine.stop());
engine.start();
// later...
await script.detach();
For obvious security reasons, in production or public settings, make sure to disable or protect access to any online editor.
@todo - document basic HTTP authentication w/ soundworks
The code has been initiated in the framework of the WAVE and CoSiMa research projects, funded by the French National Research Agency (ANR).
BSD-3-Clause