├── .gitignore ├── test ├── solutions │ ├── beep_boop.js │ ├── input_output.js │ ├── meet_pipe.js │ ├── crypt.js │ ├── read_it.js │ ├── concat.js │ ├── transform.js │ ├── websockets.js │ ├── duplexer.js │ ├── http_client.js │ ├── write_to_me.js │ ├── html_stream.js │ ├── lines.js │ ├── http_server.js │ ├── duplexer_redux.js │ ├── secretz.js │ └── combiner.js └── check.js ├── problems ├── beep_boop │ ├── solution.js │ ├── index.js │ └── problem.md ├── input_output │ ├── solution.js │ ├── problem.md │ └── index.js ├── secretz │ ├── secretz.tar.gz │ ├── extra.sh │ ├── solution.js │ ├── index.js │ └── problem.md ├── meet_pipe │ ├── solution.js │ ├── index.js │ └── problem.md ├── crypt │ ├── solution.js │ ├── index.js │ └── problem.md ├── concat │ ├── solution.js │ ├── problem.md │ └── index.js ├── transform │ ├── index.js │ ├── solution.js │ └── problem.md ├── write_to_me │ ├── index.js │ ├── solution.js │ └── problem.md ├── html_stream │ ├── index.js │ ├── solution.js │ ├── input.html │ ├── expected.html │ └── problem.md ├── websockets │ ├── solution.js │ ├── problem.md │ └── index.js ├── duplexer │ ├── solution.js │ ├── command.js │ ├── index.js │ └── problem.md ├── http_client │ ├── solution.js │ ├── problem.md │ └── index.js ├── read_it │ ├── solution.js │ ├── index.js │ └── problem.md ├── lines │ ├── index.js │ ├── solution.js │ └── problem.md ├── http_server │ ├── solution.js │ ├── index.js │ └── problem.md ├── duplexer_redux │ ├── solution.js │ ├── index.js │ └── problem.md └── combiner │ ├── solution.js │ ├── expected.json │ ├── books.json │ ├── index.js │ └── problem.md ├── bin └── cmd.js ├── .npmignore ├── lib ├── basicExercise.js ├── duplexExercise.js ├── cipherExercise.js ├── stdinExercise.js ├── stdinStreamExercise.js ├── exercise.js ├── solutionSetup.js ├── stdinStreamProcessor.js ├── words.json ├── finnegans_wake.txt ├── stdinProcessor.js ├── cipherProcessor.js ├── utils.js ├── exportFnExercise.js └── aliens.json ├── menu.json ├── i18n └── en.json ├── .github └── workflows │ └── master.yml ├── README.md ├── index.js ├── CONTRIBUTING.md ├── LICENSE ├── package.json └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /test/solutions/beep_boop.js: -------------------------------------------------------------------------------- 1 | console.log('beep boop') 2 | -------------------------------------------------------------------------------- /problems/beep_boop/solution.js: -------------------------------------------------------------------------------- 1 | console.log('beep boop') 2 | -------------------------------------------------------------------------------- /problems/input_output/solution.js: -------------------------------------------------------------------------------- 1 | process.stdin.pipe(process.stdout) 2 | -------------------------------------------------------------------------------- /test/solutions/input_output.js: -------------------------------------------------------------------------------- 1 | process.stdin.pipe(process.stdout) 2 | -------------------------------------------------------------------------------- /bin/cmd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../index').execute(process.argv.slice(2)) 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | solutions/* 2 | data/completed.json 3 | data/current.json 4 | todo.txt 5 | problems/meet_pipe/data.txt 6 | -------------------------------------------------------------------------------- /problems/beep_boop/index.js: -------------------------------------------------------------------------------- 1 | const exercise = require('../../lib/basicExercise') 2 | 3 | module.exports = exercise 4 | -------------------------------------------------------------------------------- /test/solutions/meet_pipe.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | fs.createReadStream(process.argv[2]).pipe(process.stdout) 4 | -------------------------------------------------------------------------------- /problems/secretz/secretz.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workshopper/stream-adventure/HEAD/problems/secretz/secretz.tar.gz -------------------------------------------------------------------------------- /problems/meet_pipe/solution.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const file = process.argv[2] 3 | 4 | fs.createReadStream(file).pipe(process.stdout) 5 | -------------------------------------------------------------------------------- /problems/secretz/extra.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | openssl enc -d -$1 -pass pass:$2 -nosalt \ 4 | | tar xz --to-command='md5sum | head -c 33; echo $TAR_FILENAME' 5 | -------------------------------------------------------------------------------- /problems/input_output/problem.md: -------------------------------------------------------------------------------- 1 | Take data from `process.stdin` and pipe it to `process.stdout`. 2 | 3 | With `.pipe()`. `process.stdin.pipe()` to be exact. 4 | 5 | Don't overthink this. 6 | -------------------------------------------------------------------------------- /test/solutions/crypt.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | 3 | process.stdin 4 | .pipe(crypto.createDecipheriv('aes256', process.argv[2], process.argv[3])) 5 | .pipe(process.stdout) 6 | -------------------------------------------------------------------------------- /problems/crypt/solution.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | 3 | process.stdin 4 | .pipe(crypto.createDecipheriv('aes256', process.argv[2], process.argv[3])) 5 | .pipe(process.stdout) 6 | -------------------------------------------------------------------------------- /test/solutions/read_it.js: -------------------------------------------------------------------------------- 1 | const { Readable } = require('stream') 2 | 3 | const stream = new Readable({ 4 | read () {} 5 | }) 6 | 7 | stream.push(process.argv[2]) 8 | stream.pipe(process.stdout) 9 | -------------------------------------------------------------------------------- /lib/basicExercise.js: -------------------------------------------------------------------------------- 1 | const comparestdout = require('workshopper-exercise/comparestdout') 2 | 3 | let exercise = require('./exercise') 4 | 5 | exercise = comparestdout(exercise) 6 | 7 | module.exports = exercise 8 | -------------------------------------------------------------------------------- /test/solutions/concat.js: -------------------------------------------------------------------------------- 1 | const concat = require('concat-stream') 2 | 3 | process.stdin.pipe(concat(function (src) { 4 | const s = src.toString().split('').reverse().join('') 5 | process.stdout.write(s) 6 | })) 7 | -------------------------------------------------------------------------------- /test/solutions/transform.js: -------------------------------------------------------------------------------- 1 | const through = require('through2') 2 | 3 | process.stdin.pipe(through(function (buf, _, next) { 4 | this.push(buf.toString().toUpperCase()) 5 | next() 6 | })).pipe(process.stdout) 7 | -------------------------------------------------------------------------------- /problems/concat/solution.js: -------------------------------------------------------------------------------- 1 | const concat = require('concat-stream') 2 | 3 | process.stdin.pipe(concat(function (src) { 4 | const s = src.toString().split('').reverse().join('') 5 | process.stdout.write(s) 6 | })) 7 | -------------------------------------------------------------------------------- /lib/duplexExercise.js: -------------------------------------------------------------------------------- 1 | const comparestdout = require('workshopper-exercise/comparestdout') 2 | 3 | let exercise = require('./exportFnExercise') 4 | 5 | exercise = comparestdout(exercise) 6 | 7 | module.exports = exercise 8 | -------------------------------------------------------------------------------- /problems/transform/index.js: -------------------------------------------------------------------------------- 1 | const exercise = require('../../lib/stdinExercise') 2 | const { inputFromAliens } = require('../../lib/utils') 3 | 4 | exercise.inputStdin = inputFromAliens() 5 | 6 | module.exports = exercise 7 | -------------------------------------------------------------------------------- /problems/write_to_me/index.js: -------------------------------------------------------------------------------- 1 | const exercise = require('../../lib/stdinExercise') 2 | const { inputFromAliens } = require('../../lib/utils') 3 | 4 | exercise.inputStdin = inputFromAliens() 5 | 6 | module.exports = exercise 7 | -------------------------------------------------------------------------------- /problems/input_output/index.js: -------------------------------------------------------------------------------- 1 | const exercise = require('../../lib/stdinExercise') 2 | const { inputFromAliens } = require('../../lib/utils') 3 | 4 | exercise.inputStdin = inputFromAliens() 5 | 6 | module.exports = exercise 7 | -------------------------------------------------------------------------------- /problems/html_stream/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const exercise = require('../../lib/stdinStreamExercise') 4 | 5 | exercise.inputFilePath = path.join(__dirname, '/input.html') 6 | 7 | module.exports = exercise 8 | -------------------------------------------------------------------------------- /problems/transform/solution.js: -------------------------------------------------------------------------------- 1 | const through = require('through2') 2 | 3 | const tr = through(function (buf, _, next) { 4 | this.push(buf.toString().toUpperCase()) 5 | next() 6 | }) 7 | process.stdin.pipe(tr).pipe(process.stdout) 8 | -------------------------------------------------------------------------------- /test/solutions/websockets.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws') 2 | 3 | const ws = new WebSocket('ws://localhost:8099') 4 | const stream = WebSocket.createWebSocketStream(ws) 5 | stream.write('hello\n') 6 | stream.pipe(process.stdout) 7 | -------------------------------------------------------------------------------- /problems/websockets/solution.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws') 2 | 3 | const ws = new WebSocket('ws://localhost:8099') 4 | const stream = WebSocket.createWebSocketStream(ws) 5 | stream.write('hello\n') 6 | stream.pipe(process.stdout) 7 | -------------------------------------------------------------------------------- /test/solutions/duplexer.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('child_process') 2 | const duplexer = require('duplexer2') 3 | 4 | module.exports = function (cmd, args) { 5 | const ps = spawn(cmd, args) 6 | return duplexer(ps.stdin, ps.stdout) 7 | } 8 | -------------------------------------------------------------------------------- /test/solutions/http_client.js: -------------------------------------------------------------------------------- 1 | const { request } = require('http') 2 | 3 | const options = { method: 'POST' } 4 | const req = request('http://localhost:8099', options, (res) => { 5 | res.pipe(process.stdout) 6 | }) 7 | process.stdin.pipe(req) 8 | -------------------------------------------------------------------------------- /problems/duplexer/solution.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('child_process') 2 | const duplexer = require('duplexer2') 3 | 4 | module.exports = function (cmd, args) { 5 | const ps = spawn(cmd, args) 6 | return duplexer(ps.stdin, ps.stdout) 7 | } 8 | -------------------------------------------------------------------------------- /problems/http_client/solution.js: -------------------------------------------------------------------------------- 1 | const { request } = require('http') 2 | 3 | const options = { method: 'POST' } 4 | const req = request('http://localhost:8099', options, (res) => { 5 | res.pipe(process.stdout) 6 | }) 7 | process.stdin.pipe(req) 8 | -------------------------------------------------------------------------------- /problems/read_it/solution.js: -------------------------------------------------------------------------------- 1 | const { Readable } = require('stream') 2 | 3 | class ReadableStream extends Readable { 4 | _read (size) { 5 | } 6 | } 7 | 8 | const stream = new ReadableStream() 9 | stream.push(process.argv[2]) 10 | stream.pipe(process.stdout) 11 | -------------------------------------------------------------------------------- /test/solutions/write_to_me.js: -------------------------------------------------------------------------------- 1 | const { Writable } = require('stream') 2 | 3 | const stream = new Writable({ 4 | write (chunk, encoding, callback) { 5 | console.log(`writing: ${chunk.toString()}`) 6 | callback() 7 | } 8 | }) 9 | 10 | process.stdin.pipe(stream) 11 | -------------------------------------------------------------------------------- /lib/cipherExercise.js: -------------------------------------------------------------------------------- 1 | const comparestdout = require('workshopper-exercise/comparestdout') 2 | 3 | let exercise = require('./exercise') 4 | const cipherProcessor = require('./cipherProcessor') 5 | 6 | exercise = cipherProcessor(exercise) 7 | exercise = comparestdout(exercise) 8 | 9 | module.exports = exercise 10 | -------------------------------------------------------------------------------- /lib/stdinExercise.js: -------------------------------------------------------------------------------- 1 | const comparestdout = require('workshopper-exercise/comparestdout') 2 | 3 | let exercise = require('./exercise') 4 | const stdinProcessor = require('./stdinProcessor') 5 | 6 | exercise = stdinProcessor(exercise) 7 | 8 | exercise = comparestdout(exercise) 9 | 10 | module.exports = exercise 11 | -------------------------------------------------------------------------------- /problems/write_to_me/solution.js: -------------------------------------------------------------------------------- 1 | const { Writable } = require('stream') 2 | 3 | class MyWritable extends Writable { 4 | _write (chunk, encoding, callback) { 5 | console.log(`writing: ${chunk.toString()}`) 6 | callback() 7 | } 8 | } 9 | 10 | const stream = new MyWritable() 11 | process.stdin.pipe(stream) 12 | -------------------------------------------------------------------------------- /lib/stdinStreamExercise.js: -------------------------------------------------------------------------------- 1 | const comparestdout = require('workshopper-exercise/comparestdout') 2 | 3 | let exercise = require('./exercise') 4 | const stdinStreamProcessor = require('./stdinStreamProcessor') 5 | 6 | exercise = stdinStreamProcessor(exercise) 7 | 8 | exercise = comparestdout(exercise) 9 | 10 | module.exports = exercise 11 | -------------------------------------------------------------------------------- /menu.json: -------------------------------------------------------------------------------- 1 | [ 2 | "BEEP BOOP", 3 | "MEET PIPE", 4 | "INPUT OUTPUT", 5 | "READ IT", 6 | "WRITE TO ME", 7 | "TRANSFORM", 8 | "LINES", 9 | "CONCAT", 10 | "HTTP SERVER", 11 | "HTTP CLIENT", 12 | "WEBSOCKETS", 13 | "HTML STREAM", 14 | "DUPLEXER", 15 | "DUPLEXER REDUX", 16 | "COMBINER", 17 | "CRYPT", 18 | "SECRETZ" 19 | ] 20 | -------------------------------------------------------------------------------- /problems/html_stream/solution.js: -------------------------------------------------------------------------------- 1 | const trumpet = require('trumpet') 2 | const through = require('through2') 3 | const tr = trumpet() 4 | 5 | const loud = tr.select('.loud').createStream() 6 | loud.pipe(through(function (buf, _, next) { 7 | this.push(buf.toString().toUpperCase()) 8 | next() 9 | })).pipe(loud) 10 | 11 | process.stdin.pipe(tr).pipe(process.stdout) 12 | -------------------------------------------------------------------------------- /test/solutions/html_stream.js: -------------------------------------------------------------------------------- 1 | const trumpet = require('trumpet') 2 | const through = require('through2') 3 | const tr = trumpet() 4 | 5 | const loud = tr.select('.loud').createStream() 6 | loud.pipe(through(function (buf, _, next) { 7 | this.push(buf.toString().toUpperCase()) 8 | next() 9 | })).pipe(loud) 10 | 11 | process.stdin.pipe(tr).pipe(process.stdout) 12 | -------------------------------------------------------------------------------- /problems/lines/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const exercise = require('../../lib/stdinExercise') 5 | const data = fs.readFileSync(path.join(__dirname, '../../lib/finnegans_wake.txt'), 'utf8') 6 | const input = data.split('\n') 7 | 8 | exercise.inputStdin = input 9 | exercise.stdinMessageSeparator = '\n' 10 | 11 | module.exports = exercise 12 | -------------------------------------------------------------------------------- /test/solutions/lines.js: -------------------------------------------------------------------------------- 1 | const split2 = require('split2') 2 | const through = require('through2') 3 | 4 | let count = 0 5 | process.stdin.pipe(split2()).pipe(through(function (line, _, next) { 6 | if (count++ % 2) { 7 | this.push(line.toString().toUpperCase() + '\n') 8 | } else { 9 | this.push(line.toString().toLowerCase() + '\n') 10 | } 11 | next() 12 | })).pipe(process.stdout) 13 | -------------------------------------------------------------------------------- /lib/exercise.js: -------------------------------------------------------------------------------- 1 | let exercise = require('workshopper-exercise')() 2 | const filecheck = require('workshopper-exercise/filecheck') 3 | const execute = require('workshopper-exercise/execute') 4 | const solutionSetup = require('./solutionSetup') 5 | 6 | exercise = filecheck(exercise) 7 | 8 | exercise = execute(exercise) 9 | 10 | exercise = solutionSetup(exercise) 11 | 12 | module.exports = exercise 13 | -------------------------------------------------------------------------------- /problems/read_it/index.js: -------------------------------------------------------------------------------- 1 | const exercise = require('../../lib/basicExercise') 2 | const { inputFromAliens } = require('../../lib/utils') 3 | 4 | exercise.addSetup(function (mode, callback) { 5 | const data = inputFromAliens().join('') 6 | 7 | this.submissionArgs.unshift(data) 8 | this.solutionArgs.unshift(data) 9 | 10 | process.nextTick(callback) 11 | }) 12 | 13 | module.exports = exercise 14 | -------------------------------------------------------------------------------- /test/solutions/http_server.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const through = require('through2') 3 | 4 | const server = http.createServer(function (req, res) { 5 | if (req.method === 'POST') { 6 | req.pipe(through(function (buf, _, next) { 7 | this.push(buf.toString().toUpperCase()) 8 | next() 9 | })).pipe(res) 10 | } else res.end() 11 | }) 12 | server.listen(process.argv[2]) 13 | -------------------------------------------------------------------------------- /problems/beep_boop/problem.md: -------------------------------------------------------------------------------- 1 | Make a new directory for your stream-adventure solutions (`mkdir stream-adventure` and enter it `cd ./stream-adventure`) 2 | Create a new file called beep_boop.js that uses console.log to output "beep boop". 3 | 4 | To verify your program has the expected output, run: 5 | 6 | ```sh 7 | $ {appname} verify beep_boop.js 8 | ``` 9 | 10 | for more options, run `stream-adventure help`. 11 | -------------------------------------------------------------------------------- /problems/http_server/solution.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const through = require('through2') 3 | 4 | const server = http.createServer(function (req, res) { 5 | if (req.method === 'POST') { 6 | req.pipe(through(function (buf, _, next) { 7 | this.push(buf.toString().toUpperCase()) 8 | next() 9 | })).pipe(res) 10 | } else res.end('send me a POST\n') 11 | }) 12 | server.listen(parseInt(process.argv[2])) 13 | -------------------------------------------------------------------------------- /lib/solutionSetup.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const solutionSetup = (exercise) => { 4 | exercise.addSetup(function (mode, callback) { 5 | this.solution = path.join(this.dir, 'solution.js') 6 | 7 | process.nextTick(callback) 8 | }) 9 | 10 | exercise.getSolutionFiles = function (callback) { 11 | callback(null, [this.solution]) 12 | } 13 | 14 | return exercise 15 | } 16 | 17 | module.exports = solutionSetup 18 | -------------------------------------------------------------------------------- /problems/lines/solution.js: -------------------------------------------------------------------------------- 1 | const through = require('through2') 2 | const split2 = require('split2') 3 | 4 | let lineCount = 0 5 | const tr = through(function (buf, _, next) { 6 | const line = buf.toString() 7 | this.push(lineCount % 2 === 0 8 | ? line.toLowerCase() + '\n' 9 | : line.toUpperCase() + '\n' 10 | ) 11 | lineCount++ 12 | next() 13 | }) 14 | process.stdin 15 | .pipe(split2()) 16 | .pipe(tr) 17 | .pipe(process.stdout) 18 | -------------------------------------------------------------------------------- /problems/duplexer_redux/solution.js: -------------------------------------------------------------------------------- 1 | const duplexer = require('duplexer2') 2 | const through = require('through2').obj 3 | 4 | module.exports = function (counter) { 5 | const counts = {} 6 | const input = through(write, end) 7 | return duplexer({ objectMode: true }, input, counter) 8 | 9 | function write (row, _, next) { 10 | counts[row.country] = (counts[row.country] || 0) + 1 11 | next() 12 | } 13 | function end (done) { 14 | counter.setCounts(counts) 15 | done() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/solutions/duplexer_redux.js: -------------------------------------------------------------------------------- 1 | const duplexer = require('duplexer2') 2 | const through = require('through2').obj 3 | 4 | module.exports = function (counter) { 5 | const counts = {} 6 | const input = through(write, end) 7 | return duplexer({ objectMode: true }, input, counter) 8 | 9 | function write (row, _, next) { 10 | counts[row.country] = (counts[row.country] || 0) + 1 11 | next() 12 | } 13 | function end (done) { 14 | counter.setCounts(counts) 15 | done() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "exercise": { 4 | "fail": { 5 | "connection": "Error connecting to {{{address}}}: {{{message}}}", 6 | "invalid_export": "Your solution is not exporting a function" 7 | } 8 | } 9 | }, 10 | "exercises": { 11 | "WEBSOCKETS": { 12 | "pass": { 13 | "message": "Message received correctly from ws client" 14 | }, 15 | "fail": { 16 | "message": "Message received from ws client is not correct" 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/stdinStreamProcessor.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | function stdinStreamProcessor (exercise) { 4 | exercise.addProcessor(function (mode, callback) { 5 | fs.createReadStream(this.inputFilePath).pipe(this.submissionChild.stdin) 6 | if (mode === 'verify') { 7 | fs.createReadStream(this.inputFilePath).pipe(this.solutionChild.stdin) 8 | } 9 | 10 | process.nextTick(function () { 11 | callback(null, true) 12 | }) 13 | }) 14 | 15 | return exercise 16 | } 17 | 18 | module.exports = stdinStreamProcessor 19 | -------------------------------------------------------------------------------- /.github/workflows/master.yml: -------------------------------------------------------------------------------- 1 | name: master 2 | 3 | on: 4 | push: 5 | branches: [ '**' ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [19.x, 18.x, 16.x] 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Node ${{ matrix.node_version }} 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{ matrix.node_version }} 22 | - run: npm install 23 | - run: npm run lint 24 | - run: npm test 25 | -------------------------------------------------------------------------------- /problems/crypt/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const crypto = require('crypto') 3 | 4 | const exercise = require('../../lib/cipherExercise') 5 | const words = require('../../lib/words.json') 6 | 7 | exercise.inputFilePath = path.join(__dirname, '../../lib/finnegans_wake.txt') 8 | 9 | const pw = words[Math.floor(Math.random() * words.length)] 10 | const key = crypto.createHash('md5').update(pw).digest('hex') 11 | const iv = crypto.randomBytes(8).toString('hex') 12 | 13 | exercise.cipherArgs = { algorithm: 'aes256', key, iv } 14 | exercise.execArgs = [key, iv] 15 | 16 | module.exports = exercise 17 | -------------------------------------------------------------------------------- /problems/secretz/solution.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | const tar = require('tar') 3 | const concat = require('concat-stream') 4 | 5 | const parser = new tar.Parse() 6 | parser.on('entry', function (e) { 7 | if (e.type !== 'File') return e.resume() 8 | 9 | const h = crypto.createHash('md5', { encoding: 'hex' }) 10 | e.pipe(h).pipe(concat(function (hash) { 11 | console.log(hash + ' ' + e.path) 12 | })) 13 | }) 14 | 15 | const cipher = process.argv[2] 16 | const key = process.argv[3] 17 | const iv = process.argv[4] 18 | process.stdin 19 | .pipe(crypto.createDecipheriv(cipher, key, iv)) 20 | .pipe(parser) 21 | -------------------------------------------------------------------------------- /test/solutions/secretz.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | const tar = require('tar') 3 | const concat = require('concat-stream') 4 | 5 | const parser = new tar.Parse() 6 | parser.on('entry', function (e) { 7 | if (e.type !== 'File') return e.resume() 8 | 9 | const h = crypto.createHash('md5', { encoding: 'hex' }) 10 | e.pipe(h).pipe(concat(function (hash) { 11 | console.log(hash + ' ' + e.path) 12 | })) 13 | }) 14 | 15 | const cipher = process.argv[2] 16 | const key = process.argv[3] 17 | const iv = process.argv[4] 18 | process.stdin 19 | .pipe(crypto.createDecipheriv(cipher, key, iv)) 20 | .pipe(parser) 21 | -------------------------------------------------------------------------------- /problems/duplexer/command.js: -------------------------------------------------------------------------------- 1 | const through = require('through2') 2 | const split2 = require('split2') 3 | const combine = require('stream-combiner') 4 | const offset = Number(process.argv[2]) 5 | 6 | const tr = combine(split2(), through(write)) 7 | process.stdin.pipe(tr).pipe(process.stdout) 8 | 9 | function write (buf, _, next) { 10 | const line = buf.toString() 11 | this.push(line.replace(/[A-Za-z]/g, function (s) { 12 | const c = s.charCodeAt(0) 13 | return String.fromCharCode( 14 | c < 97 15 | ? (c - 97 + offset) % 26 + 97 16 | : (c - 65 + offset) % 26 + 97 17 | ) 18 | }) + '\n') 19 | next() 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stream adventure 2 | 3 | Go on an educational stream adventure! 4 | 5 | [![build status](https://github.com/workshopper/stream-adventure/actions/workflows/master.yml/badge.svg?branch=master)](https://github.com/workshopper/stream-adventure/actions?query=branch:master) 6 | 7 | ## Install 8 | 9 | First install [node](http://nodejs.org). 10 | 11 | Once you've installed `node`, you will have an `npm` command. 12 | 13 | With [npm](https://docs.npmjs.com/cli-documentation/) do: 14 | 15 | ``` 16 | npm install -g stream-adventure 17 | ``` 18 | 19 | ## Run 20 | 21 | Now just type `stream-adventure` to play! 22 | 23 | ## Test 24 | ``` 25 | npm test 26 | ``` 27 | -------------------------------------------------------------------------------- /lib/words.json: -------------------------------------------------------------------------------- 1 | ["Tark's", 2 | "bimboowood", 3 | "so", 4 | "pleasekindly", 5 | "communicake", 6 | "with", 7 | "the", 8 | "original", 9 | "sinse", 10 | "we", 11 | "are", 12 | "only", 13 | "yearning", 14 | "as", 15 | "yet", 16 | "how", 17 | "to", 18 | "burgeon.", 19 | "It's", 20 | "meant", 21 | "milliems", 22 | "of", 23 | "centiments", 24 | "deadlost", 25 | "or", 26 | "mislaid", 27 | "on", 28 | "them", 29 | "but,", 30 | "master", 31 | "of", 32 | "snakes,", 33 | "we", 34 | "can", 35 | "sloughchange", 36 | "in", 37 | "the", 38 | "nip", 39 | "of", 40 | "a", 41 | "napple", 42 | "solongas", 43 | "we", 44 | "can", 45 | "allsee", 46 | "for", 47 | "deedsetton", 48 | "your", 49 | "quick."] 50 | -------------------------------------------------------------------------------- /problems/meet_pipe/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const os = require('os') 4 | 5 | const exercise = require('../../lib/basicExercise') 6 | const { inputFromAliens } = require('../../lib/utils') 7 | const testFile = path.resolve(os.tmpdir(), 'meet-pipe-data.txt') 8 | 9 | exercise.addSetup(function (mode, callback) { 10 | const data = inputFromAliens().join('') 11 | 12 | this.submissionArgs.unshift(testFile) 13 | this.solutionArgs.unshift(testFile) 14 | 15 | fs.writeFile(testFile, data, callback) 16 | }) 17 | 18 | exercise.addCleanup(function (mode, passed, callback) { 19 | fs.unlink(testFile, callback) 20 | }) 21 | 22 | module.exports = exercise 23 | -------------------------------------------------------------------------------- /problems/websockets/problem.md: -------------------------------------------------------------------------------- 1 | In this adventure, write a websocket client that uses the `ws` 2 | module, generate a stream on top of the websocket client, write 3 | the string "hello\n" to the stream and pipe it to `process.stdout`. 4 | 5 | To open a stream with `ws` on localhost:8099, just write: 6 | 7 | ```js 8 | const WebSocket = require('ws') 9 | const ws = new WebSocket('ws://localhost:8099') 10 | const stream = WebSocket.createWebSocketStream(ws) 11 | ``` 12 | 13 | The readme for `ws` has more info if you're curious about how to 14 | write the server side code: https://github.com/websockets/ws 15 | 16 | Make sure to `npm install ws` in the directory where your solution 17 | file lives. 18 | -------------------------------------------------------------------------------- /problems/crypt/problem.md: -------------------------------------------------------------------------------- 1 | Your program will be given a passphrase on `process.argv[2]`, an initialization value on `process.argv[3]` and 'aes256' 2 | encrypted data will be written to stdin. 3 | 4 | Simply decrypt the data and stream the result to process.stdout. 5 | 6 | You can use the `crypto.createDecipheriv()` api from node core to solve this 7 | challenge. Here's an example: 8 | 9 | ```js 10 | const crypto = require('crypto') 11 | const stream = crypto.createDecipher('RC4', 'robots') 12 | stream.pipe(process.stdout) 13 | stream.write(Buffer([ 135, 197, 164, 92, 129, 90, 215, 63, 92 ])) 14 | stream.end() 15 | ``` 16 | 17 | Instead of calling `.write()` yourself, just pipe stdin into your decrypter. 18 | -------------------------------------------------------------------------------- /problems/http_client/problem.md: -------------------------------------------------------------------------------- 1 | Send an HTTP POST request to http://localhost:8099 and pipe process.stdin into 2 | it. Pipe the response stream to process.stdout. 3 | 4 | You can use the `http` module in node core, specifically the `request` method, to solve this challenge. 5 | 6 | Here's an example to make a POST request using `http.request()`: 7 | 8 | ```js 9 | const { request } = require('http') 10 | 11 | const options = { method: 'POST' } 12 | const req = request('http://beep.boop:80/', options, (res) => { 13 | /* Do something with res*/ 14 | }) 15 | ``` 16 | 17 | Hint: The `req` object that you get back from `request()` is a writable stream 18 | and the `res` object in the callback function is a readable stream. -------------------------------------------------------------------------------- /problems/html_stream/input.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | beep boop 4 | 5 | 6 |

