debut des details de la page. Vu que c'est le troisieme (euh quatrieme?) composant, c'etait un peu plus rapide, mais heureusement que claude est la pour repasser derriere mes erreurs prcq en solo je n'y arriverais pas du tout!
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Gianluca Guarini
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+353
@@ -0,0 +1,353 @@
|
||||
<img alt="erre.js" src="https://cdn.rawgit.com/GianlucaGuarini/erre/main/erre-logo.svg" width="50%"/>
|
||||
|
||||
[![Build Status][ci-image]][ci-url]
|
||||
|
||||
[![NPM version][npm-version-image]][npm-url]
|
||||
[![NPM downloads][npm-downloads-image]][npm-url]
|
||||
[![MIT License][license-image]][license-url]
|
||||
|
||||
## Description
|
||||
|
||||
Erre is a modern, performant and tiny (~0.5kb minified) streams script using generators that runs on modern browsers and node.
|
||||
It can be used to manage any kind of sync and async event series and it's inspired to bigger libraries like:
|
||||
- [baconjs](https://baconjs.github.io/)
|
||||
- [RxJS](http://reactivex.io/rxjs/)
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
npm i erre -S
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
You can use it to create and manipulate simple event streams
|
||||
|
||||
```js
|
||||
import erre from 'erre'
|
||||
|
||||
const stream = erre(
|
||||
string => string.toUpperCase(),
|
||||
string => [...string].reverse().join('')
|
||||
)
|
||||
|
||||
stream.on.value(console.log) // EVOL, ETAH
|
||||
|
||||
stream.push('love')
|
||||
stream.push(async () => await 'hate') // async values
|
||||
```
|
||||
|
||||
It supports async and sync event chains thanks to [ruit](https://github.com/GianlucaGuarini/ruit)
|
||||
|
||||
```js
|
||||
const userNamesStream = erre(
|
||||
async user => await patchUsers(user), // async function returning a users collection
|
||||
users => users.map(user => user.name)
|
||||
)
|
||||
|
||||
userNamesStream.on.value(console.log) // ['John'...]
|
||||
|
||||
userNamesStream.push({
|
||||
name: 'John',
|
||||
role: 'Doctor',
|
||||
age: 24
|
||||
})
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### erre(...functions)
|
||||
##### @returns [`stream`](#stream)
|
||||
|
||||
Create an `erre` stream object.
|
||||
The initial `functions` list is optional and it represents the chain of async or sync events needed to generate the final stream output received via [`on.value`](#streamonvaluecallback) callbacks
|
||||
|
||||
### stream
|
||||
|
||||
It's an enhanced [Generator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator) object having additional API methods
|
||||
|
||||
#### stream.push(value)
|
||||
##### @returns `stream`
|
||||
|
||||
Push a new value into the stream that will be asynchronously modified and returned as argument to [`stream.on.value`](#streamonvaluecallback) method
|
||||
|
||||
<details>
|
||||
<summary>Example</summary>
|
||||
|
||||
```js
|
||||
const stream = erre()
|
||||
stream.on.value(console.log) // 1
|
||||
stream.push(1)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### stream.on.value(callback)
|
||||
##### @returns `stream`
|
||||
|
||||
Add a callback that will be called receiving the output of the stream asynchronously
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Example</summary>
|
||||
|
||||
```js
|
||||
const stream = erre(val => val + 1)
|
||||
|
||||
stream.on.value(console.log) // 2
|
||||
stream.on.value(val => console.log(val * 2)) // 4
|
||||
|
||||
stream.push(1)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### stream.on.error(callback)
|
||||
##### @returns `stream`
|
||||
|
||||
Add a callback that will be called in case of errors or promise rejections during the output generation
|
||||
|
||||
<details>
|
||||
<summary>Example</summary>
|
||||
|
||||
```js
|
||||
const stream = erre(val => {
|
||||
throw 'error'
|
||||
})
|
||||
|
||||
stream.on.value(console.log) // never called!!
|
||||
stream.on.error(console.log) // 'error'
|
||||
|
||||
stream.push(1)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### stream.on.end(callback)
|
||||
##### @returns `stream`
|
||||
|
||||
Add a callback that will be called when the stream will be ended
|
||||
|
||||
<details>
|
||||
<summary>Example</summary>
|
||||
|
||||
```js
|
||||
const stream = erre()
|
||||
|
||||
stream.on.end(() => console.log('ended!')) // ended!
|
||||
|
||||
stream.end()
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### stream.off.value(callback)
|
||||
##### @returns `stream`
|
||||
##### @throws `Error` if `callback` isn't registered
|
||||
|
||||
Removes a previously-registered callback
|
||||
|
||||
<details>
|
||||
<summary>Example</summary>
|
||||
|
||||
```js
|
||||
const stream = erre()
|
||||
|
||||
const handler = (value) => console.log('handling', value)
|
||||
stream.on.value(handler)
|
||||
stream.push(1) // handler called, logs: handling 1
|
||||
|
||||
stream.off.value(handler)
|
||||
stream.push(2) // handler is not called
|
||||
|
||||
// throws, because the handler is not registered
|
||||
const someOtherHandler = () => console.log(`don't register me`)
|
||||
stream.off.value(someOtherHandler)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### stream.off.error(callback)
|
||||
##### @returns `stream`
|
||||
##### @throws `Error` if `callback` isn't registered
|
||||
|
||||
#### stream.off.end(callback)
|
||||
##### @returns `stream`
|
||||
##### @throws `Error` if `callback` isn't registered
|
||||
|
||||
#### stream.connect(function)
|
||||
##### @returns `stream`
|
||||
|
||||
Enhance the stream adding a new operation to the functions chain to generate its output
|
||||
|
||||
<details>
|
||||
<summary>Example</summary>
|
||||
|
||||
```js
|
||||
const stream = erre(val => val + 1)
|
||||
|
||||
stream.on.value(console.log) // 2, 4
|
||||
stream.push(1)
|
||||
|
||||
// enhance the stream
|
||||
stream.connect(val => val * 2)
|
||||
stream.push(1)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### stream.end()
|
||||
##### @returns `stream`
|
||||
|
||||
End the stream
|
||||
|
||||
<details>
|
||||
<summary>Example</summary>
|
||||
|
||||
```js
|
||||
const stream = erre(val => val + 1)
|
||||
|
||||
stream.on.value(console.log) // 2
|
||||
stream.push(1)
|
||||
|
||||
// end the stream
|
||||
stream.end()
|
||||
|
||||
// no more events
|
||||
stream.push(1)
|
||||
stream.push(1)
|
||||
stream.push(1)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### stream.fork()
|
||||
##### @returns new `stream` object
|
||||
|
||||
Create a new stream object inheriting the function chain from its parent
|
||||
|
||||
<details>
|
||||
<summary>Example</summary>
|
||||
|
||||
```js
|
||||
const stream = erre(val => val + 1)
|
||||
|
||||
stream.on.value(console.log) // 2, 3
|
||||
stream.push(1)
|
||||
|
||||
const fork = stream.fork()
|
||||
fork.on.value(console.log)
|
||||
fork.connect(val => val * 10) // 20, 60
|
||||
|
||||
// 2 independent streams
|
||||
fork.push(1)
|
||||
stream.push(2)
|
||||
fork.push(5)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### stream.next(value)
|
||||
##### @returns { done: true|false, value: Promise|undefined }
|
||||
|
||||
Run a single stream sequence (**without dispatching any event**) returning as `value` a promise result of the stream computation.
|
||||
If the stream was ended the `done` value will be `true` and the `value` will be `undefined`.
|
||||
|
||||
<details>
|
||||
<summary>Example</summary>
|
||||
|
||||
```js
|
||||
const stream = erre(val => val + 1)
|
||||
|
||||
stream.on.value(console.log) // never called
|
||||
|
||||
const { value } = stream.next(1)
|
||||
|
||||
value.then(console.log) // 2
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### erre.cancel()
|
||||
|
||||
Static function that if returned by any of the stream functions chain can be used to filter or stop the computation
|
||||
|
||||
<details>
|
||||
<summary>Example</summary>
|
||||
|
||||
```js
|
||||
const stream = erre(val => {
|
||||
if (typeof val !== 'number') return erre.cancel()
|
||||
return val + 1
|
||||
})
|
||||
|
||||
stream.on.value(console.log) // 2, 3
|
||||
stream.push(1)
|
||||
stream.push('foo') // filtered
|
||||
stream.push('1') // filtered
|
||||
stream.push(2)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### erre.off()
|
||||
|
||||
Static function that if returned by any of the subscribed callbacks can be used to unsubscribe it
|
||||
|
||||
<details>
|
||||
<summary>Example</summary>
|
||||
|
||||
```js
|
||||
const stream = erre(val => val + 1)
|
||||
|
||||
stream.on.value(val => {
|
||||
// if this condition will be matched, this callback will be unsubscribed
|
||||
if (typeof val !== 'number') return erre.off()
|
||||
console.log(val)
|
||||
}) // 2
|
||||
stream.push(1)
|
||||
// this value will let the previous listener unsubscribe itself
|
||||
stream.push('foo')
|
||||
stream.push('1')
|
||||
// this value will not be logged because the stream.on.value was unsubscribed
|
||||
stream.push(2)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### erre.install(name, fn)
|
||||
##### @returns [`erre`](#errefunctions)
|
||||
|
||||
Extend erre adding custom API methods. Any plugin must have at lease a `name` (as string) and a `function`
|
||||
|
||||
<details>
|
||||
<summary>Example</summary>
|
||||
|
||||
```js
|
||||
// alias the `console.log` with `erre.log`
|
||||
erre.install('log', console.log)
|
||||
|
||||
const stream = erre(val => val + 1)
|
||||
|
||||
stream.on.value(erre.log) // 2, 3
|
||||
stream.push(1)
|
||||
stream.push(2)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
# TODO List
|
||||
|
||||
- [x] [erre.fromDOM](https://github.com/GianlucaGuarini/erre.fromDOM) - to stream DOM nodes events
|
||||
- [x] [erre.merge](https://github.com/GianlucaGuarini/erre.merge) - to merge multiple stream results into one
|
||||
|
||||
[ci-image]: https://img.shields.io/github/actions/workflow/status/gianlucaguarini/erre/test.yml?style=flat-square
|
||||
[ci-url]: https://github.com/gianlucaguarini/erre/actions
|
||||
|
||||
[license-image]:http://img.shields.io/badge/license-MIT-000000.svg?style=flat-square
|
||||
[license-url]:LICENSE
|
||||
|
||||
[npm-version-image]:http://img.shields.io/npm/v/erre.svg?style=flat-square
|
||||
[npm-downloads-image]:http://img.shields.io/npm/dm/erre.svg?style=flat-square
|
||||
[npm-url]:https://npmjs.org/package/erre
|
||||
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.erre = factory());
|
||||
})(this, (function () { 'use strict';
|
||||
|
||||
/**
|
||||
* Cancel token
|
||||
* @private
|
||||
* @type { Symbol }
|
||||
*/
|
||||
const CANCEL = Symbol();
|
||||
|
||||
/**
|
||||
* Helper that can be returned by ruit function to cancel the tasks chain
|
||||
* @returns { Symbol } internal private constant
|
||||
* @example
|
||||
*
|
||||
* ruit(
|
||||
* 100,
|
||||
* num => Math.random() * num
|
||||
* num => num > 50 ? ruit.cancel() : num
|
||||
* num => num - 2
|
||||
* ).then(result => {
|
||||
* console.log(result) // here we will get only number lower than 50
|
||||
* })
|
||||
*
|
||||
*/
|
||||
ruit.cancel = () => CANCEL;
|
||||
|
||||
/**
|
||||
* The same as ruit() but with the arguments inverted from right to left
|
||||
* @param { * } tasks - list of tasks to process sequentially
|
||||
* @returns { Promise } a promise containing the result of the whole chain
|
||||
* @example
|
||||
*
|
||||
* const curry = f => a => b => f(a, b)
|
||||
* const add = (a, b) => a + b
|
||||
*
|
||||
* const addOne = curry(add)(1)
|
||||
*
|
||||
* const squareAsync = (num) => {
|
||||
* return new Promise(r => {
|
||||
* setTimeout(r, 500, num * 2)
|
||||
* })
|
||||
* }
|
||||
*
|
||||
* // a -> a + a -> a * 2
|
||||
* // basically from right to left: 1 => 1 + 1 => 2 * 2
|
||||
* ruit.compose(squareAsync, addOne, 1).then(result => console.log(result)) // 4
|
||||
*/
|
||||
ruit.compose = (...tasks) => ruit(...tasks.reverse());
|
||||
|
||||
/**
|
||||
* Serialize a list of sync and async tasks from left to right
|
||||
* @param { * } tasks - list of tasks to process sequentially
|
||||
* @returns { Promise } a promise containing the result of the whole chain
|
||||
* @example
|
||||
*
|
||||
* const curry = f => a => b => f(a, b)
|
||||
* const add = (a, b) => a + b
|
||||
*
|
||||
* const addOne = curry(add)(1)
|
||||
*
|
||||
* const squareAsync = (num) => {
|
||||
* return new Promise(r => {
|
||||
* setTimeout(r, 500, num * 2)
|
||||
* })
|
||||
* }
|
||||
*
|
||||
* // a -> a + a -> a * 2
|
||||
* // basically from left to right: 1 => 1 + 1 => 2 * 2
|
||||
* ruit(1, addOne, squareAsync).then(result => console.log(result)) // 4
|
||||
*/
|
||||
function ruit(...tasks) {
|
||||
return new Promise((resolve, reject) => {
|
||||
return (function run(queue, result) {
|
||||
if (!queue.length) return resolve(result)
|
||||
|
||||
const [task, ...rest] = queue;
|
||||
const value = typeof task === 'function' ? task(result) : task;
|
||||
const done = v => run(rest, v);
|
||||
|
||||
// check against nil values
|
||||
if (value != null) {
|
||||
if (value === CANCEL) return
|
||||
if (value.then) return value.then(done, reject)
|
||||
}
|
||||
|
||||
return Promise.resolve(done(value))
|
||||
})(tasks)
|
||||
})
|
||||
}
|
||||
|
||||
// Store the erre the API methods to handle the plugins installation
|
||||
const API_METHODS = new Set();
|
||||
const UNSUBSCRIBE_SYMBOL = Symbol();
|
||||
const UNSUBSCRIBE_METHOD = 'off';
|
||||
const CANCEL_METHOD = 'cancel';
|
||||
|
||||
/**
|
||||
* Factory function to create the stream generator
|
||||
* @private
|
||||
* @param {Set} modifiers - stream input modifiers
|
||||
* @returns {Generator} the stream generator
|
||||
*/
|
||||
function createStream(modifiers) {
|
||||
const stream = (function *stream() {
|
||||
while (true) {
|
||||
// get the initial stream value
|
||||
const input = yield;
|
||||
|
||||
// run the input sequence
|
||||
yield ruit(input, ...modifiers);
|
||||
}
|
||||
})();
|
||||
|
||||
// start the stream
|
||||
stream.next();
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a value to several listeners
|
||||
* @private
|
||||
* @param {Set} callbacks - callbacks collection
|
||||
* @param {*} value - anything
|
||||
* @returns {Set} the callbacks received
|
||||
*/
|
||||
function dispatch(callbacks, value) {
|
||||
callbacks.forEach(f => {
|
||||
// unsubscribe the callback if erre.unsubscribe() will be returned
|
||||
if (f(value) === UNSUBSCRIBE_SYMBOL) callbacks.delete(f);
|
||||
});
|
||||
|
||||
return callbacks
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw a panic error
|
||||
* @param {string} message - error message
|
||||
* @returns {Error} an error object
|
||||
*/
|
||||
function panic(message) {
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Install an erre plugin adding it to the API
|
||||
* @param {string} name - plugin name
|
||||
* @param {Function} fn - new erre API method
|
||||
* @returns {Function} return the erre function
|
||||
*/
|
||||
erre.install = function(name, fn) {
|
||||
if (!name || typeof name !== 'string')
|
||||
panic('Please provide a name (as string) for your erre plugin');
|
||||
if (!fn || typeof fn !== 'function')
|
||||
panic('Please provide a function for your erre plugin');
|
||||
|
||||
if (API_METHODS.has(name)) {
|
||||
panic(`The ${name} is already part of the erre API, please provide a different name`);
|
||||
} else {
|
||||
erre[name] = fn;
|
||||
API_METHODS.add(name);
|
||||
}
|
||||
|
||||
return erre
|
||||
};
|
||||
|
||||
// alias for ruit canel to stop a stream chain
|
||||
erre.install(CANCEL_METHOD, ruit.cancel);
|
||||
|
||||
// unsubscribe helper
|
||||
erre.install(UNSUBSCRIBE_METHOD, () => UNSUBSCRIBE_SYMBOL);
|
||||
|
||||
/**
|
||||
* Stream constuction function
|
||||
* @param {...Function} fns - stream modifiers
|
||||
* @returns {Object} erre instance
|
||||
*/
|
||||
function erre(...fns) {
|
||||
const
|
||||
[success, error, end, modifiers] = [new Set(), new Set(), new Set(), new Set(fns)],
|
||||
generator = createStream(modifiers),
|
||||
stream = Object.create(generator),
|
||||
addToCollection = (collection) => (fn) => collection.add(fn) && stream,
|
||||
deleteFromCollection = (collection) => (fn) => collection.delete(fn) ? stream
|
||||
: panic('Couldn\'t remove handler passed by reference');
|
||||
|
||||
return Object.assign(stream, {
|
||||
on: Object.freeze({
|
||||
value: addToCollection(success),
|
||||
error: addToCollection(error),
|
||||
end: addToCollection(end)
|
||||
}),
|
||||
off: Object.freeze({
|
||||
value: deleteFromCollection(success),
|
||||
error: deleteFromCollection(error),
|
||||
end: deleteFromCollection(end)
|
||||
}),
|
||||
connect: addToCollection(modifiers),
|
||||
push(input) {
|
||||
const { value, done } = stream.next(input);
|
||||
|
||||
// dispatch the stream events
|
||||
if (!done) {
|
||||
value.then(
|
||||
res => dispatch(success, res),
|
||||
err => dispatch(error, err)
|
||||
);
|
||||
}
|
||||
|
||||
return stream
|
||||
},
|
||||
end() {
|
||||
// kill the stream
|
||||
generator.return();
|
||||
// dispatch the end event
|
||||
dispatch(end)
|
||||
// clean up all the collections
|
||||
;[success, error, end, modifiers].forEach(el => el.clear());
|
||||
|
||||
return stream
|
||||
},
|
||||
fork() {
|
||||
return erre(...modifiers)
|
||||
},
|
||||
next(input) {
|
||||
// get the input and run eventually the promise
|
||||
const result = generator.next(input);
|
||||
|
||||
// pause to the next iteration
|
||||
generator.next();
|
||||
|
||||
return result
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return erre;
|
||||
|
||||
}));
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
type Callback = (...args: any[]) => any
|
||||
|
||||
export declare function cancel(): symbol
|
||||
export declare function off(): symbol
|
||||
export declare function install(name: string, fn: Callback): any
|
||||
|
||||
export type ErreStream<T = unknown, TReturn = any, TNext = unknown> = {
|
||||
on: {
|
||||
value: (fn: Callback) => ErreStream<T, TReturn, TNext>
|
||||
error: (fn: Callback) => ErreStream<T, TReturn, TNext>
|
||||
end: (fn: Callback) => ErreStream<T, TReturn, TNext>
|
||||
}
|
||||
off: {
|
||||
value: (fn: Callback) => ErreStream<T, TReturn, TNext>
|
||||
error: (fn: Callback) => ErreStream<T, TReturn, TNext>
|
||||
end: (fn: Callback) => ErreStream<T, TReturn, TNext>
|
||||
}
|
||||
connect: (fn: Callback) => ErreStream<T, TReturn, TNext>
|
||||
push: (value: any) => ErreStream<T, TReturn, TNext>
|
||||
end: () => ErreStream<T, TReturn, TNext>
|
||||
fork: () => ErreStream<T, TReturn, TNext>
|
||||
} & Generator<T, TReturn, TNext>
|
||||
|
||||
declare const erre: <T = unknown, TReturn = any, TNext = unknown>(...fns: any[]) => ErreStream<T, TReturn, TNext>
|
||||
|
||||
export default erre
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Cancel token
|
||||
* @private
|
||||
* @type { Symbol }
|
||||
*/
|
||||
const CANCEL = Symbol();
|
||||
|
||||
/**
|
||||
* Helper that can be returned by ruit function to cancel the tasks chain
|
||||
* @returns { Symbol } internal private constant
|
||||
* @example
|
||||
*
|
||||
* ruit(
|
||||
* 100,
|
||||
* num => Math.random() * num
|
||||
* num => num > 50 ? ruit.cancel() : num
|
||||
* num => num - 2
|
||||
* ).then(result => {
|
||||
* console.log(result) // here we will get only number lower than 50
|
||||
* })
|
||||
*
|
||||
*/
|
||||
ruit.cancel = () => CANCEL;
|
||||
|
||||
/**
|
||||
* The same as ruit() but with the arguments inverted from right to left
|
||||
* @param { * } tasks - list of tasks to process sequentially
|
||||
* @returns { Promise } a promise containing the result of the whole chain
|
||||
* @example
|
||||
*
|
||||
* const curry = f => a => b => f(a, b)
|
||||
* const add = (a, b) => a + b
|
||||
*
|
||||
* const addOne = curry(add)(1)
|
||||
*
|
||||
* const squareAsync = (num) => {
|
||||
* return new Promise(r => {
|
||||
* setTimeout(r, 500, num * 2)
|
||||
* })
|
||||
* }
|
||||
*
|
||||
* // a -> a + a -> a * 2
|
||||
* // basically from right to left: 1 => 1 + 1 => 2 * 2
|
||||
* ruit.compose(squareAsync, addOne, 1).then(result => console.log(result)) // 4
|
||||
*/
|
||||
ruit.compose = (...tasks) => ruit(...tasks.reverse());
|
||||
|
||||
/**
|
||||
* Serialize a list of sync and async tasks from left to right
|
||||
* @param { * } tasks - list of tasks to process sequentially
|
||||
* @returns { Promise } a promise containing the result of the whole chain
|
||||
* @example
|
||||
*
|
||||
* const curry = f => a => b => f(a, b)
|
||||
* const add = (a, b) => a + b
|
||||
*
|
||||
* const addOne = curry(add)(1)
|
||||
*
|
||||
* const squareAsync = (num) => {
|
||||
* return new Promise(r => {
|
||||
* setTimeout(r, 500, num * 2)
|
||||
* })
|
||||
* }
|
||||
*
|
||||
* // a -> a + a -> a * 2
|
||||
* // basically from left to right: 1 => 1 + 1 => 2 * 2
|
||||
* ruit(1, addOne, squareAsync).then(result => console.log(result)) // 4
|
||||
*/
|
||||
function ruit(...tasks) {
|
||||
return new Promise((resolve, reject) => {
|
||||
return (function run(queue, result) {
|
||||
if (!queue.length) return resolve(result)
|
||||
|
||||
const [task, ...rest] = queue;
|
||||
const value = typeof task === 'function' ? task(result) : task;
|
||||
const done = v => run(rest, v);
|
||||
|
||||
// check against nil values
|
||||
if (value != null) {
|
||||
if (value === CANCEL) return
|
||||
if (value.then) return value.then(done, reject)
|
||||
}
|
||||
|
||||
return Promise.resolve(done(value))
|
||||
})(tasks)
|
||||
})
|
||||
}
|
||||
|
||||
// Store the erre the API methods to handle the plugins installation
|
||||
const API_METHODS = new Set();
|
||||
const UNSUBSCRIBE_SYMBOL = Symbol();
|
||||
const UNSUBSCRIBE_METHOD = 'off';
|
||||
const CANCEL_METHOD = 'cancel';
|
||||
|
||||
/**
|
||||
* Factory function to create the stream generator
|
||||
* @private
|
||||
* @param {Set} modifiers - stream input modifiers
|
||||
* @returns {Generator} the stream generator
|
||||
*/
|
||||
function createStream(modifiers) {
|
||||
const stream = (function *stream() {
|
||||
while (true) {
|
||||
// get the initial stream value
|
||||
const input = yield;
|
||||
|
||||
// run the input sequence
|
||||
yield ruit(input, ...modifiers);
|
||||
}
|
||||
})();
|
||||
|
||||
// start the stream
|
||||
stream.next();
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a value to several listeners
|
||||
* @private
|
||||
* @param {Set} callbacks - callbacks collection
|
||||
* @param {*} value - anything
|
||||
* @returns {Set} the callbacks received
|
||||
*/
|
||||
function dispatch(callbacks, value) {
|
||||
callbacks.forEach(f => {
|
||||
// unsubscribe the callback if erre.unsubscribe() will be returned
|
||||
if (f(value) === UNSUBSCRIBE_SYMBOL) callbacks.delete(f);
|
||||
});
|
||||
|
||||
return callbacks
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw a panic error
|
||||
* @param {string} message - error message
|
||||
* @returns {Error} an error object
|
||||
*/
|
||||
function panic(message) {
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Install an erre plugin adding it to the API
|
||||
* @param {string} name - plugin name
|
||||
* @param {Function} fn - new erre API method
|
||||
* @returns {Function} return the erre function
|
||||
*/
|
||||
erre.install = function(name, fn) {
|
||||
if (!name || typeof name !== 'string')
|
||||
panic('Please provide a name (as string) for your erre plugin');
|
||||
if (!fn || typeof fn !== 'function')
|
||||
panic('Please provide a function for your erre plugin');
|
||||
|
||||
if (API_METHODS.has(name)) {
|
||||
panic(`The ${name} is already part of the erre API, please provide a different name`);
|
||||
} else {
|
||||
erre[name] = fn;
|
||||
API_METHODS.add(name);
|
||||
}
|
||||
|
||||
return erre
|
||||
};
|
||||
|
||||
// alias for ruit canel to stop a stream chain
|
||||
erre.install(CANCEL_METHOD, ruit.cancel);
|
||||
|
||||
// unsubscribe helper
|
||||
erre.install(UNSUBSCRIBE_METHOD, () => UNSUBSCRIBE_SYMBOL);
|
||||
|
||||
/**
|
||||
* Stream constuction function
|
||||
* @param {...Function} fns - stream modifiers
|
||||
* @returns {Object} erre instance
|
||||
*/
|
||||
function erre(...fns) {
|
||||
const
|
||||
[success, error, end, modifiers] = [new Set(), new Set(), new Set(), new Set(fns)],
|
||||
generator = createStream(modifiers),
|
||||
stream = Object.create(generator),
|
||||
addToCollection = (collection) => (fn) => collection.add(fn) && stream,
|
||||
deleteFromCollection = (collection) => (fn) => collection.delete(fn) ? stream
|
||||
: panic('Couldn\'t remove handler passed by reference');
|
||||
|
||||
return Object.assign(stream, {
|
||||
on: Object.freeze({
|
||||
value: addToCollection(success),
|
||||
error: addToCollection(error),
|
||||
end: addToCollection(end)
|
||||
}),
|
||||
off: Object.freeze({
|
||||
value: deleteFromCollection(success),
|
||||
error: deleteFromCollection(error),
|
||||
end: deleteFromCollection(end)
|
||||
}),
|
||||
connect: addToCollection(modifiers),
|
||||
push(input) {
|
||||
const { value, done } = stream.next(input);
|
||||
|
||||
// dispatch the stream events
|
||||
if (!done) {
|
||||
value.then(
|
||||
res => dispatch(success, res),
|
||||
err => dispatch(error, err)
|
||||
);
|
||||
}
|
||||
|
||||
return stream
|
||||
},
|
||||
end() {
|
||||
// kill the stream
|
||||
generator.return();
|
||||
// dispatch the end event
|
||||
dispatch(end)
|
||||
// clean up all the collections
|
||||
;[success, error, end, modifiers].forEach(el => el.clear());
|
||||
|
||||
return stream
|
||||
},
|
||||
fork() {
|
||||
return erre(...modifiers)
|
||||
},
|
||||
next(input) {
|
||||
// get the input and run eventually the promise
|
||||
const result = generator.next(input);
|
||||
|
||||
// pause to the next iteration
|
||||
generator.next();
|
||||
|
||||
return result
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export { erre as default };
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
import ruit from 'ruit'
|
||||
|
||||
// Store the erre the API methods to handle the plugins installation
|
||||
const API_METHODS = new Set()
|
||||
const UNSUBSCRIBE_SYMBOL = Symbol()
|
||||
const UNSUBSCRIBE_METHOD = 'off'
|
||||
const CANCEL_METHOD = 'cancel'
|
||||
|
||||
/**
|
||||
* Factory function to create the stream generator
|
||||
* @private
|
||||
* @param {Set} modifiers - stream input modifiers
|
||||
* @returns {Generator} the stream generator
|
||||
*/
|
||||
function createStream(modifiers) {
|
||||
const stream = (function *stream() {
|
||||
while (true) {
|
||||
// get the initial stream value
|
||||
const input = yield
|
||||
|
||||
// run the input sequence
|
||||
yield ruit(input, ...modifiers)
|
||||
}
|
||||
})()
|
||||
|
||||
// start the stream
|
||||
stream.next()
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a value to several listeners
|
||||
* @private
|
||||
* @param {Set} callbacks - callbacks collection
|
||||
* @param {*} value - anything
|
||||
* @returns {Set} the callbacks received
|
||||
*/
|
||||
function dispatch(callbacks, value) {
|
||||
callbacks.forEach(f => {
|
||||
// unsubscribe the callback if erre.unsubscribe() will be returned
|
||||
if (f(value) === UNSUBSCRIBE_SYMBOL) callbacks.delete(f)
|
||||
})
|
||||
|
||||
return callbacks
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw a panic error
|
||||
* @param {string} message - error message
|
||||
* @returns {Error} an error object
|
||||
*/
|
||||
function panic(message) {
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Install an erre plugin adding it to the API
|
||||
* @param {string} name - plugin name
|
||||
* @param {Function} fn - new erre API method
|
||||
* @returns {Function} return the erre function
|
||||
*/
|
||||
erre.install = function(name, fn) {
|
||||
if (!name || typeof name !== 'string')
|
||||
panic('Please provide a name (as string) for your erre plugin')
|
||||
if (!fn || typeof fn !== 'function')
|
||||
panic('Please provide a function for your erre plugin')
|
||||
|
||||
if (API_METHODS.has(name)) {
|
||||
panic(`The ${name} is already part of the erre API, please provide a different name`)
|
||||
} else {
|
||||
erre[name] = fn
|
||||
API_METHODS.add(name)
|
||||
}
|
||||
|
||||
return erre
|
||||
}
|
||||
|
||||
// alias for ruit canel to stop a stream chain
|
||||
erre.install(CANCEL_METHOD, ruit.cancel)
|
||||
|
||||
// unsubscribe helper
|
||||
erre.install(UNSUBSCRIBE_METHOD, () => UNSUBSCRIBE_SYMBOL)
|
||||
|
||||
/**
|
||||
* Stream constuction function
|
||||
* @param {...Function} fns - stream modifiers
|
||||
* @returns {Object} erre instance
|
||||
*/
|
||||
export default function erre(...fns) {
|
||||
const
|
||||
[success, error, end, modifiers] = [new Set(), new Set(), new Set(), new Set(fns)],
|
||||
generator = createStream(modifiers),
|
||||
stream = Object.create(generator),
|
||||
addToCollection = (collection) => (fn) => collection.add(fn) && stream,
|
||||
deleteFromCollection = (collection) => (fn) => collection.delete(fn) ? stream
|
||||
: panic('Couldn\'t remove handler passed by reference')
|
||||
|
||||
return Object.assign(stream, {
|
||||
on: Object.freeze({
|
||||
value: addToCollection(success),
|
||||
error: addToCollection(error),
|
||||
end: addToCollection(end)
|
||||
}),
|
||||
off: Object.freeze({
|
||||
value: deleteFromCollection(success),
|
||||
error: deleteFromCollection(error),
|
||||
end: deleteFromCollection(end)
|
||||
}),
|
||||
connect: addToCollection(modifiers),
|
||||
push(input) {
|
||||
const { value, done } = stream.next(input)
|
||||
|
||||
// dispatch the stream events
|
||||
if (!done) {
|
||||
value.then(
|
||||
res => dispatch(success, res),
|
||||
err => dispatch(error, err)
|
||||
)
|
||||
}
|
||||
|
||||
return stream
|
||||
},
|
||||
end() {
|
||||
// kill the stream
|
||||
generator.return()
|
||||
// dispatch the end event
|
||||
dispatch(end)
|
||||
// clean up all the collections
|
||||
;[success, error, end, modifiers].forEach(el => el.clear())
|
||||
|
||||
return stream
|
||||
},
|
||||
fork() {
|
||||
return erre(...modifiers)
|
||||
},
|
||||
next(input) {
|
||||
// get the input and run eventually the promise
|
||||
const result = generator.next(input)
|
||||
|
||||
// pause to the next iteration
|
||||
generator.next()
|
||||
|
||||
return result
|
||||
}
|
||||
})
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "erre",
|
||||
"version": "3.0.1",
|
||||
"description": "Modern, performant and tiny streams script using generators",
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"module": "index.js",
|
||||
"exports": {
|
||||
"types": "./index.d.ts",
|
||||
"import": "./index.js",
|
||||
"require": "./index.cjs"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "npm run build && npm test",
|
||||
"lint": "eslint index.next.js test.js rollup.config.js",
|
||||
"build": "rollup -c",
|
||||
"bench": "node bench",
|
||||
"test": "npm run lint && mocha test.js"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
"index.cjs",
|
||||
"index.d.ts",
|
||||
"index.next.js"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/gianlucaguarini/erre.git"
|
||||
},
|
||||
"keywords": [
|
||||
"es6",
|
||||
"es2015",
|
||||
"streams",
|
||||
"generator",
|
||||
"functional",
|
||||
"reactive"
|
||||
],
|
||||
"author": "Gianluca Guarini <gianluca.guarini@gmail.com> (http://gianlucaguarini.com)",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/gianlucaguarini/erre/issues"
|
||||
},
|
||||
"homepage": "https://github.com/gianlucaguarini/erre#readme",
|
||||
"devDependencies": {
|
||||
"@gianlucaguarini/eslint-config": "^2.0.0",
|
||||
"benchmark": "^2.1.4",
|
||||
"eslint": "^8.49.0",
|
||||
"mocha": "^10.2.0",
|
||||
"rollup": "^3.29.2",
|
||||
"rollup-plugin-node-resolve": "^5.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ruit": "^1.0.4"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user