├── .babelrc ├── .gitignore ├── HISTORY.md ├── README.md ├── index.d.ts ├── package.json ├── rx-typings.d.ts ├── src ├── index.ts ├── makeAsyncDriver.ts └── makeDriverSource.ts ├── test └── test.js ├── tsconfig.json ├── typings.json ├── xstream-typings.d.ts ├── xstream.d.ts └── xstream.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Editor-specific 2 | .idea 3 | 4 | # Installed libs 5 | node_modules 6 | typings 7 | 8 | # Generated 9 | dist/ 10 | 11 | # Misc 12 | npm-debug.log -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ### v2.0.0-beta.0 2 | * Completly new version, with no stream lib dependendecies 3 | * Driver source is not a stream. 4 | * Only `select` (name is customizable) helpler 5 | * Flatten helpers removed 6 | * Pull helper removed 7 | 8 | ### v1.4.0 9 | * Added successAll and failureAll helpers 10 | * select() without argument returns stream itself (to be compatible with future version) 11 | 12 | ### v1.3.0 13 | * Added pull helper (two options: using new driver or using isolate mechanics) 14 | 15 | ### v1.2.0 16 | * Added flatten helpers `success` and `failure` 17 | * Added default selector helper `select` with default `category` prop 18 | * Exports `attachHelpers` and other helper methods for extenal use 19 | 20 | ### v1.1.2 21 | * Use `normalizeRequest` in isolate if no `isolateMap` 22 | 23 | ### v1.1.1 24 | * Added ability to create driver repsonse from callback and (native) Promise (`getResponse`) 25 | * Added `makeAsyncDriver` (as recommended method), `createdDriver` stays 26 | * Added `responseProp` 27 | 28 | ### v1.0.0 29 | * Basic API via `createDriver` method -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cycle-async-driver 2 | > Higher order factory for creating [cycle.js](http://cycle.js.org) async request/response drivers. 3 | 4 | ![npm (scoped)](https://img.shields.io/npm/v/cycle-async-driver.svg?maxAge=86400) 5 | 6 | > Notice that API of version 2.x has significantly changed since 7 | [1.x](https://github.com/whitecolor/cycle-async-driver/tree/e1edceb28fc808e755449c3dbf0073184135dfa8), 8 | which where using `rxjs4` and has some excessive features that where removed in 2.x. 9 | 10 | **Currently 2.0 (diversity) version is in beta, so use:** 11 | ```bash 12 | npm install cycle-async-driver@beta -S 13 | ``` 14 | 15 | Allows you easily create fully functional **cycle.js driver** 16 | which side effect is executed using async function with **promise or callback**. 17 | It also can serve as a simple backbone for your more sophisticated driver. 18 | 19 | Such driver will work in the same manner as 20 | [official cycle HTTP driver](https://github.com/cyclejs/cyclejs/tree/master/http), so basically: 21 | 22 | * driver sink will expect stream of requests 23 | * driver source will provide you with "metastream" of responses 24 | * driver source will provide standard *isolate* mechanics (`@cycle/isolate`) 25 | * driver will be compatible with any stream library that cycle.js can work with 26 | (it doesn't use any under the hood, but uses cyclejs stream adapter's API). 27 | 28 | ## API 29 | 30 | ### `makeAsyncDriver` 31 | 32 | Async driver factory. 33 | 34 | - `getResponse` *(Function)*: function that takes `request` object as first param 35 | and `callback` as second param 36 | returns any kind of Promise or uses passed node style `callback` 37 | to give back async response for passed `request`. **required** (if no `getProgressiveResponse`) 38 | 39 | - `getProgressiveResponse` *(Function)*: function that takes `request` object as first param 40 | and `observer` object as second param which allows to create a custom response 41 | containing more then one value. **required** (if no `getResponse`) 42 | 43 | - `normalizeRequest` *(Function)*: transform function that will be applied to the request before handling. 44 | 45 | - `requestProp` *(String)*: name of the property that will be attached to every response stream. *default value: `request`* 46 | 47 | - `isolate` *(Boolean)*: makes driver ready to work with [`@cycle/isolate`](#isolation). *default value: `true`* 48 | 49 | - `isolateNormalize` *(Function)*: transform function that will be 50 | applied to the request before its isolation, if not present `normalizeRequest` will be used instead. 51 | 52 | - `isolateProp` *(String)*: name of the property that will be used for keeping isolation namespace. *default value: `namespace`* 53 | 54 | - `lazy` *(Boolean)*: makes all driver requests [lazy](#lazy-drivers-and-requests) by default, 55 | can be overridden by particular request options. *default value: `false`* 56 | 57 | ## Usage 58 | 59 | ### Basic example 60 | Let's create cycle.js driver which will be able to read files 61 | using node.js `fs` module: 62 | 63 | ```js 64 | import {makeAsyncDriver} from 'cycle-async-driver' 65 | import fs from 'fs' 66 | import {run} from '@cycle/rx-run' 67 | import {Observable as O} from 'rx' 68 | 69 | let readFileDriver = makeAsyncDriver((request, callback) => { 70 | fs.readFile(requst.path, request.encoding || 'utf-8', callback) 71 | // instead of using `callback` param you may return Promise 72 | }) 73 | 74 | ... 75 | 76 | const Main = ({readFile}) => { 77 | return { 78 | readFile: O.of({path: '/path/to/file/to/read'}), 79 | output: read 80 | .select() 81 | // select() is used to get all response$ streams 82 | // you may also filter responses by `category` field 83 | // or by request filter function 84 | .mergeAll() 85 | // as we get metastream of responses - 86 | // we should flatten it to get file data 87 | } 88 | } 89 | 90 | run(Main, { 91 | output: (ouput$) => ouput$.forEach(::console.log) 92 | readFile: readFileDriver 93 | }) 94 | 95 | ``` 96 | 97 | ### Metastream of responses 98 | 99 | `makeAsyncDriver` creates a driver function which accepts 100 | stream of requests (`request$`) and returns driver source 101 | which is an object that you use to access responses that come from the driver. 102 | 103 | To access responses driver sources provides special selector method 104 | (`select` is default name) which takes nothing or *string* `category` 105 | (default name of selector property) and returns stream with all responses 106 | or filtered by request's `category` field. 107 | 108 | Stream returned by `select()` is a metastream of responses. This means 109 | that each element of it is a stream itself, so it is **a stream of streams** 110 | and usually referred as `response$$` (stream of `response$`). 111 | Each element of it is `response$` stream that produces resulting values 112 | originated from particular request you send to driver sink (`request$`). 113 | 114 | ```js 115 | // to get plain response data you should flatten metastream of repsonses 116 | Driverstream 117 | .select('something-special') // returns metastream of responses (response$$) 118 | .mergeAll() // gets flatten stream of repsonses data 119 | ``` 120 | 121 | Each `response$` has attached 122 | property `request` (default name) which contains corresponding 123 | *normalized* request. In simple case this stream produces only 124 | one resulting value (actual response data). In case of *progressive* response 125 | it may produce multiple values before completion. 126 | 127 | Also driver source has method `filter` witch takes filtering function for 128 | `response$$` metastream and returns *filtered* driver source. 129 | 130 | ```js 131 | // is some cases you may want to get filtered source 132 | Driverstream 133 | .filter(r$$ => r$$.request.method === 'DELETE') // returns filtered driver source 134 | .select() // gets all response$$ stream 135 | ``` 136 | 137 | Each of `response$` streams 138 | **potentially may produce an error** which should be [properly handled](#requests-error-handling). 139 | Metastream `response$$` will produce an error only if `request$` 140 | produces it and will **end** when `request$` stream completes. 141 | 142 | ### Isolation 143 | 144 | By default driver source will provider 145 | [standard isolation](https://github.com/cyclejs/cyclejs/tree/master/isolate) 146 | strategy based on scoped namespaces. For this to each `request` object 147 | passed though isolated component boundaries isolation scope value 148 | is attached to it using special property `_namespace` (default name). 149 | Isolated driver source will automatically filter responses 150 | corresponding to requests belonging to isolation scope. 151 | So parent components **have access to isolated child's** responses. 152 | 153 | ### Requests error handling 154 | 155 | As it was said that `select()` method returns metastream of responses 156 | (`response$$`), which produces response streams (`response$`) each of which 157 | may produce an error if something goes wrong while performing request. 158 | 159 | It was mentioned also that to for responses you need eventually to flatten 160 | metastream of responses. But **notice** that if you handle/catch an error 161 | on the flattened `response$$` like that: 162 | ```js 163 | // rxjs 164 | yourDriver 165 | .select() // responses$$ stream 166 | .mergeAll() // flatten stream of all plain responses 167 | .catch(error => of({error})) // replace the error 168 | // this stream will not have exception 169 | // but will end right after first error caught 170 | ``` 171 | the stream will be completed and you **won't get there anything after first error**. 172 | 173 | So usually for proper handling you need to handle error 174 | on each response stream (`response$`), for example like that: 175 | 176 | ```js 177 | // xstream 178 | yourDriver 179 | .select() // responses$$ stream 180 | .map(r$ => r$ // catch error for each response$ 181 | .map(success => ({success})) 182 | .replaceError(error => ({error})) 183 | ) 184 | ``` 185 | 186 | One of the recommended methods of dealing with successful and failed 187 | requests from driver is to use simple helpers that will leave 188 | requests with needed result: 189 | 190 | ```js 191 | // rxjs 192 | let failure = r$ => r$.skip().catch(of) 193 | let success = r$ => r$.catch(empty) 194 | ``` 195 | 196 | ```js 197 | // xstream 198 | let failure = r$ => r$.drop().replaceError(xs.of) 199 | let success = r$ => r$.replaceError(xs.empty) 200 | ``` 201 | 202 | Then you can get only successful responses without errors: 203 | ```js 204 | // rxjs 205 | HTTP.select() 206 | .flatMapLatest(success) 207 | ``` 208 | ```js 209 | // xstream 210 | HTTP.select() 211 | .map(success) 212 | .flatten() 213 | ``` 214 | 215 | ### Accessing response/request pairs 216 | Sometimes you may find yourself in a need to access corresponding 217 | response and request pairs, to do this follow such approach: 218 | 219 | ```js 220 | const getPairs = r$ => r$.map(res => ({res, req: r$.request})) 221 | // get all succesfful response/request pairs 222 | let goodResReqPair$ = asyncDriver.select() 223 | .map(success) 224 | .map(getPairs) 225 | .mergeAll() 226 | ``` 227 | You can even create something like that: 228 | ```js 229 | // create such success mapper factory 230 | const success = (mapper = (_ => _)) => 231 | r$ => r$.catch(empty).map(res => mapper(res, r$.request)) 232 | ... 233 | // map succesfful response/request pair to something 234 | let goodReqResMapped$ = asyncDriver.select() 235 | .flatMap(success( 236 | (response, request) => ... 237 | )) 238 | ``` 239 | *It is all functional approach. Compose functions as you feel it needs to be.* 240 | 241 | ### Lazy drivers and requests 242 | 243 | By default **all requests are eager** (start to perform a side effect just after 244 | they get "into" the driver) and response streams (which correspond to particular request) 245 | are **hot (multicated)** and **remembered** (has short memory) which means 246 | that any number of subscriber may listen to response stream and while only one request will be performed 247 | all the subscribers will get response value(s), even late subscribers will 248 | get the **one last value** from response stream 249 | (you should consider this when dealing with progressive responses). 250 | 251 | Lazy request on the other hand starts performing side effect 252 | when they get subscriber. Depending of the stream library you use 253 | for lazy requests you will get either cold (*rxjs*, *most*) 254 | or hot (*xstream* - where all streams are hot) `response$` stream. 255 | 256 | Note that if you subscribe to lazy and **cold stream** (`response$`) in you app, 257 | **request will start to be performed each time you subscribe** to it, 258 | this thing is very important to consider (when using rx.js, most.js). 259 | 260 | To get lazy driver just pass `lazy` option set to `true`, so all requests by default will be lazy: 261 | ```js 262 | let readFileDriver = makeAsyncDriver({ 263 | getResponse: request, callback) => { 264 | fs.readFile(requst.path, request.encoding || 'utf-8', callback) 265 | }, 266 | lazy: true 267 | }) 268 | ``` 269 | 270 | Or you can always override driver setting and make any request *lazy* (or *eager*) if required 271 | by adding `lazy: true` (or `lazy: false`) option to the request inside your app's logic: 272 | ```js 273 | readFile: O.of({ 274 | path: '/path/to/file/to/read', 275 | lazy: true 276 | }), 277 | ``` 278 | 279 | ### Cancellation (and abortion) 280 | Basically, when you want request to be cancelled you should 281 | stop listening to corresponding `response$` (response stream). 282 | By default request in drivers are *eager* thus start without 283 | subscription in you apps logic. 284 | 285 | That said request cancellation **works only for *lazy* requests** because such requests 286 | start on subscription creation and may be cancelled/aborted if subscription 287 | is dropped before request is finished to performed. 288 | 289 | Often automatic subscription drop is done using flattening the stream of responses 290 | to the latest response: 291 | 292 | ```js 293 | // rxjs 294 | myCoolDriver 295 | .select('something_special') 296 | .flatMapLatest() // or .switch() 297 | .map(...) 298 | // so here you will get only responses from last request started 299 | // you won't see responses of the requets that started before 300 | // the last one and were not finished before 301 | ``` 302 | 303 | If you want to implement driver that on cancellation makes 304 | some action - for example aborts not completed requests, you should 305 | follow this approach: 306 | 307 | ```js 308 | import {makeAsyncDriver} from 'cycle-async-driver' 309 | 310 | // this example also shows you how to use `getProgressiveResponse` 311 | // say we have some `coolSource` to which we can make requests 312 | // and get some response stream back, 313 | // and we want translate it to a cycle driver 314 | let myCoolDriver = makeAsyncDriver({ 315 | getProgressiveResponse: (request, observer, onDispose) => { 316 | const coolRequest = coolSource.makeRequest(request, (coolStream) => { 317 | coolStream.on('data', observer.next) 318 | coolStream.on('error', observer.error) 319 | coolStream.on('end', observer.completed) 320 | }) 321 | // third param of `getResponse` or `getProgressiveResponse` 322 | // is a function `onDispose` that takes 323 | // a handler which will be called when no listeners 324 | // is needing response for this request anymore, 325 | // in this case if it happens before the request is completed 326 | // you may want to abort it 327 | 328 | onDispose(() => !coolRequest.isCompleted() && coolRequest.abort()) 329 | } 330 | }) 331 | ``` 332 | Note that `onDispose` handler will be called always when request completed successfully. 333 | 334 | ## Tests 335 | ``` 336 | npm install 337 | npm run test 338 | ``` 339 | For running test in dev mode with watching `node-dev` should be installed globally (`npm i node-dev -g`) -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import {StreamAdapter} from '@cycle/base'; 2 | 3 | interface ResponseStream { 4 | request: Request; 5 | } 6 | 7 | export type GetResponseCallback = (err: any, response?: Response) => any 8 | 9 | export type GetResponse = ( 10 | request: Request, 11 | callback: GetResponseCallback, 12 | disposeCallback: any 13 | ) => any 14 | 15 | export type GetProgressiveResponseObserver = { err: any, next: any, complete: any } 16 | 17 | export type GetProgressiveResponse = ( 18 | request: Request, 19 | observer: GetProgressiveResponseObserver, 20 | disposeCallback: any 21 | ) => any 22 | 23 | export function makeAsyncDriver 24 | (getResponse: GetResponse): 25 | (sink$: any, runSA: any) => DriverSource 26 | 27 | export function makeAsyncDriver 28 | ( 29 | params: { 30 | getResponse: GetResponse 31 | lazy?: boolean 32 | }): 33 | (sink$: any, runSA: StreamAdapter) => DriverSource 34 | 35 | export function makeAsyncDriver 36 | ( 37 | params: { 38 | getProgressiveResponse: GetProgressiveResponse 39 | lazy?: boolean 40 | }): 41 | (sink$: any, runSA: StreamAdapter) => DriverSource 42 | 43 | export function makeAsyncDriver 44 | ( 45 | params: { 46 | getResponse: GetResponse 47 | normalizeRequest?(request: Request): NormalizedRequest, 48 | isolateMap?(request: Request): NormalizedRequest, 49 | lazy?: boolean 50 | }): 51 | (sink$: any, runSA: StreamAdapter) => DriverSource 52 | 53 | export function makeAsyncDriver 54 | ( 55 | params: { 56 | getProgressiveResponse: GetProgressiveResponse 57 | normalizeRequest?(request: Request): NormalizedRequest, 58 | isolateMap?(request: Request): NormalizedRequest, 59 | lazy?: boolean 60 | }): 61 | (sink$: any, runSA: StreamAdapter) => DriverSource 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cycle-async-driver", 3 | "version": "2.0.0-beta.0", 4 | "description": "Factory for creating async monadic cycle.js drivers", 5 | "keywords": [ 6 | "cyclejs", 7 | "driver", 8 | "async" 9 | ], 10 | "main": "lib/index.js", 11 | "types": "index.d.ts", 12 | "scripts": { 13 | "clean": "rimraf lib", 14 | "build": "npm run clean && babel src -d lib", 15 | "build-watch": "babel -w src -d lib", 16 | "test": "node -r babel-register test/test.js", 17 | "test-watch": "node-dev --respawn -r babel-register test/test.js", 18 | "prepublish": "npm run clean && npm run build", 19 | "vmd-readme": "vmd README.md" 20 | }, 21 | "author": "whitecolor", 22 | "license": "ISC", 23 | "devDependencies": { 24 | "@cycle/core": "^6.0.0", 25 | "@cycle/isolate": "^1.2.0", 26 | "@cycle/most-adapter": "^4.0.1", 27 | "@cycle/most-run": "^4.0.0", 28 | "@cycle/rx-adapter": "^3.0.0", 29 | "@cycle/rx-run": "^7.0.0", 30 | "@cycle/xstream-adapter": "^1.0.5", 31 | "@cycle/xstream-run": "^3.0.4", 32 | "babel-cli": "^6.5.1", 33 | "babel-preset-es2015": "^6.3.13", 34 | "babel-preset-stage-0": "^6.3.13", 35 | "most": "^1.0.1", 36 | "rx": "^4.0.7", 37 | "tape": "^4.4.0", 38 | "xstream": "^6.1.0" 39 | }, 40 | "standard": { 41 | "parser": "babel-eslint" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /rx-typings.d.ts: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | import {ResponseStream} from './index' 3 | 4 | export interface RxAsyncSource { 5 | filter(predicate: (request: Request) => boolean): RxAsyncSource 6 | select(category?: string): Observable & ResponseStream> 7 | } 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import makeDriverSource from './makeDriverSource' 2 | import makeAsyncDriver from './makeAsyncDriver' 3 | 4 | export {makeAsyncDriver, makeDriverSource} 5 | 6 | export default makeAsyncDriver -------------------------------------------------------------------------------- /src/makeAsyncDriver.ts: -------------------------------------------------------------------------------- 1 | import makeDriverSource from './makeDriverSource' 2 | const isFunction = (f) => typeof f === 'function' 3 | 4 | const makeAsyncDriver = (options) => { 5 | let { 6 | getResponse, 7 | getProgressiveResponse, 8 | requestProp = 'request', 9 | normalizeRequest, 10 | isolate = true, 11 | isolateProp = '_namespace', 12 | isolateNormalize = null, 13 | selectHelperName = 'select', 14 | selectDefaultProp = 'category', 15 | lazy = false 16 | } = options 17 | 18 | if (normalizeRequest && !isFunction(normalizeRequest)) { 19 | throw new Error(`'normalize' option should be a function.`) 20 | } 21 | if (normalizeRequest && !isolateNormalize){ 22 | isolateNormalize = normalizeRequest 23 | } 24 | if (isFunction(options)) { 25 | getResponse = options 26 | } 27 | if (!isFunction(getResponse) && !isFunction(getProgressiveResponse)) { 28 | throw new Error(`'getResponse' param is method is required.`) 29 | } 30 | 31 | return (request$, runStreamAdapter) => { 32 | if (!runStreamAdapter){ 33 | throw new Error(`Stream adapter is required as second parameter`) 34 | } 35 | const empty = () => {} 36 | const emptySubscribe = (stream) => 37 | runStreamAdapter.streamSubscribe((stream), { 38 | next: empty, 39 | error: empty, 40 | complete: empty 41 | }) 42 | 43 | let response$$ = runStreamAdapter.adapt({}, (_, observer) => { 44 | runStreamAdapter.streamSubscribe(request$, { 45 | next: (request) => { 46 | const requestNormalized = normalizeRequest 47 | ? normalizeRequest(request) 48 | : request 49 | let isLazyRequest = typeof requestNormalized.lazy === 'boolean' 50 | ? requestNormalized.lazy : lazy 51 | let response$ = runStreamAdapter.adapt({}, (_, observer) => { 52 | let dispose 53 | const disposeCallback = (_) => dispose = _ 54 | if (getProgressiveResponse) { 55 | const contextFreeObserver = { 56 | next: observer.next.bind(observer), 57 | error: observer.error.bind(observer), 58 | complete: observer.complete.bind(observer) 59 | } 60 | getProgressiveResponse( 61 | requestNormalized, contextFreeObserver, disposeCallback 62 | ) 63 | } else { 64 | const callback = (err, result) => { 65 | if (err){ 66 | observer.error(err) 67 | } else { 68 | observer.next(result) 69 | observer.complete() 70 | } 71 | } 72 | let res = getResponse(request, callback, disposeCallback) 73 | if (res && isFunction(res.then)){ 74 | res.then((result) => callback(null, result), callback) 75 | } 76 | } 77 | return () => { 78 | isFunction(dispose) && dispose() 79 | } 80 | }) 81 | if (!isLazyRequest){ 82 | response$ = runStreamAdapter.remember(response$) 83 | emptySubscribe(response$) 84 | } 85 | if (requestProp){ 86 | Object.defineProperty(response$, requestProp, { 87 | value: requestNormalized, 88 | writable: false 89 | }) 90 | } 91 | observer.next(response$) 92 | }, 93 | error: observer.error.bind(observer), 94 | complete: observer.complete.bind(observer) 95 | }) 96 | }) 97 | response$$ = runStreamAdapter.remember(response$$) 98 | emptySubscribe(response$$) 99 | 100 | return makeDriverSource(response$$, { 101 | runStreamAdapter, 102 | selectHelperName, 103 | selectDefaultProp, 104 | requestProp, 105 | isolate, 106 | isolateProp, 107 | isolateNormalize 108 | }) 109 | } 110 | } 111 | 112 | export default makeAsyncDriver 113 | -------------------------------------------------------------------------------- /src/makeDriverSource.ts: -------------------------------------------------------------------------------- 1 | const makeFilter = (streamAdapter) => 2 | (stream, predicate) => 3 | streamAdapter.adapt({}, (_, observer) => 4 | streamAdapter.streamSubscribe(stream, { 5 | next: (r$) => { 6 | if (predicate(r$)) { 7 | observer.next(r$) 8 | } 9 | }, 10 | error: observer.error.bind(observer), 11 | complete: observer.complete.bind(observer) 12 | }) 13 | ) 14 | 15 | const makeDriverSource = (response$$, options) => { 16 | let { 17 | runStreamAdapter, 18 | selectHelperName, 19 | selectDefaultProp, 20 | requestProp, 21 | isolate, 22 | isolateProp, 23 | isolateNormalize 24 | } = options 25 | 26 | let filterStream = makeFilter(runStreamAdapter) 27 | 28 | let driverSource = { 29 | filter(predicate): any { 30 | const filteredResponse$$ = filterStream( 31 | response$$, (r$) => predicate(r$.request) 32 | ) 33 | return makeDriverSource(filteredResponse$$, options) 34 | }, 35 | isolateSink(request$, scope) { 36 | return request$.map(req => { 37 | req = isolateNormalize ? isolateNormalize(req) : req 38 | req[isolateProp] = req[isolateProp] || [] 39 | req[isolateProp].push(scope) 40 | return req 41 | }) 42 | }, 43 | isolateSource: (source, scope) => { 44 | let requestPredicate = (req) => { 45 | return Array.isArray(req[isolateProp]) && 46 | req[isolateProp].indexOf(scope) !== -1 47 | } 48 | 49 | return source.filter(requestPredicate) 50 | }, 51 | select(category) { 52 | if (!category) { 53 | return response$$ 54 | } 55 | if (typeof category !== 'string') { 56 | throw new Error(`category should be a string`) 57 | } 58 | let requestPredicate = 59 | (request) => request && request.category === category 60 | return driverSource.filter(requestPredicate).select() 61 | } 62 | } 63 | return driverSource 64 | } 65 | 66 | export default makeDriverSource -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | import xs from 'xstream' 2 | import xsCycle from '@cycle/xstream-run' 3 | import most from 'most' 4 | import mostCycle from '@cycle/most-run' 5 | import rxAdapter from '@cycle/rx-adapter' 6 | import {makeAsyncDriver} from '../lib/index' 7 | import {Observable as O, Subject} from 'rx' 8 | import isolate from '@cycle/isolate' 9 | import test from 'tape' 10 | 11 | var basicDriver = makeAsyncDriver((request, _, setDispose) => { 12 | let completed = false 13 | setDispose(() => completed ? '' : request.aborted = true) 14 | return new Promise((resolve, reject) => { 15 | setTimeout(() => { 16 | request.name 17 | ? resolve('async ' + request.name) 18 | : reject('async error') 19 | completed = true 20 | }, 10) 21 | }) 22 | }) 23 | 24 | var lazyDriver = makeAsyncDriver({ 25 | getResponse: (request, callback) => { 26 | setTimeout(() => 27 | callback(null, 'async ' + request.name + Math.random()) 28 | ) 29 | }, 30 | lazy: true 31 | }) 32 | 33 | var customDriver = makeAsyncDriver({ 34 | requestProp: 'query', 35 | isolateProp: '_scope', 36 | isolateNormalize: (name) => ({name}), 37 | normalizeRequest: (request) => ({...request, normalized: true}), 38 | getResponse: (request, cb) => { 39 | cb(null, 'async ' + request.name) 40 | } 41 | }) 42 | 43 | var progressiveDriver = makeAsyncDriver({ 44 | getProgressiveResponse: (request, observer) => { 45 | setTimeout(() => { 46 | observer.next(1) 47 | setTimeout(() => { 48 | observer.next(2) 49 | }) 50 | setTimeout(() => { 51 | observer.next(3) 52 | observer.complete() 53 | }) 54 | }) 55 | } 56 | }) 57 | 58 | test('Basic driver from promise', (t) => { 59 | const request = {name: 'John'} 60 | const response = 'async John' 61 | const source = basicDriver(O.of(request), rxAdapter) 62 | 63 | source.select() 64 | .do(r$ => t.deepEqual(r$.request, request, 'response$.request is present and correct')) 65 | .mergeAll() 66 | .subscribe(x => { 67 | t.deepEqual(x, response, 'response') 68 | t.end() 69 | }) 70 | }) 71 | 72 | test('Basic driver - cancellation with abort (lazy requests)', (t) => { 73 | const requests = [ 74 | {name: 'John', category: 'john', lazy: true}, 75 | {name: 'Alex', type: 'alex', lazy: true} 76 | ] 77 | const response = 'async Alex' 78 | const source = basicDriver(O.fromArray(requests).delay(0), rxAdapter) 79 | 80 | source.select() 81 | .switch() 82 | .share() 83 | .subscribe(x => { 84 | t.ok(requests[0].aborted, 'fist request was aborted') 85 | t.deepEqual(x, response, 'response is correct') 86 | t.end() 87 | }) 88 | }) 89 | 90 | test('Basic driver - select method', (t) => { 91 | const requests = [ 92 | {name: 'John', category: 'john'}, 93 | {name: 'Alex', type: 'alex'} 94 | ] 95 | const responses = ['async John', 'async Alex'] 96 | const source = basicDriver(O.fromArray(requests).delay(0), rxAdapter) 97 | 98 | source.select('john') 99 | .do(r$ => t.deepEqual(r$.request, requests[0], 'response$.request is present and correct')) 100 | .mergeAll() 101 | .subscribe(x => { 102 | t.deepEqual(x, responses[0], 'response 1 is correct') 103 | }) 104 | 105 | source.filter(r => r.type === 'alex').select() 106 | .do(r$ => t.deepEqual(r$.request, requests[1], 'response$.request is present and correct')) 107 | .mergeAll() 108 | .subscribe(x => { 109 | t.deepEqual(x, responses[1], 'response 2 is correct') 110 | }) 111 | 112 | source.select().mergeAll() 113 | .bufferWithCount(2) 114 | .filter(x => x.length == 2) 115 | .subscribe(x => t.end()) 116 | }) 117 | 118 | test('Lazy driver (async callback)', (t) => { 119 | const request = {name: 'John'} 120 | //const response = 'async John' 121 | const source = lazyDriver(O.of(request), rxAdapter) 122 | let res1 123 | source.select() 124 | .mergeAll() 125 | .subscribe(x => { 126 | res1 = x 127 | }) 128 | 129 | source.select() 130 | .mergeAll() 131 | .delay(100) 132 | .subscribe(res2 => { 133 | console.log('here', res1, res2) 134 | t.notEqual(res1, res2, 'response are different') 135 | t.end() 136 | }) 137 | }) 138 | 139 | test('Basic driver - source filter method', (t) => { 140 | const requests = [ 141 | {name: 'John', category: 'john'}, 142 | {name: 'Alex', type: 'alex'} 143 | ] 144 | const responses = ['async John', 'async Alex'] 145 | const source = basicDriver(O.fromArray(requests).delay(0), rxAdapter) 146 | 147 | source 148 | .filter(request => request.category === 'john') 149 | .select() 150 | .do(r$ => t.deepEqual(r$.request, requests[0], 'response$.request is present and correct')) 151 | .mergeAll() 152 | .subscribe(x => { 153 | t.deepEqual(x, responses[0], 'response') 154 | }) 155 | 156 | source.select() 157 | .mergeAll() 158 | .bufferWithCount(2) 159 | .filter(x => x.length === 2) 160 | .subscribe(x => { 161 | t.end() 162 | }) 163 | }) 164 | 165 | test('Basic driver isolation', (t) => { 166 | const request = {name: 'John'} 167 | const response = 'async John' 168 | 169 | const expected = {name: 'asyncJohn'} 170 | 171 | const dataflow = ({source}) => { 172 | source.select() 173 | .do(r$ => { 174 | t.same(r$.request.name, 'John', 'request is correct') 175 | t.same(r$.request._namespace, ['scope0'], 'request _namespace is correct') 176 | }) 177 | .mergeAll() 178 | .subscribe(x => { 179 | t.deepEqual(x, response, 'response') 180 | t.end() 181 | }) 182 | return { 183 | source: O.of(request) 184 | } 185 | } 186 | const request$ = new Subject() 187 | const source = basicDriver(request$, rxAdapter) 188 | isolate(dataflow, 'scope0')({source}).source.subscribe((request) => { 189 | request$.onNext(request) 190 | }) 191 | request$.onNext({name: 'Alex', _namespace: ['scope1']}) 192 | }) 193 | 194 | test('Basic driver from promise failure', (t) => { 195 | const request = {name: ''} 196 | const source = basicDriver(O.of(request), rxAdapter) 197 | const expected = {name: 'asyncJohn'} 198 | 199 | source.select() 200 | .map(r$ => r$.catch(O.of('error'))) 201 | .mergeAll() 202 | .subscribe(x => { 203 | t.deepEqual(x, 'error', 'error sent') 204 | t.end() 205 | }) 206 | }) 207 | 208 | test.skip('Custom source driver with isolation, normalization and sync callback', (t) => { 209 | const request = 'John' 210 | const response = 'async John' 211 | 212 | const expected = {name: 'asyncJohn'} 213 | 214 | const dataflow = ({source}) => { 215 | source.select() 216 | .do(({query}) => { 217 | t.same(query.name, 'John', 'request is correct') 218 | t.same(query._scope, ['scope0'], 'request _namespace is correct') 219 | t.ok(query.normalized, 'request is normalized') 220 | }) 221 | .mergeAll() 222 | .subscribe(x => { 223 | t.deepEqual(x, response, 'response is correct') 224 | t.end() 225 | }) 226 | return { 227 | source: O.of(request) 228 | } 229 | } 230 | const request$ = new Subject() 231 | const source = customDriver(request$, rxAdapter) 232 | isolate(dataflow, 'scope0')({source}).source 233 | .subscribe((request) => { 234 | request$.onNext(request) 235 | }) 236 | request$.onNext('Alex') 237 | }) 238 | 239 | test('Progressive response driver', (t) => { 240 | const request = {name: 'John'} 241 | const response = 'async John' 242 | const source = progressiveDriver(O.of(request), rxAdapter) 243 | let values = [] 244 | source.select() 245 | .do(r$ => t.deepEqual(r$.request, request, 'response$.request is present and correct')) 246 | .mergeAll() 247 | .subscribe(x => { 248 | values.push(x) 249 | if (values.length === 3){ 250 | t.deepEqual(values, [1, 2, 3], 'progressive response is ok') 251 | t.end() 252 | } 253 | }) 254 | }) 255 | 256 | test('xstream run (isolation, cancellation)', (t) => { 257 | const requests0 = [{name: 'John', lazy: true}, {name: 'Alex', lazy: true}] 258 | const requests1 = [{name: 'Jane'}] 259 | 260 | const Dataflow = ({driver, request$}, number) => { 261 | return { 262 | result: driver.select() 263 | .flatten().map(data => ({ 264 | number, data 265 | })), 266 | driver: request$ 267 | } 268 | } 269 | 270 | const Main = ({driver}) => { 271 | const dataflow0 = isolate(Dataflow, 'scope0')({ 272 | request$: xs.fromArray(requests0), 273 | driver 274 | }, '0') 275 | const dataflow1 = isolate(Dataflow, 'scope1')({ 276 | request$: xs.fromArray(requests1), 277 | driver 278 | }, '1') 279 | return { 280 | result: xs.merge(dataflow0.result, dataflow1.result), 281 | driver: xs.merge(dataflow0.driver, dataflow1.driver) 282 | } 283 | } 284 | let count = 0 285 | xsCycle.run(Main, { 286 | result: (result$) => { 287 | result$.addListener({ 288 | next: (res) => { 289 | if (res.number === '0') { 290 | t.is(res.data, 'async Alex') 291 | count++ 292 | } 293 | if (res.number === '1') { 294 | t.is(res.data, 'async Jane') 295 | t.is(res.data, 'async Jane') 296 | count++ 297 | } 298 | if (count >= 2){ 299 | setTimeout(() => { 300 | t.is(count, 2, 'two requests done') 301 | t.ok(requests0[0].aborted, 'first lazy request aborted') 302 | t.notOk(requests0[1].aborted, 'second not aborted') 303 | t.notOk(requests1[0].aborted, 'third not aborted') 304 | t.end() 305 | }, 50) 306 | } 307 | }, 308 | error: () => {}, 309 | complete: () => {} 310 | }) 311 | return {} 312 | }, 313 | driver: basicDriver 314 | }) 315 | }) 316 | 317 | test('most run (isolation, cancellation)', (t) => { 318 | const requests0 = [{name: 'John', lazy: true}, {name: 'Alex', lazy: true}] 319 | const requests1 = [{name: 'Jane'}] 320 | 321 | const Dataflow = ({driver, request$}, number) => { 322 | return { 323 | result: driver.select() 324 | .switch().map(data => ({ 325 | number, data 326 | })), 327 | driver: request$ 328 | } 329 | } 330 | 331 | const Main = ({driver}) => { 332 | const dataflow0 = isolate(Dataflow, 'scope0')({ 333 | request$: most.from(requests0), 334 | driver 335 | }, '0') 336 | const dataflow1 = isolate(Dataflow, 'scope1')({ 337 | request$: most.from(requests1), 338 | driver 339 | }, '1') 340 | return { 341 | result: most.merge(dataflow0.result, dataflow1.result), 342 | driver: most.merge(dataflow0.driver, dataflow1.driver) 343 | } 344 | } 345 | let count = 0 346 | mostCycle.run(Main, { 347 | result: (result$) => { 348 | result$.subscribe({ 349 | next: (res) => { 350 | if (res.number === '0') { 351 | t.is(res.data, 'async Alex') 352 | count++ 353 | } 354 | if (res.number === '1') { 355 | t.is(res.data, 'async Jane') 356 | t.is(res.data, 'async Jane') 357 | count++ 358 | } 359 | if (count >= 2){ 360 | setTimeout(() => { 361 | t.is(count, 2, 'two requests done') 362 | t.ok(requests0[0].aborted, 'first lazy request aborted') 363 | t.notOk(requests0[1].aborted, 'second not aborted') 364 | t.notOk(requests1[0].aborted, 'third not aborted') 365 | t.end() 366 | }, 50) 367 | } 368 | }, 369 | error: () => {}, 370 | complete: () => {} 371 | }) 372 | return {} 373 | }, 374 | driver: basicDriver 375 | }) 376 | }) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "removeComments": false, 5 | "strictNullChecks": true, 6 | "declaration": false, 7 | "target": "es5", 8 | "preserveConstEnums": true, 9 | "noImplicitAny": false, 10 | "outDir": "lib", 11 | "noLib": false, 12 | "sourceMap": true 13 | }, 14 | "formatCodeOptions": { 15 | "indentSize": 2, 16 | "tabSize": 2 17 | }, 18 | "exclude": [ 19 | "node_modules", 20 | ".vscode-test" 21 | ] 22 | } -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@cycle/base": "npm:@cycle/base" 4 | }, 5 | "devDependencies": {}, 6 | "globalDependencies": { 7 | "es6-shim": "github:DefinitelyTyped/DefinitelyTyped/es6-shim/es6-shim.d.ts#6697d6f7dadbf5773cb40ecda35a76027e0783b2", 8 | "require": "github:DefinitelyTyped/DefinitelyTyped/requirejs/require.d.ts#56295f5058cac7ae458540423c50ac2dcf9fc711", 9 | "rx.all": "github:Reactive-Extensions/RxJS/ts/rx.all.d.ts" 10 | } 11 | } -------------------------------------------------------------------------------- /xstream-typings.d.ts: -------------------------------------------------------------------------------- 1 | import {Stream, MemoryStream} from 'xstream'; 2 | import {ResponseStream} from './index' 3 | 4 | export interface XStreamAsyncSource { 5 | filter(predicate: (request: Request) => boolean): XStreamAsyncSource 6 | select(category?: string): Stream & ResponseStream> 7 | } 8 | -------------------------------------------------------------------------------- /xstream.d.ts: -------------------------------------------------------------------------------- 1 | import {Stream, MemoryStream} from 'xstream'; 2 | import {StreamAdapter} from '@cycle/base'; 3 | import {XStreamAsyncSource} from './xstream-typings' 4 | import {GetResponse, GetProgressiveResponse, ResponseStream} from './index.d.ts' 5 | 6 | export function makeAsyncDriver 7 | (getResponse: GetResponse): 8 | (sink$: Stream, runSA: StreamAdapter) => XStreamAsyncSource 9 | 10 | export function makeAsyncDriver 11 | ( 12 | params: { 13 | getResponse: GetResponse 14 | lazy?: boolean 15 | }): 16 | (sink$: Stream, runSA: StreamAdapter) => XStreamAsyncSource 17 | 18 | export function makeAsyncDriver 19 | ( 20 | params: { 21 | getProgressiveResponse: GetProgressiveResponse 22 | lazy?: boolean 23 | }): 24 | (sink$: Stream, runSA: StreamAdapter) => XStreamAsyncSource 25 | 26 | export function makeAsyncDriver 27 | ( 28 | params: { 29 | getResponse: GetResponse 30 | normalizeRequest?(request: Request): NormalizedRequest, 31 | isolateMap?(request: Request): NormalizedRequest, 32 | lazy?: boolean 33 | }): 34 | (sink$: Stream, runSA: StreamAdapter) => 35 | XStreamAsyncSource 36 | 37 | export function makeAsyncDriver 38 | ( 39 | params: { 40 | getProgressiveResponse: GetProgressiveResponse 41 | normalizeRequest?(request: Request): NormalizedRequest, 42 | isolateMap?(request: Request): NormalizedRequest, 43 | lazy?: boolean 44 | }): 45 | (sink$: Stream, runSA: StreamAdapter) => 46 | XStreamAsyncSource -------------------------------------------------------------------------------- /xstream.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib') --------------------------------------------------------------------------------