7 | Four score and several years ago, our fathers bought four swiss 8 | continents, a new vacation, covered in liberty. 9 | And predicated to the preposition that tall men created a 10 | sequel. 11 |

12 | 13 |

14 | How we are offstage in a great livermore, testing weather stations, or any 15 | station so conceived in altogether fitting and little note, nor long 16 | remember, they who fought here and take increased devoation, 17 | that government love the people, beside the people, 18 | four of the people, shall not perish from this earth. 19 |

20 | 21 | 22 | -------------------------------------------------------------------------------- /problems/html_stream/expected.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | beep boop 4 | 5 | 6 |

7 | Four score and several years ago, our fathers bought four swiss 8 | continents, a new vacation, covered in liberty. 9 | And predicated to the preposition that tall men created a 10 | sequel. 11 |

12 | 13 |

14 | How we are offstage in a great livermore, testing weather stations, or any 15 | station so conceived in altogether fitting and little note, nor long 16 | remember, they who fought here and take increased devoation, 17 | that GOVERNMENT LOVE THE PEOPLE, BESIDE THE PEOPLE, 18 | FOUR OF THE PEOPLE, SHALL NOT PERISH FROM THIS EARTH. 19 |

20 | 21 | 22 | -------------------------------------------------------------------------------- /problems/secretz/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const crypto = require('crypto') 3 | 4 | const exercise = require('../../lib/cipherExercise') 5 | 6 | function phrase () { 7 | let s = '' 8 | for (let i = 0; i < 16; i++) { 9 | s += String.fromCharCode(Math.random() * 26 + 97) 10 | } 11 | return s 12 | } 13 | 14 | const ciphers = [{ 15 | algorithm: 'aes-192-cbc', 16 | key: crypto.createHash('md5').update(phrase()).digest('base64'), 17 | iv: crypto.randomBytes(8).toString('hex') 18 | }] 19 | 20 | const { algorithm, key, iv } = ciphers[Math.floor(Math.random() * ciphers.length)] 21 | 22 | exercise.inputFilePath = path.join(__dirname, '/secretz.tar.gz') 23 | 24 | exercise.cipherArgs = { algorithm, key, iv } 25 | exercise.execArgs = [algorithm, key, iv] 26 | 27 | module.exports = exercise 28 | -------------------------------------------------------------------------------- /problems/html_stream/problem.md: -------------------------------------------------------------------------------- 1 | Your program will get some html written to stdin. Convert all the inner html to 2 | upper-case for elements with a class name of "loud", 3 | and pipe all the html to stdout. 4 | 5 | You can use `trumpet` and `through2` to solve this adventure. 6 | 7 | With `trumpet` you can create a transform stream from a css selector: 8 | ```js 9 | const trumpet = require('trumpet') 10 | const fs = require('fs') 11 | const tr = trumpet() 12 | fs.createReadStream('input.html').pipe(tr) 13 | 14 | const stream = tr.select('.beep').createStream() 15 | ``` 16 | 17 | Now `stream` outputs all the inner html content at `'.beep'` and the data you 18 | write to `stream` will appear as the new inner html content. 19 | 20 | Make sure to `npm install trumpet through2` in the directory where your solution 21 | file lives. 22 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const workshopper = require('workshopper-adventure') 3 | const util = require('workshopper-adventure/util') 4 | 5 | const exerciseDir = path.join(__dirname, './problems') 6 | const shop = workshopper({ 7 | name: 'stream-adventure', 8 | title: 'STREAM ADVENTURE', 9 | exerciseDir, 10 | header: require('workshopper-adventure/default/header'), 11 | footer: require('workshopper-adventure/default/footer'), 12 | fail: require('workshopper-adventure/default/fail'), 13 | pass: require('workshopper-adventure/default/pass'), 14 | appDir: __dirname 15 | }) 16 | 17 | require('./menu.json').forEach(function (name) { 18 | const dir = util.dirFromName(exerciseDir, name) 19 | const exerciseFile = path.join(dir, './index.js') 20 | shop.add({ name, dir, exerciseFile }) 21 | }) 22 | 23 | module.exports = shop 24 | -------------------------------------------------------------------------------- /problems/combiner/solution.js: -------------------------------------------------------------------------------- 1 | const combine = require('stream-combiner') 2 | const through = require('through2') 3 | const split2 = require('split2') 4 | const zlib = require('zlib') 5 | 6 | module.exports = function () { 7 | const grouper = through(write, end) 8 | let current 9 | 10 | function write (line, _, next) { 11 | if (line.length === 0) return next() 12 | const row = JSON.parse(line) 13 | 14 | if (row.type === 'genre') { 15 | if (current) { 16 | this.push(JSON.stringify(current) + '\n') 17 | } 18 | current = { name: row.name, books: [] } 19 | } else if (row.type === 'book') { 20 | current.books.push(row.name) 21 | } 22 | next() 23 | } 24 | function end (next) { 25 | if (current) { 26 | this.push(JSON.stringify(current) + '\n') 27 | } 28 | next() 29 | } 30 | 31 | return combine(split2(), grouper, zlib.createGzip()) 32 | } 33 | -------------------------------------------------------------------------------- /test/solutions/combiner.js: -------------------------------------------------------------------------------- 1 | const combine = require('stream-combiner') 2 | const through = require('through2') 3 | const split2 = require('split2') 4 | const zlib = require('zlib') 5 | 6 | module.exports = function () { 7 | const grouper = through(write, end) 8 | let current 9 | 10 | function write (line, _, next) { 11 | if (line.length === 0) return next() 12 | const row = JSON.parse(line) 13 | 14 | if (row.type === 'genre') { 15 | if (current) { 16 | this.push(JSON.stringify(current) + '\n') 17 | } 18 | current = { name: row.name, books: [] } 19 | } else if (row.type === 'book') { 20 | current.books.push(row.name) 21 | } 22 | next() 23 | } 24 | function end (next) { 25 | if (current) { 26 | this.push(JSON.stringify(current) + '\n') 27 | } 28 | next() 29 | } 30 | 31 | return combine(split2(), grouper, zlib.createGzip()) 32 | } 33 | -------------------------------------------------------------------------------- /test/check.js: -------------------------------------------------------------------------------- 1 | var spawn = require('child_process').spawn 2 | var path = require('path') 3 | var test = require('tape') 4 | 5 | var adventures = require('../menu.json') 6 | adventures.forEach(function (name) { 7 | test(name, function (t) { 8 | t.plan(2) 9 | var file = name.toLowerCase().replace(/\s+/g, '_') + '.js' 10 | var solution = path.join(__dirname, 'solutions', file) 11 | 12 | var ps = run(['select', name]) 13 | ps.on('exit', selected) 14 | ps.stderr.pipe(process.stderr) 15 | 16 | function selected (code) { 17 | t.equal(code, 0) 18 | var ps = run(['verify', solution]) 19 | ps.on('exit', verified) 20 | ps.stderr.pipe(process.stderr) 21 | } 22 | 23 | function verified (code) { 24 | t.equal(code, 0) 25 | } 26 | }) 27 | }) 28 | 29 | function run (args) { 30 | args.unshift(path.join(__dirname, '../bin/cmd.js')) 31 | return spawn(process.execPath, args) 32 | } 33 | -------------------------------------------------------------------------------- /lib/finnegans_wake.txt: -------------------------------------------------------------------------------- 1 | riverrun, past Eve and Adam's, from swerve of shore to bend 2 | of bay, brings us by a commodius vicus of recirculation back to 3 | Howth Castle and Environs. 4 | 5 | Sir Tristram, violer d'amores, fr'over the short sea, had passen- 6 | core rearrived from North Armorica on this side the scraggy 7 | isthmus of Europe Minor to wielderfight his penisolate war: nor 8 | had topsawyer's rocks by the stream Oconee exaggerated themselse 9 | to Laurens County's gorgios while they went doublin their mumper 10 | all the time: nor avoice from afire bellowsed mishe mishe to 11 | tauftauf thuartpeatrick: not yet, though venissoon after, had a 12 | kidscad buttended a bland old isaac: not yet, though all's fair in 13 | vanessy, were sosie sesthers wroth with twone nathandjoe. Rot a 14 | peck of pa's malt had Jhem or Shen brewed by arclight and rory 15 | end to the regginbrow was to be seen ringsome on the aquaface. 16 | -------------------------------------------------------------------------------- /problems/duplexer_redux/index.js: -------------------------------------------------------------------------------- 1 | const provinces = require('provinces') 2 | const exercise = require('../../lib/duplexExercise') 3 | const { readableStream } = require('../../lib/utils') 4 | 5 | const getInput = () => { 6 | const input = [] 7 | const len = 50 + Math.floor(Math.random() * 25) 8 | for (let i = 0; i < len; i++) { 9 | const p = provinces[Math.floor(Math.random() * provinces.length)] 10 | input.push(p) 11 | } 12 | return input 13 | } 14 | 15 | exercise.inputStdin = getInput() 16 | 17 | const getCounter = () => { 18 | const counter = readableStream() 19 | counter.setCounts = function (counts) { 20 | const self = this 21 | Object.keys(counts).sort().forEach(function (key) { 22 | self.push(`${key} => ${counts[key]}\n`) 23 | }) 24 | this.push(null) 25 | } 26 | return counter 27 | } 28 | 29 | exercise.submissionArgs = getCounter() 30 | exercise.solutionArgs = getCounter() 31 | 32 | module.exports = exercise 33 | -------------------------------------------------------------------------------- /problems/websockets/index.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const WebSocket = require('ws') 3 | const exercise = require('../../lib/basicExercise') 4 | 5 | const initMsg = 'hello\n' 6 | const responseMsg = 'beep bop boop\n' 7 | 8 | exercise.addSetup(function (mode, callback) { 9 | this.server = http.createServer() 10 | this.server.listen(8099, function () { 11 | callback() 12 | }) 13 | 14 | this.wss = new WebSocket.Server({ server: this.server }) 15 | this.wss.on('connection', (ws) => { 16 | ws.on('message', (data) => { 17 | const received = data.toString() 18 | ws.send(received) 19 | if (received === initMsg) { 20 | ws.send(responseMsg) 21 | } 22 | ws.close() 23 | }) 24 | }) 25 | }) 26 | 27 | exercise.addCleanup(function (mode, passed, callback) { 28 | if (!this.server) { 29 | return process.nextTick(callback) 30 | } 31 | 32 | this.wss.close() 33 | this.server.close(callback) 34 | }) 35 | module.exports = exercise 36 | -------------------------------------------------------------------------------- /problems/duplexer/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const exercise = require('../../lib/duplexExercise') 4 | 5 | const words = [ 6 | 'beetle', 7 | 'biscuit', 8 | 'bat', 9 | 'bobbin', 10 | 'bequeath', 11 | 'brûlée', 12 | 'byzantine', 13 | 'bazaar', 14 | 'blip', 15 | 'byte', 16 | 'beep', 17 | 'boop', 18 | 'bust', 19 | 'bite', 20 | 'balloon', 21 | 'box', 22 | 'beet', 23 | 'boolean', 24 | 'bake', 25 | 'bottle', 26 | 'bug', 27 | 'burrow' 28 | ] 29 | 30 | const getInput = () => { 31 | const input = [] 32 | const len = 10 + Math.floor(Math.random() * 5) 33 | for (let i = 0; i < len; i++) { 34 | const word = words[Math.floor(Math.random() * words.length)] 35 | input.push(`${word}\n`) 36 | } 37 | return input 38 | } 39 | 40 | exercise.inputStdin = getInput() 41 | 42 | const n = 1 + Math.floor(Math.random() * 25) 43 | const cmd = path.resolve(__dirname, 'command.js') 44 | exercise.execArgs = [cmd, n] 45 | 46 | module.exports = exercise 47 | -------------------------------------------------------------------------------- /lib/stdinProcessor.js: -------------------------------------------------------------------------------- 1 | function messageStdin (submissionStdin, solutionStdin, input, separator) { 2 | const iv = setInterval(function () { 3 | if (input.length) { 4 | const msg = separator ? input.shift() + separator : input.shift() 5 | submissionStdin.write(msg) 6 | if (solutionStdin) solutionStdin.write(msg) 7 | } else { 8 | clearInterval(iv) 9 | submissionStdin.end() 10 | if (solutionStdin) solutionStdin.end() 11 | } 12 | }, 50) 13 | } 14 | 15 | function stdinProcessor (exercise) { 16 | exercise.addProcessor(function (mode, callback) { 17 | const solutionStdin = (mode === 'verify') ? this.solutionChild.stdin : null 18 | 19 | messageStdin( 20 | this.submissionChild.stdin, 21 | solutionStdin, 22 | this.inputStdin, 23 | this.stdinMessageSeparator 24 | ) 25 | 26 | process.nextTick(function () { 27 | callback(null, true) 28 | }) 29 | }) 30 | 31 | return exercise 32 | } 33 | 34 | module.exports = stdinProcessor 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Code contributions are welcome and highly encouraged! For instructions on and help with creating a great pull request, please read the [workshopper contributing document](https://github.com/workshopper/org/blob/master/CONTRIBUTING.md). 4 | 5 | If you have questions about contributing, please create an issue. 6 | 7 | ## Lead Maintainers 8 | 9 | The role of lead maintainers is to triage and categorize issues, answer questions about contributing to the repository, review and give feedback on PRs, and maintain the quality of a workshopper's codebase and repository. 10 | 11 | [Current Lead Maintainers](https://github.com/orgs/workshopper/teams/stream-adventure-leads) 12 | 13 | ### Volunteer 14 | 15 | Submitting many PRs? Please volunteer to lead this repository! Lead maintainers are selected in the philosophy of [Open Open Source](http://openopensource.org/): 16 | 17 | > Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. 18 | -------------------------------------------------------------------------------- /lib/cipherProcessor.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const crypto = require('crypto') 3 | 4 | function cipherProcessor (exercise) { 5 | exercise.addSetup(function (mode, callback) { 6 | const { algorithm, key, iv } = this.cipherArgs 7 | 8 | this.submissionArgs = this.submissionArgs.concat(this.execArgs) 9 | this.solutionArgs = this.solutionArgs.concat(this.execArgs) 10 | 11 | this.submissionCipher = crypto.createCipheriv(algorithm, key, iv) 12 | this.solutionCipher = crypto.createCipheriv(algorithm, key, iv) 13 | 14 | process.nextTick(callback) 15 | }) 16 | 17 | exercise.addProcessor(function (mode, callback) { 18 | fs.createReadStream(this.inputFilePath).pipe(this.submissionCipher).pipe(this.submissionChild.stdin) 19 | 20 | if (mode === 'verify') { 21 | fs.createReadStream(this.inputFilePath).pipe(this.solutionCipher).pipe(this.solutionChild.stdin) 22 | } 23 | 24 | process.nextTick(callback) 25 | }) 26 | 27 | return exercise 28 | } 29 | 30 | module.exports = cipherProcessor 31 | -------------------------------------------------------------------------------- /problems/combiner/expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "cyberpunk", 4 | "books": [ 5 | "Accelerando", 6 | "Snow Crash", 7 | "Neuromancer", 8 | "The Diamond Age", 9 | "Heavy Weather" 10 | ] 11 | }, 12 | { 13 | "name": "new wave", 14 | "books": [ 15 | "Bug Jack Barron", 16 | "The Heat Death of the Universe", 17 | "Dangerous Visions" 18 | ] 19 | }, 20 | { 21 | "name": "apocalypse", 22 | "books": [ 23 | "Earth Abides", 24 | "Alas, Babylon", 25 | "Riddley Walker" 26 | ] 27 | }, 28 | { 29 | "name": "time travel", 30 | "books": [ 31 | "A Connecticut Yankee in King Arthur's Court", 32 | "The Time Machine" 33 | ] 34 | }, 35 | { 36 | "name": "space opera", 37 | "books": [ 38 | "A Deepness in the Sky", 39 | "Skylark", 40 | "Void" 41 | ] 42 | }, 43 | { 44 | "name": "alternate history", 45 | "books": [ 46 | "The Man in the High Castle", 47 | "Bring the Jubilee" 48 | ] 49 | } 50 | ] 51 | -------------------------------------------------------------------------------- /problems/combiner/books.json: -------------------------------------------------------------------------------- 1 | [{ "name": "Neuromancer","genre": "cyberpunk" }, 2 | { "name": "Snow Crash", "genre": "cyberpunk" }, 3 | { "name": "Accelerando", "genre": "cyberpunk" }, 4 | { "name": "The Diamond Age", "genre": "cyberpunk" }, 5 | { "name": "Heavy Weather", "genre": "cyberpunk" }, 6 | { "name": "The Heat Death of the Universe", "genre": "new wave" }, 7 | { "name": "Bug Jack Barron", "genre": "new wave" }, 8 | { "name": "Dangerous Visions", "genre": "new wave" }, 9 | { "name": "A Connecticut Yankee in King Arthur's Court", "genre": "time travel" }, 10 | { "name": "The Time Machine", "genre": "time travel" }, 11 | { "name": "Earth Abides", "genre": "apocalypse" }, 12 | { "name": "Alas, Babylon", "genre": "apocalypse" }, 13 | { "name": "Riddley Walker", "genre": "apocalypse" }, 14 | { "name": "A Deepness in the Sky", "genre": "space opera" }, 15 | { "name": "Void", "genre": "space opera" }, 16 | { "name": "Skylark", "genre": "space opera" }, 17 | { "name": "The Man in the High Castle", "genre": "alternate history" }, 18 | { "name": "Bring the Jubilee", "genre": "alternate history" }] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is released under the MIT license: 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /problems/http_client/index.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | 3 | const through = require('through2') 4 | 5 | const exercise = require('../../lib/stdinExercise') 6 | const { inputFromWords } = require('../../lib/utils') 7 | 8 | exercise.inputStdin = inputFromWords() 9 | 10 | function convert (buf) { 11 | return buf.toString().replace(/\S/g, function (c) { 12 | const x = c.charCodeAt(0) 13 | if (/[a-z]/.test(c)) { 14 | return String.fromCharCode(137 * (x - 97) % 26 + 97) 15 | } else if (/[A-Z]/.test(c)) { 16 | return String.fromCharCode(139 * (x - 65) % 26 + 65) 17 | } else return c 18 | }) 19 | } 20 | 21 | exercise.addSetup(function (mode, callback) { 22 | const port = 8099 23 | this.server = http.createServer(function (req, res) { 24 | if (req.method !== 'POST') { 25 | return res.end('not a POST request') 26 | } 27 | req.pipe(through(function (buf, _, next) { 28 | this.push(convert(buf)) 29 | next() 30 | })).pipe(res) 31 | }) 32 | 33 | this.server.listen(port, function () { 34 | callback() 35 | }) 36 | }) 37 | 38 | exercise.addCleanup(function (mode, passed, callback) { 39 | if (!this.server) { 40 | return process.nextTick(callback) 41 | } 42 | 43 | this.server.close(callback) 44 | }) 45 | 46 | module.exports = exercise 47 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | const { Readable } = require('stream') 2 | 3 | const aliens = require('./aliens.json') 4 | const words = require('./words.json') 5 | 6 | const inputFromAliens = () => { 7 | const input = [] 8 | for (let i = 0; i < 10; i++) { 9 | const alien = aliens[Math.floor(Math.random() * aliens.length)] 10 | input.push(`${alien}\n`) 11 | } 12 | return input 13 | } 14 | 15 | const inputFromWords = () => { 16 | const input = [] 17 | const offset = Math.floor(words.length * Math.random()) 18 | for (let i = 0; i < 10; i++) { 19 | const word = words[(offset + i) % words.length] 20 | input.push(`${word}\n`) 21 | } 22 | return input 23 | } 24 | 25 | const rndPort = () => Math.floor(Math.random() * 40000 + 10000) 26 | 27 | const writeStream = (stream, input, time) => { 28 | let count = 0 29 | const iv = setInterval(function () { 30 | stream.write(input[count].trim() + '\n') 31 | 32 | if (++count === input.length) { 33 | clearInterval(iv) 34 | stream.end() 35 | } 36 | }, time) 37 | } 38 | 39 | const readableStream = () => { 40 | const stream = new Readable({ objectMode: true }) 41 | stream._read = function () {} 42 | return stream 43 | } 44 | 45 | module.exports = { 46 | inputFromAliens, 47 | inputFromWords, 48 | rndPort, 49 | writeStream, 50 | readableStream 51 | } 52 | -------------------------------------------------------------------------------- /problems/concat/problem.md: -------------------------------------------------------------------------------- 1 | Create a new file called concat.js. 2 | 3 | You will be given text on `process.stdin`, convert buffer to string and reverse it 4 | using the `concat-stream` module before writing it to `process.stdout`. 5 | 6 | `concat-stream` is a writable stream that concatenate all buffers from a stream 7 | and give you the result in the callback you pass like parameter. 8 | 9 | Here's an example that uses `concat-stream` to buffer POST content in order to 10 | JSON.parse() the submitted data: 11 | 12 | ```js 13 | const concat = require('concat-stream') 14 | const http = require('http') 15 | 16 | const server = http.createServer(function (req, res) { 17 | if (req.method === 'POST') { 18 | req.pipe(concat(function (body) { 19 | const obj = JSON.parse(body) 20 | res.end(Object.keys(obj).join('\n')) 21 | })); 22 | } 23 | else res.end() 24 | }); 25 | server.listen(5000) 26 | ``` 27 | 28 | In your adventure you'll only need to buffer input with `concat()` from 29 | process.stdin. 30 | 31 | Make sure to `npm install concat-stream` in the directory where your solution 32 | file is located. 33 | 34 | ## Hint: 35 | 36 | Both `process.stdout` and `concat-stream` are writeable streams, so they can't 37 | be piped together. 38 | 39 | To verify your solution run: 40 | 41 | ```sh 42 | $ {appname} verify concat.js 43 | ``` 44 | -------------------------------------------------------------------------------- /problems/concat/index.js: -------------------------------------------------------------------------------- 1 | const chunky = require('chunky') 2 | const wrap = require('wordwrap')(30) 3 | const exercise = require('../../lib/stdinExercise') 4 | 5 | const format = [ 6 | `Every $noun in the village heard the $adj clamor from the town square. 7 | Looking $adv into the distance, Constable Franklin $verb his $adj 8 | periscope to locate the $adj source. Unwittingly, a nearby $noun 9 | $adv $verb high-velocity $adj particles.\n` 10 | ] 11 | 12 | const words = { 13 | noun: [ 14 | 'cat', 'pebble', 'conifer', 'dingo', 'toaster oven', 'x-ray', 15 | 'microwave', 'isotope' 16 | ], 17 | verb: ['steered', 'flipped', 'twiddled', 'consumed', 'emitted'], 18 | adj: [ 19 | 'piercing', 'confusing', 'apt', 'unhelpful', 'radiometric', 20 | 'digital', 'untrustworthy', 'ionizing' 21 | ], 22 | adv: ['verily', 'yawnily', 'zestily', 'unparadoxically'] 23 | } 24 | 25 | function createSentence () { 26 | const fmt = format[Math.floor(Math.random() * format.length)] 27 | return wrap(fmt.replace(/\$(\w+)/g, function (_, x) { 28 | return take(words[x]) 29 | })) 30 | 31 | function take (xs) { 32 | const ix = Math.floor(Math.random() * xs.length) 33 | return xs.splice(ix, 1)[0] 34 | } 35 | } 36 | 37 | const input = chunky(createSentence()) 38 | 39 | exercise.inputStdin = input 40 | 41 | module.exports = exercise 42 | -------------------------------------------------------------------------------- /problems/lines/problem.md: -------------------------------------------------------------------------------- 1 | Instead of transforming every line as in the previous "TRANSFORM" example, 2 | for this challenge, convert even-numbered lines to upper-case and odd-numbered 3 | lines to lower-case. Consider the first line to be odd-numbered. For example 4 | given this input: 5 | 6 | One 7 | Two 8 | Three 9 | Four 10 | 11 | Your program should output: 12 | 13 | one 14 | TWO 15 | three 16 | FOUR 17 | 18 | Even though it's not obligatory, you can use the `split2` module 19 | to split input by newlines. For example: 20 | 21 | ```js 22 | const split2 = require('split2') 23 | const through2 = require('through2') 24 | process.stdin 25 | .pipe(split2()) 26 | .pipe(through2(function (line, _, next) { 27 | console.dir(line.toString()) 28 | next(); 29 | })) 30 | ``` 31 | 32 | `split2` will buffer chunks on newlines before you get them. With example 33 | above, we will get separate events for each line even though all the data 34 | probably arrives on the same chunk: 35 | 36 | ```sh 37 | $ echo -e 'one\ntwo\nthree' | node split.js 38 | 'one' 39 | 'two' 40 | 'three' 41 | ``` 42 | 43 | Your own program could use `split2` in this way, and you should transform the 44 | input and pipe the output through to `process.stdout`. 45 | 46 | You are free to solve the challenge without `split2` module. In this case, 47 | you would have to add a new line after each line to have a passing match. 48 | 49 | Make sure to `npm install split2 through2` in the directory where your solution 50 | file lives. 51 | -------------------------------------------------------------------------------- /problems/secretz/problem.md: -------------------------------------------------------------------------------- 1 | An encrypted, gzipped tar file will be piped in on process.stdin. To beat this 2 | challenge, for each file in the tar input, print a hex-encoded md5 hash of the 3 | file contents followed by a single space followed by the file path, then a 4 | newline. 5 | 6 | You will receive the cipher algorithm name as process.argv[2], the cipher key as 7 | process.argv[3] and the cipher initialization vector as process.argv[4]. 8 | You can pass these arguments directly through to `crypto.createDecipheriv()`. 9 | 10 | The `tar` module from npm has a `tar.Parse()` constructor that can unzip gzipped 11 | tar files automatically ( if detected ) and emits `entry` events for each file 12 | in the tar input. 13 | 14 | Each `entry` object is a readable stream of the file contents from the archive and: 15 | 16 | `entry.type` is the kind of file ('File', 'Directory', etc) 17 | `entry.path` is the file path 18 | 19 | Using the tar module looks like: 20 | 21 | ```js 22 | const tar = require('tar') 23 | const parser = new tar.Parse() 24 | parser.on('entry', function (e) { 25 | console.dir(e) 26 | }); 27 | const fs = require('fs') 28 | fs.createReadStream('file.tar').pipe(parser) 29 | ``` 30 | 31 | Use `crypto.createHash('md5', { encoding: 'hex' })` to generate a stream that 32 | outputs a hex md5 hash for the content written to it. 33 | 34 | The `concat-stream` module could be useful to concatenate all stream data. 35 | 36 | Make sure to run `npm install tar concat-stream` in the directory where your solution 37 | file lives. 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stream-adventure", 3 | "version": "4.3.2", 4 | "description": "an educational stream adventure", 5 | "bin": { 6 | "stream-adventure": "bin/cmd.js" 7 | }, 8 | "main": "index.js", 9 | "dependencies": { 10 | "chunky": "~0.0.0", 11 | "clone": "^2.1.2", 12 | "concat-stream": "^2.0.0", 13 | "duplexer2": "~0.1.4", 14 | "hyperquest": "^2.1.3", 15 | "osenv": "^0.1.0", 16 | "provinces": "^1.11.0", 17 | "split2": "^3.1.1", 18 | "stream-combiner": "^0.2.1", 19 | "tar": "^6.0.1", 20 | "through2": "^4.0.2", 21 | "trumpet": "^1.7.0", 22 | "wordwrap": "~1.0.0", 23 | "workshopper-adventure": "^6.1.0", 24 | "workshopper-exercise": "^3.0.1", 25 | "ws": "^7.3.0" 26 | }, 27 | "devDependencies": { 28 | "standard": "^14.3.3", 29 | "standard-version": "^9.0.0", 30 | "tape": "^5.0.1" 31 | }, 32 | "scripts": { 33 | "lint": "standard", 34 | "test": "npm run lint && tape test/*.js", 35 | "release": "standard-version" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "git://github.com/substack/stream-adventure.git" 40 | }, 41 | "homepage": "https://github.com/substack/stream-adventure", 42 | "keywords": [ 43 | "stream", 44 | "educational", 45 | "guide", 46 | "tutorial", 47 | "learn" 48 | ], 49 | "author": { 50 | "name": "James Halliday", 51 | "email": "mail@substack.net", 52 | "url": "http://substack.net" 53 | }, 54 | "license": "MIT", 55 | "standard-version": { 56 | "bumpFiles": [ 57 | "package.json" 58 | ] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /problems/combiner/index.js: -------------------------------------------------------------------------------- 1 | const zlib = require('zlib') 2 | 3 | const comparestdout = require('workshopper-exercise/comparestdout') 4 | 5 | let exercise = require('../../lib/exportFnExercise') 6 | const books = require('./books.json') 7 | 8 | function shuffle (xs) { 9 | return xs.sort(cmp) 10 | function cmp () { return Math.random() > 0.5 ? 1 : -1 } 11 | } 12 | 13 | const getInput = () => { 14 | const genres = books.reduce(function (acc, book) { 15 | acc[book.genre] = { 16 | type: 'book', 17 | name: book.genre, 18 | books: [] 19 | } 20 | return acc 21 | }, {}) 22 | 23 | books.forEach(function (book) { 24 | book.type = 'book' 25 | genres[book.genre].books.push(book) 26 | }) 27 | 28 | const keys = shuffle(Object.keys(genres)) 29 | 30 | const data = keys.reduce(function (acc, key) { 31 | const g = genres[key] 32 | acc.push(JSON.stringify({ type: 'genre', name: g.name })) 33 | return acc.concat(shuffle(g.books).map(function (book) { 34 | return JSON.stringify({ type: 'book', name: book.name }) 35 | })) 36 | }, []) 37 | return data 38 | } 39 | 40 | exercise.inputStdin = getInput() 41 | exercise.stdinMessageSeparator = '\n' 42 | 43 | exercise.addProcessor(function (mode, callback) { 44 | this.submissionStdout = this.submissionChild.pipe(zlib.createGunzip()) 45 | 46 | if (mode === 'verify') { 47 | this.solutionStdout = this.submissionChild.pipe(zlib.createGunzip()) 48 | } 49 | 50 | process.nextTick(function () { 51 | callback(null, true) 52 | }) 53 | }) 54 | 55 | exercise = comparestdout(exercise) 56 | 57 | module.exports = exercise 58 | -------------------------------------------------------------------------------- /problems/duplexer_redux/problem.md: -------------------------------------------------------------------------------- 1 | In this example, you will be given a readable stream, `counter`, as the first 2 | argument to your exported function: 3 | 4 | ```js 5 | module.exports = function (counter) { 6 | // return a duplex stream to count countries on the writable side 7 | // and pass through `counter` on the readable side 8 | } 9 | ``` 10 | 11 | Return a duplex stream with the `counter` as the readable side. You will be 12 | written objects with a 2-character `country` field as input, such as these: 13 | 14 | ```json 15 | {"short":"OH","name":"Ohio","country":"US"} 16 | {"name":"West Lothian","country":"GB","region":"Scotland"} 17 | {"short":"NSW","name":"New South Wales","country":"AU"} 18 | ``` 19 | 20 | Create an object to track the number of occurrences of each unique country code. 21 | 22 | For example: 23 | ```json 24 | {"US": 2, "GB": 3, "CN": 1} 25 | ``` 26 | 27 | Once the input ends, call `counter.setCounts()` with your counts object. 28 | 29 | The `duplexer2` module will again be very handy in this example. 30 | 31 | If you use duplexer, make sure to `npm install duplexer2` in the directory where 32 | your solution file is located. 33 | 34 | Keep in mind that you will have to work with objects, not buffers. 35 | Consult the documentation for further details: 36 | https://nodejs.org/api/stream.html#stream_object_mode 37 | 38 | When you switch on the object mode, remember to do the same for all 39 | additional dependencies that you work with (i.e. through2) 40 | 41 | Create a new file called duplexer-redux.js which will hold your solution. 42 | 43 | To verify your solution run: 44 | 45 | ```sh 46 | $ {appname} verify duplexer-redux.js 47 | ``` 48 | -------------------------------------------------------------------------------- /problems/duplexer/problem.md: -------------------------------------------------------------------------------- 1 | Write a program that exports a function that spawns a process from a `cmd` 2 | string and an `args` array and returns a single duplex stream joining together 3 | the stdin and stdout of the spawned process: 4 | 5 | ```js 6 | const { spawn } = require('child_process') 7 | 8 | module.exports = function (cmd, args) { 9 | // spawn the process and return a single stream 10 | // joining together the stdin and stdout here 11 | } 12 | ``` 13 | 14 | There is a very handy module you can use here: duplexer2. The duplexer2 module 15 | exports a single function `duplexer2(writable, readable)` that joins together a 16 | writable stream and a readable stream into a single, readable/writable duplex 17 | stream. 18 | 19 | If you use duplexer2, make sure to `npm install duplexer2` in the directory where 20 | your solution file is located. 21 | 22 | Keep in mind that the main and child processes will have different stream interface. 23 | 24 | process.stdin is a Readable stream 25 | process.stdout is a Writable stream 26 | 27 | For process you're inside the process to stdin is readable to you. 28 | For child process you're outside so that process's stdin is writable to you. 29 | 30 | childProc.stdin is a Writable stream 31 | childProc.stdout is a Readable stream 32 | 33 | Also, have a look at the duplexer2 documentation and notice that signature 34 | of the exported function is `duplexer2([options], writable, readable)` 35 | which means that you might need to pass an options argument. 36 | 37 | Create a new file called duplexer.js which will hold your solution. 38 | 39 | To verify your solution run: 40 | 41 | ```sh 42 | $ {appname} verify duplexer.js 43 | ``` 44 | -------------------------------------------------------------------------------- /problems/transform/problem.md: -------------------------------------------------------------------------------- 1 | Convert data from `process.stdin` to upper-case data on `process.stdout` 2 | using the `through2` module. 3 | 4 | To get the `through2` module you'll need to do: 5 | ```sh 6 | $ npm install through2 7 | ``` 8 | A transform stream takes input data and applies an operation to the data to 9 | produce the output data. 10 | 11 | Create a through stream with a `write` and `end` function: 12 | 13 | ```js 14 | const through = require('through2') 15 | const stream = through(write, end) 16 | ``` 17 | 18 | The `write` function is called for every buffer of available input: 19 | 20 | ```js 21 | function write (buffer, encoding, next) { 22 | // ... 23 | } 24 | ``` 25 | 26 | and the `end` function is called when there is no more data: 27 | 28 | ```js 29 | function end () { 30 | // ... 31 | } 32 | ``` 33 | 34 | Inside the write function, call `this.push()` to produce output data and call 35 | `next()` when you're ready to receive the next chunk: 36 | 37 | ```js 38 | function write (buffer, encoding, next) { 39 | this.push('I got some data: ' + buffer + '\n') 40 | next() 41 | } 42 | ``` 43 | 44 | and call `done()` to finish the output: 45 | 46 | ```js 47 | function end (done) { 48 | done() 49 | } 50 | ``` 51 | 52 | `write` and `end` are both optional. 53 | 54 | If `write` is not specified, the default implementation passes the input data to 55 | the output unmodified. 56 | 57 | If `end` is not specified, the default implementation calls `this.push(null)` 58 | to close the output side when the input side ends. 59 | 60 | Make sure to pipe `process.stdin` into your transform stream 61 | and pipe your transform stream into `process.stdout`, like this: 62 | 63 | ```js 64 | process.stdin.pipe(stream).pipe(process.stdout) 65 | ``` 66 | 67 | To convert a buffer to a string, call `buffer.toString()`. 68 | -------------------------------------------------------------------------------- /problems/http_server/index.js: -------------------------------------------------------------------------------- 1 | const through2 = require('through2') 2 | const hyperquest = require('hyperquest') 3 | const comparestdout = require('workshopper-exercise/comparestdout') 4 | 5 | let exercise = require('../../lib/exercise') 6 | const { inputFromWords, rndPort, writeStream } = require('../../lib/utils') 7 | 8 | const input = inputFromWords() 9 | 10 | exercise.addSetup(function (mode, callback) { 11 | this.submissionPort = rndPort() 12 | this.solutionPort = this.submissionPort + 1 13 | 14 | this.submissionArgs.push(this.submissionPort) 15 | this.solutionArgs.push(this.solutionPort) 16 | 17 | process.nextTick(callback) 18 | }) 19 | 20 | exercise.addProcessor(function (mode, callback) { 21 | this.submissionStdout.pipe(process.stdout) 22 | 23 | this.submissionStdout = through2() 24 | if (mode === 'verify') { 25 | this.solutionStdout = through2() 26 | } 27 | 28 | setTimeout(query.bind(this, mode), 500) 29 | 30 | process.nextTick(function () { 31 | callback(null, true) 32 | }) 33 | }) 34 | 35 | function request (port, stream, exercise) { 36 | const url = `http://localhost:${port}` 37 | const hq = hyperquest.post(url) 38 | .on('error', function (err) { 39 | exercise.emit( 40 | 'fail' 41 | , exercise.__('fail.connection', { address: url, message: err.message }) 42 | ) 43 | }) 44 | hq.pipe(stream) 45 | 46 | setTimeout(function () { 47 | stream.unpipe(hq) 48 | stream.end() 49 | }, 5000) 50 | 51 | writeStream(hq, input, 50) 52 | } 53 | 54 | function query (mode) { 55 | const exercise = this 56 | 57 | request(this.submissionPort, this.submissionStdout, exercise) 58 | 59 | if (mode === 'verify') { 60 | request(this.solutionPort, this.solutionStdout, exercise) 61 | } 62 | } 63 | 64 | exercise = comparestdout(exercise) 65 | 66 | module.exports = exercise 67 | -------------------------------------------------------------------------------- /problems/write_to_me/problem.md: -------------------------------------------------------------------------------- 1 | ## Writable Streams 2 | 3 | To create a custom `Writable` stream you must call the `new stream.Writable(options)` 4 | constructor and implement the `_write()` and/or `_writev()` method 5 | 6 | ```js 7 | const { Writable } = require('stream') 8 | 9 | const myWritable = new Writable({ 10 | write(chunk, encoding, callback) {} 11 | }) 12 | ``` 13 | 14 | or 15 | 16 | ```js 17 | const { Writable } = require('stream') 18 | 19 | class MyCustomWritable extends Writable { 20 | _write(chunk, encoding, callback) { 21 | // ... 22 | } 23 | } 24 | ``` 25 | 26 | ### `_write` method 27 | 28 | The `_write` method is used to send data to the underlying resource. 29 | This method MUST NOT be called by your application code directly. Instead it's 30 | called by internal `Writable` class methods only. 31 | 32 | The method receive the following arguments: 33 | 34 | * `chunk` is the value to be written, commonly a Buffer converted from the 35 | string you passed to `stream.write()`. 36 | * `encoding`, if the chunk is a string, will be the character encoding for the 37 | string. Otherwise it may be ignored. 38 | * `callback` function that will be called when the processing for the supplied 39 | chunk is complete. 40 | 41 | The callback function receive one argument, this argument must be an `Error` 42 | object if the write process fail or `null` if succeded. 43 | 44 | ### Using a Writable stream 45 | 46 | To write data to a writable stream you need to call the `write()` method on the 47 | stream instance. 48 | 49 | ```js 50 | readable.on('data', (chunk) => { 51 | writable.write(chunk) 52 | }) 53 | ``` 54 | 55 | Also you can use the `pipe` method, that we'd learned before. 56 | 57 | ### Challenge 58 | 59 | Implement a writable stream that writes in console `writing: ` + the given chunk 60 | New chunks are sent through stdin. 61 | -------------------------------------------------------------------------------- /problems/meet_pipe/problem.md: -------------------------------------------------------------------------------- 1 | ## What are streams? 2 | 3 | A stream is an abstract interface for working with streaming data in Node.js. 4 | 5 | That means you can consume data as it is loaded or produced, chunk by chunk (or 6 | piece by piece), instead of getting it all into memory to start consuming it. 7 | 8 | Streams can be readable, writable, or both. 9 | 10 | There are four types of streams: 11 | 12 | * `Readable` stream, which data can be read. 13 | * `Writable` stream, which data can be written. 14 | * `Duplex` stream, which is both `Readable` and `Writable`. 15 | * `Transform` stream, which is a `Duplex` stream that can modify or transform 16 | the data as it is written and read. 17 | 18 | Streams are present in many Node.js modules, for example `http.request()`, 19 | `zlib.createGzip()`, `fs.createReadStream()`, `process.stdout` ... all of these 20 | return streams. 21 | 22 | ## The `pipe` method 23 | 24 | The `pipe` method allows you to connect the output of the readable stream as the 25 | input of the writable stream 26 | 27 | ```js 28 | readable.pipe(writable) 29 | ``` 30 | 31 | If you pipe into a duplex stream you can chain to other stream. 32 | 33 | ```js 34 | readable.pipe(duplex).pipe(writable) 35 | ``` 36 | 37 | ## Challenge 38 | 39 | You will get a file as the first argument to your program (process.argv[2]). 40 | 41 | Use `fs.createReadStream()` to pipe the given file to `process.stdout`. 42 | 43 | `fs.createReadStream()` takes a file as an argument and returns a readable 44 | stream that you can call `.pipe()` on. Here's a readable stream that pipes its 45 | data to `process.stderr`: 46 | 47 | ```js 48 | const fs = require('fs') 49 | fs.createReadStream('data.txt').pipe(process.stderr) 50 | ``` 51 | 52 | Your program is basically the same idea, but instead of `'data.txt'`, the 53 | filename comes from `process.argv[2]` and you should pipe to stdout, not stderr. 54 | -------------------------------------------------------------------------------- /lib/exportFnExercise.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | let exercise = require('workshopper-exercise')() 4 | const filecheck = require('workshopper-exercise/filecheck') 5 | 6 | const { readableStream } = require('./utils') 7 | const stdinProcessor = require('./stdinProcessor') 8 | const solutionSetup = require('./solutionSetup') 9 | 10 | exercise = solutionSetup(exercise) 11 | 12 | exercise = filecheck(exercise) 13 | 14 | exercise.addSetup(function (mode, callback) { 15 | this.submissionFn = require(path.resolve(this.args[0])) 16 | 17 | if (typeof this.submissionFn !== 'function') { 18 | this.emit('fail', this.__('fail.invalid_export')) 19 | } 20 | this.solutionFn = require(this.solution) 21 | 22 | process.nextTick(callback) 23 | }) 24 | 25 | exercise.addProcessor(function (mode, callback) { 26 | if (this.execArgs) { 27 | this.submissionChild = this.submissionFn(process.execPath, this.execArgs) 28 | } else { 29 | this.submissionChild = this.submissionFn(this.submissionArgs) 30 | } 31 | this.submissionStdout = this.submissionChild 32 | this.submissionChild.stdin = this.submissionChild 33 | 34 | // We need a readable stream for `submissionChild.stderr`, since is piped to 35 | // `process.stderr` later by comparestdout 36 | // https://github.com/workshopper/workshopper-exercise/blob/master/comparestdout.js#L37 37 | this.submissionChild.stderr = readableStream() 38 | 39 | if (mode === 'verify') { 40 | if (this.execArgs) { 41 | this.solutionChild = this.solutionFn(process.execPath, this.execArgs) 42 | } else { 43 | this.solutionChild = this.solutionFn(this.solutionArgs) 44 | } 45 | this.solutionStdout = this.solutionChild 46 | this.solutionChild.stdin = this.solutionChild 47 | } 48 | 49 | process.nextTick(function () { 50 | callback(null, true) 51 | }) 52 | }) 53 | 54 | exercise = stdinProcessor(exercise) 55 | 56 | module.exports = exercise 57 | -------------------------------------------------------------------------------- /problems/http_server/problem.md: -------------------------------------------------------------------------------- 1 | In this challenge, write an http server that uses a through stream to write back 2 | the request stream as upper-cased response data for POST requests. 3 | 4 | Streams aren't just for text files and stdin/stdout. Did you know that http 5 | request and response objects from node core's `http.createServer()` handler are 6 | also streams? 7 | 8 | For example, we can stream a file to the response object: 9 | 10 | ```js 11 | const http = require('http') 12 | const fs = require('fs') 13 | const server = http.createServer(function (req, res) { 14 | fs.createReadStream('file.txt').pipe(res) 15 | }); 16 | server.listen(process.argv[2]) 17 | ``` 18 | 19 | This is great because our server can respond immediately without buffering 20 | everything in memory first. 21 | 22 | We can also stream a request to populate a file with data: 23 | 24 | ```js 25 | const http = require('http') 26 | const fs = require('fs') 27 | const server = http.createServer(function (req, res) { 28 | if (req.method === 'POST') { 29 | req.pipe(fs.createWriteStream('post.txt')) 30 | } 31 | res.end('beep boop\n') 32 | }); 33 | server.listen(process.argv[2]) 34 | ``` 35 | 36 | You can test this post server with curl: 37 | 38 | ```sh 39 | $ node server.js 8000 & 40 | $ echo hack the planet | curl -d@- http://localhost:8000 41 | beep boop 42 | $ cat post.txt 43 | hack the planet 44 | ``` 45 | 46 | Your http server should listen on the port given at process.argv[2] and convert 47 | the POST request written to it to upper-case using the same approach as the 48 | TRANSFORM example. 49 | 50 | As a refresher, here's an example with the default through2 callbacks explicitly 51 | defined: 52 | 53 | ```js 54 | const through = require('through2'); 55 | process.stdin.pipe(through(write, end)).pipe(process.stdout); 56 | 57 | function write (buf, _, next) { 58 | this.push(buf); 59 | next(); 60 | } 61 | function end (done) { done(); } 62 | ``` 63 | 64 | Do that, but send upper-case data in your http server in response to POST data. 65 | 66 | Make sure to `npm install through2` in the directory where your solution file 67 | lives. 68 | -------------------------------------------------------------------------------- /problems/read_it/problem.md: -------------------------------------------------------------------------------- 1 | ## Implementing a Readable Stream 2 | 3 | To implement a `Readable` stream, you need to construct an object, or inherit, 4 | from `stream.Readable` class and implement a `_read()` method in it. 5 | 6 | ```js 7 | const { Readable } = require('stream') 8 | 9 | const myStream = new Readable({}) 10 | myStream._read = () => {} 11 | ``` 12 | 13 | or 14 | 15 | ```js 16 | const { Readable } = require('stream') 17 | 18 | class MyStream extends Readable { 19 | _read() {} 20 | } 21 | ``` 22 | 23 | Note: This `_read` method MUST NOT be called by application code directly. 24 | It should be called by the internal `Readable` class methods only. 25 | 26 | ### Reading modes 27 | 28 | `Readable` streams operate in one of two modes: flowing and paused. 29 | 30 | * In flowing mode, data is read from the underlying system automatically and 31 | provided as quickly as possible. 32 | 33 | * In paused mode, the `read()` method must be called explicitly to read chunks 34 | of data from the stream. 35 | 36 | All Readable streams begin in paused mode but can be switched to flowing mode 37 | and also can be switched back to paused mode. 38 | 39 | ### Consuming a Readable Stream 40 | 41 | * `readable.pipe(writable)`, attaching `Writable` stream to the readable, cause 42 | it to switch automatically into flowing mode and push all of its data to the 43 | attached `Writable`. 44 | 45 | * `readable.on('readable', ...)`, here the stream (`readable`) is in paused mode 46 | and have to use the `read(size)` method for start consuming the data. 47 | 48 | * `readable.on('data', ...)`, adding the `data` event handler switch the stream 49 | to a flowing mode. We can pause and resume the stream by using `pause()` 50 | and `resume()` methods respectively. This is useful when you need to do some 51 | time-consuming action with the stream's data (such as writing to a database) 52 | 53 | ### Adding data to stream 54 | 55 | You can use the `push()` method to add content into the readable internal Buffer. 56 | 57 | ### Challenge 58 | 59 | Implement a Readable stream, initiate a new stream instance from your implementation 60 | and pipe to `process.stdout`. 61 | You will receive the content to add to your stream as the first argument to your 62 | program (`process.argv[2]`). 63 | 64 | ### Docs 65 | * `stream.Readable`: https://nodejs.org/api/stream.html#stream_class_stream_readable 66 | * `readable._read()`: https://nodejs.org/api/stream.html#stream_readable_read_size_1 67 | * stream reading modes: https://nodejs.org/api/stream.html#stream_two_reading_modes 68 | -------------------------------------------------------------------------------- /problems/combiner/problem.md: -------------------------------------------------------------------------------- 1 | Create a module in a new file named combiner.js, it should return a readable/writable stream using the 2 | `stream-combiner` module. 3 | 4 | You can use this code to start with: 5 | 6 | ```js 7 | const combine = require('stream-combiner') 8 | 9 | module.exports = function () { 10 | return combine( 11 | // read newline-separated json, 12 | // group books into genres, 13 | // then gzip the output 14 | ) 15 | } 16 | ``` 17 | Your stream will be written a newline-separated JSON list of science fiction 18 | genres and books. All the books after a `"type":"genre"` row belong in that 19 | genre until the next `"type":"genre"` comes along in the output. 20 | 21 | ```json 22 | {"type":"genre","name":"cyberpunk"} 23 | {"type":"book","name":"Neuromancer"} 24 | {"type":"book","name":"Snow Crash"} 25 | {"type":"genre","name":"space opera"} 26 | {"type":"book","name":"A Deepness in the Sky"} 27 | {"type":"book","name":"Void"} 28 | ``` 29 | 30 | Your program should generate a newline-separated list of JSON lines of genres, 31 | each with a `"books"` array containing all the books in that genre. The input 32 | above would yield the output: 33 | 34 | ```json 35 | {"name":"cyberpunk","books":["Neuromancer","Snow Crash"]} 36 | {"name":"space opera","books":["A Deepness in the Sky","Void"]} 37 | ``` 38 | 39 | Your stream should take this list of JSON lines and gzip it with 40 | `zlib.createGzip()`. 41 | 42 | ## * HINTS * 43 | 44 | The `stream-combiner` module creates a pipeline from a list of streams, 45 | returning a single stream that exposes the first stream as the writable side and 46 | the last stream as the readable side like the `duplexer` module, but with an 47 | arbitrary number of streams in between. Unlike the `duplexer` module, each 48 | stream is piped to the next. For example: 49 | 50 | ```js 51 | const combine = require('stream-combiner') 52 | const stream = combine(a, b, c, d) 53 | ``` 54 | 55 | will internally do `a.pipe(b).pipe(c).pipe(d)` but the `stream` returned by 56 | `combine()` has its writable side hooked into `a` and its readable side hooked 57 | into `d`. 58 | 59 | Your module should return the combined stream that will be fed input into the 60 | front 'end' of the stream, reads the associated JSON, processes the input book 61 | data by grouping it by genre and produces a gzipped result stream from which 62 | the result may be read. 63 | 64 | As in the previous LINES adventure, the `split2` module is very handy here. You 65 | can put a split2 stream directly into the stream-combiner pipeline. 66 | Note that split2 can send empty lines too. 67 | 68 | If you end up using `split2` and `stream-combiner`, make sure to install them 69 | into the directory where your solution file resides by doing: 70 | 71 | ```sh 72 | $ npm install stream-combiner split2 73 | ``` 74 | 75 | To verify your solution run: 76 | stream-adventure verify combiner.js 77 | -------------------------------------------------------------------------------- /lib/aliens.json: -------------------------------------------------------------------------------- 1 | ["Abaddon", 2 | "Abzorbaloff", 3 | "Adipose", 4 | "Aeolian", 5 | "Aggedor", 6 | "Akhaten humanoid", 7 | "Alpha Centauran", 8 | "Alzarian", 9 | "Alzarian spider", 10 | "Ambassadors", 11 | "Ancient Lights", 12 | "Androgum", 13 | "Andromedan", 14 | "Androzani bat", 15 | "Androzani tree", 16 | "Anethan", 17 | "Animus", 18 | "Anti-matter organism", 19 | "Anubian", 20 | "Arcateenian", 21 | "Arcturan", 22 | "Argolin", 23 | "Aridian", 24 | "Atraxi", 25 | "Auton", 26 | "Axos/Axon/Axonite", 27 | "Bandril", 28 | "Bane", 29 | "Bell Plant", 30 | "Berserker", 31 | "Biomechanoid", 32 | "Blowfish", 33 | "Bodach", 34 | "Brain parasite", 35 | "Carrionite", 36 | "Cash Cow", 37 | "Cassandra O'Brien.∆17", 38 | "Castrovalva humanoid", 39 | "Catkind", 40 | "Celestial Toymaker", 41 | "Cell 114", 42 | "Centuripede", 43 | "Chameleon", 44 | "Cheetah person", 45 | "Chelonian", 46 | "Chimeron", 47 | "Chloris humanoid", 48 | "Chronovore", 49 | "Chula", 50 | "Cowled figure", 51 | "Crespallion", 52 | "Crinothian", 53 | "Crooked person", 54 | "Cryon", 55 | "Cyberman", 56 | "Cyberman", 57 | "Cybermat", 58 | "Cybershade", 59 | "Dæmon", 60 | "Dalek", 61 | "Dalek/Human hybrid", 62 | "Dalek puppet", 63 | "Dark Hoarde", 64 | "Dauntless Prison inmates", 65 | "Diblosian", 66 | "Dido humanoid", 67 | "Digi-human", 68 | "Dogon", 69 | "Dominator", 70 | "Draconian", 71 | "Drahvin", 72 | "Drashig", 73 | "Drornidian", 74 | "Dulcian", 75 | "Duroc", 76 | "Eight Legs", 77 | "Eknodine", 78 | "Elder", 79 | "Empty Child plague", 80 | "Erasmus Darkening", 81 | "Eternal", 82 | "Etydion", 83 | "Eve", 84 | "Exxilon", 85 | "Face of Boe", 86 | "Fairy", 87 | "Family of Blood", 88 | "Fear entity", 89 | "Fendahl", 90 | "Fenric", 91 | "Fleshkind", 92 | "Flying stingray", 93 | "Foamasi", 94 | "Force-grown clone", 95 | "Futurekind", 96 | "Ganger", 97 | "Gastropod", 98 | "Gaztak", 99 | "Gee-jee fly", 100 | "Gelth", 101 | "Gond", 102 | "Gorgon", 103 | "Graske", 104 | "Great Intelligence", 105 | "Great Vampire", 106 | "Groske", 107 | "Gryffen family ghosts", 108 | "Guardian of Time", 109 | "Gubbage cone", 110 | "Haemo-Goth", 111 | "Haemovore", 112 | "Hand of Omega", 113 | "Hath", 114 | "Headless monk", 115 | "Hetocumtek", 116 | "Hitchhiker", 117 | "Hooloovoo", 118 | "Ice Warrior", 119 | "Inter Minorian", 120 | "International Gallery paintings", 121 | "Isolus", 122 | "Jadondan", 123 | "Jagaroth", 124 | "Jagrafess", 125 | "Jeggorabax energy entity", 126 | "Jixen", 127 | "Judoon", 128 | "Kahler", 129 | "Kaled", 130 | "Karfelon", 131 | "Karn humanoid", 132 | "Kastrian", 133 | "Keller Machine", 134 | "Kinda", 135 | "Kitling", 136 | "Korven", 137 | "Kraal", 138 | "Krafayis", 139 | "Krarg", 140 | "Krillitane", 141 | "Kroll", 142 | "Krontep humanoid", 143 | "Kroton", 144 | "Krynoid", 145 | "Lakertyan", 146 | "Land of Fiction beings", 147 | "Levithian", 148 | "Logopolitan", 149 | "Lucanian", 150 | "Lugal-Irra-Kush", 151 | "Lurman", 152 | "Macra", 153 | "Magma beast", 154 | "Malmooth", 155 | "Malus", 156 | "Mandragora Helix", 157 | "Mandrel", 158 | "Manussan", 159 | "Mara", 160 | "Marshman", 161 | "Master Brain", 162 | "Mayfly", 163 | "Mede", 164 | "Megara", 165 | "Melissa Majoria bees", 166 | "Memory worm", 167 | "Mentor", 168 | "Metalkind", 169 | "Minotaur", 170 | "Minyan", 171 | "Mire beast", 172 | "Mogarian", 173 | "Monoid", 174 | "Morestran", 175 | "Morlox", 176 | "Morok", 177 | "Mutated maggots/flies", 178 | "Mutated rat", 179 | "Mute", 180 | "Myrka", 181 | "Navarino", 182 | "Nestene Consciousness", 183 | "New Human", 184 | "Night Travellers", 185 | "Nimon", 186 | "Nostrovite", 187 | "Ogri", 188 | "Ogron", 189 | "Ood", 190 | "Ood Brain", 191 | "Oroborus", 192 | "Osirian", 193 | "Pan-babylonian", 194 | "Patchwork person", 195 | "Peg doll", 196 | "Peladonian", 197 | "Pelushi", 198 | "Pipe person", 199 | "Plasmaton", 200 | "Plasmavore", 201 | "''Platform One'' guests", 202 | "Posicarian", 203 | "Primord", 204 | "Proamonian", 205 | "Proto-human", 206 | "Psychic Circus", 207 | "Psychic pollen", 208 | "Pyrovile", 209 | "Qetesh", 210 | "Racnoss", 211 | "Raxacoricofallapatorian", 212 | "Reaper", 213 | "Red Leech", 214 | "Refusian", 215 | "Ribosian", 216 | "Richard Lazarus", 217 | "Rill", 218 | "Rutan", 219 | "Sand beast", 220 | "Saturnyn", 221 | "Scarecrow", 222 | "Screamer", 223 | "Sea Devil", 224 | "Seaweed creature", 225 | "Seed pod", 226 | "Sensorite", 227 | "Serpentine defence mechanism", 228 | "Sex gas", 229 | "Shadow Proclamation humanoids", 230 | "Shakri", 231 | "Shansheeth", 232 | "Shrivenzale", 233 | "Silurian", 234 | "Siren", 235 | "Sirian", 236 | "Skarasen", 237 | "Skonnan", 238 | "Skullion", 239 | "Sky fish", 240 | "Slyther", 241 | "Solonian", 242 | "Sontaran", 243 | "Space Pig", 244 | "Spiridon", 245 | "Star Whale", 246 | "Stigorax", 247 | "Stingray", 248 | "Swampie", 249 | "Sycorax", 250 | "Taran", 251 | "Tenza", 252 | "Terileptil", 253 | "Terraberserker", 254 | "Tetrap", 255 | "Thal", 256 | "Tharil", 257 | "The 4-5-6", 258 | "The Beast", 259 | "The Captain", 260 | "The Destroyer", 261 | "The Ergon", 262 | "The Flood", 263 | "The Garm", 264 | "The Hunger", 265 | "The Raak", 266 | "The Shadow", 267 | "The Silence", 268 | "The Swarm", 269 | "The Trickster", 270 | "The Wire", 271 | "Thoros Alphan", 272 | "Tigellan", 273 | "Time Beetle", 274 | "Time Brain", 275 | "Time Lord", 276 | "Tivoli species", 277 | "Toclafane", 278 | "Torajii", 279 | "Tractator", 280 | "Trakenite", 281 | "Travist Polong", 282 | "Trion", 283 | "Tritovore", 284 | "Tumour alien", 285 | "Tythonian", 286 | "Ukkan", 287 | "Ultramancer", 288 | "Urbankan", 289 | "Usurian", 290 | "Uvodni", 291 | "Uxariean", 292 | "Validium", 293 | "Vardan", 294 | "Varga plant", 295 | "Vashta Nerada", 296 | "Veil", 297 | "Vervoid", 298 | "Vespiform", 299 | "Vigil", 300 | "Vinvocci", 301 | "Vishklar", 302 | "Visian", 303 | "Vogan", 304 | "Voord", 305 | "Vortis", 306 | "War Lord", 307 | "Weeping Angel", 308 | "Weevil", 309 | "Werewolf", 310 | "Whisper men", 311 | "Winder", 312 | "Wirrn", 313 | "Wolfweed", 314 | "Wood beast", 315 | "Xeraphin", 316 | "Xeron", 317 | "Xylok", 318 | "Zanak humanoid", 319 | "Zocci", 320 | "Zolfa-Thuran", 321 | "Zygon"] 322 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [4.3.2](https://github.com/substack/stream-adventure/compare/v4.3.1...v4.3.2) (2021-03-27) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * **lines:** add separator for input message fix [#254](https://github.com/substack/stream-adventure/issues/254) ([2b085d9](https://github.com/substack/stream-adventure/commit/2b085d9f7ab091857b381de887ab58953725cb68)) 11 | * **read_it/meet_pipe:** typos fix [#259](https://github.com/substack/stream-adventure/issues/259) ([468d2eb](https://github.com/substack/stream-adventure/commit/468d2eb2577df4e9ba80e612f86c0735d6b19108)) 12 | 13 | ### [4.3.1](https://github.com/substack/stream-adventure/compare/v4.3.0...v4.3.1) (2021-01-23) 14 | 15 | 16 | ### Bug Fixes 17 | 18 | * ** write_to_me:** Typos ([eb9b09c](https://github.com/substack/stream-adventure/commit/eb9b09c88954530876d5ef291eab8cfdc29065eb)) 19 | * **meet_pipe:** clarify stream explanation ([6e109b5](https://github.com/substack/stream-adventure/commit/6e109b5ecb63c4fc45d4e966a518e347a0203c05)) 20 | * **read_it:** remove constructor args ([216bae6](https://github.com/substack/stream-adventure/commit/216bae6c843883919d4e0a16740633f232e2b536)) 21 | 22 | ## [4.3.0](https://github.com/substack/stream-adventure/compare/v4.2.1...v4.3.0) (2020-09-16) 23 | 24 | 25 | ### Features 26 | 27 | * add writable problem ([01a2be1](https://github.com/substack/stream-adventure/commit/01a2be1fdc82a8baa25fee6e8507280f8de91c96)) 28 | 29 | 30 | ### Bug Fixes 31 | 32 | * **duplexer_redux:** fix input message ([89795a0](https://github.com/substack/stream-adventure/commit/89795a040cdd6f59ff0d1d349debbe3266608c7d)) 33 | * **read_it:** adjust wording on challenge instructions ([210cb33](https://github.com/substack/stream-adventure/commit/210cb33d92765cfcdae52fecf083364a0da8c563)) 34 | * **read_it:** simplify reference solution ([4c8d6e2](https://github.com/substack/stream-adventure/commit/4c8d6e2ca19865a6122f79553f27ed1c3b6fc7c4)) 35 | 36 | ### [4.2.1](https://github.com/substack/stream-adventure/compare/v4.2.0...v4.2.1) (2020-06-20) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * **read_it:** specify how readable will receive content ([b877f22](https://github.com/substack/stream-adventure/commit/b877f22dacbc0448b84ca26905c3dcbcf4ad1f3f)) 42 | 43 | ## 4.2.0 (2020-06-20) 44 | 45 | 46 | ### Features 47 | 48 | * **meet_pipe:** add an introduction about stream and pipe ([f38d649](https://github.com/substack/stream-adventure/commit/f38d64931eb3a0a5a946b45be17593d52f4e447c)) 49 | * add problem to implement a Readable stream ([7dcab70](https://github.com/substack/stream-adventure/commit/7dcab70e165897d433d4d132222ae5fc59e39043)) 50 | * change problems files to markdown ([6704af0](https://github.com/substack/stream-adventure/commit/6704af081794bcec13651a366a9d38e34e53357f)) 51 | 52 | 53 | ### Bug Fixes 54 | 55 | * add default messages ([ba718c9](https://github.com/substack/stream-adventure/commit/ba718c96e2e1c1b8d317ee8a9df31023113e8cc0)) 56 | * **secretz:** update problem description ([6a8bbf4](https://github.com/substack/stream-adventure/commit/6a8bbf459fa06fdbf04ae4dde7aa1c21c8e2b9e5)) 57 | * call solutionSetup after execute on exercise ([2183015](https://github.com/substack/stream-adventure/commit/2183015c56b29aa2e766c817c4e65af32ce3063e)) 58 | * output message ([76200f2](https://github.com/substack/stream-adventure/commit/76200f29303622838d8517608ad8b298aa8e93c8)) 59 | * set appDir to fix i18n lookup ([0c4b04c](https://github.com/substack/stream-adventure/commit/0c4b04cc64c832f0af02ebb71fc956b993e87644)) 60 | * stop cheating in concat solution ([a2fd01a](https://github.com/substack/stream-adventure/commit/a2fd01a6ce0353874837f2021d6e709c3f21f06d)) 61 | * update concat problem description ([6bcb767](https://github.com/substack/stream-adventure/commit/6bcb76793e5fb2c5231c5e45d69afe3a55c9cf82)) 62 | * use `crypto.createCipheriv` instead `crypto.createCipher` ([d0342fb](https://github.com/substack/stream-adventure/commit/d0342fbb5925176b348b63fe9ad14a07a1d100b4)) 63 | 64 | ### 4.1.2 (2020-04-23) 65 | 66 | ### Updates 67 | 68 | * use workshopper-adventure/adventure instead adventure ([5bcf696](https://github.com/workshopper/stream-adventure/commit/5bcf696b7d77d363c584ee1c52577c4c402cf5e0)) 69 | * update problems to use workshopper-exercise ([35db979](https://github.com/workshopper/stream-adventure/commit/35db979e60172b4573c4a12104e10bd48fcc5b1b)) 70 | * use workshopper-adventure core ([29c9e85](https://github.com/workshopper/stream-adventure/commit/29c9e85a1a7f71e8145a7e5cb7cc42ec5f50764b)) 71 | * update websockets problem ([74160ac](https://github.com/workshopper/stream-adventure/commit/74160ac46dcb1b669d9a17a6d76f48debcb53bc6)) 72 | 73 | ### Bug Fixes 74 | 75 | * add default messages ([ba718c9](https://github.com/substack/stream-adventure/commit/ba718c96e2e1c1b8d317ee8a9df31023113e8cc0)) 76 | * output message ([76200f2](https://github.com/substack/stream-adventure/commit/76200f29303622838d8517608ad8b298aa8e93c8)) 77 | * set appDir to fix i18n lookup ([0c4b04c](https://github.com/substack/stream-adventure/commit/0c4b04cc64c832f0af02ebb71fc956b993e87644)) 78 | * use `crypto.createCipheriv` instead `crypto.createCipher` ([d0342fb](https://github.com/substack/stream-adventure/commit/d0342fbb5925176b348b63fe9ad14a07a1d100b4)) 79 | 80 | 81 | ## [4.1.1](https://github.com/substack/stream-adventure/compare/4.1.0...4.1.1) (2018-01-22) 82 | 83 | * tar.Parse class has to be invoked with new. 84 | * Remove objectMode: true for the duplexer solution 85 | 86 | 87 | # [4.1.0](https://github.com/workshopper/stream-adventure/compare/4.0.5...4.1.0) (2017-04-05) 88 | 89 | 90 | Contains several updates in definitions of problems, making exercises more accessible. 91 | 92 | ### Updates 93 | 94 | * [Add documentation on contributing](https://github.com/workshopper/stream-adventure/pull/187) 95 | * [Create directory for stream adventure and unique program](https://github.com/workshopper/stream-adventure/pull/188) 96 | * [Add a new file creation step, and verify command - combiner](https://github.com/workshopper/stream-adventure/pull/189) 97 | * [Add a new file creation step, and verify command - concat](https://github.com/workshopper/stream-adventure/pull/190) 98 | * [Update badge](https://github.com/workshopper/stream-adventure/pull/192) 99 | * [Update LINES problem definition](https://github.com/workshopper/stream-adventure/pull/193) 100 | * [Update http-client problem definition](https://github.com/workshopper/stream-adventure/pull/194) 101 | * [Update duplexer problem](https://github.com/workshopper/stream-adventure/pull/196) 102 | * [Update duplexer-redux problem definition](https://github.com/workshopper/stream-adventure/pull/197) 103 | --------------------------------------------------------------------------------