Installations
npm install iplug
Developer Guide
Typescript
No
Module System
ESM
Node Version
18.13.0
NPM Version
8.19.3
Score
71.9
Supply Chain
98.4
Quality
75.4
Maintenance
100
Vulnerability
99.6
License
Releases
Unable to fetch releases
Contributors
Languages
JavaScript (100%)
Developer
cubelets
Download Statistics
Total Downloads
4,475
Last Day
1
Last Week
5
Last Month
36
Last Year
2,771
GitHub Statistics
3 Stars
28 Commits
2 Watching
9 Branches
1 Contributors
Bundle Size
809.00 B
Minified
449.00 B
Minified + Gzipped
Package Meta Information
Latest Version
0.7.0
Package Id
iplug@0.7.0
Unpacked Size
34.88 kB
Size
10.41 kB
File Count
40
NPM Version
8.19.3
Node Version
18.13.0
Publised On
09 Nov 2023
Total Downloads
Cumulative downloads
Total Downloads
4,475
Last day
0%
1
Compared to previous day
Last week
-28.6%
5
Compared to previous week
Last month
140%
36
Compared to previous month
Last year
155.4%
2,771
Compared to previous year
Daily Downloads
Weekly Downloads
Monthly Downloads
Yearly Downloads
Dev Dependencies
21
iPlug
The lightest JavaScript plugin system / plugin manager / messagebus for the map/reduce world
Installation
Using npm:
1npm install --save iplug
Usage
You want to use plugins to dynamically extend the functionality of your code.
The core part of your application loads your plugins and creates one or more message buses to communicate with them, through a number of topics. Plugins can register their interest to any number of topics. When you want to defer control to plugins you emit a message on a topic and all registered plugins will be invoked, sequentially or in parallel.
Hello World
main.js
1import iplug from 'iplug' 2 3import module1 from './plugins/module1.js' 4import module2 from './plugins/module2.js' 5 6const modules = { 7 module1, 8 module2, 9} 10 11const plugins = iplug(modules).init() 12 13function main() { 14 const input = 'test payload' 15 const output = plugins.chain('test:message', input) 16 console.log(output) 17} 18 19main()
Each plugin module can export:
- A manifest object:
1 export default { 2 // ... 3 'message': config => data => ... 4 }
- a function that can take an optional argument and returns a manifest object
1 export default (moduleConfig) => { 2 // keep state here for better testability 3 4 return { 5 'message': config => data => ... 6 // ... 7 } 8 }
A manifest object is one whose keys are messagebus messages we want to register to, and handlers to be called when those are emitted
1type Handler<T> = (config: T) => (data: any) => any; 2 3type Manifest<T> = { 4 [message: string]: Handler<T>; 5};
example-plugin.js
1export default { 2 'category1:message1': config => data => `result for a call to category1:message1 - payload was ${data}`, 3 'category2:message2': config => data => `result for a call to category1:message1 - payload was ${data}`, 4}
Map, Reduce, Chain, All
There are two ways you can call plugins: in sequence, or in parallel.
Sequential processing
Calling plugins in sequence means starting with an optional initial value, and chaining it through each, so the output of one becomes the input of the next, and the final result is the output of the last. This is useful when your plugins can be somewhat aware of each-other and the output of any may be the final one.
main.js
1// this returns the output of the last plugin in the chain 2plugins.chain(<message> [, initial data]) 3plugins.reduce(<message> [, initial data]) 4plugins(<message>, [initial data])
reduce
is an alias for chain
.
For chained calls you can also omit both the chain
or reduce
keywords and just call:
Parallel processing
Calling plugins in parallel means passing the same optional initial value, then collecting the results of each together, which come out as an array, perhaps for further processing. This is useful if you want to run many plugins in parallel, especially async ones, or that need to run in isolation from the input or the output of the other plugins.
main.js
1// this returns an array of your plugins' output 2plugins.map(<message> [, initial data]) 3plugins.all(<message> [, initial data]) 4plugins.parallel(<message> [, initial data])
all
is an alias for map
Example: Content Moderation
You want to use plugins to moderate content before rendering it, by passing it through a number of plugins, each of which has to approve the content.
module.js
1import {moderation} from './moderation.js 2const pluginsList = [moderation] 3const config = { } 4const plugins = iplug(pluginsList, config) 5 6const initialData[] = await fetch('/api/getMessages').then(x=>x.json()) 7const result = plugins('moderate', data)
moderation.js
1export default { 2 'moderate': config => { 3 const blackList = await fetch('/word-blacklist').then(x=>x.json()) 4 return data[] => data.map(str => blackList.forEach(word => str.replace(word, '###redacted###'))) 5 } 6}
Advanced usage: streaming plugins with Observables
You may want each plugin to emit more data over time, effectively exposing an Observable interface back to the main application
plugin.js
1import { Observable } from 'rxjs' 2const sourceStream = Observable(...) 3 4export default { 5 'getdata': config => data => sourceStream, 6}
app.js
1import { merge } from 'rxjs' 2 3// Get an Observable from each plugin. 4const streams = streamingPlugins.map('getdata') 5merge(streams) 6 .subscribe(doSomething) 7
Advanced usage: duplex streams via Observables
You can pass an observable to each of your plugins and get one back to enable two-way communication over time
echo.js
1import { Observable } from 'rxjs' 2const sourceStream = Observable(...) 3 4export default { 5 'duplex': config => { inputStream } => inputStream.map(inputMessage=>`This is a reply to ${inputMessage}.`, 6}
app.js
1import { Subject } from 'rxjs' 2import { merge } from 'rxjs' 3 4const outputStream = new Subject() 5 6const streams = streamingPlugins.map('duplex', outputStream) 7merge(streams) 8 .subscribe(doSomething) 9
Why should every plugin export a function returning a function?
This extra step allows some plugins to perform some one-time (perhaps slow) initialisation and return a "production" function that's geared up for faster, repeated executions. It's often best to perform initialisation in the main handler function to enable multiple instances of the same plugin to be used in different isolated contexts (multiple message buses in the same application).
plugin.js
1// some global initialisation can go here, but the state will be shared! 2// ... 3 4export default { 5 'message:handler': config => { 6 // perform some more initialisation here when you want more isolation. 7 // const slow = slowOperation 8 return data => { 9 // put your performance-optimised code here, to run multiple times 10 } 11 } 12}
main.js
1// The following two message buses are meant to run independently 2const messageBus1 = plugins() 3const messageBus2 = plugins() 4 5// The following two calls will be run in isolation from each-other 6messageBus1.chain('message:handler') 7messageBus2.chain('message:handler')
Unit testing your plugins
Writing unit tests for your plugins should be just as simple as calling the function they export for a particular event/topic.
1import plugin from '/plugins/double-it.js' 2 3describe('plugin1', () => { 4 describe('when handling a "test:topic"', () => { 5 6 it('doubles its input', () => { 7 const fn = plugin['test:topic']() 8 expect(fn(2)).toEqual(4) 9 }); 10 11 }); 12});
Following is an example unit test for a hypothetical plugin that returns 0, written for Jest, but easily adaptable to other test frameworks.
1import plugin from '/plugins/plugin2.js' 2import fixture from '/plugins/plugin2.fixture.js' 3 4describe('plugin2', () => { 5 describe('when handling a "test:event"', () => { 6 7 it('returns 0', () => { 8 const fn = plugin['test:event']() 9 const result = fn(fixture) 10 expect(result).toEqual(0) 11 }); 12 13 }); 14});
Using Globals
Global variables, even inside an ES6 module, can pose various challenges to unit testing. Consider the follwing plugin:
1const globalState = get_some_state() 2 3export default { 4 'message:handler': config => data => globalState(data), 5}
The problem here is sometimes it can be hard for test frameworks to mock or stub globalState
in order to force a certain behaviour.
What you may experience is the first time a unit test runs, the globalState may be mocked as expected, but at subsequent runs, re-mocking or re-stubbing may just not work, failing the tests.
A solution to this problem is creating a plugin that exports a function, which in turn will return everything else.
1export default function() { 2 const globalState = get_some_state() 3 4 return { 5 'message:handler': config => data => globalState(data), 6 } 7}
This way, no global state will remain between test runs.
1import initModule from '/plugins/plugin.js' 2 3describe('plugin', () => { 4 describe('when handling a "test:event"', () => { 5 6 it('returns 0', () => { 7 // Loading the plugin from a test will need this one extra line 8 const plugin = initModule() 9 10 const fn = plugin['test:event']() 11 const result = fn(fixture) 12 expect(result).toEqual(0) 13 }); 14 15 }); 16});
Examples
You can find more examples in the respective folder
No vulnerabilities found.
No security vulnerabilities found.