├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── source ├── index.ts ├── share-with │ ├── closed-subscription.ts │ ├── default-ref-count-spec.ts │ ├── default-ref-count.ts │ ├── delayed-ref-count-spec.ts │ ├── delayed-ref-count.ts │ ├── index.ts │ ├── limited-ref-count-spec.ts │ ├── limited-ref-count.ts │ ├── no-ref-count-spec.ts │ ├── no-ref-count.ts │ ├── scheduled-ref-count-spec.ts │ ├── scheduled-ref-count.ts │ ├── share-strategy.ts │ ├── share-with-spec.ts │ └── share-with.ts ├── switch-map-with │ ├── index.ts │ ├── switch-map-with-spec.ts │ └── switch-map-with.ts └── traverse │ ├── index.ts │ ├── queued-notifications-spec.ts │ ├── queued-notifications.ts │ ├── traverse-spec.ts │ └── traverse.ts ├── tsconfig-dist-cjs.json ├── tsconfig-dist-esm.json ├── tsconfig-dist.json ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | indent_size = 4 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@cartant/eslint-config", 4 | "@cartant/eslint-config-etc", 5 | "@cartant/eslint-config-rxjs" 6 | ], 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "comments": true, 10 | "ecmaVersion": 2019, 11 | "project": "tsconfig.json", 12 | "sourceType": "module" 13 | }, 14 | "plugins": [ 15 | "@typescript-eslint", 16 | "etc", 17 | "import", 18 | "rxjs" 19 | ], 20 | "root": true, 21 | "rules": { 22 | "etc/deprecation": ["off"] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /build 3 | /bundles 4 | /dist 5 | /node_modules 6 | /temp 7 | yarn-error.log 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /build 2 | /node_modules 3 | /temp 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | # https://github.com/greenkeeperio/greenkeeper-lockfile/issues/98 5 | before_install: 6 | - export PATH=$PATH:`yarn global bin` 7 | - yarn global add greenkeeper-lockfile@1 8 | before_script: greenkeeper-lockfile-update 9 | after_script: greenkeeper-lockfile-upload 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cartant/rxjs-pluggables/2c0b6c02467e7c25ed74c4c92807495a0c74ea08/CHANGELOG.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Nicholas Jamieson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rxjs-pluggables 2 | 3 | This package might not be a WIP, but this `README` is. 😅 4 | 5 | I've spent to much time tweaking slides. I'll get the docs for this done soon, I promise. 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Nicholas Jamieson ", 3 | "bugs": { 4 | "url": "https://github.com/cartant/rxjs-pluggables/issues" 5 | }, 6 | "dependencies": {}, 7 | "description": "RxJS observables and operators with pluggable strategies", 8 | "devDependencies": { 9 | "@cartant/eslint-config": "^2.0.0", 10 | "@cartant/eslint-config-etc": "^2.0.0", 11 | "@cartant/eslint-config-rxjs": "^2.0.0", 12 | "@types/chai": "^4.0.0", 13 | "@types/mocha": "^8.0.0", 14 | "@types/node": "^14.0.0", 15 | "@typescript-eslint/eslint-plugin": "^4.0.0", 16 | "@typescript-eslint/parser": "^4.0.0", 17 | "chai": "^4.0.0", 18 | "cpy-cli": "^3.0.0", 19 | "eslint": "^7.3.1", 20 | "husky": "^4.0.0", 21 | "lint-staged": "^10.0.8", 22 | "mkdirp": "^1.0.3", 23 | "mocha": "^8.0.0", 24 | "prettier": "^2.0.0", 25 | "rimraf": "^3.0.0", 26 | "rxjs": "^6.0.0", 27 | "rxjs-marbles": "^6.0.1", 28 | "ts-loader": "^8.0.0", 29 | "ts-node": "^9.0.0", 30 | "typescript": "~4.1.2", 31 | "webpack": "^4.0.0", 32 | "webpack-cli": "^3.0.0", 33 | "webpack-rxjs-externals": "^2.0.0" 34 | }, 35 | "homepage": "https://github.com/cartant/rxjs-pluggables", 36 | "husky": { 37 | "hooks": { 38 | "pre-commit": "lint-staged" 39 | } 40 | }, 41 | "keywords": [ 42 | "observable", 43 | "pluggable", 44 | "rxjs", 45 | "strategies" 46 | ], 47 | "license": "MIT", 48 | "lint-staged": { 49 | "*.{js,ts}": "prettier --write", 50 | "*.ts": "eslint" 51 | }, 52 | "main": "./dist/cjs/index.js", 53 | "module": "./dist/esm/index.js", 54 | "name": "rxjs-pluggables", 55 | "optionalDependencies": {}, 56 | "peerDependencies": { 57 | "rxjs": "^6.0.0" 58 | }, 59 | "private": false, 60 | "publishConfig": { 61 | "tag": "latest" 62 | }, 63 | "repository": { 64 | "type": "git", 65 | "url": "https://github.com/cartant/rxjs-pluggables.git" 66 | }, 67 | "scripts": { 68 | "dist": "yarn run lint && yarn run dist:clean && yarn run dist:build && yarn run dist:copy", 69 | "dist:build": "yarn run dist:build:cjs && yarn run dist:build:esm && yarn run dist:build:bundle", 70 | "dist:build:bundle": "webpack --config webpack.config.js && webpack --config webpack.config.js --env.production", 71 | "dist:build:cjs": "tsc -p tsconfig-dist-cjs.json", 72 | "dist:build:esm": "tsc -p tsconfig-dist-esm.json", 73 | "dist:clean": "rimraf dist && rimraf bundles/rxjs-pluggables.* && mkdirp bundles", 74 | "dist:copy": "cpy bundles/rxjs-pluggables.* dist/bundles/", 75 | "lint": "eslint \"./source/**/*.{js,ts}\"", 76 | "prepublishOnly": "yarn run test && yarn run dist", 77 | "prettier": "prettier --write \"./source/**/*.{js,json,ts}\"", 78 | "prettier:ci": "prettier --check \"./source/**/*.{js,json,ts}\"", 79 | "test": "yarn run test:build && yarn run test:mocha", 80 | "test:build": "yarn run test:clean && tsc -p tsconfig.json", 81 | "test:clean": "rimraf build", 82 | "test:mocha": "mocha build/**/*-spec.js" 83 | }, 84 | "types": "./dist/esm/index.d.ts", 85 | "unpkg": "./dist/bundles/rxjs-pluggables.min.umd.js", 86 | "version": "0.0.1-alpha.2" 87 | } 88 | -------------------------------------------------------------------------------- /source/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-pluggables 4 | */ 5 | 6 | export * from "./share-with"; 7 | export * from "./switch-map-with"; 8 | export * from "./traverse"; 9 | -------------------------------------------------------------------------------- /source/share-with/closed-subscription.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-pluggables 4 | */ 5 | 6 | import { Subscription } from "rxjs"; 7 | 8 | export const closedSubscription = (function () { 9 | const subscription = new Subscription(); 10 | subscription.unsubscribe(); 11 | return subscription; 12 | })(); 13 | -------------------------------------------------------------------------------- /source/share-with/default-ref-count-spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-pluggables 4 | */ 5 | /* eslint rxjs/no-ignored-subscription: "off" */ 6 | 7 | import { expect } from "chai"; 8 | import { concat, defer, NEVER, of } from "rxjs"; 9 | import { marbles } from "rxjs-marbles"; 10 | import { finalize } from "rxjs/operators"; 11 | import { defaultRefCount } from "./default-ref-count"; 12 | import { shareWith } from "./share-with"; 13 | 14 | describe("defaultRefCount", () => { 15 | it("should support a synchronous source", () => { 16 | let unsubscribes = 0; 17 | const values: number[] = []; 18 | 19 | const source = concat(of(1, 2, 3), NEVER).pipe( 20 | finalize(() => ++unsubscribes) 21 | ); 22 | const shared = source.pipe(shareWith(defaultRefCount())); 23 | 24 | const subscription = shared.subscribe((value) => values.push(value)); 25 | expect(values).to.deep.equal([1, 2, 3]); 26 | subscription.unsubscribe(); 27 | expect(unsubscribes).to.equal(1); 28 | }); 29 | 30 | it("should not share multiple subscriptions to a synchronous source", () => { 31 | let completes = 0; 32 | let subscribes = 0; 33 | const values: number[] = []; 34 | 35 | const source = defer(() => { 36 | ++subscribes; 37 | return of(1, 2, 3); 38 | }); 39 | const shared = source.pipe(shareWith(defaultRefCount())); 40 | 41 | shared.subscribe({ 42 | complete() { 43 | ++completes; 44 | }, 45 | next(value) { 46 | values.push(value); 47 | }, 48 | }); 49 | expect(completes).to.equal(1); 50 | expect(subscribes).to.equal(1); 51 | expect(values).to.deep.equal([1, 2, 3]); 52 | 53 | shared.subscribe({ 54 | complete() { 55 | ++completes; 56 | }, 57 | next(value) { 58 | values.push(value); 59 | }, 60 | }); 61 | expect(completes).to.equal(2); 62 | expect(subscribes).to.equal(2); 63 | expect(values).to.deep.equal([1, 2, 3, 1, 2, 3]); 64 | }); 65 | 66 | it( 67 | "should share concurrent subscriptions", 68 | marbles((m) => { 69 | const source = m.cold(" -1-2-3| "); 70 | const sourceSub1 = " ^-----! "; 71 | // -1-2-3| 72 | const sourceSub2 = " --------^-----!"; 73 | 74 | const sharedSub1 = " ^--------------"; 75 | const expected1 = " -1-2-3| "; 76 | const sharedSub2 = " ----^----------"; 77 | const expected2 = " -----3| "; 78 | const sharedSub3 = " --------^------"; 79 | const expected3 = " ---------1-2-3|"; 80 | 81 | const shared = source.pipe(shareWith(defaultRefCount())); 82 | m.expect(source).toHaveSubscriptions([sourceSub1, sourceSub2]); 83 | m.expect(shared, sharedSub1).toBeObservable(expected1); 84 | m.expect(shared, sharedSub2).toBeObservable(expected2); 85 | m.expect(shared, sharedSub3).toBeObservable(expected3); 86 | }) 87 | ); 88 | }); 89 | -------------------------------------------------------------------------------- /source/share-with/default-ref-count.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-pluggables 4 | */ 5 | 6 | import { Observable, OperatorFunction, Subscription } from "rxjs"; 7 | import { closedSubscription } from "./closed-subscription"; 8 | import { ShareStrategy } from "./share-strategy"; 9 | 10 | export function defaultRefCount(): ShareStrategy { 11 | return { 12 | operator: (connect) => defaultRefCountOperator(connect), 13 | shouldReuseSubject: () => false, 14 | }; 15 | } 16 | 17 | export function defaultRefCountOperator( 18 | connect: () => Subscription 19 | ): OperatorFunction { 20 | return (source) => { 21 | let connectSubscription = closedSubscription; 22 | let count = 0; 23 | 24 | return new Observable((observer) => { 25 | const subscription = source.subscribe(observer); 26 | if (++count === 1) { 27 | connectSubscription = connect(); 28 | } 29 | subscription.add(() => { 30 | if (--count === 0) { 31 | connectSubscription.unsubscribe(); 32 | } 33 | }); 34 | return subscription; 35 | }); 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /source/share-with/delayed-ref-count-spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-pluggables 4 | */ 5 | /* eslint rxjs/no-ignored-subscription: "off" */ 6 | 7 | import { expect } from "chai"; 8 | import { asyncScheduler, concat, defer, NEVER, of, ReplaySubject } from "rxjs"; 9 | import { marbles } from "rxjs-marbles"; 10 | import { finalize } from "rxjs/operators"; 11 | import { delayedRefCount } from "./delayed-ref-count"; 12 | import { shareWith } from "./share-with"; 13 | 14 | describe("delayedRefCount", () => { 15 | it("should support a synchronous source", (done: Mocha.Done) => { 16 | let unsubscribes = 0; 17 | const values: number[] = []; 18 | 19 | const source = concat(of(1, 2, 3), NEVER).pipe( 20 | finalize(() => ++unsubscribes) 21 | ); 22 | const shared = source.pipe(shareWith(delayedRefCount(10))); 23 | 24 | const subscription = shared.subscribe((value) => values.push(value)); 25 | expect(values).to.deep.equal([1, 2, 3]); 26 | subscription.unsubscribe(); 27 | expect(unsubscribes).to.equal(0); 28 | 29 | asyncScheduler.schedule(() => { 30 | expect(unsubscribes).to.equal(1); 31 | done(); 32 | }, 20); 33 | }); 34 | 35 | it("should share multiple subscriptions to a synchronous source", (done: Mocha.Done) => { 36 | let completes = 0; 37 | let subscribes = 0; 38 | const values: number[] = []; 39 | 40 | const source = defer(() => { 41 | ++subscribes; 42 | return of(1, 2, 3); 43 | }); 44 | const shared = source.pipe(shareWith(delayedRefCount(0))); 45 | 46 | shared.subscribe({ 47 | complete() { 48 | ++completes; 49 | }, 50 | next(value) { 51 | values.push(value); 52 | }, 53 | }); 54 | expect(completes).to.equal(1); 55 | expect(subscribes).to.equal(1); 56 | expect(values).to.deep.equal([1, 2, 3]); 57 | 58 | shared.subscribe({ 59 | complete() { 60 | ++completes; 61 | }, 62 | next(value) { 63 | values.push(value); 64 | }, 65 | }); 66 | expect(completes).to.equal(2); 67 | expect(subscribes).to.equal(1); 68 | expect(values).to.deep.equal([1, 2, 3]); 69 | 70 | asyncScheduler.schedule(() => { 71 | done(); 72 | }); 73 | }); 74 | 75 | it( 76 | "should multicast to multiple observers and complete", 77 | marbles((m) => { 78 | const source = m.cold(" -1-2-3----4-|"); 79 | const sourceSub1 = " ^-----------!"; 80 | const delay = m.time(" --| "); 81 | 82 | const sharedSub1 = " ^------------"; 83 | const expected1 = " -1-2-3----4-|"; 84 | const sharedSub2 = " ----^--------"; 85 | const expected2 = " -----3----4-|"; 86 | const sharedSub3 = " --------^----"; 87 | const expected3 = " ----------4-|"; 88 | 89 | const shared = source.pipe(shareWith(delayedRefCount(delay))); 90 | m.expect(source).toHaveSubscriptions([sourceSub1]); 91 | m.expect(shared, sharedSub1).toBeObservable(expected1); 92 | m.expect(shared, sharedSub2).toBeObservable(expected2); 93 | m.expect(shared, sharedSub3).toBeObservable(expected3); 94 | }) 95 | ); 96 | 97 | it( 98 | "should multicast an error to multiple observers", 99 | marbles((m) => { 100 | const source = m.cold(" -1-2-3----4-#"); 101 | const sourceSub1 = " ^-----------!"; 102 | const delay = m.time(" --| "); 103 | 104 | const sharedSub1 = " ^------------"; 105 | const expected1 = " -1-2-3----4-#"; 106 | const sharedSub2 = " ----^--------"; 107 | const expected2 = " -----3----4-#"; 108 | const sharedSub3 = " --------^----"; 109 | const expected3 = " ----------4-#"; 110 | 111 | const shared = source.pipe(shareWith(delayedRefCount(delay))); 112 | m.expect(source).toHaveSubscriptions([sourceSub1]); 113 | m.expect(shared, sharedSub1).toBeObservable(expected1); 114 | m.expect(shared, sharedSub2).toBeObservable(expected2); 115 | m.expect(shared, sharedSub3).toBeObservable(expected3); 116 | }) 117 | ); 118 | 119 | it( 120 | "should disconnect after the specified duration once the last subscriber unsubscribes", 121 | marbles((m) => { 122 | const source = m.cold(" -1-2-3----4---"); 123 | const sourceSub1 = " ^-------------!"; 124 | const delay = m.time(" --| "); 125 | // --| 126 | const sharedSub1 = " ^---!----------"; 127 | const expected1 = " -1-2-----------"; 128 | // --| 129 | const sharedSub2 = " ----^----!-----"; 130 | const expected2 = " -----3---------"; 131 | // --| 132 | const sharedSub3 = " --------^---!--"; 133 | const expected3 = " ----------4----"; 134 | 135 | const shared = source.pipe(shareWith(delayedRefCount(delay))); 136 | m.expect(source).toHaveSubscriptions([sourceSub1]); 137 | m.expect(shared, sharedSub1).toBeObservable(expected1); 138 | m.expect(shared, sharedSub2).toBeObservable(expected2); 139 | m.expect(shared, sharedSub3).toBeObservable(expected3); 140 | }) 141 | ); 142 | 143 | it( 144 | "should not disconnect if a subscription occurs within the duration", 145 | marbles((m) => { 146 | const source = m.cold(" -1-2-3----4-5---"); 147 | const sourceSub1 = " ^--------------!"; 148 | const delay = m.time(" --| "); 149 | // --| 150 | const sharedSub1 = " ^------! "; 151 | const expected1 = " -1-2-3-- "; 152 | // --| 153 | const sharedSub2 = " ----^----! "; 154 | const expected2 = " -----3---- "; 155 | // --| 156 | const sharedSub3 = " -----------^-! "; 157 | const expected3 = " ------------5- "; 158 | 159 | const shared = source.pipe(shareWith(delayedRefCount(delay))); 160 | m.expect(source).toHaveSubscriptions([sourceSub1]); 161 | m.expect(shared, sharedSub1).toBeObservable(expected1); 162 | m.expect(shared, sharedSub2).toBeObservable(expected2); 163 | m.expect(shared, sharedSub3).toBeObservable(expected3); 164 | }) 165 | ); 166 | 167 | it( 168 | "should reconnect if a subscription occurs after the duration", 169 | marbles((m) => { 170 | const source = m.cold(" -1-2-3----4-5---------"); 171 | const sourceSub1 = " ^----------! "; 172 | const sourceSub2 = " -----------------^---!"; 173 | const delay = m.time(" --| "); 174 | // --| 175 | const sharedSub1 = " ^------! "; 176 | const expected1 = " -1-2-3-- "; 177 | // --| 178 | const sharedSub2 = " ----^----! "; 179 | const expected2 = " -----3---- "; 180 | // --| 181 | const sharedSub3 = " -----------------^-! "; 182 | const expected3 = " ------------------1- "; 183 | 184 | const shared = source.pipe(shareWith(delayedRefCount(delay))); 185 | m.expect(source).toHaveSubscriptions([sourceSub1, sourceSub2]); 186 | m.expect(shared, sharedSub1).toBeObservable(expected1); 187 | m.expect(shared, sharedSub2).toBeObservable(expected2); 188 | m.expect(shared, sharedSub3).toBeObservable(expected3); 189 | }) 190 | ); 191 | 192 | it( 193 | "should retry", 194 | marbles((m) => { 195 | const source = m.cold(" -1-2-#--------------- "); 196 | const sourceSub1 = " ^----! "; 197 | const sourceSub2 = " ------^----! "; 198 | const sourceSub3 = " ---------------^----! "; 199 | const delay = m.time(" --| "); 200 | // --| 201 | const sharedSub1 = " ^----- "; 202 | const expected1 = " -1-2-# "; 203 | // --| 204 | const sharedSub2 = " ------^----- "; 205 | const expected2 = " -------1-2-# "; 206 | // --| 207 | const sharedSub3 = " ---------------^----- "; 208 | const expected3 = " ----------------1-2-# "; 209 | 210 | const shared = source.pipe(shareWith(delayedRefCount(delay))); 211 | m.expect(source).toHaveSubscriptions([ 212 | sourceSub1, 213 | sourceSub2, 214 | sourceSub3, 215 | ]); 216 | m.expect(shared, sharedSub1).toBeObservable(expected1); 217 | m.expect(shared, sharedSub2).toBeObservable(expected2); 218 | m.expect(shared, sharedSub3).toBeObservable(expected3); 219 | }) 220 | ); 221 | 222 | it( 223 | "should support a ReplaySubject", 224 | marbles((m) => { 225 | const source = m.cold(" --(r|) "); 226 | const sourceSub1 = " ^-! "; 227 | const sourceSub2 = " -----------------^-! "; 228 | const delay = m.time(" -----| "); 229 | // -----| 230 | const sharedSub1 = " ^-- "; 231 | const expected1 = " --(r|) "; 232 | // -----| 233 | const sharedSub2 = " ------^ "; 234 | const expected2 = " ------(r|) "; 235 | // -----| 236 | const sharedSub3 = " -----------------^-- "; 237 | const expected3 = " -------------------(r|) "; 238 | 239 | const shared = source.pipe( 240 | shareWith(delayedRefCount(delay), () => new ReplaySubject(1)) 241 | ); 242 | m.expect(source).toHaveSubscriptions([sourceSub1, sourceSub2]); 243 | m.expect(shared, sharedSub1).toBeObservable(expected1); 244 | m.expect(shared, sharedSub2).toBeObservable(expected2); 245 | m.expect(shared, sharedSub3).toBeObservable(expected3); 246 | }) 247 | ); 248 | }); 249 | -------------------------------------------------------------------------------- /source/share-with/delayed-ref-count.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-pluggables 4 | */ 5 | 6 | import { 7 | asapScheduler, 8 | NEVER, 9 | Observable, 10 | OperatorFunction, 11 | SchedulerLike, 12 | Subject, 13 | timer, 14 | Subscription, 15 | } from "rxjs"; 16 | import { scan, switchMap, tap } from "rxjs/operators"; 17 | import { closedSubscription } from "./closed-subscription"; 18 | import { ShareStrategy } from "./share-strategy"; 19 | 20 | export function delayedRefCount( 21 | delay: number, 22 | scheduler: SchedulerLike = asapScheduler 23 | ): ShareStrategy { 24 | return { 25 | operator: (connect) => delayedRefCountOperator(connect, delay, scheduler), 26 | shouldReuseSubject: ({ connected, kind }) => connected && kind === "C", 27 | }; 28 | } 29 | 30 | export function delayedRefCountOperator( 31 | connect: () => Subscription, 32 | delay: number, 33 | scheduler: SchedulerLike = asapScheduler 34 | ): OperatorFunction { 35 | return (source) => { 36 | let connectSubscription = closedSubscription; 37 | let connectorSubscription = closedSubscription; 38 | 39 | const notifier = new Subject(); 40 | const connector = notifier.pipe( 41 | scan((count, step) => count + step, 0), 42 | switchMap((count) => { 43 | if (count === 0) { 44 | return timer(delay, scheduler).pipe( 45 | tap(() => { 46 | connectSubscription.unsubscribe(); 47 | connectorSubscription.unsubscribe(); 48 | }) 49 | ); 50 | } 51 | if (count > 0 && connectSubscription.closed) { 52 | connectSubscription = connect(); 53 | } 54 | return NEVER; 55 | }) 56 | ); 57 | 58 | return new Observable((observer) => { 59 | if (connectorSubscription.closed) { 60 | connectorSubscription = connector.subscribe(); 61 | } 62 | const subscription = source.subscribe(observer); 63 | notifier.next(1); 64 | // The decrementing teardown is added *after* the increment to ensure the 65 | // reference count cannot go negative. If the source completes 66 | // synchronously, the decrementing teardown will run when it's added to 67 | // the subscription. 68 | subscription.add(() => notifier.next(-1)); 69 | return subscription; 70 | }); 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /source/share-with/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-pluggables 4 | */ 5 | 6 | export * from "./default-ref-count"; 7 | export * from "./delayed-ref-count"; 8 | export * from "./limited-ref-count"; 9 | export * from "./no-ref-count"; 10 | export * from "./scheduled-ref-count"; 11 | export * from "./share-strategy"; 12 | export * from "./share-with"; 13 | -------------------------------------------------------------------------------- /source/share-with/limited-ref-count-spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-pluggables 4 | */ 5 | /* eslint rxjs/no-ignored-subscription: "off" */ 6 | 7 | import { expect } from "chai"; 8 | import { concat, defer, NEVER, of } from "rxjs"; 9 | import { marbles } from "rxjs-marbles"; 10 | import { finalize } from "rxjs/operators"; 11 | import { limitedRefCount } from "./limited-ref-count"; 12 | import { shareWith } from "./share-with"; 13 | 14 | describe("limitedRefCount", () => { 15 | it("should support a synchronous source", () => { 16 | let subscribes = 0; 17 | let unsubscribes = 0; 18 | const values: number[] = []; 19 | 20 | const source = defer(() => { 21 | ++subscribes; 22 | return concat(of(1, 2, 3), NEVER).pipe(finalize(() => ++unsubscribes)); 23 | }); 24 | const shared = source.pipe(shareWith(limitedRefCount(2))); 25 | 26 | const a = shared.subscribe((value) => values.push(value)); 27 | a.unsubscribe(); 28 | expect(subscribes).to.equal(0); 29 | expect(values).to.deep.equal([]); 30 | expect(unsubscribes).to.equal(0); 31 | 32 | const b = shared.subscribe((value) => values.push(value)); 33 | const c = shared.subscribe((value) => values.push(value)); 34 | b.unsubscribe(); 35 | c.unsubscribe(); 36 | expect(subscribes).to.equal(1); 37 | expect(values).to.deep.equal([1, 1, 2, 2, 3, 3]); 38 | expect(unsubscribes).to.equal(1); 39 | }); 40 | 41 | it("should not share multiple subscriptions to a synchronous source", () => { 42 | let subscribes = 0; 43 | let unsubscribes = 0; 44 | const values: number[] = []; 45 | 46 | const source = defer(() => { 47 | ++subscribes; 48 | return concat(of(1, 2, 3), NEVER).pipe(finalize(() => ++unsubscribes)); 49 | }); 50 | const shared = source.pipe(shareWith(limitedRefCount(2))); 51 | 52 | const a = shared.subscribe((value) => values.push(value)); 53 | const b = shared.subscribe((value) => values.push(value)); 54 | a.unsubscribe(); 55 | b.unsubscribe(); 56 | expect(subscribes).to.equal(1); 57 | expect(values).to.deep.equal([1, 1, 2, 2, 3, 3]); 58 | expect(unsubscribes).to.equal(1); 59 | 60 | const c = shared.subscribe((value) => values.push(value)); 61 | const d = shared.subscribe((value) => values.push(value)); 62 | c.unsubscribe(); 63 | d.unsubscribe(); 64 | expect(subscribes).to.equal(2); 65 | expect(values).to.deep.equal([1, 1, 2, 2, 3, 3, 1, 1, 2, 2, 3, 3]); 66 | expect(unsubscribes).to.equal(2); 67 | }); 68 | 69 | it( 70 | "should honour the limit", 71 | marbles((m) => { 72 | const source = m.hot(" -1-2-3-4-5-6-7-"); 73 | const sourceSub1 = " --^-------!----"; 74 | 75 | const sharedSub1 = " ^-------!------"; 76 | const expected1 = " ---2-3-4-------"; 77 | const sharedSub2 = " --^-------!----"; 78 | const expected2 = " ---2-3-4-5-----"; 79 | const sharedSub3 = " ----^----------"; 80 | const expected3 = " -----3-4-5-----"; 81 | 82 | const shared = source.pipe(shareWith(limitedRefCount(2))); 83 | m.expect(source).toHaveSubscriptions([sourceSub1]); 84 | m.expect(shared, sharedSub1).toBeObservable(expected1); 85 | m.expect(shared, sharedSub2).toBeObservable(expected2); 86 | m.expect(shared, sharedSub3).toBeObservable(expected3); 87 | }) 88 | ); 89 | }); 90 | -------------------------------------------------------------------------------- /source/share-with/limited-ref-count.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-pluggables 4 | */ 5 | 6 | import { Observable, OperatorFunction, Subscription } from "rxjs"; 7 | import { closedSubscription } from "./closed-subscription"; 8 | import { ShareStrategy } from "./share-strategy"; 9 | 10 | export function limitedRefCount(limit: number): ShareStrategy { 11 | return { 12 | operator: (connect) => limitedRefCountOperator(connect, limit), 13 | shouldReuseSubject: () => true, 14 | }; 15 | } 16 | 17 | export function limitedRefCountOperator( 18 | connect: () => Subscription, 19 | limit: number 20 | ): OperatorFunction { 21 | return (source) => { 22 | let connectSubscription = closedSubscription; 23 | let count = 0; 24 | 25 | return new Observable((observer) => { 26 | const subscription = source.subscribe(observer); 27 | if (++count === limit) { 28 | connectSubscription = connect(); 29 | } 30 | subscription.add(() => { 31 | if (--count < limit) { 32 | connectSubscription.unsubscribe(); 33 | } 34 | }); 35 | return subscription; 36 | }); 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /source/share-with/no-ref-count-spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-pluggables 4 | */ 5 | 6 | import { expect } from "chai"; 7 | import { concat, of, NEVER } from "rxjs"; 8 | import { marbles } from "rxjs-marbles"; 9 | import { finalize } from "rxjs/operators"; 10 | import { noRefCount } from "./no-ref-count"; 11 | import { shareWith } from "./share-with"; 12 | 13 | describe("noRefCount", () => { 14 | it("should not unsubscribe from the source", () => { 15 | let unsubscribes = 0; 16 | const values: number[] = []; 17 | const source = concat(of(1, 2, 3), NEVER).pipe( 18 | finalize(() => ++unsubscribes) 19 | ); 20 | const shared = source.pipe(shareWith(noRefCount())); 21 | const subscription = shared.subscribe((value) => values.push(value)); 22 | expect(values).to.deep.equal([1, 2, 3]); 23 | subscription.unsubscribe(); 24 | expect(unsubscribes).to.equal(0); 25 | }); 26 | 27 | it( 28 | "should subscribe to the source once", 29 | marbles((m) => { 30 | const source = m.cold(" -1-2-3----4--"); 31 | const sourceSub1 = " ^------------"; 32 | 33 | const sharedSub1 = " ^------------"; 34 | const expected1 = " -1-2-3----4--"; 35 | const sharedSub2 = " ----^--------"; 36 | const expected2 = " -----3----4--"; 37 | const sharedSub3 = " --------^----"; 38 | const expected3 = " ----------4--"; 39 | 40 | const shared = source.pipe(shareWith(noRefCount())); 41 | m.expect(source).toHaveSubscriptions([sourceSub1]); 42 | m.expect(shared, sharedSub1).toBeObservable(expected1); 43 | m.expect(shared, sharedSub2).toBeObservable(expected2); 44 | m.expect(shared, sharedSub3).toBeObservable(expected3); 45 | }) 46 | ); 47 | 48 | it( 49 | "should not unsubscribe on a ref count of zero", 50 | marbles((m) => { 51 | const source = m.cold(" -1-2-3----4--"); 52 | const sourceSub1 = " ^------------"; 53 | 54 | const sharedSub1 = " ^-----------!"; 55 | const expected1 = " -1-2-3----4--"; 56 | const sharedSub2 = " ----^-------!"; 57 | const expected2 = " -----3----4--"; 58 | const sharedSub3 = " --------^---!"; 59 | const expected3 = " ----------4--"; 60 | 61 | const shared = source.pipe(shareWith(noRefCount())); 62 | m.expect(source).toHaveSubscriptions([sourceSub1]); 63 | m.expect(shared, sharedSub1).toBeObservable(expected1); 64 | m.expect(shared, sharedSub2).toBeObservable(expected2); 65 | m.expect(shared, sharedSub3).toBeObservable(expected3); 66 | }) 67 | ); 68 | 69 | it( 70 | "should unsubscribe on completion", 71 | marbles((m) => { 72 | const source = m.cold(" -1-2-3----4-|"); 73 | const sourceSub1 = " ^-----------!"; 74 | 75 | const sharedSub1 = " ^------------"; 76 | const expected1 = " -1-2-3----4-|"; 77 | const sharedSub2 = " ----^--------"; 78 | const expected2 = " -----3----4-|"; 79 | const sharedSub3 = " --------^----"; 80 | const expected3 = " ----------4-|"; 81 | 82 | const shared = source.pipe(shareWith(noRefCount())); 83 | m.expect(source).toHaveSubscriptions([sourceSub1]); 84 | m.expect(shared, sharedSub1).toBeObservable(expected1); 85 | m.expect(shared, sharedSub2).toBeObservable(expected2); 86 | m.expect(shared, sharedSub3).toBeObservable(expected3); 87 | }) 88 | ); 89 | 90 | it( 91 | "should unsubscribe on completion", 92 | marbles((m) => { 93 | const source = m.cold(" -1-2-3----4-#"); 94 | const sourceSub1 = " ^-----------!"; 95 | 96 | const sharedSub1 = " ^------------"; 97 | const expected1 = " -1-2-3----4-#"; 98 | const sharedSub2 = " ----^--------"; 99 | const expected2 = " -----3----4-#"; 100 | const sharedSub3 = " --------^----"; 101 | const expected3 = " ----------4-#"; 102 | 103 | const shared = source.pipe(shareWith(noRefCount())); 104 | m.expect(source).toHaveSubscriptions([sourceSub1]); 105 | m.expect(shared, sharedSub1).toBeObservable(expected1); 106 | m.expect(shared, sharedSub2).toBeObservable(expected2); 107 | m.expect(shared, sharedSub3).toBeObservable(expected3); 108 | }) 109 | ); 110 | }); 111 | -------------------------------------------------------------------------------- /source/share-with/no-ref-count.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-pluggables 4 | */ 5 | 6 | import { OperatorFunction, Observable, Subscription } from "rxjs"; 7 | import { closedSubscription } from "./closed-subscription"; 8 | import { ShareStrategy } from "./share-strategy"; 9 | 10 | export function noRefCount(): ShareStrategy { 11 | return { 12 | operator: (connect) => noRefCountOperator(connect), 13 | shouldReuseSubject: () => false, 14 | }; 15 | } 16 | 17 | export function noRefCountOperator( 18 | connect: () => Subscription 19 | ): OperatorFunction { 20 | return (source) => { 21 | let connectSubscription = closedSubscription; 22 | 23 | return new Observable((observer) => { 24 | const subscription = source.subscribe(observer); 25 | if (connectSubscription.closed) { 26 | connectSubscription = connect(); 27 | } 28 | return subscription; 29 | }); 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /source/share-with/scheduled-ref-count-spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-pluggables 4 | */ 5 | 6 | import { expect } from "chai"; 7 | import { asapScheduler, concat, defer, NEVER, of, queueScheduler } from "rxjs"; 8 | import { finalize } from "rxjs/operators"; 9 | import { scheduledRefCount } from "./scheduled-ref-count"; 10 | import { shareWith } from "./share-with"; 11 | 12 | describe("scheduledRefCount", () => { 13 | it("should support a synchronous source", (done: Mocha.Done) => { 14 | let unsubscribes = 0; 15 | const values: number[] = []; 16 | const source = concat(of(1, 2, 3), NEVER).pipe( 17 | finalize(() => ++unsubscribes) 18 | ); 19 | 20 | const shared = source.pipe(shareWith(scheduledRefCount(asapScheduler))); 21 | const subscription = shared.subscribe((value) => values.push(value)); 22 | expect(values).to.deep.equal([]); 23 | asapScheduler.schedule(() => { 24 | expect(values).to.deep.equal([1, 2, 3]); 25 | subscription.unsubscribe(); 26 | expect(unsubscribes).to.equal(0); 27 | }); 28 | asapScheduler.schedule(() => { 29 | expect(unsubscribes).to.equal(1); 30 | done(); 31 | }, 10); 32 | }); 33 | 34 | it("should support queue-scheduled actions", () => { 35 | let receives = 0; 36 | let subscribes = 0; 37 | 38 | const source = defer(() => { 39 | ++subscribes; 40 | return of(42); 41 | }); 42 | 43 | queueScheduler.schedule(() => { 44 | const subscription = source 45 | .pipe(shareWith(scheduledRefCount(queueScheduler))) 46 | .subscribe(() => ++receives); 47 | subscription.unsubscribe(); 48 | }); 49 | 50 | expect(receives).to.equal(0); 51 | expect(subscribes).to.equal(0); 52 | 53 | queueScheduler.schedule(() => { 54 | const subscription = source 55 | .pipe(shareWith(scheduledRefCount(queueScheduler))) 56 | .subscribe(() => ++receives); 57 | queueScheduler.schedule(() => { 58 | subscription.unsubscribe(); 59 | }); 60 | }); 61 | 62 | expect(receives).to.equal(1); 63 | expect(subscribes).to.equal(1); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /source/share-with/scheduled-ref-count.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-pluggables 4 | */ 5 | 6 | import { 7 | Observable, 8 | OperatorFunction, 9 | SchedulerLike, 10 | Subscription, 11 | } from "rxjs"; 12 | import { closedSubscription } from "./closed-subscription"; 13 | import { ShareStrategy } from "./share-strategy"; 14 | 15 | export function scheduledRefCount(scheduler: SchedulerLike): ShareStrategy { 16 | return { 17 | operator: (connect) => scheduledRefCountOperator(connect, scheduler), 18 | shouldReuseSubject: ({ connected, kind }) => connected && kind === "C", 19 | }; 20 | } 21 | 22 | export function scheduledRefCountOperator( 23 | connect: () => Subscription, 24 | scheduler: SchedulerLike 25 | ): OperatorFunction { 26 | return (source) => { 27 | let connectSubscription = closedSubscription; 28 | let count = 0; 29 | return new Observable((observer) => { 30 | ++count; 31 | const subscription = source.subscribe(observer); 32 | subscription.add( 33 | scheduler.schedule(() => { 34 | if (count > 0 && connectSubscription.closed) { 35 | connectSubscription = connect(); 36 | } 37 | }) 38 | ); 39 | subscription.add(() => { 40 | --count; 41 | scheduler.schedule(() => { 42 | if (count === 0) { 43 | connectSubscription.unsubscribe(); 44 | } 45 | }); 46 | }); 47 | return subscription; 48 | }); 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /source/share-with/share-strategy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-pluggables 4 | */ 5 | 6 | import { OperatorFunction, Subject, Subscription } from "rxjs"; 7 | 8 | export type ShareStrategy = { 9 | operator: (connect: () => Subscription) => OperatorFunction; 10 | shouldReuseSubject: (state: { 11 | connected: boolean; 12 | kind: "C" | "E" | undefined; 13 | subject: Subject; 14 | }) => boolean; 15 | }; 16 | -------------------------------------------------------------------------------- /source/share-with/share-with-spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-pluggables 4 | */ 5 | /* eslint no-unused-expressions: "off" */ 6 | 7 | import { expect } from "chai"; 8 | import { ReplaySubject, Subject } from "rxjs"; 9 | import { marbles } from "rxjs-marbles"; 10 | import { defaultRefCountOperator } from "./default-ref-count"; 11 | import { shareWith } from "./share-with"; 12 | 13 | describe("shareWith", () => { 14 | it( 15 | "should indicate closing via a complete notification", 16 | marbles((m) => { 17 | let kind: "C" | "E" | undefined = undefined; 18 | let subject: Subject | undefined = undefined; 19 | 20 | const source = m.cold(" a-|"); 21 | // a-# 22 | const sourceSub1 = " ^-!----"; 23 | const sharedSub1 = " ^------"; 24 | const time1 = m.time(" -|-----"); 25 | const expected1 = " a-|----"; 26 | // a-# 27 | const sourceSub2 = " ---^-!-"; 28 | const sharedSub2 = " ---^---"; 29 | const time2 = m.time(" ----|--"); 30 | const expected2 = " ---a-|-"; 31 | const time3 = m.time(" ------|"); 32 | 33 | const shared = source.pipe( 34 | shareWith({ 35 | operator: (connect) => defaultRefCountOperator(connect), 36 | shouldReuseSubject: (state) => { 37 | kind = state.kind; 38 | subject = state.subject; 39 | return false; 40 | }, 41 | }) 42 | ); 43 | m.expect(source).toHaveSubscriptions([sourceSub1, sourceSub2]); 44 | m.expect(shared, sharedSub1).toBeObservable(expected1); 45 | m.expect(shared, sharedSub2).toBeObservable(expected2); 46 | 47 | m.scheduler.schedule(() => { 48 | expect(kind).to.equal(undefined); 49 | expect(subject).to.be.undefined; 50 | }, time1); 51 | m.scheduler.schedule(() => { 52 | expect(kind).to.equal("C"); 53 | expect(subject).to.not.be.undefined; 54 | }, time2); 55 | m.scheduler.schedule(() => { 56 | expect(kind).to.equal("C"); 57 | expect(subject).to.not.be.undefined; 58 | }, time3); 59 | }) 60 | ); 61 | 62 | it( 63 | "should indicate closing via an error notification", 64 | marbles((m) => { 65 | let kind: "C" | "E" | undefined = undefined; 66 | let subject: Subject | undefined = undefined; 67 | 68 | const source = m.cold(" a-#"); 69 | // a-# 70 | const sourceSub1 = " ^-!----"; 71 | const sharedSub1 = " ^------"; 72 | const time1 = m.time(" -|-----"); 73 | const expected1 = " a-#----"; 74 | // a-# 75 | const sourceSub2 = " ---^-!-"; 76 | const sharedSub2 = " ---^---"; 77 | const time2 = m.time(" ----|--"); 78 | const expected2 = " ---a-#-"; 79 | const time3 = m.time(" ------|"); 80 | 81 | const shared = source.pipe( 82 | shareWith({ 83 | operator: (connect) => defaultRefCountOperator(connect), 84 | shouldReuseSubject: (state) => { 85 | kind = state.kind; 86 | subject = state.subject; 87 | return false; 88 | }, 89 | }) 90 | ); 91 | m.expect(source).toHaveSubscriptions([sourceSub1, sourceSub2]); 92 | m.expect(shared, sharedSub1).toBeObservable(expected1); 93 | m.expect(shared, sharedSub2).toBeObservable(expected2); 94 | 95 | m.scheduler.schedule(() => { 96 | expect(kind).to.equal(undefined); 97 | expect(subject).to.be.undefined; 98 | }, time1); 99 | m.scheduler.schedule(() => { 100 | expect(kind).to.equal("E"); 101 | expect(subject).to.not.be.undefined; 102 | }, time2); 103 | m.scheduler.schedule(() => { 104 | expect(kind).to.equal("E"); 105 | expect(subject).to.not.be.undefined; 106 | }, time3); 107 | }) 108 | ); 109 | 110 | it( 111 | "should indicate closing via an unsubscription", 112 | marbles((m) => { 113 | let kind: "C" | "E" | undefined = undefined; 114 | let subject: Subject | undefined = undefined; 115 | 116 | const source = m.cold(" a--"); 117 | // a-- 118 | const sharedSub1 = " ^-!----"; 119 | const time1 = m.time(" -|-----"); 120 | const expected1 = " a------"; 121 | // a-- 122 | const sharedSub2 = " ---^-!-"; 123 | const time2 = m.time(" ----|--"); 124 | const expected2 = " ---a---"; 125 | const time3 = m.time(" ------|"); 126 | 127 | const shared = source.pipe( 128 | shareWith({ 129 | operator: (connect) => defaultRefCountOperator(connect), 130 | shouldReuseSubject: (state) => { 131 | kind = state.kind; 132 | subject = state.subject; 133 | return false; 134 | }, 135 | }) 136 | ); 137 | m.expect(source).toHaveSubscriptions([sharedSub1, sharedSub2]); 138 | m.expect(shared, sharedSub1).toBeObservable(expected1); 139 | m.expect(shared, sharedSub2).toBeObservable(expected2); 140 | 141 | m.scheduler.schedule(() => { 142 | expect(kind).to.equal(undefined); 143 | expect(subject).to.be.undefined; 144 | }, time1); 145 | m.scheduler.schedule(() => { 146 | expect(kind).to.equal(undefined); 147 | expect(subject).to.not.be.undefined; 148 | }, time2); 149 | m.scheduler.schedule(() => { 150 | expect(kind).to.equal(undefined); 151 | expect(subject).to.not.be.undefined; 152 | }, time3); 153 | }) 154 | ); 155 | 156 | it( 157 | "should be able to reuse the subject", 158 | marbles((m) => { 159 | const source = m.cold(" ab| "); 160 | // ab| 161 | const sourceSub1 = " ^-! "; 162 | const sharedSub1 = " ^-------"; 163 | const expected1 = " ab|-----"; 164 | // ab| 165 | const sharedSub2 = " ---^----"; 166 | const expected2 = " ---(b|)-"; 167 | 168 | const shared = source.pipe( 169 | shareWith( 170 | { 171 | operator: (connect) => defaultRefCountOperator(connect), 172 | shouldReuseSubject: () => true, 173 | }, 174 | () => new ReplaySubject(1) 175 | ) 176 | ); 177 | m.expect(source).toHaveSubscriptions([sourceSub1]); 178 | m.expect(shared, sharedSub1).toBeObservable(expected1); 179 | m.expect(shared, sharedSub2).toBeObservable(expected2); 180 | }) 181 | ); 182 | }); 183 | -------------------------------------------------------------------------------- /source/share-with/share-with.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-pluggables 4 | */ 5 | 6 | import { defer, OperatorFunction, Subject, Subscription } from "rxjs"; 7 | import { tap } from "rxjs/operators"; 8 | import { closedSubscription } from "./closed-subscription"; 9 | import { ShareStrategy } from "./share-strategy"; 10 | 11 | export function shareWith( 12 | strategy: ShareStrategy, 13 | factory: () => Subject = () => new Subject() 14 | ): OperatorFunction { 15 | const { operator, shouldReuseSubject } = strategy; 16 | let kind: "C" | "E" | undefined = undefined; 17 | let subject: Subject | undefined = undefined; 18 | 19 | return (source) => { 20 | function connect(): Subscription { 21 | if (!subject || subject.isStopped) { 22 | return closedSubscription as Subscription; 23 | } 24 | // The lifetime of the subject is not bound to the lifetime of the source 25 | // subscription. The subject remains available for reuse until the 26 | // strategy indicates it is not to be reused or until the operator calls 27 | // unsubscribe on the subscription returned from connect - which 28 | // indicates that the operator no longer needs the shared subject. 29 | const connectSubject = subject; 30 | const connectSubscription = new Subscription(); 31 | const sourceSubscription = source 32 | .pipe( 33 | tap({ 34 | complete() { 35 | kind = "C"; 36 | }, 37 | error() { 38 | kind = "E"; 39 | }, 40 | }) 41 | ) 42 | .subscribe(connectSubject); 43 | sourceSubscription.add(() => { 44 | if ( 45 | !shouldReuseSubject({ 46 | connected: true, 47 | kind, 48 | subject: connectSubject, 49 | }) 50 | ) { 51 | subject = undefined; 52 | connectSubscription.unsubscribe(); 53 | } 54 | }); 55 | // Although the subject's lifetime is not bound to the source 56 | // subscription, the reverse is not true. If the operator no longer needs 57 | // the subject - and unsubscribes from the subscription returned from 58 | // connect - the subject should be unsubscribed from the source. 59 | connectSubscription.add(sourceSubscription); 60 | connectSubscription.add(() => { 61 | if ( 62 | !shouldReuseSubject({ 63 | connected: false, 64 | kind, 65 | subject: connectSubject, 66 | }) 67 | ) { 68 | subject = undefined; 69 | } 70 | }); 71 | return connectSubscription; 72 | } 73 | return defer(() => subject ?? (subject = factory())).pipe( 74 | operator(connect) 75 | ); 76 | }; 77 | } 78 | -------------------------------------------------------------------------------- /source/switch-map-with/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-pluggables 4 | */ 5 | 6 | export * from "./switch-map-with"; 7 | -------------------------------------------------------------------------------- /source/switch-map-with/switch-map-with-spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-pluggables 4 | */ 5 | 6 | import { marbles } from "rxjs-marbles"; 7 | import { auditTime, debounceTime, delay, sampleTime } from "rxjs/operators"; 8 | import { switchMapWith } from "./switch-map-with"; 9 | 10 | describe("switchMapWith", () => { 11 | it( 12 | "should play nice with auditTime", 13 | marbles((m) => { 14 | const source = m.cold(" ss-s-------"); 15 | const sourceSub1 = " ^----------"; 16 | const duration = m.time(" --| "); 17 | const inner = m.cold(" --i| "); 18 | // --| 19 | // --i| 20 | const innerSub1 = " --^!-------"; 21 | // --| 22 | // --i| 23 | const innerSub2 = " -----^--!--"; 24 | const expected = " -------i---"; 25 | 26 | const result = source.pipe( 27 | switchMapWith(auditTime(duration), () => inner) 28 | ); 29 | m.expect(result).toBeObservable(expected); 30 | m.expect(source).toHaveSubscriptions([sourceSub1]); 31 | m.expect(inner).toHaveSubscriptions([innerSub1, innerSub2]); 32 | }) 33 | ); 34 | 35 | it( 36 | "should play nice with debounceTime", 37 | marbles((m) => { 38 | const source = m.cold(" ss--s------"); 39 | const sourceSub1 = " ^----------"; 40 | const duration = m.time(" --| "); 41 | const inner = m.cold(" --i| "); 42 | // --| 43 | // --i| 44 | const innerSub1 = " ---^!------"; 45 | // --| 46 | // --i| 47 | const innerSub2 = " ------^--!-"; 48 | const expected = " --------i--"; 49 | 50 | const result = source.pipe( 51 | switchMapWith(debounceTime(duration), () => inner) 52 | ); 53 | m.expect(result).toBeObservable(expected); 54 | m.expect(source).toHaveSubscriptions([sourceSub1]); 55 | m.expect(inner).toHaveSubscriptions([innerSub1, innerSub2]); 56 | }) 57 | ); 58 | 59 | it( 60 | "should play nice with delay", 61 | marbles((m) => { 62 | const source = m.cold(" s--s-------"); 63 | const sourceSub1 = " ^----------"; 64 | const duration = m.time(" --| "); 65 | const inner = m.cold(" --i| "); 66 | // --| 67 | // --i| 68 | const innerSub1 = " --^!-------"; 69 | // --| 70 | // --i| 71 | const innerSub2 = " -----^--!--"; 72 | const expected = " -------i---"; 73 | 74 | const result = source.pipe(switchMapWith(delay(duration), () => inner)); 75 | m.expect(result).toBeObservable(expected); 76 | m.expect(source).toHaveSubscriptions([sourceSub1]); 77 | m.expect(inner).toHaveSubscriptions([innerSub1, innerSub2]); 78 | }) 79 | ); 80 | 81 | it( 82 | "should play nice with sampleTime", 83 | marbles((m) => { 84 | const source = m.cold(" ss-s------|"); 85 | const sourceSub1 = " ^---------!"; 86 | const duration = m.time(" --| "); 87 | const inner = m.cold(" --i| "); 88 | // --| 89 | // --i| 90 | const innerSub1 = " --^!-------"; 91 | // --| 92 | // --i| 93 | const innerSub2 = " ----^--!---"; 94 | const expected = " ------i---|"; 95 | 96 | const result = source.pipe( 97 | switchMapWith(sampleTime(duration), () => inner) 98 | ); 99 | m.expect(result).toBeObservable(expected); 100 | m.expect(source).toHaveSubscriptions([sourceSub1]); 101 | m.expect(inner).toHaveSubscriptions([innerSub1, innerSub2]); 102 | }) 103 | ); 104 | }); 105 | -------------------------------------------------------------------------------- /source/switch-map-with/switch-map-with.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-pluggables 4 | */ 5 | 6 | import { Observable, OperatorFunction } from "rxjs"; 7 | import { publish, switchMap, takeUntil } from "rxjs/operators"; 8 | 9 | export function switchMapWith( 10 | before: OperatorFunction, 11 | project: (value: I, index: number) => Observable 12 | ): OperatorFunction { 13 | return (source) => 14 | source.pipe( 15 | publish((published) => 16 | published.pipe( 17 | before, 18 | switchMap((value, index) => 19 | project(value, index).pipe(takeUntil(published)) 20 | ) 21 | ) 22 | ) 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /source/traverse/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-pluggables 4 | */ 5 | 6 | export * from "./traverse"; 7 | -------------------------------------------------------------------------------- /source/traverse/queued-notifications-spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-pluggables 4 | */ 5 | /* eslint rxjs/no-ignored-subscription: "off" */ 6 | 7 | import { AsyncSubject } from "rxjs"; 8 | import { marbles } from "rxjs-marbles"; 9 | import { concatMap } from "rxjs/operators"; 10 | import { QueuedNotifications } from "./queued-notifications"; 11 | 12 | describe("QueuedNotifications", () => { 13 | it( 14 | "should emit nothing without a notification", 15 | marbles((m) => { 16 | const notifier = m.hot(" --"); 17 | const queuings = m.cold(" q-"); 18 | const expected = m.cold(" --"); 19 | 20 | const notifications = new QueuedNotifications(notifier); 21 | notifications.connect(); 22 | 23 | const result = queuings.pipe( 24 | concatMap(() => { 25 | const subject = new AsyncSubject(); 26 | notifications.queue.subscribe( 27 | (index) => subject.error(new Error("Unexpected index.")), 28 | (error: unknown) => subject.error(error), 29 | () => subject.error(new Error("Unexpected completion.")) 30 | ); 31 | return subject; 32 | }) 33 | ); 34 | 35 | m.expect(result).toBeObservable(expected); 36 | }) 37 | ); 38 | 39 | it( 40 | "should emit the subscription index upon notification", 41 | marbles((m) => { 42 | const notifier = m.hot(" -n"); 43 | const queuings = m.cold(" q-"); 44 | const expected = m.cold(" -0"); 45 | 46 | const notifications = new QueuedNotifications(notifier); 47 | notifications.connect(); 48 | 49 | const result = queuings.pipe( 50 | concatMap(() => { 51 | const subject = new AsyncSubject(); 52 | notifications.queue.subscribe( 53 | (index) => subject.next(index.toString()), 54 | (error: unknown) => subject.error(error), 55 | () => subject.complete() 56 | ); 57 | return subject; 58 | }) 59 | ); 60 | 61 | m.expect(result).toBeObservable(expected); 62 | }) 63 | ); 64 | 65 | it( 66 | "should emit the subscription index for each notification", 67 | marbles((m) => { 68 | const notifier = m.hot(" -----n-n"); 69 | const queuings = m.cold(" (qq)----"); 70 | const expected = m.cold(" -----0-1"); 71 | 72 | const notifications = new QueuedNotifications(notifier); 73 | notifications.connect(); 74 | 75 | const result = queuings.pipe( 76 | concatMap(() => { 77 | const subject = new AsyncSubject(); 78 | notifications.queue.subscribe( 79 | (index) => subject.next(index.toString()), 80 | (error: unknown) => subject.error(error), 81 | () => subject.complete() 82 | ); 83 | return subject; 84 | }) 85 | ); 86 | 87 | m.expect(result).toBeObservable(expected); 88 | }) 89 | ); 90 | 91 | it( 92 | "should queue notifications", 93 | marbles((m) => { 94 | const notifier = m.hot(" (nn)----"); 95 | const queuings = m.cold(" -----q-q"); 96 | const expected = m.cold(" -----0-1"); 97 | 98 | const notifications = new QueuedNotifications(notifier); 99 | notifications.connect(); 100 | 101 | const result = queuings.pipe( 102 | concatMap(() => { 103 | const subject = new AsyncSubject(); 104 | notifications.queue.subscribe( 105 | (index) => subject.next(index.toString()), 106 | (error: unknown) => subject.error(error), 107 | () => subject.complete() 108 | ); 109 | return subject; 110 | }) 111 | ); 112 | 113 | m.expect(result).toBeObservable(expected); 114 | }) 115 | ); 116 | }); 117 | -------------------------------------------------------------------------------- /source/traverse/queued-notifications.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-pluggables 4 | */ 5 | /* eslint rxjs/no-connectable: "off" */ 6 | 7 | import { 8 | ConnectableObservable, 9 | Observable, 10 | Subject, 11 | Subscription, 12 | zip, 13 | } from "rxjs"; 14 | 15 | import { first, map, publish } from "rxjs/operators"; 16 | 17 | export class QueuedNotifications { 18 | private _count = 0; 19 | private _indices: Subject; 20 | private _notifications: ConnectableObservable; 21 | private _queue: Observable; 22 | 23 | constructor(notifier: Observable) { 24 | this._indices = new Subject(); 25 | this._notifications = zip(notifier, this._indices).pipe( 26 | map(([, index]) => index), 27 | publish() 28 | ) as ConnectableObservable; 29 | this._queue = new Observable((observer) => { 30 | const index = this._count++; 31 | const subscription = this._notifications 32 | .pipe(first((value) => value === index)) 33 | .subscribe(observer); 34 | this._indices.next(index); 35 | return subscription; 36 | }); 37 | } 38 | 39 | connect(): Subscription { 40 | return this._notifications.connect(); 41 | } 42 | 43 | get queue(): Observable { 44 | return this._queue; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /source/traverse/traverse-spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-pluggables 4 | */ 5 | /* eslint rxjs/no-ignored-subscription: "off" */ 6 | 7 | import { expect } from "chai"; 8 | import { 9 | concat, 10 | EMPTY, 11 | Observable, 12 | of, 13 | SchedulerLike, 14 | Subject, 15 | timer, 16 | } from "rxjs"; 17 | import { marbles } from "rxjs-marbles"; 18 | import { delay, ignoreElements, map } from "rxjs/operators"; 19 | import { traverse } from "./traverse"; 20 | 21 | describe("traverse", () => { 22 | describe("lists", () => { 23 | const createFactory = ( 24 | max = Infinity, 25 | count?: number, 26 | time?: number, 27 | scheduler?: SchedulerLike 28 | ) => ( 29 | marker: number | undefined 30 | ): Observable<{ markers: number[]; values: string[] }> => { 31 | const at = marker === undefined ? 0 : marker + 1; 32 | const markers = [at]; 33 | const values: string[] = []; 34 | for (let c = 0; c < (count || 1); ++c) { 35 | values.push((at + c).toString()); 36 | } 37 | const source = at <= max ? of({ markers, values }) : EMPTY; 38 | return time !== undefined && scheduler !== undefined 39 | ? source.pipe(delay(time, scheduler)) 40 | : source; 41 | }; 42 | 43 | it( 44 | "should complete if there is no data", 45 | marbles((m) => { 46 | const notifier = m.hot(" --n----|"); 47 | const notifierSubs = " ^---!"; 48 | const expected = m.cold(" ----|"); 49 | 50 | const factory = createFactory(-1, 1, m.time("--|"), m.scheduler); 51 | const traversed = traverse({ factory, notifier }); 52 | m.expect(traversed).toBeObservable(expected); 53 | m.expect(notifier).toHaveSubscriptions(notifierSubs); 54 | }) 55 | ); 56 | 57 | it( 58 | "should traverse the first chunk of data", 59 | marbles((m) => { 60 | const notifier = m.hot(" n-"); 61 | const expected = m.cold(" 0-"); 62 | 63 | const factory = createFactory(); 64 | const traversed = traverse({ factory, notifier }); 65 | m.expect(traversed).toBeObservable(expected); 66 | }) 67 | ); 68 | 69 | it( 70 | "should traverse further chunks in response to the notifier", 71 | marbles((m) => { 72 | const notifier = m.hot(" n-n----n--n--"); 73 | const expected = m.cold(" 0-1----2--3--"); 74 | 75 | const factory = createFactory(); 76 | const traversed = traverse({ factory, notifier }); 77 | m.expect(traversed).toBeObservable(expected); 78 | }) 79 | ); 80 | 81 | it( 82 | "should flatten values within chunks", 83 | marbles((m) => { 84 | const notifier = m.hot(" n----n-------n-----n-----"); 85 | const expected = m.cold(" (01)-(12)----(23)--(34)--"); 86 | 87 | const factory = createFactory(Infinity, 2); 88 | const traversed = traverse({ factory, notifier }); 89 | m.expect(traversed).toBeObservable(expected); 90 | }) 91 | ); 92 | 93 | it( 94 | "should queue notifications", 95 | marbles((m) => { 96 | const notifier = m.hot(" nnn------------"); 97 | const expected = m.cold(" ----0---1---2--"); 98 | 99 | const factory = createFactory( 100 | Infinity, 101 | 1, 102 | m.time("----|"), 103 | m.scheduler 104 | ); 105 | const traversed = traverse({ factory, notifier }); 106 | m.expect(traversed).toBeObservable(expected); 107 | }) 108 | ); 109 | 110 | it( 111 | "should traverse without a notifier", 112 | marbles((m) => { 113 | const expected = m.cold("----0---1---2---|"); 114 | const factory = createFactory(2, 1, m.time("----|"), m.scheduler); 115 | const traversed = traverse({ factory }); 116 | m.expect(traversed).toBeObservable(expected); 117 | }) 118 | ); 119 | 120 | it( 121 | "should traverse with an operator", 122 | marbles((m) => { 123 | const other = m.cold(" |"); 124 | const subs = [ 125 | " ----(^!)---------", 126 | " --------(^!)-----", 127 | " ------------(^!)-", 128 | ]; 129 | const expected = m.cold(" ----0---1---2---|"); 130 | 131 | const factory = createFactory(2, 1, m.time("----|"), m.scheduler); 132 | const traversed = traverse({ 133 | factory, 134 | operator: (source) => concat(source, other), 135 | }); 136 | m.expect(traversed).toBeObservable(expected); 137 | m.expect(other).toHaveSubscriptions(subs); 138 | }) 139 | ); 140 | 141 | it( 142 | "should traverse with an asynchonous operator", 143 | marbles((m) => { 144 | const other = m.cold(" ----|"); 145 | const subs = [ 146 | " ^---!-------", 147 | " ----^---!---", 148 | " --------^---!", 149 | ]; 150 | const expected = m.cold(" 0---1---2---|"); 151 | 152 | const factory = createFactory(2); 153 | const traversed = traverse({ 154 | factory, 155 | operator: (source) => concat(source, other), 156 | }); 157 | m.expect(traversed).toBeObservable(expected); 158 | m.expect(other).toHaveSubscriptions(subs); 159 | }) 160 | ); 161 | 162 | it( 163 | "should serialize production", 164 | marbles((m) => { 165 | const values = { 166 | w: { markers: ["x", "y", "z"], values: [] }, 167 | x: { markers: [], values: ["a", "b"] }, 168 | y: { markers: [], values: ["c", "d"] }, 169 | z: { markers: [], values: ["e", "f"] }, 170 | }; 171 | 172 | const w = m.cold(" -----(w|)", values); 173 | const x = m.cold(" -----(x|)", values); 174 | const y = m.cold(" -----(y|)", values); 175 | const z = m.cold(" -----(z|)", values); 176 | 177 | const expected = m.cold(" ----------(ab)-(cd)-(ef|)"); 178 | const wSubs = " ^----!-------------------"; 179 | const xSubs = " -----^----!--------------"; 180 | const ySubs = " ----------^----!---------"; 181 | const zSubs = " ---------------^----!----"; 182 | 183 | const factory = (marker: string | undefined, index: number) => { 184 | switch (marker) { 185 | case undefined: 186 | return w; 187 | case "x": 188 | return x; 189 | case "y": 190 | return y; 191 | case "z": 192 | return z; 193 | default: 194 | return EMPTY; 195 | } 196 | }; 197 | 198 | const traversed = traverse({ factory }); 199 | m.expect(traversed).toBeObservable(expected); 200 | m.expect(w).toHaveSubscriptions(wSubs); 201 | m.expect(x).toHaveSubscriptions(xSubs); 202 | m.expect(y).toHaveSubscriptions(ySubs); 203 | m.expect(z).toHaveSubscriptions(zSubs); 204 | }) 205 | ); 206 | }); 207 | 208 | describe("graphs", () => { 209 | const createFactory = (time?: number, scheduler?: SchedulerLike) => ( 210 | marker: any 211 | ): Observable<{ markers: any[]; values: string[] }> => { 212 | const data = { 213 | a: { 214 | d: {}, 215 | e: {}, 216 | }, 217 | b: { 218 | f: {}, 219 | }, 220 | c: {}, 221 | }; 222 | const node = marker ? marker : data; 223 | const element: any = { markers: [], values: [] }; 224 | for (const key of Object.keys(node)) { 225 | if (Object.keys(node[key]).length) { 226 | element.markers.push(node[key]); 227 | } 228 | element.values.push(key); 229 | } 230 | return time && scheduler 231 | ? concat( 232 | timer(time, scheduler).pipe(ignoreElements()) as Observable, 233 | of(element) 234 | ) 235 | : of(element); 236 | }; 237 | 238 | it( 239 | "should traverse graphs with a notifier", 240 | marbles((m) => { 241 | const notifier = m.hot(" n-----n-----n---"); 242 | const expected = m.cold(" (abc)-(de)--(f|)"); 243 | 244 | const factory = createFactory(); 245 | const traversed = traverse({ factory, notifier }); 246 | m.expect(traversed).toBeObservable(expected); 247 | }) 248 | ); 249 | 250 | it( 251 | "should queue notifications for graphs", 252 | marbles((m) => { 253 | const notifier = m.hot(" nnn-------------------"); 254 | const expected = m.cold(" ------(abc)-(de)--(f|)"); 255 | 256 | const factory = createFactory(m.time("------|"), m.scheduler); 257 | const traversed = traverse({ factory, notifier }); 258 | m.expect(traversed).toBeObservable(expected); 259 | }) 260 | ); 261 | 262 | it( 263 | "should traverse graphs without a notifier", 264 | marbles((m) => { 265 | const expected = m.cold("------(abc)-(de)--(f|)"); 266 | const factory = createFactory(m.time("------|"), m.scheduler); 267 | const traversed = traverse({ factory }); 268 | m.expect(traversed).toBeObservable(expected); 269 | }) 270 | ); 271 | 272 | it( 273 | "should support concurrency", 274 | marbles((m) => { 275 | const expected = m.cold("------(abc)-(def|)"); 276 | const factory = createFactory(m.time("------|"), m.scheduler); 277 | const traversed = traverse({ concurrency: Infinity, factory }); 278 | m.expect(traversed).toBeObservable(expected); 279 | }) 280 | ); 281 | }); 282 | 283 | describe("GitHub usage example", () => { 284 | function get( 285 | url: string 286 | ): Observable<{ 287 | content: { html_url: string }[]; 288 | next: string | null; 289 | }> { 290 | // The next URL would be obtained from the Link header. 291 | // https://blog.angularindepth.com/rxjs-understanding-expand-a5f8b41a3602 292 | 293 | switch (url) { 294 | case "https://api.github.com/users/cartant/repos": 295 | return of({ 296 | content: [ 297 | { 298 | html_url: "https://github.com/cartant/rxjs-etc", 299 | }, 300 | { 301 | html_url: "https://github.com/cartant/rxjs-marbles", 302 | }, 303 | ], 304 | next: "https://api.github.com/users/cartant/repos?page=2", 305 | }); 306 | case "https://api.github.com/users/cartant/repos?page=2": 307 | return of({ 308 | content: [ 309 | { 310 | html_url: "https://github.com/cartant/rxjs-spy", 311 | }, 312 | { 313 | html_url: "https://github.com/cartant/rxjs-tslint-rules", 314 | }, 315 | ], 316 | next: null, 317 | }); 318 | default: 319 | throw new Error("Unexpected URL."); 320 | } 321 | } 322 | 323 | describe("with notifier", () => { 324 | it("should traverse the pages", (callback: any) => { 325 | const notifier = new Subject(); 326 | const urls = traverse({ 327 | factory: (marker?: string) => 328 | get(marker ?? "https://api.github.com/users/cartant/repos").pipe( 329 | map((response) => ({ 330 | markers: response.next ? [response.next] : [], 331 | values: response.content, 332 | })) 333 | ), 334 | notifier, 335 | }).pipe(map((repo) => repo.html_url)); 336 | 337 | const received: string[] = []; 338 | urls.subscribe( 339 | (url) => received.push(url), 340 | callback, 341 | () => { 342 | expect(received).to.deep.equal([ 343 | "https://github.com/cartant/rxjs-etc", 344 | "https://github.com/cartant/rxjs-marbles", 345 | "https://github.com/cartant/rxjs-spy", 346 | "https://github.com/cartant/rxjs-tslint-rules", 347 | ]); 348 | callback(); 349 | } 350 | ); 351 | notifier.next(); 352 | notifier.next(); 353 | }); 354 | }); 355 | 356 | describe("with an operator", () => { 357 | it("should traverse the pages", (callback: any) => { 358 | const urls = traverse({ 359 | factory: (marker?: string) => 360 | get(marker ?? "https://api.github.com/users/cartant/repos").pipe( 361 | map((response) => ({ 362 | markers: response.next ? [response.next] : [], 363 | values: response.content, 364 | })) 365 | ), 366 | operator: (repos) => repos.pipe(map((repo) => repo.html_url)), 367 | }); 368 | 369 | const received: string[] = []; 370 | urls.subscribe( 371 | (url) => received.push(url), 372 | callback, 373 | () => { 374 | expect(received).to.deep.equal([ 375 | "https://github.com/cartant/rxjs-etc", 376 | "https://github.com/cartant/rxjs-marbles", 377 | "https://github.com/cartant/rxjs-spy", 378 | "https://github.com/cartant/rxjs-tslint-rules", 379 | ]); 380 | callback(); 381 | } 382 | ); 383 | }); 384 | }); 385 | }); 386 | }); 387 | -------------------------------------------------------------------------------- /source/traverse/traverse.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-pluggables 4 | */ 5 | 6 | import { 7 | concat, 8 | from, 9 | identity, 10 | MonoTypeOperatorFunction, 11 | Observable, 12 | ObservableInput, 13 | of, 14 | OperatorFunction, 15 | Subject, 16 | } from "rxjs"; 17 | 18 | import { expand, ignoreElements, mergeMap, tap } from "rxjs/operators"; 19 | import { QueuedNotifications } from "./queued-notifications"; 20 | 21 | export type TraverseElement = { 22 | markers: ObservableInput; 23 | values: ObservableInput; 24 | }; 25 | export type TraverseFactory = ( 26 | marker: M | undefined, 27 | index: number 28 | ) => Observable>; 29 | 30 | export function traverse(options: { 31 | concurrency?: number; 32 | factory: TraverseFactory; 33 | notifier: Observable; 34 | }): Observable; 35 | 36 | export function traverse(options: { 37 | concurrency?: number; 38 | factory: TraverseFactory; 39 | operator: OperatorFunction; 40 | }): Observable; 41 | 42 | export function traverse(options: { 43 | concurrency?: number; 44 | factory: TraverseFactory; 45 | }): Observable; 46 | 47 | export function traverse({ 48 | concurrency = 1, 49 | factory, 50 | operator = identity, 51 | notifier, 52 | }: { 53 | concurrency?: number; 54 | factory: TraverseFactory; 55 | operator?: OperatorFunction; 56 | notifier?: Observable; 57 | }): Observable { 58 | return new Observable((observer) => { 59 | let notifications: QueuedNotifications; 60 | let queueOperator: MonoTypeOperatorFunction; 61 | 62 | if (notifier) { 63 | notifications = new QueuedNotifications(notifier); 64 | queueOperator = identity; 65 | } else { 66 | const subject = new Subject(); 67 | notifications = new QueuedNotifications(subject); 68 | queueOperator = (markers) => { 69 | subject.next(); 70 | return markers; 71 | }; 72 | } 73 | 74 | const destination = new Subject(); 75 | const subscription = destination.subscribe(observer); 76 | subscription.add(notifications.connect()); 77 | subscription.add( 78 | of(undefined) 79 | .pipe( 80 | expand( 81 | (marker: M | undefined) => 82 | notifications.queue.pipe( 83 | mergeMap((index) => 84 | factory(marker, index).pipe( 85 | mergeMap(({ markers, values }) => 86 | concat( 87 | from(values).pipe( 88 | operator, 89 | tap((value) => destination.next(value)), 90 | ignoreElements() 91 | ), 92 | from(markers) 93 | ) 94 | ) 95 | ) 96 | ), 97 | queueOperator 98 | ), 99 | concurrency 100 | ) 101 | ) 102 | .subscribe({ 103 | complete() { 104 | destination.complete(); 105 | }, 106 | error(error: unknown) { 107 | destination.error(error); 108 | }, 109 | }) 110 | ); 111 | return subscription; 112 | }); 113 | } 114 | -------------------------------------------------------------------------------- /tsconfig-dist-cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "outDir": "dist/cjs", 5 | "target": "esnext" 6 | }, 7 | "extends": "./tsconfig-dist.json" 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig-dist-esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "outDir": "dist/esm", 5 | "target": "esnext" 6 | }, 7 | "extends": "./tsconfig-dist.json" 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig-dist.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "sourceMap": true 5 | }, 6 | "exclude": [ 7 | "source/**/*-spec.ts" 8 | ], 9 | "extends": "./tsconfig.json" 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": false, 4 | "importHelpers": false, 5 | "lib": ["esnext"], 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "noEmitHelpers": false, 9 | "outDir": "build", 10 | "removeComments": true, 11 | "skipLibCheck": true, 12 | "sourceMap": false, 13 | "strict": true, 14 | "target": "esnext" 15 | }, 16 | "exclude": [], 17 | "include": [ 18 | "source/**/*.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | const webpack = require("webpack"); 5 | const webpackRxjsExternals = require("webpack-rxjs-externals"); 6 | 7 | module.exports = (env) => { 8 | let filename = "rxjs-pluggables.umd.js"; 9 | let mode = "development"; 10 | if (env && env.production) { 11 | filename = "rxjs-pluggables.min.umd.js"; 12 | mode = "production"; 13 | } 14 | return { 15 | context: path.join(__dirname, "./"), 16 | entry: { 17 | index: "./source/index.ts", 18 | }, 19 | externals: webpackRxjsExternals(), 20 | mode, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.ts$/, 25 | use: { 26 | loader: "ts-loader", 27 | options: { 28 | compilerOptions: { 29 | declaration: false, 30 | }, 31 | configFile: "tsconfig-dist-cjs.json", 32 | }, 33 | }, 34 | }, 35 | ], 36 | }, 37 | output: { 38 | filename, 39 | library: "rxjsPluggables", 40 | libraryTarget: "umd", 41 | path: path.resolve(__dirname, "./bundles"), 42 | }, 43 | resolve: { 44 | extensions: [".ts", ".js"], 45 | }, 46 | }; 47 | }; 48 | --------------------------------------------------------------------------------