├── .gitignore ├── .npmignore ├── README.md ├── package.json └── src ├── circulate ├── index.ts ├── most.ts ├── rx.ts ├── rxjs.ts └── xstream.ts ├── index.ts ├── most.ts ├── rx.ts ├── rxjs.ts ├── test ├── most.ts ├── rx.ts ├── rxjs.ts ├── test.ts └── xstream.ts └── xstream.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Editor-specific 2 | 3 | # Installed libs 4 | node_modules 5 | typings 6 | 7 | # Generated 8 | *.js 9 | *.js.map 10 | *.d.ts 11 | tsconfig.json 12 | 13 | !src/**/* 14 | 15 | # Misc 16 | npm-debug.log 17 | _* -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | tsconfig.json 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cycle-proxy 2 | 3 | > Create imitating proxy and circular dataflows in your [cycle.js](http://cycle.js.org) apps. 4 | 5 | This helper allows to create a stream which can **attach to other stream** 6 | and emmit target stream values. Like [`imitate`](https://github.com/staltz/xstream#imitate) 7 | in `xstream`. 8 | 9 | ```bash 10 | npm install cycle-proxy -S 11 | ``` 12 | 13 | ## Why? 14 | 15 | This is usually useful when you have **circular dependency of streams**, 16 | when say some stream `bar$` depends on stream `foo$` and at the same time 17 | `foo$` actually depends on `bar$` emissions, this 18 | is **not a dead lock** - this is actually quite common situation - 19 | this us usual cyclic dependency between action and result, nest result 20 | give a birth to next action, and so on. 21 | 22 | ```js 23 | const foo = Foo({value$: bar.value$, DOM}) 24 | const bar = Bar({HTTP, prop$: foo.prop$}) 25 | ``` 26 | 27 | ### Usage 28 | 29 | ```js 30 | // import proxy for particular stream library: 31 | // rx, rxjs, most, xtream 32 | import proxy from 'cycle-proxy/rx' 33 | ... 34 | 35 | const barValueProxy$ = proxy() 36 | // proxy() can take compose function 37 | // that will be applied to transform imitated stream: 38 | // const barValueProxy$ = proxy(_ => _.startWith(0)) 39 | const foo = Foo({value$: barValueProxy$, DOM}) 40 | const bar = Bar({HTTP, prop$: foo.prop$}) 41 | 42 | bar.value$ 43 | .let(barValueProxy$.proxy) // let's proxy (mimic) bar.value$ -> barValueProxy$ 44 | .subscribe(...) 45 | ``` 46 | - `barValueProxy$` here is created imitating proxy stream it has 47 | `proxy(targetStream)` method that takes a target stream 48 | and **transparently** attaches to it - **after external subscription** is created. 49 | - **When external subscription ends** it detaches from target stream 50 | and proxy stream stops emitting values. 51 | - Because we create internal subscription to target stream dependent on external one (which is 52 | managed managed externally by your code) we are able to avoid potential memory leak. 53 | 54 | ### Difference from [`xstream`'s `imitate`](https://github.com/staltz/xstream#imitate) 55 | 56 | So to launch this proxied stream it needs to be subscribed externally. 57 | This means that such code won't work: 58 | ```js 59 | barValueProxy$.proxy(bar.value$) // <- needs to be subscribed! 60 | .... 61 | bar.value$.subscribe(...) 62 | ``` 63 | Though with `xstream` and `imitate` such code will work: 64 | ```js 65 | barValueProxy$.imitate(bar.value$) // <- no need to be subscribed 66 | .... 67 | bar.value$.addListener(...) 68 | ``` 69 | 70 | This is the main difference from `imitate` which uses some internal 71 | mechanics of `xstream` to track circular subscriptions 72 | (which may lead to memory leak eventually) and allows to use more simple API. 73 | This module **is more general purpose**. 74 | If you are using `xstream` it is recommended to use `imitate`. But 75 | this `proxy` also supports attaching to any kind of streams 76 | (even `MemoryStreams` of `xstream`, which `imitate` does not support). 77 | 78 | ### Circulate 79 | 80 | There is also useful `circulate` utitlity helper included 81 | using which you can make circular dataflows (without explicit use of `proxy` API): 82 | 83 | It allows you with ease to create circular state 84 | in cycle.js application [with reducer patter](http://staltz.com/reducer-pattern-in-cyclejs.html): 85 | 86 | ```ts 87 | import circulate from 'cycle-proxy/circulate/rxjs' 88 | ... 89 | 90 | let Main({DOM, state$}: Sources) => { 91 | 92 | const reducer$ = merge( 93 | of(() => ({count: 1})), // set initial state 94 | DOM.select('add').map(({count}) => ({count: count + 1})), 95 | DOM.select('.substruct').map(({count}) => ({count: count - 1})) 96 | ) 97 | return { 98 | DOM: state$.map(({count}) => div([ 99 | div('count:' + count), 100 | button('.add', '+1'), 101 | button('.add', '-1') 102 | ])), 103 | state$: reducer$ 104 | .scan((state, reducer) => reducer(state)) 105 | } 106 | } 107 | 108 | // `circulate` will connect sinks' state$ to sources' state$ stream 109 | // so each value that will go to sink, you will see in the source 110 | // `state$` is a name of circulated stream, default name is `circular$` 111 | let StatifiedMain = circulate(Main, 'state$') 112 | 113 | run(StatifiedMain, { 114 | DOM: makeDOMDriver() 115 | }) 116 | ``` 117 | 118 | NB! `circulate` is leak free. It will stop the curculating stream 119 | when all the sinks of the dataflow will be unsubscribed. 120 | 121 | ## Licence 122 | ISC. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cycle-proxy", 3 | "version": "3.0.0", 4 | "description": "Create streams that mimic (proxy) other.", 5 | "author": "whitecolor", 6 | "repository": "github:whitecolor/cycle-proxy", 7 | "license": "ISC", 8 | "keywords": [ 9 | "rx", 10 | "rxjs", 11 | "xstream", 12 | "mostjs", 13 | "cyclejs" 14 | ], 15 | "scripts": { 16 | "clean": "rimraf *.js *.js.map *.d.ts", 17 | "build": "node ../make-tsconfig && ../node_modules/.bin/tsc --outDir .", 18 | "build:watch": "npm run build -- -w", 19 | "test": "node -r source-map-support/register test/test.js", 20 | "test:watch": "node-dev --respawn -r source-map-support/register test/test.js", 21 | "prepublish": "npm run clean & npm run build && npm run test", 22 | "vmd-readme": "vmd README.md" 23 | }, 24 | "devDependencies": { 25 | "typescript": "^2.2.0-dev.20161121" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/circulate/index.ts: -------------------------------------------------------------------------------- 1 | import { StreamAdapter, Observer } from '@cycle/base' 2 | 3 | export type Dataflow = (sources: So, ...rest: any[]) => Si 4 | 5 | export type Fields = string | string[] | { [index: string]: true } 6 | 7 | export const makeCirculate = (proxy: any, adapter: StreamAdapter) => 8 | function circulate(dataflow: any, circularName: string = 'circular$'): 9 | Dataflow { 10 | return function (sources: any, ...rest: any[]) { 11 | let circular$ = sources[circularName] = proxy() 12 | let sinks = dataflow(sources, ...rest) as any 13 | 14 | const proxyStreams: any = {} 15 | const proxiedSinks: any = {} 16 | 17 | const disposeCircular = adapter.streamSubscribe( 18 | circular$.proxy(sinks[circularName]), { 19 | next: () => { }, error: () => { }, complete: () => { } 20 | } 21 | ) 22 | 23 | const checkAllDisposed = () => { 24 | let disposed = true 25 | for (const key in proxyStreams) { 26 | if (proxyStreams[key].__proxyRefs) { 27 | disposed = false 28 | } 29 | } 30 | if (disposed) { 31 | disposeCircular() 32 | } 33 | } 34 | 35 | for (const key in sinks) { 36 | // TODO: decide if we we need 37 | // to remove circular$ stream from sinks 38 | //if (key === circularName) return 39 | const sink = sinks[key] 40 | if (sink && adapter.isValidStream(sink)) { 41 | proxyStreams[key] = proxy() 42 | proxiedSinks[key] = proxyStreams[key].proxy(sink) 43 | proxyStreams[key].__onProxyDispose = checkAllDisposed 44 | // TODO: probably can use proxy completition 45 | // if proxy can be subscribed once? 46 | // adapter.streamSubscribe(proxyStreams[key], { 47 | // next: () => { }, 48 | // error: () => { }, 49 | // complete: () => { console.log(key + 'completed') } 50 | // }) 51 | } else { 52 | proxiedSinks[key] = sink 53 | } 54 | } 55 | return proxiedSinks 56 | } 57 | } 58 | 59 | export default makeCirculate 60 | -------------------------------------------------------------------------------- /src/circulate/most.ts: -------------------------------------------------------------------------------- 1 | import proxy from '../most' 2 | import adapter from '@cycle/most-adapter' 3 | import { makeCirculate, Dataflow } from './index' 4 | 5 | export function circulate 6 | (dataflow: Dataflow, name?: string): Dataflow { 7 | return makeCirculate(proxy, adapter)(dataflow, name) 8 | } 9 | 10 | export default circulate -------------------------------------------------------------------------------- /src/circulate/rx.ts: -------------------------------------------------------------------------------- 1 | import proxy from '../rx' 2 | import adapter from '@cycle/rx-adapter' 3 | import { makeCirculate, Dataflow } from './index' 4 | import { Observable } from 'rx' 5 | 6 | export function circulate 7 | (dataflow: Dataflow, name?: string): Dataflow { 8 | return makeCirculate(proxy, adapter)(dataflow, name) 9 | } 10 | 11 | export default circulate -------------------------------------------------------------------------------- /src/circulate/rxjs.ts: -------------------------------------------------------------------------------- 1 | import proxy from '../rxjs' 2 | import adapter from '@cycle/rxjs-adapter' 3 | import { makeCirculate, Dataflow } from './index' 4 | import { Observable } from 'rxjs/Observable' 5 | 6 | export function circulate 7 | (dataflow: Dataflow, name?: string): Dataflow { 8 | return makeCirculate(proxy, adapter)(dataflow, name) 9 | } 10 | 11 | export default circulate -------------------------------------------------------------------------------- /src/circulate/xstream.ts: -------------------------------------------------------------------------------- 1 | import proxy from '../xstream' 2 | import adapter from '@cycle/xstream-adapter' 3 | import { makeCirculate, Dataflow } from './index' 4 | 5 | export function circulate 6 | (dataflow: Dataflow, name?: string): Dataflow { 7 | return makeCirculate(proxy, adapter)(dataflow, name) 8 | } 9 | 10 | export default circulate -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { StreamAdapter, Observer} from '@cycle/base' 2 | 3 | export type Stream = any 4 | 5 | export interface ProxyFn { 6 | (originalStream: Stream) : Stream 7 | } 8 | 9 | export const makeProxy = (adapter: StreamAdapter) => { 10 | return (composeFn = (_: any) => _): Stream & { proxy: ProxyFn } => { 11 | const subject = adapter.makeSubject() 12 | let proxyDispose: any 13 | let targetStream: Stream 14 | const proxyStream = subject.stream 15 | const proxyObserver = subject.observer 16 | proxyStream.proxy = (target: Stream) => { 17 | if (!target || !adapter.isValidStream(target)){ 18 | throw new Error('You should provide a valid target stream to proxy') 19 | } 20 | if (targetStream){ 21 | throw new Error('You may provide only one target stream to proxy') 22 | } 23 | targetStream = composeFn(target) 24 | proxyStream.__proxyRefs = 0 25 | return adapter.adapt({}, (_: any, observer: Observer) => { 26 | let dispose = adapter.streamSubscribe(target, observer) 27 | if (proxyStream.__proxyRefs++ === 0) { 28 | proxyDispose = adapter.streamSubscribe( 29 | targetStream, proxyObserver 30 | ) 31 | } 32 | return () => { 33 | dispose() 34 | if (--proxyStream.__proxyRefs === 0) { 35 | proxyDispose() 36 | proxyObserver.complete() 37 | if (proxyStream.__onProxyDispose) { 38 | proxyStream.__onProxyDispose() 39 | } 40 | } 41 | } 42 | }) 43 | } 44 | return proxyStream 45 | } 46 | } 47 | 48 | export default makeProxy 49 | -------------------------------------------------------------------------------- /src/most.ts: -------------------------------------------------------------------------------- 1 | import adapter from '@cycle/most-adapter' 2 | import { makeProxy } from './index' 3 | import { Stream } from 'most' 4 | 5 | export interface ProxyStream extends Stream { 6 | /** 7 | * Attaches to other stream to emit its events. 8 | * Returns proxied stream that needs to be subscribed. 9 | * @param {Stream} target 10 | * @returns Stream 11 | */ 12 | proxy(target: Stream): Stream 13 | } 14 | 15 | /** 16 | * Creates proxy stream that can attach to other `target` observable 17 | * to emit the same events. 18 | * Created stream has `proxy` method, that is used to attach to `target`. 19 | * 20 | * This is used to allow **circular dependency of streams**. 21 | * @param {(stream:Stream)=>Stream} compose? 22 | */ 23 | export function proxy(compose?: (stream: Stream) => Stream) 24 | : ProxyStream { 25 | return makeProxy(adapter)(compose) 26 | } 27 | 28 | export default proxy -------------------------------------------------------------------------------- /src/rx.ts: -------------------------------------------------------------------------------- 1 | import adapter from '@cycle/rx-adapter' 2 | import { makeProxy } from './index' 3 | import { Observable } from 'rx' 4 | 5 | export interface ProxyObservable extends Observable { 6 | /** 7 | * Attaches to other observable to emit its events. 8 | * Returns proxied stream that needs to be subscribed. 9 | * @param {Observable} target 10 | * @returns Observable 11 | */ 12 | proxy(target: Observable): Observable 13 | } 14 | 15 | /** 16 | * Creates proxy stream that can attach to other `target` observable 17 | * to emit the same events. 18 | * Created stream has `proxy` method, that is used to attach to `target`. 19 | * 20 | * This is used to allow **circular dependency of streams**. 21 | * Note that to start created proxy streams needs to be subscribed. 22 | * @param {(stream:Observable)=>Observable} compose? 23 | */ 24 | export function proxy(compose?: (stream: Observable) => Observable) 25 | : ProxyObservable { 26 | return makeProxy(adapter)(compose) 27 | } 28 | 29 | export default proxy -------------------------------------------------------------------------------- /src/rxjs.ts: -------------------------------------------------------------------------------- 1 | import adapter from '@cycle/rxjs-adapter' 2 | import { makeProxy } from './index' 3 | import { Observable } from 'rxjs/Observable' 4 | 5 | export interface ProxyObservable extends Observable { 6 | /** 7 | * Attaches to other observable to emit its events. 8 | * Returns proxied stream that needs to be subscribed. 9 | * @param {Observable} target 10 | * @returns Observable 11 | */ 12 | proxy(target: Observable): Observable 13 | } 14 | 15 | /** 16 | * Creates proxy stream that can attach to other `target` observable 17 | * to emit the same events. 18 | * Created stream has `proxy` method, that is used to attach to `target`. 19 | * 20 | * This is used to allow **circular dependency of streams**. 21 | * Note that to start created proxy streams needs to be subscribed. 22 | * @param {(stream:Observable)=>Observable} compose? 23 | */ 24 | export function proxy(compose?: (stream: Observable) => Observable) 25 | : ProxyObservable { 26 | return makeProxy(adapter)(compose) 27 | } 28 | 29 | export default proxy -------------------------------------------------------------------------------- /src/test/most.ts: -------------------------------------------------------------------------------- 1 | import * as test from 'tape' 2 | import * as most from 'most' 3 | import { Stream } from 'most' 4 | import proxy from '../most' 5 | import { circulate } from '../circulate/most' 6 | 7 | const emptyListener = { next: () => { }, complete: () => { }, error: () => { } } 8 | 9 | test('most: target$ should not start when proxy$ attached', (t) => { 10 | let target$ = most.from([1, 2, 3, 5, 6]) 11 | .map((x) => { 12 | t.fail() 13 | return x 14 | }) 15 | let proxy$ = proxy() 16 | proxy$.proxy(target$) 17 | setTimeout(() => t.end()) 18 | }) 19 | 20 | test('most: target$ should not start if proxy$ subscribed', (t) => { 21 | let target$ = most.from([1, 2, 3, 5, 6]) 22 | .map((x) => { 23 | t.fail() 24 | return x 25 | }) 26 | let proxy$ = proxy() 27 | proxy$.proxy(target$) 28 | 29 | proxy$.subscribe(emptyListener) 30 | setTimeout(() => t.end()) 31 | }) 32 | 33 | 34 | test('most: proxy$ should not emit if target$ subscribed', (t) => { 35 | let target$ = most.from([1, 2, 3, 5, 6]) 36 | let proxy$ = proxy() 37 | proxy$.map(() => t.fail()).subscribe(emptyListener) 38 | proxy$.proxy(target$) 39 | target$.subscribe(emptyListener) 40 | setTimeout(() => t.end()) 41 | }) 42 | 43 | test('most: proxy$ should not emit if target$ subscribed and proxy$ subscribed', (t) => { 44 | let target$ = most.from([1, 2, 3, 5, 6]) 45 | let proxy$ = proxy() 46 | 47 | proxy$.proxy(target$) 48 | 49 | proxy$.map(() => t.fail()).subscribe(emptyListener) 50 | 51 | target$.subscribe(emptyListener) 52 | 53 | setTimeout(() => t.end()) 54 | }) 55 | 56 | test('most: proxy$ should emit if proxied$ subscribed', (t) => { 57 | let target$ = most.from([1, 2, 3, 5, 6]) 58 | let proxy$ = proxy() 59 | 60 | let proxied$ = proxy$.proxy(target$) 61 | let emitted = 0 62 | proxy$.map(() => emitted++).subscribe(emptyListener) 63 | 64 | proxied$.subscribe(emptyListener) 65 | 66 | setTimeout(() => { 67 | t.equal(emitted, 5) 68 | t.end() 69 | }) 70 | }) 71 | 72 | test('most: proxy$ should stop emitting when proxied$ unsubscribed', (t) => { 73 | let target$ = most.periodic(1) 74 | let proxy$ = proxy() 75 | 76 | let proxied$ = proxy$.proxy(target$) 77 | let emitted = 0 78 | proxy$.subscribe({ 79 | next: () => { 80 | emitted++ 81 | }, 82 | error: () => { }, 83 | complete: () => { } 84 | }) 85 | 86 | let listener = { 87 | next: () => { 88 | if (emitted === 2) { 89 | sub.unsubscribe() 90 | } 91 | }, 92 | error: () => { }, 93 | complete: () => { } 94 | } 95 | let sub = proxied$.subscribe(listener) 96 | 97 | // target may still be subscribed 98 | target$.subscribe(emptyListener) 99 | 100 | setTimeout(() => { 101 | //t.equal(emitted, 2) 102 | t.ok(emitted === 2 || emitted === 3) 103 | t.end() 104 | }, 50) 105 | }) 106 | 107 | // test('most: circulate (factory)', (t) => { 108 | // let circ = circulate('target$') 109 | // ((target$) => { 110 | // return { 111 | // target$: target$.map(x => x * 2) 112 | // .startWith(1) 113 | // .delay(1) 114 | // } 115 | // }) 116 | // let results: number[] = [] 117 | // let sub = circ.target$.subscribe({ 118 | // next: (x) => { 119 | // results.push(x) 120 | // if (results.length === 4) { 121 | // sub.unsubscribe() 122 | // t.deepEqual(results, [1, 2, 4, 8], 'results ok') 123 | // t.end() 124 | // } 125 | // }, 126 | // error: () => { }, 127 | // complete: () => { } 128 | // }) 129 | // }) 130 | 131 | test('most: circulate', (t) => { 132 | type Sources = {} 133 | type Circular = { circular$: Stream } 134 | type Sinks = { target$: Stream } & Circular 135 | 136 | let emitted = 0 137 | const Dataflow = ({ circular$}: Sources & Circular): Sinks & Circular => { 138 | return { 139 | circular$: circular$.map(x => x * 2) 140 | .startWith(1) 141 | .delay(10), 142 | target$: circular$.map(x => x * 10) 143 | } 144 | } 145 | 146 | let circ = circulate(Dataflow) 147 | let results: number[] = [] 148 | let sub = circ({}).target$.subscribe({ 149 | next: (x: any) => { 150 | results.push(x) 151 | if (results.length === 4) { 152 | sub.unsubscribe() 153 | t.deepEqual(results, [10, 20, 40, 80], 'results ok') 154 | const emittedFinal = emitted 155 | setTimeout(() => { 156 | t.ok(emittedFinal === emitted, 'no leak') 157 | t.end() 158 | }, 100) 159 | } 160 | }, 161 | error: () => { }, complete: () => { } 162 | }) 163 | }) -------------------------------------------------------------------------------- /src/test/rx.ts: -------------------------------------------------------------------------------- 1 | import * as test from 'tape' 2 | import { Observable as O } from 'rx' 3 | import { proxy } from '../rx' 4 | import { circulate } from '../circulate/rx' 5 | 6 | test('rx: target$ should not start when proxy$ attached', (t) => { 7 | let target$ = O.fromArray([1, 2, 3, 5, 6]) 8 | .do(() => t.fail()) 9 | let proxy$ = proxy() 10 | proxy$.proxy(target$) 11 | setTimeout(() => t.end()) 12 | }) 13 | 14 | test('rx: target$ should not start if proxy$ subscribed', (t) => { 15 | let target$ = O.fromArray([1, 2, 3, 5, 6]) 16 | .do(() => t.fail()) 17 | let proxy$ = proxy() 18 | proxy$.proxy(target$) 19 | 20 | proxy$.subscribe() 21 | setTimeout(() => t.end()) 22 | }) 23 | 24 | 25 | test('rx: proxy$ should not emit if target$ subscribed', (t) => { 26 | let target$ = O.fromArray([1, 2, 3, 5, 6]) 27 | let proxy$ = proxy() 28 | proxy$.do(() => t.fail()).subscribe() 29 | proxy$.proxy(target$) 30 | target$.subscribe() 31 | setTimeout(() => t.end()) 32 | }) 33 | 34 | test('rx: proxy$ should not emit if target$ subscribed and proxy$ subscribed', (t) => { 35 | let target$ = O.fromArray([1, 2, 3, 5, 6]) 36 | let proxy$ = proxy() 37 | 38 | proxy$.proxy(target$) 39 | 40 | proxy$.subscribe(() => t.fail()) 41 | 42 | target$.subscribe() 43 | 44 | setTimeout(() => t.end()) 45 | }) 46 | 47 | test('rx: proxy$ should emit if proxied$ subscribed', (t) => { 48 | let target$ = O.fromArray([1, 2, 3, 5, 6]) 49 | let proxy$ = proxy() 50 | 51 | let proxied$ = proxy$.proxy(target$) 52 | let emitted = 0 53 | proxy$.subscribe(() => emitted++) 54 | 55 | proxied$.subscribe() 56 | 57 | setTimeout(() => { 58 | t.equal(emitted, 5) 59 | t.end() 60 | }) 61 | }) 62 | 63 | test('rx: proxy$ should stop emitting when proxied$ unsubscribed', (t) => { 64 | let target$ = O.fromArray([1, 2, 3, 5, 6]).delay(0).share() 65 | let proxy$ = proxy() 66 | 67 | let proxied$ = proxy$.proxy(target$) 68 | let emitted = 0 69 | proxy$.subscribe(() => { 70 | emitted++ 71 | }) 72 | 73 | let sub = proxied$.subscribe(() => { 74 | if (emitted === 2) sub.dispose() 75 | }) 76 | 77 | // target may still be subscribed 78 | target$.subscribe() 79 | 80 | setTimeout(() => { 81 | t.equal(emitted, 2) 82 | t.end() 83 | }, 10) 84 | }) 85 | 86 | 87 | test('rx: circulate', (t) => { 88 | type Sources = { } 89 | type Circular = {circular$: O } 90 | type Sinks = { target$: O } & Circular 91 | 92 | let emitted = 0 93 | const Dataflow = ({ circular$}: Sources & Circular): Sinks & Circular => { 94 | return { 95 | circular$: circular$.map(x => x * 2) 96 | .startWith(1) 97 | .delay(10).do(() => emitted++), 98 | target$: circular$.map(x => x*10) 99 | } 100 | } 101 | 102 | let circ = circulate(Dataflow) 103 | let results: number[] = [] 104 | let sub = circ({}).target$.subscribe((x) => { 105 | results.push(x) 106 | if (results.length === 4) { 107 | sub.dispose() 108 | t.deepEqual(results, [10, 20, 40, 80], 'results ok') 109 | const emittedFinal = emitted 110 | setTimeout(() => { 111 | t.ok(emittedFinal === emitted, 'no leak') 112 | t.end() 113 | }, 100) 114 | } 115 | }) 116 | }) -------------------------------------------------------------------------------- /src/test/rxjs.ts: -------------------------------------------------------------------------------- 1 | import * as test from 'tape' 2 | import { Observable as O } from 'rxjs' 3 | import { proxy } from '../rxjs' 4 | import { circulate } from '../circulate/rxjs' 5 | 6 | test('rxjs: target$ should not start when proxy$ attached', (t) => { 7 | let target$ = O.from([1, 2, 3, 5, 6]) 8 | .do(() => t.fail()) 9 | let proxy$ = proxy() 10 | proxy$.proxy(target$) 11 | setTimeout(() => t.end()) 12 | }) 13 | 14 | test('rxjs: target$ should not start if proxy$ subscribed', (t) => { 15 | let target$ = O.from([1, 2, 3, 5, 6]) 16 | .do(() => t.fail()) 17 | let proxy$ = proxy() 18 | proxy$.proxy(target$) 19 | 20 | proxy$.subscribe() 21 | setTimeout(() => t.end()) 22 | }) 23 | 24 | 25 | test('rxjs: proxy$ should not emit if target$ subscribed', (t) => { 26 | let target$ = O.from([1, 2, 3, 5, 6]) 27 | let proxy$ = proxy() 28 | proxy$.do(() => t.fail()).subscribe() 29 | proxy$.proxy(target$) 30 | target$.subscribe() 31 | setTimeout(() => t.end()) 32 | }) 33 | 34 | test('rxjs: proxy$ should not emit if target$ subscribed and proxy$ subscribed', (t) => { 35 | let target$ = O.from([1, 2, 3, 5, 6]) 36 | let proxy$ = proxy() 37 | 38 | proxy$.proxy(target$) 39 | 40 | proxy$.subscribe(() => t.fail()) 41 | 42 | target$.subscribe() 43 | 44 | setTimeout(() => t.end()) 45 | }) 46 | 47 | test('rxjs: proxy$ should emit if proxied$ subscribed', (t) => { 48 | let target$ = O.from([1, 2, 3, 5, 6]) 49 | let proxy$ = proxy() 50 | 51 | let proxied$ = proxy$.proxy(target$) 52 | let emitted = 0 53 | proxy$.subscribe(() => emitted++) 54 | 55 | proxied$.subscribe() 56 | 57 | setTimeout(() => { 58 | t.equal(emitted, 5) 59 | t.end() 60 | }) 61 | }) 62 | 63 | test('rxjs: proxy$ should stop emitting when proxied$ unsubscribed', (t) => { 64 | let target$ = O.from([1, 2, 3, 5, 6]).delay(0).share() 65 | let proxy$ = proxy() 66 | 67 | let proxied$ = proxy$.proxy(target$) 68 | let emitted = 0 69 | proxy$.subscribe(() => { 70 | emitted++ 71 | }) 72 | 73 | let sub = proxied$.subscribe(() => { 74 | if (emitted === 2) sub.unsubscribe() 75 | }) 76 | 77 | // target may still be subscribed 78 | target$.subscribe() 79 | 80 | setTimeout(() => { 81 | t.equal(emitted, 2) 82 | t.end() 83 | }, 10) 84 | }) 85 | 86 | test('rxjs: circulate', (t) => { 87 | type Sources = { } 88 | type Circular = {circular$: O } 89 | type Sinks = { target$: O } & Circular 90 | 91 | let emitted = 0 92 | const Dataflow = ({ circular$}: Sources & Circular): Sinks & Circular => { 93 | return { 94 | circular$: circular$.map(x => x * 2) 95 | .startWith(1) 96 | .delay(10).do(() => emitted++), 97 | target$: circular$.map(x => x*10) 98 | } 99 | } 100 | 101 | let circ = circulate(Dataflow) 102 | let results: number[] = [] 103 | let sub = circ({}).target$.subscribe((x) => { 104 | results.push(x) 105 | if (results.length === 4) { 106 | sub.unsubscribe() 107 | t.deepEqual(results, [10, 20, 40, 80], 'results ok') 108 | const emittedFinal = emitted 109 | setTimeout(() => { 110 | t.ok(emittedFinal === emitted, 'no leak') 111 | t.end() 112 | }, 100) 113 | } 114 | }) 115 | }) 116 | -------------------------------------------------------------------------------- /src/test/test.ts: -------------------------------------------------------------------------------- 1 | import * as test from 'tape' 2 | import './rx' 3 | import './rxjs' 4 | import './xstream' 5 | import './most' 6 | 7 | let anyTest = test 8 | anyTest.onFinish(() => process.exit(0)) -------------------------------------------------------------------------------- /src/test/xstream.ts: -------------------------------------------------------------------------------- 1 | import * as test from 'tape' 2 | import xs, { Stream } from 'xstream' 3 | import proxy from '../xstream' 4 | import { circulate } from '../circulate/xstream' 5 | import delay from 'xstream/extra/delay' 6 | 7 | const emptyListener = { next: () => { }, complete: () => { }, error: () => { } } 8 | 9 | test('xstream: target$ should not start when proxy$ attached', (t) => { 10 | let target$ = xs.fromArray([1, 2, 3, 5, 6]) 11 | .map((x) => { 12 | t.fail() 13 | return x 14 | }) 15 | let proxy$ = proxy() 16 | proxy$.proxy(target$) 17 | setTimeout(() => t.end()) 18 | }) 19 | 20 | test('xstream: target$ should not start if proxy$ subscribed', (t) => { 21 | let target$ = xs.fromArray([1, 2, 3, 5, 6]) 22 | .map((x) => { 23 | t.fail() 24 | return x 25 | }) 26 | let proxy$ = proxy() 27 | proxy$.proxy(target$) 28 | 29 | proxy$.addListener(emptyListener) 30 | setTimeout(() => t.end()) 31 | }) 32 | 33 | 34 | test('xstream: proxy$ should not emit if target$ subscribed', (t) => { 35 | let target$ = xs.fromArray([1, 2, 3, 5, 6]) 36 | let proxy$ = proxy() 37 | proxy$.map(() => t.fail()).addListener(emptyListener) 38 | proxy$.proxy(target$) 39 | target$.addListener(emptyListener) 40 | setTimeout(() => t.end()) 41 | }) 42 | 43 | test('xstream: proxy$ should not emit if target$ subscribed and proxy$ subscribed', (t) => { 44 | let target$ = xs.fromArray([1, 2, 3, 5, 6]) 45 | let proxy$ = proxy() 46 | 47 | proxy$.proxy(target$) 48 | 49 | proxy$.map(() => t.fail()).addListener(emptyListener) 50 | 51 | target$.addListener(emptyListener) 52 | 53 | setTimeout(() => t.end()) 54 | }) 55 | 56 | test('xstream: proxy$ should emit if proxied$ subscribed', (t) => { 57 | let target$ = xs.fromArray([1, 2, 3, 5, 6]) 58 | let proxy$ = proxy() 59 | 60 | let proxied$ = proxy$.proxy(target$) 61 | let emitted = 0 62 | proxy$.map(() => emitted++).addListener(emptyListener) 63 | 64 | proxied$.addListener(emptyListener) 65 | 66 | setTimeout(() => { 67 | t.equal(emitted, 5) 68 | t.end() 69 | }) 70 | }) 71 | 72 | test('xstream: proxy$ should stop emitting when proxied$ unsubscribed', (t) => { 73 | let target$ = xs.periodic(0) 74 | let proxy$ = proxy() 75 | 76 | let proxied$ = proxy$.proxy(target$) 77 | let emitted = 0 78 | proxy$.addListener({ 79 | next: () => { 80 | emitted++ 81 | }, 82 | error: () => { }, 83 | complete: () => { } 84 | }) 85 | 86 | let listener = { 87 | next: () => { 88 | if (emitted === 2) { 89 | proxied$.removeListener(listener) 90 | } 91 | }, 92 | error: () => { }, 93 | complete: () => { } 94 | } 95 | proxied$.addListener(listener) 96 | 97 | // target may still be subscribed 98 | target$.addListener(emptyListener) 99 | 100 | setTimeout(() => { 101 | t.equal(emitted, 3) 102 | t.end() 103 | }, 50) 104 | }) 105 | 106 | // test('xstream: circulate (factory)', (t) => { 107 | // let circ = circulate('target$') 108 | // ((target$) => { 109 | // return { 110 | // target$: target$.map(x => x * 2) 111 | // .startWith(1) 112 | // .compose(delay(1)) 113 | // } 114 | // }) 115 | // let results: number[] = [] 116 | // let listener = { 117 | // next: (x: number) => { 118 | // results.push(x) 119 | // if (results.length === 4) { 120 | // circ.target$.removeListener(listener) 121 | // t.deepEqual(results, [1, 2, 4, 8], 'results ok') 122 | // t.end() 123 | // } 124 | // }, 125 | // error: () => { }, 126 | // complete: () => { } 127 | // } 128 | // circ.target$.addListener(listener) 129 | // }) 130 | 131 | test('xstream: circulate', (t) => { 132 | type Sources = {} 133 | type Circular = { circular$: Stream } 134 | type Sinks = { target$: Stream } & Circular 135 | 136 | let emitted = 0 137 | const Dataflow = ({ circular$}: Sources & Circular): Sinks & Circular => { 138 | return { 139 | circular$: circular$.map(x => x * 2) 140 | .startWith(1) 141 | .compose(delay(10)), 142 | target$: circular$.map(x => x * 10) 143 | } 144 | } 145 | 146 | let circ = circulate(Dataflow) 147 | let results: number[] = [] 148 | let sub = circ({}).target$.subscribe({ 149 | next: (x: any) => { 150 | results.push(x) 151 | if (results.length === 4) { 152 | sub.unsubscribe() 153 | t.deepEqual(results, [10, 20, 40, 80], 'results ok') 154 | const emittedFinal = emitted 155 | setTimeout(() => { 156 | t.ok(emittedFinal === emitted, 'no leak') 157 | t.end() 158 | }, 100) 159 | } 160 | }, 161 | error: () => { }, complete: () => { } 162 | }) 163 | }) -------------------------------------------------------------------------------- /src/xstream.ts: -------------------------------------------------------------------------------- 1 | import adapter from '@cycle/xstream-adapter' 2 | import { makeProxy } from './index' 3 | import { Stream } from 'xstream' 4 | 5 | export interface ProxyStream extends Stream { 6 | /** 7 | * Attaches to other stream to emit its events. 8 | * Returns proxied stream that needs to be subscribed. 9 | * @param {Stream} target 10 | * @returns Stream 11 | */ 12 | proxy(target: Stream): Stream 13 | } 14 | 15 | /** 16 | * Creates proxy stream that can attach to other `target` observable 17 | * to emit the same events. 18 | * Created stream has `proxy` method, that is used to attach to `target`. 19 | * 20 | * This is used to allow **circular dependency of streams**. 21 | * @param {(stream:Stream)=>Stream} compose? 22 | */ 23 | export function proxy(compose?: (stream: Stream) => Stream) 24 | : ProxyStream { 25 | return makeProxy(adapter)(compose) 26 | } 27 | 28 | export default proxy --------------------------------------------------------------------------------