├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── index.js ├── package-lock.json ├── package.json ├── readme.md ├── test.js └── types.d.ts /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [10.x, 12.x, 14.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm ci 28 | - run: npm run build --if-present 29 | - run: npm test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | function makeSubject() { 2 | let sinks = []; 3 | let done = false; 4 | return (type, data) => { 5 | if (done) return; 6 | if (type === 0) { 7 | const sink = data; 8 | sinks.push(sink); 9 | sink(0, t => { 10 | if (t === 2) { 11 | const i = sinks.indexOf(sink); 12 | if (i > -1) sinks.splice(i, 1); 13 | } 14 | }); 15 | } else { 16 | const zinkz = sinks.slice(0); 17 | for (let i = 0, n = zinkz.length, sink; i < n; i++) { 18 | sink = zinkz[i]; 19 | if (sinks.indexOf(sink) > -1) sink(type, data); 20 | } 21 | if (type === 2) { 22 | done = true; 23 | sinks.length = 0; 24 | } 25 | } 26 | } 27 | } 28 | 29 | module.exports = makeSubject; -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "callbag-subject", 3 | "version": "2.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "balanced-match": { 8 | "version": "1.0.0", 9 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 10 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 11 | "dev": true 12 | }, 13 | "brace-expansion": { 14 | "version": "1.1.8", 15 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", 16 | "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", 17 | "dev": true, 18 | "requires": { 19 | "balanced-match": "^1.0.0", 20 | "concat-map": "0.0.1" 21 | } 22 | }, 23 | "callbag": { 24 | "version": "1.3.0", 25 | "resolved": "https://registry.npmjs.org/callbag/-/callbag-1.3.0.tgz", 26 | "integrity": "sha512-iqNvQVzdgKqpNk3bx8r1MjNuCrTcyJ0zcaFFXJyfzfIMU+DnPy4uOZr2pCezEMSH9cpKqktUHD8DsVzWIBeH+g==" 27 | }, 28 | "concat-map": { 29 | "version": "0.0.1", 30 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 31 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 32 | "dev": true 33 | }, 34 | "deep-equal": { 35 | "version": "1.0.1", 36 | "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", 37 | "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", 38 | "dev": true 39 | }, 40 | "define-properties": { 41 | "version": "1.1.2", 42 | "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", 43 | "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", 44 | "dev": true, 45 | "requires": { 46 | "foreach": "^2.0.5", 47 | "object-keys": "^1.0.8" 48 | } 49 | }, 50 | "defined": { 51 | "version": "1.0.0", 52 | "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", 53 | "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", 54 | "dev": true 55 | }, 56 | "es-abstract": { 57 | "version": "1.10.0", 58 | "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.10.0.tgz", 59 | "integrity": "sha512-/uh/DhdqIOSkAWifU+8nG78vlQxdLckUdI/sPgy0VhuXi2qJ7T8czBmqIYtLQVpCIFYafChnsRsB5pyb1JdmCQ==", 60 | "dev": true, 61 | "requires": { 62 | "es-to-primitive": "^1.1.1", 63 | "function-bind": "^1.1.1", 64 | "has": "^1.0.1", 65 | "is-callable": "^1.1.3", 66 | "is-regex": "^1.0.4" 67 | } 68 | }, 69 | "es-to-primitive": { 70 | "version": "1.1.1", 71 | "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", 72 | "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", 73 | "dev": true, 74 | "requires": { 75 | "is-callable": "^1.1.1", 76 | "is-date-object": "^1.0.1", 77 | "is-symbol": "^1.0.1" 78 | } 79 | }, 80 | "for-each": { 81 | "version": "0.3.2", 82 | "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.2.tgz", 83 | "integrity": "sha1-LEBFC5NI6X8oEyJZO6lnBLmr1NQ=", 84 | "dev": true, 85 | "requires": { 86 | "is-function": "~1.0.0" 87 | } 88 | }, 89 | "foreach": { 90 | "version": "2.0.5", 91 | "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", 92 | "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", 93 | "dev": true 94 | }, 95 | "fs.realpath": { 96 | "version": "1.0.0", 97 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 98 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 99 | "dev": true 100 | }, 101 | "function-bind": { 102 | "version": "1.1.1", 103 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 104 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 105 | "dev": true 106 | }, 107 | "glob": { 108 | "version": "7.1.2", 109 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 110 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 111 | "dev": true, 112 | "requires": { 113 | "fs.realpath": "^1.0.0", 114 | "inflight": "^1.0.4", 115 | "inherits": "2", 116 | "minimatch": "^3.0.4", 117 | "once": "^1.3.0", 118 | "path-is-absolute": "^1.0.0" 119 | } 120 | }, 121 | "has": { 122 | "version": "1.0.1", 123 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", 124 | "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", 125 | "dev": true, 126 | "requires": { 127 | "function-bind": "^1.0.2" 128 | } 129 | }, 130 | "inflight": { 131 | "version": "1.0.6", 132 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 133 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 134 | "dev": true, 135 | "requires": { 136 | "once": "^1.3.0", 137 | "wrappy": "1" 138 | } 139 | }, 140 | "inherits": { 141 | "version": "2.0.3", 142 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 143 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 144 | "dev": true 145 | }, 146 | "is-callable": { 147 | "version": "1.1.3", 148 | "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", 149 | "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=", 150 | "dev": true 151 | }, 152 | "is-date-object": { 153 | "version": "1.0.1", 154 | "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", 155 | "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", 156 | "dev": true 157 | }, 158 | "is-function": { 159 | "version": "1.0.1", 160 | "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.1.tgz", 161 | "integrity": "sha1-Es+5i2W1fdPRk6MSH19uL0N2ArU=", 162 | "dev": true 163 | }, 164 | "is-regex": { 165 | "version": "1.0.4", 166 | "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", 167 | "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", 168 | "dev": true, 169 | "requires": { 170 | "has": "^1.0.1" 171 | } 172 | }, 173 | "is-symbol": { 174 | "version": "1.0.1", 175 | "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", 176 | "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", 177 | "dev": true 178 | }, 179 | "minimatch": { 180 | "version": "3.0.4", 181 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 182 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 183 | "dev": true, 184 | "requires": { 185 | "brace-expansion": "^1.1.7" 186 | } 187 | }, 188 | "minimist": { 189 | "version": "1.2.6", 190 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", 191 | "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", 192 | "dev": true 193 | }, 194 | "object-inspect": { 195 | "version": "1.3.0", 196 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.3.0.tgz", 197 | "integrity": "sha512-OHHnLgLNXpM++GnJRyyhbr2bwl3pPVm4YvaraHrRvDt/N3r+s/gDVHciA7EJBTkijKXj61ssgSAikq1fb0IBRg==", 198 | "dev": true 199 | }, 200 | "object-keys": { 201 | "version": "1.0.11", 202 | "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", 203 | "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=", 204 | "dev": true 205 | }, 206 | "once": { 207 | "version": "1.4.0", 208 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 209 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 210 | "dev": true, 211 | "requires": { 212 | "wrappy": "1" 213 | } 214 | }, 215 | "path-is-absolute": { 216 | "version": "1.0.1", 217 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 218 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 219 | "dev": true 220 | }, 221 | "path-parse": { 222 | "version": "1.0.5", 223 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", 224 | "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", 225 | "dev": true 226 | }, 227 | "resolve": { 228 | "version": "1.4.0", 229 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz", 230 | "integrity": "sha512-aW7sVKPufyHqOmyyLzg/J+8606v5nevBgaliIlV7nUpVMsDnoBGV/cbSLNjZAg9q0Cfd/+easKVKQ8vOu8fn1Q==", 231 | "dev": true, 232 | "requires": { 233 | "path-parse": "^1.0.5" 234 | } 235 | }, 236 | "resumer": { 237 | "version": "0.0.0", 238 | "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", 239 | "integrity": "sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=", 240 | "dev": true, 241 | "requires": { 242 | "through": "~2.3.4" 243 | } 244 | }, 245 | "string.prototype.trim": { 246 | "version": "1.1.2", 247 | "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz", 248 | "integrity": "sha1-0E3iyJ4Tf019IG8Ia17S+ua+jOo=", 249 | "dev": true, 250 | "requires": { 251 | "define-properties": "^1.1.2", 252 | "es-abstract": "^1.5.0", 253 | "function-bind": "^1.0.2" 254 | } 255 | }, 256 | "tape": { 257 | "version": "4.8.0", 258 | "resolved": "https://registry.npmjs.org/tape/-/tape-4.8.0.tgz", 259 | "integrity": "sha512-TWILfEnvO7I8mFe35d98F6T5fbLaEtbFTG/lxWvid8qDfFTxt19EBijWmB4j3+Hoh5TfHE2faWs73ua+EphuBA==", 260 | "dev": true, 261 | "requires": { 262 | "deep-equal": "~1.0.1", 263 | "defined": "~1.0.0", 264 | "for-each": "~0.3.2", 265 | "function-bind": "~1.1.0", 266 | "glob": "~7.1.2", 267 | "has": "~1.0.1", 268 | "inherits": "~2.0.3", 269 | "minimist": "~1.2.0", 270 | "object-inspect": "~1.3.0", 271 | "resolve": "~1.4.0", 272 | "resumer": "~0.0.0", 273 | "string.prototype.trim": "~1.1.2", 274 | "through": "~2.3.8" 275 | } 276 | }, 277 | "through": { 278 | "version": "2.3.8", 279 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 280 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", 281 | "dev": true 282 | }, 283 | "wrappy": { 284 | "version": "1.0.2", 285 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 286 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 287 | "dev": true 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "callbag-subject", 3 | "version": "2.1.0", 4 | "description": "A callbag listener sink which is also a listenable source", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/staltz/callbag-subject.git" 8 | }, 9 | "main": "index.js", 10 | "types": "types.d.ts", 11 | "scripts": { 12 | "test": "tape test.js" 13 | }, 14 | "author": "staltz.com", 15 | "license": "MIT", 16 | "keywords": [ 17 | "callbag" 18 | ], 19 | "devDependencies": { 20 | "tape": "^4.8.0" 21 | }, 22 | "dependencies": { 23 | "callbag": "^1.3.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # callbag-subject 2 | 3 | A callbag listener sink which is also a listenable source, and maintains an internal list of listeners. Use this like you would use RxJS Subject. 4 | 5 | `npm install callbag-subject` 6 | 7 | ## example 8 | 9 | First call `makeSubject` to create a `subject` which is then a normal callbag, so: 10 | 11 | - Call it with args `(1, data)` to send data into the subject 12 | - Call it with args `(2, err)` to send an error into the subject 13 | - Call it with args `(2)` to make the subject complete 14 | 15 | ```js 16 | const observe = require('callbag-observe'); 17 | const makeSubject = require('callbag-subject'); 18 | 19 | const subject = makeSubject(); 20 | 21 | setInterval(() => { subject(1, 'a'); }, 1000); 22 | 23 | // First observer is added immediately 24 | observe(x => console.log(x + 1))(subject); 25 | 26 | // First observer is added after 2.5 seconds 27 | setTimeout(() => { 28 | observe(x => console.log(x + 2))(subject); 29 | }, 2500); 30 | 31 | // a1 32 | // a1 33 | // a1 34 | // a2 35 | // a1 36 | // a2 37 | // ... 38 | ``` 39 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const test = require('tape'); 2 | const makeSubject = require('./index'); 3 | 4 | test('it shares an async finite listenable source', t => { 5 | t.plan(24); 6 | const upwardsExpected = [[0, 'function']]; 7 | 8 | const downwardsExpectedTypeA = [ 9 | [0, 'function'], 10 | [1, 'number'], 11 | [1, 'number'], 12 | [1, 'number'], 13 | [2, 'undefined'], 14 | ]; 15 | const downwardsExpectedA = [10, 20, 30]; 16 | 17 | const downwardsExpectedTypeB = [ 18 | [0, 'function'], 19 | [1, 'number'], 20 | [1, 'number'], 21 | [2, 'undefined'], 22 | ]; 23 | const downwardsExpectedB = [20, 30]; 24 | 25 | function makeSource() { 26 | let sent = 0; 27 | const source = (type, data) => { 28 | const e = upwardsExpected.shift(); 29 | t.equals(type, e[0], 'upwards type is expected: ' + e[0]); 30 | t.equals(typeof data, e[1], 'upwards data is expected: ' + e[1]); 31 | if (type === 0) { 32 | const sink = data; 33 | const id = setInterval(() => { 34 | if (sent === 0) { 35 | sent++; 36 | sink(1, 10); 37 | return; 38 | } 39 | if (sent === 1) { 40 | sent++; 41 | sink(1, 20); 42 | return; 43 | } 44 | if (sent === 2) { 45 | sent++; 46 | sink(1, 30); 47 | return; 48 | } 49 | if (sent === 3) { 50 | sink(2); 51 | clearInterval(id); 52 | return; 53 | } 54 | }, 100); 55 | sink(0, source); 56 | } 57 | }; 58 | return source; 59 | } 60 | 61 | function sinkA(type, data) { 62 | const et = downwardsExpectedTypeA.shift(); 63 | t.equals(type, et[0], 'downwards A type is expected: ' + et[0]); 64 | t.equals(typeof data, et[1], 'downwards A data type is expected: ' + et[1]); 65 | if (type === 1) { 66 | const e = downwardsExpectedA.shift(); 67 | t.equals(data, e, 'downwards A data is expected: ' + e); 68 | } 69 | } 70 | 71 | function sinkB(type, data) { 72 | const et = downwardsExpectedTypeB.shift(); 73 | t.equals(type, et[0], 'downwards B type is expected: ' + et[0]); 74 | t.equals(typeof data, et[1], 'downwards B data type is expected: ' + et[1]); 75 | if (type === 1) { 76 | const e = downwardsExpectedB.shift(); 77 | t.equals(data, e, 'downwards B data is expected: ' + e); 78 | } 79 | } 80 | 81 | const subject = makeSubject(); 82 | subject(0, sinkA); 83 | setTimeout(() => { 84 | subject(0, sinkB); 85 | }, 150); 86 | 87 | let sent = 0; 88 | const id = setInterval(() => { 89 | if (sent === 0) { 90 | sent++; 91 | subject(1, 10); 92 | return; 93 | } 94 | if (sent === 1) { 95 | sent++; 96 | subject(1, 20); 97 | return; 98 | } 99 | if (sent === 2) { 100 | sent++; 101 | subject(1, 30); 102 | return; 103 | } 104 | if (sent === 3) { 105 | subject(2); 106 | clearInterval(id); 107 | return; 108 | } 109 | }, 100); 110 | 111 | setTimeout(() => { 112 | t.pass('nothing else happens'); 113 | t.end(); 114 | }, 700); 115 | }); 116 | 117 | test('it does not emit data after it is completed and does not accept further sinks.', t => { 118 | t.plan(5); 119 | 120 | const expectedSink1 = [42, 'Hellow world']; 121 | const expectedSink2 = ['Hellow world']; 122 | 123 | const subject = makeSubject(); 124 | const r = []; 125 | 126 | subject(0, (t, d) => { 127 | if (t === 1) 128 | r.push(d); 129 | }); 130 | 131 | subject(1, 42); 132 | 133 | const r2 = []; 134 | subject(0, (t, d) => { 135 | if (t === 1) r2.push(d); 136 | }); 137 | 138 | subject(1, 'Hellow world'); 139 | t.deepEqual(r, expectedSink1, 'expected value for what sink 1 recorded before completion: ' + expectedSink1); 140 | t.deepEqual(r2, expectedSink2, 'expected value for what sink 2 recorded before completion: ' + expectedSink2); 141 | subject(2); 142 | 143 | let respondedToSink3 = false; 144 | subject(0, () => { 145 | respondedToSink3 = true; 146 | }); 147 | 148 | subject(1, 'Well ...'); 149 | t.deepEqual(r, expectedSink1, 'expected value for what sink 1 recorded after completion: ' + expectedSink1); 150 | t.deepEqual(r2, expectedSink2, 'expected value for what sink 2 recorded after completion: ' + expectedSink2); 151 | t.equal(respondedToSink3, false, 'expected to not have responded to sink 3 at all.'); 152 | }); 153 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | import { Callbag } from 'callbag'; 2 | 3 | export default function makeSubject(): Callbag; 4 | --------------------------------------------------------------------------------