├── .gitignore ├── readme.md ├── slide-10 ├── test.js └── rule.js ├── slide-6 ├── test.js └── rule.js ├── slide-18 ├── rule.js ├── test.js ├── transducers.js └── bonus.js ├── package.json ├── slide-15 └── test.js ├── slide-14 ├── rule.js ├── transducers.js └── test.js ├── slide-5 ├── test.js └── rule.js ├── slide-16 └── test.js └── data ├── transactions.js └── transactions.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Project exclude paths 2 | /node_modules/ -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Code du meetup LyonJS, le 21/07/2021 2 | 3 | les slides sont disponibles [ici](https://docs.google.com/presentation/d/e/2PACX-1vQY0ukjI8MU29DTsfsErkWWJSj91GK6oY-kUTHRkvSOF5ziB1bFrhbaE42kovlp3KS4abc6YfutpSVV/pub?start=false&loop=false&delayms=3000) 4 | -------------------------------------------------------------------------------- /slide-10/test.js: -------------------------------------------------------------------------------- 1 | import {test} from 'zora'; 2 | import rule from './rule.js'; 3 | import fixture from '../data/transactions.js'; 4 | 5 | test('rule should return the sum of positive balances where category is "income"', ({eq}) => { 6 | eq(rule(fixture), 19000, '4500 + 8000 + 6500'); 7 | }); 8 | -------------------------------------------------------------------------------- /slide-6/test.js: -------------------------------------------------------------------------------- 1 | import {test} from 'zora'; 2 | import rule from './rule.js'; 3 | import fixture from '../data/transactions.js'; 4 | 5 | test('rule should return the sum of positive balances where category is "income"', ({eq}) => { 6 | eq(rule(fixture), 19000, '4500 + 8000 + 6500'); 7 | }); 8 | -------------------------------------------------------------------------------- /slide-18/rule.js: -------------------------------------------------------------------------------- 1 | import {compose} from '../slide-10/rule.js'; 2 | import {isBalancePositive, isCategoryIncome, pickBalance, sum} from '../slide-5/rule.js'; 3 | import {filter, map, reduce} from './transducers.js'; 4 | 5 | export default compose([ 6 | reduce(sum, 0), 7 | map(pickBalance), 8 | filter(isBalancePositive), 9 | filter(isCategoryIncome) 10 | ]); 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lyon-js", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "pta": "~1.0.0-alpha.2", 14 | "zora": "~5.0.0-beta.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /slide-6/rule.js: -------------------------------------------------------------------------------- 1 | import {isBalancePositive, isCategoryIncome, pickBalance, sum} from '../slide-5/rule.js'; 2 | 3 | export default (transactions) => { 4 | let total = 0; 5 | for (const transaction of transactions) { 6 | if(isBalancePositive(transaction) && isCategoryIncome(transaction)){ 7 | total = sum(total, pickBalance(transaction)); 8 | } 9 | } 10 | return total; 11 | } 12 | -------------------------------------------------------------------------------- /slide-15/test.js: -------------------------------------------------------------------------------- 1 | import {test} from 'zora'; 2 | 3 | test('array is an iterable', ({eq, fail}) =>{ 4 | const array = [1,2,3,4]; 5 | 6 | eq(typeof array[Symbol.iterator], 'function'); 7 | eq([...array], array); 8 | 9 | try{ 10 | for (const number of array) { 11 | console.log(number); 12 | } 13 | } catch (e){ 14 | fail('should not throw'); 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /slide-14/rule.js: -------------------------------------------------------------------------------- 1 | import {isBalancePositive, isCategoryIncome, pickBalance, sum} from '../slide-5/rule.js'; 2 | import {compose} from '../slide-10/rule.js'; 3 | import {filter, map, transduceArrayValues, transduceEventEmitter} from './transducers.js'; 4 | 5 | export const pipeline = compose([ 6 | filter(isCategoryIncome), 7 | filter(isBalancePositive), 8 | map(pickBalance) 9 | ])(sum); 10 | 11 | export const fromArray = transduceArrayValues(pipeline); 12 | export const fromEventEmitter = transduceEventEmitter(pipeline); 13 | -------------------------------------------------------------------------------- /slide-5/test.js: -------------------------------------------------------------------------------- 1 | import {test} from 'zora'; 2 | import rule, {isBalancePositive} from './rule.js'; 3 | import fixture from '../data/transactions.js'; 4 | 5 | test('rule should return the sum of positive balances where category is "income"', ({eq}) => { 6 | eq(rule(fixture), 19000, '4500 + 8000 + 6500'); 7 | }); 8 | 9 | test.skip('utility functions', ({test}) => { 10 | test('isBalancePositive', ({eq}) => { 11 | eq(isBalancePositive({balance: 32}), true); 12 | eq(isBalancePositive({balance: -43}), false); 13 | }); 14 | 15 | /* etc */ 16 | }); 17 | -------------------------------------------------------------------------------- /slide-10/rule.js: -------------------------------------------------------------------------------- 1 | import {isBalancePositive, isCategoryIncome, pickBalance, sum} from '../slide-5/rule.js'; 2 | 3 | export const compose = (fns) => (arg) => fns.reduceRight((y, nextFunc) => nextFunc(y), arg); 4 | 5 | const filter = (predicate) => (filterable) => filterable.filter(predicate); 6 | 7 | const map = (mapFn) => (functor) => functor.map(mapFn); 8 | 9 | const reduce = (reducer, init) => (foldable) => foldable.reduce(reducer, init); 10 | 11 | export default compose([ 12 | reduce(sum, 0), 13 | map(pickBalance), 14 | filter(isBalancePositive), 15 | filter(isCategoryIncome) 16 | ]); 17 | -------------------------------------------------------------------------------- /slide-16/test.js: -------------------------------------------------------------------------------- 1 | import {test} from 'zora'; 2 | 3 | test('generator', ({eq}) => { 4 | 5 | function* countDown(limit = 5) { 6 | while (limit >= 0) { 7 | yield limit; 8 | limit--; 9 | } 10 | } 11 | 12 | eq([...countDown(3)], [3, 2, 1, 0]); 13 | 14 | for (const number of countDown(5)) { 15 | console.log(number); 16 | } 17 | }); 18 | 19 | test('recursive yield', ({eq}) => { 20 | 21 | function* generator(limit ) { 22 | yield 1; 23 | yield 2; 24 | yield *[3,4] 25 | } 26 | 27 | eq([...generator()], [1, 2, 3, 4]); 28 | }); 29 | -------------------------------------------------------------------------------- /slide-5/rule.js: -------------------------------------------------------------------------------- 1 | export default (transactions) => transactions 2 | .filter(isCategoryIncome) 3 | .filter(isBalancePositive) 4 | .map(pickBalance) 5 | .reduce(sum, 0) 6 | 7 | export const isCategoryIncome = (transaction) => { 8 | console.log('isCategoryIncome'); 9 | return transaction.category === 'income'; 10 | } 11 | export const isBalancePositive = ({balance}) => { 12 | console.log('isBalancePositive'); 13 | return balance > 0; 14 | }; 15 | export const pickBalance = ({balance}) => { 16 | console.log('pickBalance'); 17 | return balance; 18 | } 19 | export const sum = (total, balance) => { 20 | console.log('sum'); 21 | return total + balance; 22 | }; 23 | -------------------------------------------------------------------------------- /slide-14/transducers.js: -------------------------------------------------------------------------------- 1 | export const filter = (predicate) => (reducer) => ((acc, curr) => predicate(curr) ? reducer(acc, curr) : acc); 2 | 3 | export const map = (mapFn) => (reducer) => ((acc, curr) => reducer(acc, mapFn(curr))); 4 | 5 | 6 | // transduce different protocols 7 | export const transduceArrayValues = (transducers, init = 0) => (array) => array.reduce(transducers, init); 8 | 9 | export const transduceEventEmitter = (transducers, init = 0) => (emitter, eventName) => new Promise(resolve => { 10 | let current = init; 11 | const listener = (transaction) => current = transducers(current, transaction); 12 | emitter.on(eventName, listener); 13 | emitter.on('removeListener', (event) => { 14 | if (eventName === event) { 15 | resolve(current); 16 | } 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /slide-14/test.js: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'events'; 2 | import {test} from 'zora'; 3 | import {fromArray, fromEventEmitter} from './rule.js'; 4 | import fixture from '../data/transactions.js'; 5 | import transactions from '../data/transactions.js'; 6 | 7 | test('rule should return the sum of positive balances where category is "income"', ({eq}) => { 8 | eq(fromArray(fixture), 19000, '4500 + 8000 + 6500'); 9 | }); 10 | 11 | test('using an event emitter', async ({eq}) => { 12 | const emitter = new EventEmitter(); 13 | 14 | const awaitingValue = fromEventEmitter(emitter, 'transaction'); 15 | 16 | for (const transaction of transactions) { 17 | emitter.emit('transaction', transaction); 18 | } 19 | 20 | // stop stream 21 | emitter.removeAllListeners('transaction'); 22 | 23 | eq(await awaitingValue, 19000, '4500 + 8000 + 6500'); 24 | }); 25 | -------------------------------------------------------------------------------- /data/transactions.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | {description:'paiement madame Dupont', balance:4500, category:'income', date:'2021-01-15'}, 3 | {description:'paiement Mr Georges', balance:8000, category:'income', date:'2021-01-19'}, 4 | {description:'facture fournisseur', balance:-9900, category:'spending', date:'2021-01-17'}, 5 | {description:'remboursement TCL', balance:2500, category:'transport', date:'2021-02-15'}, 6 | {description:'plein essence', balance:-6000, category:'transport', date:'2021-03-15'}, 7 | {description:'facture internet', balance:-3500, category:'furniture', date:'2021-04-06'}, 8 | {description:'garantie Darty', balance:12000, category:'furniture', date:'2021-03-19'}, 9 | {description:'remboursement fournisseur', balance:1500, category:'spending', date:'2021-01-15'}, 10 | {description:'paiement famille Fabre', balance:6500, category:'income', date:'2021-05-06'}, 11 | {description:'remboursement Mr Georges', balance:-2000, category:'income', date:'2021-04-19'}, 12 | ]; 13 | -------------------------------------------------------------------------------- /data/transactions.txt: -------------------------------------------------------------------------------- 1 | {"description":"paiement madame Dupont", "balance":4500, "category":"income", "date":"2021-01-15"} 2 | {"description":"paiement Mr Georges", "balance":8000, "category":"income", "date":"2021-01-19"} 3 | {"description":"facture fournisseur", "balance":-9900, "category":"spending", "date":"2021-01-17"} 4 | {"description":"remboursement TCL", "balance":2500, "category":"transport", "date":"2021-02-15"} 5 | {"description":"plein essence", "balance":-6000, "category":"transport", "date":"2021-03-15"} 6 | {"description":"facture internet", "balance":-3500, "category":"furniture", "date":"2021-04-06"} 7 | {"description":"garantie Darty", "balance":12000, "category":"furniture", "date":"2021-03-19"} 8 | {"description":"remboursement fournisseur", "balance":1500, "category":"spending", "date":"2021-01-15"} 9 | {"description":"paiement famille Fabre", "balance":6500, "category":"income", "date":"2021-05-06"} 10 | {"description":"remboursement Mr Georges", "balance":-2000, "category":"income", "date":"2021-04-19"} 11 | -------------------------------------------------------------------------------- /slide-18/test.js: -------------------------------------------------------------------------------- 1 | import {createReadStream} from 'fs'; 2 | import {EventEmitter, on} from 'events'; 3 | import {test} from 'zora'; 4 | import rule from './rule.js'; 5 | import fixture from '../data/transactions.js'; 6 | import transactions from '../data/transactions.js'; 7 | import {compose} from '../slide-10/rule.js'; 8 | import {map, split} from './transducers.js'; 9 | 10 | const wait = (time = 200) => new Promise((resolve) => setTimeout(() => resolve(), time)); 11 | 12 | test('rule should return the sum of positive balances where category is "income"', (t) => { 13 | t.test('from array', async ({eq}) => { 14 | eq(await rule(fixture), 19000, '4500 + 8000 + 6500'); 15 | }); 16 | 17 | t.test('from Readable stream', async ({eq}) => { 18 | const fileStream = createReadStream('./data/transactions.txt', {encoding: 'utf8'}); 19 | 20 | const readSource = compose([ 21 | map(JSON.parse), 22 | split() 23 | ]); 24 | 25 | const pipeline = compose([rule, readSource]); 26 | 27 | eq(await pipeline(fileStream), 19000, '4500 + 8000 + 6500'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /slide-18/transducers.js: -------------------------------------------------------------------------------- 1 | import {EOL} from 'os'; 2 | 3 | export const filter = (predicate) => async function* (stream) { 4 | for await (const element of stream) { 5 | if (predicate(element)) { 6 | yield element; 7 | } 8 | } 9 | }; 10 | 11 | export const map = (mapFn) => async function* (stream) { 12 | for await (const element of stream) { 13 | yield mapFn(element); 14 | } 15 | }; 16 | 17 | export const reduce = (fn, init = 0) => async (stream) => { 18 | let acc = init; 19 | for await (const element of stream) { 20 | acc = fn(acc, element); 21 | } 22 | return acc; 23 | } 24 | 25 | export const split = (char = EOL) => async function* (stream) { 26 | let buffer = ''; 27 | for await (const element of stream) { 28 | const newParts = (buffer + element).toString().split(char); 29 | buffer = newParts.pop(); 30 | yield* newParts; 31 | } 32 | if (buffer) { 33 | yield buffer; 34 | } 35 | }; 36 | 37 | // export const take = (number) => async function* (stream) { 38 | // for await (const element of stream) { 39 | // if (number <= 0) { 40 | // break; 41 | // } 42 | // yield element; 43 | // number--; 44 | // } 45 | // }; 46 | -------------------------------------------------------------------------------- /slide-18/bonus.js: -------------------------------------------------------------------------------- 1 | import {pipeline, Readable} from 'stream'; 2 | import {createReadStream, createWriteStream} from 'fs'; 3 | import {map, split} from './transducers.js'; 4 | import {on, EventEmitter} from 'events'; 5 | 6 | // ex readable from iterator 7 | const wait = (time = 1000) => new Promise((resolve) => { 8 | setTimeout(() => resolve(), time); 9 | }); 10 | // 11 | const generator = async function* () { 12 | yield 'hello\n'; 13 | await wait(); 14 | yield 'world\n'; 15 | await wait(); 16 | yield '!\n'; 17 | }; 18 | // // 19 | const readable = Readable.from(generator()); 20 | readable.pipe(process.stdout); 21 | 22 | // ex event emitter as async iterable 23 | // const emitter = new EventEmitter(); 24 | // process.nextTick(() =>{ 25 | // emitter.emit('foo','hello'); 26 | // emitter.emit('foo','world'); 27 | // emitter.removeAllListeners('foo'); 28 | // }); 29 | // 30 | // for await(const event of on(emitter,'foo')) { 31 | // console.log(event); 32 | // } 33 | 34 | // ex pipeline 35 | pipeline( 36 | createReadStream('./data/transactions.txt', {encoding: 'utf8'}), 37 | split(), 38 | map(JSON.parse), 39 | map((obj) => `${obj.description}\n`), 40 | process.stdout, 41 | (err) =>{ 42 | if (err) { 43 | console.error('Pipeline failed.', err); 44 | } else { 45 | console.log('Pipeline succeeded.'); 46 | } 47 | } 48 | ); 49 | 50 | 51 | 52 | 53 | 54 | 55 | --------------------------------------------------------------------------------