├── .editorconfig ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── LICENSE ├── README.md ├── angular.json ├── package-lock.json ├── package.json ├── projects ├── svelte-store-rxjs-showcase │ ├── browserslist │ ├── e2e │ │ ├── protractor.conf.js │ │ ├── src │ │ │ ├── app.e2e-spec.ts │ │ │ └── app.po.ts │ │ └── tsconfig.json │ ├── karma.conf.js │ ├── src │ │ ├── app │ │ │ ├── app.component.html │ │ │ ├── app.component.scss │ │ │ ├── app.component.ts │ │ │ └── app.module.ts │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── main.ts │ │ ├── polyfills.ts │ │ ├── styles.scss │ │ └── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ └── tslint.json └── svelte-store-rxjs │ ├── README.md │ ├── karma.conf.js │ ├── ng-package.json │ ├── package.json │ ├── src │ ├── lib │ │ ├── store.spec.ts │ │ └── store.ts │ ├── public-api.ts │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ └── tslint.json ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.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: Tests 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] 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 test:svelte-store-rxjs:github 29 | env: 30 | CI: true 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Florian Spier 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 | [![NPM](https://img.shields.io/npm/v/svelte-store-rxjs)](https://www.npmjs.com/package/svelte-store-rxjs) 2 | [![Tests](https://github.com/spierala/svelte-store-rxjs/actions/workflows/nodejs.yml/badge.svg)](https://github.com/spierala/svelte-store-rxjs/actions/workflows/nodejs.yml) 3 | 4 | # Svelte Store RxJS 5 | 6 | **Svelte Store RxJS** provides [Svelte Stores](https://svelte.dev/docs#svelte_store) (**readable**, **writable**, **derived**) as [RxJS](https://github.com/ReactiveX/rxjs) Observables. 7 | 8 | | WARNING: This project is still experimental! Do not use in production! | 9 | | --- | 10 | 11 | Example: 12 | 13 | ```ts 14 | import { derived, readable, Writable, writable } from 'svelte-store-rxjs'; 15 | import { filter } from 'rxjs/operators'; 16 | import { Observable } from 'rxjs'; 17 | 18 | // Writable 19 | const a: Writable = writable(1); 20 | a.subscribe(a => console.log('writable', a)); 21 | a.set(2); 22 | a.update(state => state + 1); 23 | 24 | // Readable 25 | const b: Observable = readable(12); 26 | b.subscribe(b => console.log('readable', b)); 27 | 28 | // Derived 29 | const abSummed: Observable = derived([a, b], ([a, b]) => { 30 | return a + b; 31 | }); 32 | abSummed.subscribe(abSummed => console.log('summed', abSummed)); 33 | 34 | // All stores are true RxJS Observables, we can have RxJS fun with more than 100 RxJS operators :) 35 | abSummed.pipe( 36 | // Filter out non-even numbers 37 | filter((num: number) => num % 2 === 0) 38 | ).subscribe(sum => console.log('RxJS pipe result', sum)) 39 | ``` 40 | ## RxJS 41 | Readable, writable, derived are true RxJS Observables. 42 | - `writable()` returns a `Writable` instance. `Writable` extends RxJS BehaviorSubject. 43 | - `readable()` and `derived()` return a RxJS Observable 44 | 45 | ## References 46 | - [Svelte store github](https://github.com/sveltejs/svelte/blob/master/src/runtime/store/index.ts) 47 | - [Svelte store API docs](https://svelte.dev/docs#svelte_store) 48 | - [RxJS Github issue about "SvelteSubject"](https://github.com/ReactiveX/rxjs/issues/4740#issuecomment-490601347) 49 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "svelte-store-rxjs": { 7 | "projectType": "library", 8 | "root": "projects/svelte-store-rxjs", 9 | "sourceRoot": "projects/svelte-store-rxjs/src", 10 | "prefix": "lib", 11 | "architect": { 12 | "build": { 13 | "builder": "@angular-devkit/build-ng-packagr:build", 14 | "options": { 15 | "tsConfig": "projects/svelte-store-rxjs/tsconfig.lib.json", 16 | "project": "projects/svelte-store-rxjs/ng-package.json" 17 | }, 18 | "configurations": { 19 | "production": { 20 | "tsConfig": "projects/svelte-store-rxjs/tsconfig.lib.prod.json" 21 | } 22 | } 23 | }, 24 | "test": { 25 | "builder": "@angular-devkit/build-angular:karma", 26 | "options": { 27 | "main": "projects/svelte-store-rxjs/src/test.ts", 28 | "tsConfig": "projects/svelte-store-rxjs/tsconfig.spec.json", 29 | "karmaConfig": "projects/svelte-store-rxjs/karma.conf.js" 30 | } 31 | }, 32 | "lint": { 33 | "builder": "@angular-devkit/build-angular:tslint", 34 | "options": { 35 | "tsConfig": [ 36 | "projects/svelte-store-rxjs/tsconfig.lib.json", 37 | "projects/svelte-store-rxjs/tsconfig.spec.json" 38 | ], 39 | "exclude": [ 40 | "**/node_modules/**" 41 | ] 42 | } 43 | } 44 | } 45 | }, 46 | "svelte-store-rxjs-showcase": { 47 | "projectType": "application", 48 | "schematics": { 49 | "@schematics/angular:component": { 50 | "style": "scss" 51 | } 52 | }, 53 | "root": "projects/svelte-store-rxjs-showcase", 54 | "sourceRoot": "projects/svelte-store-rxjs-showcase/src", 55 | "prefix": "app", 56 | "architect": { 57 | "build": { 58 | "builder": "@angular-devkit/build-angular:browser", 59 | "options": { 60 | "outputPath": "dist/svelte-store-rxjs-showcase", 61 | "index": "projects/svelte-store-rxjs-showcase/src/index.html", 62 | "main": "projects/svelte-store-rxjs-showcase/src/main.ts", 63 | "polyfills": "projects/svelte-store-rxjs-showcase/src/polyfills.ts", 64 | "tsConfig": "projects/svelte-store-rxjs-showcase/tsconfig.app.json", 65 | "aot": true, 66 | "assets": [ 67 | "projects/svelte-store-rxjs-showcase/src/favicon.ico", 68 | "projects/svelte-store-rxjs-showcase/src/assets" 69 | ], 70 | "styles": [ 71 | "projects/svelte-store-rxjs-showcase/src/styles.scss" 72 | ], 73 | "scripts": [] 74 | }, 75 | "configurations": { 76 | "production": { 77 | "fileReplacements": [ 78 | { 79 | "replace": "projects/svelte-store-rxjs-showcase/src/environments/environment.ts", 80 | "with": "projects/svelte-store-rxjs-showcase/src/environments/environment.prod.ts" 81 | } 82 | ], 83 | "optimization": true, 84 | "outputHashing": "all", 85 | "sourceMap": false, 86 | "extractCss": true, 87 | "namedChunks": false, 88 | "extractLicenses": true, 89 | "vendorChunk": false, 90 | "buildOptimizer": true, 91 | "budgets": [ 92 | { 93 | "type": "initial", 94 | "maximumWarning": "2mb", 95 | "maximumError": "5mb" 96 | }, 97 | { 98 | "type": "anyComponentStyle", 99 | "maximumWarning": "6kb", 100 | "maximumError": "10kb" 101 | } 102 | ] 103 | } 104 | } 105 | }, 106 | "serve": { 107 | "builder": "@angular-devkit/build-angular:dev-server", 108 | "options": { 109 | "browserTarget": "svelte-store-rxjs-showcase:build" 110 | }, 111 | "configurations": { 112 | "production": { 113 | "browserTarget": "svelte-store-rxjs-showcase:build:production" 114 | } 115 | } 116 | }, 117 | "extract-i18n": { 118 | "builder": "@angular-devkit/build-angular:extract-i18n", 119 | "options": { 120 | "browserTarget": "svelte-store-rxjs-showcase:build" 121 | } 122 | }, 123 | "test": { 124 | "builder": "@angular-devkit/build-angular:karma", 125 | "options": { 126 | "main": "projects/svelte-store-rxjs-showcase/src/test.ts", 127 | "polyfills": "projects/svelte-store-rxjs-showcase/src/polyfills.ts", 128 | "tsConfig": "projects/svelte-store-rxjs-showcase/tsconfig.spec.json", 129 | "karmaConfig": "projects/svelte-store-rxjs-showcase/karma.conf.js", 130 | "assets": [ 131 | "projects/svelte-store-rxjs-showcase/src/favicon.ico", 132 | "projects/svelte-store-rxjs-showcase/src/assets" 133 | ], 134 | "styles": [ 135 | "projects/svelte-store-rxjs-showcase/src/styles.scss" 136 | ], 137 | "scripts": [] 138 | } 139 | }, 140 | "lint": { 141 | "builder": "@angular-devkit/build-angular:tslint", 142 | "options": { 143 | "tsConfig": [ 144 | "projects/svelte-store-rxjs-showcase/tsconfig.app.json", 145 | "projects/svelte-store-rxjs-showcase/tsconfig.spec.json", 146 | "projects/svelte-store-rxjs-showcase/e2e/tsconfig.json" 147 | ], 148 | "exclude": [ 149 | "**/node_modules/**" 150 | ] 151 | } 152 | }, 153 | "e2e": { 154 | "builder": "@angular-devkit/build-angular:protractor", 155 | "options": { 156 | "protractorConfig": "projects/svelte-store-rxjs-showcase/e2e/protractor.conf.js", 157 | "devServerTarget": "svelte-store-rxjs-showcase:serve" 158 | }, 159 | "configurations": { 160 | "production": { 161 | "devServerTarget": "svelte-store-rxjs-showcase:serve:production" 162 | } 163 | } 164 | } 165 | } 166 | }}, 167 | "defaultProject": "svelte-store-rxjs" 168 | } 169 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-store-rxjs", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e", 11 | "build:lib": "ng build svelte-store-rxjs --prod && copy LICENSE dist/svelte-store-rxjs && copy README.md dist/svelte-store-rxjs", 12 | "test:svelte-store-rxjs": "ng test svelte-store-rxjs", 13 | "test:svelte-store-rxjs:github": "ng test svelte-store-rxjs --watch=false --browsers=ChromeHeadless" 14 | }, 15 | "private": true, 16 | "dependencies": { 17 | "@angular/animations": "~9.1.13", 18 | "@angular/common": "~9.1.13", 19 | "@angular/compiler": "~9.1.13", 20 | "@angular/core": "~9.1.13", 21 | "@angular/forms": "~9.1.13", 22 | "@angular/platform-browser": "~9.1.13", 23 | "@angular/platform-browser-dynamic": "~9.1.13", 24 | "@angular/router": "~9.1.13", 25 | "rxjs": "~6.5.4", 26 | "tslib": "^1.10.0", 27 | "zone.js": "~0.10.2" 28 | }, 29 | "devDependencies": { 30 | "@angular-devkit/build-angular": "~0.901.15", 31 | "@angular-devkit/build-ng-packagr": "~0.901.15", 32 | "@angular/cli": "~9.1.15", 33 | "@angular/compiler-cli": "~9.1.13", 34 | "@types/node": "^12.11.1", 35 | "@types/jasmine": "~3.5.0", 36 | "@types/jasminewd2": "~2.0.3", 37 | "codelyzer": "^5.1.2", 38 | "copy": "^0.3.2", 39 | "jasmine-core": "~3.5.0", 40 | "jasmine-spec-reporter": "~4.2.1", 41 | "karma": "~5.0.0", 42 | "karma-chrome-launcher": "~3.1.0", 43 | "karma-coverage-istanbul-reporter": "~2.1.0", 44 | "karma-jasmine": "~3.0.1", 45 | "karma-jasmine-html-reporter": "^1.4.2", 46 | "mocha": "^7.0.0", 47 | "ng-packagr": "^9.0.0", 48 | "protractor": "~7.0.0", 49 | "ts-node": "~8.3.0", 50 | "tslint": "~6.1.0", 51 | "typescript": "~3.8.3" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs-showcase/browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /projects/svelte-store-rxjs-showcase/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /projects/svelte-store-rxjs-showcase/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('svelte-store-rxjs-showcase app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs-showcase/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo(): Promise { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs-showcase/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs-showcase/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage/svelte-store-rxjs-showcase'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs-showcase/src/app/app.component.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spierala/svelte-store-rxjs/2f7f943fb45b9738ebf8e206c09c34a1bf016639/projects/svelte-store-rxjs-showcase/src/app/app.component.html -------------------------------------------------------------------------------- /projects/svelte-store-rxjs-showcase/src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spierala/svelte-store-rxjs/2f7f943fb45b9738ebf8e206c09c34a1bf016639/projects/svelte-store-rxjs-showcase/src/app/app.component.scss -------------------------------------------------------------------------------- /projects/svelte-store-rxjs-showcase/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { derived, readable, Writable, writable } from 'svelte-store-rxjs'; 3 | import { filter } from 'rxjs/operators'; 4 | import { Observable } from 'rxjs'; 5 | 6 | // Writable 7 | const a: Writable = writable(1); 8 | a.subscribe(a => console.log('writable', a)); 9 | a.set(2); 10 | a.update(state => state + 1); 11 | 12 | // Readable 13 | const b: Observable = readable(12); 14 | b.subscribe(b => console.log('readable', b)); 15 | 16 | // Derived 17 | const abSummed: Observable = derived([a, b], ([a, b]) => { 18 | return a + b; 19 | }); 20 | abSummed.subscribe(abSummed => console.log('summed', abSummed)); 21 | 22 | // All stores are true RxJS Observables, we can have RxJS fun with more than 100 RxJS operators :) 23 | abSummed.pipe( 24 | // Filter out non-even numbers 25 | filter((num: number) => num % 2 === 0) 26 | ).subscribe(sum => console.log('RxJS pipe result', sum)) 27 | 28 | a.set(4); 29 | 30 | const multiplied = derived(a, a => a * 2); 31 | multiplied.subscribe(multiplied => console.log('multiplied', multiplied)) 32 | 33 | @Component({ 34 | selector: 'app-root', 35 | templateUrl: './app.component.html', 36 | styleUrls: ['./app.component.scss'] 37 | }) 38 | export class AppComponent { 39 | 40 | } 41 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs-showcase/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { AppComponent } from './app.component'; 5 | 6 | @NgModule({ 7 | declarations: [ 8 | AppComponent 9 | ], 10 | imports: [ 11 | BrowserModule 12 | ], 13 | providers: [], 14 | bootstrap: [AppComponent] 15 | }) 16 | export class AppModule { } 17 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs-showcase/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spierala/svelte-store-rxjs/2f7f943fb45b9738ebf8e206c09c34a1bf016639/projects/svelte-store-rxjs-showcase/src/assets/.gitkeep -------------------------------------------------------------------------------- /projects/svelte-store-rxjs-showcase/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs-showcase/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs-showcase/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spierala/svelte-store-rxjs/2f7f943fb45b9738ebf8e206c09c34a1bf016639/projects/svelte-store-rxjs-showcase/src/favicon.ico -------------------------------------------------------------------------------- /projects/svelte-store-rxjs-showcase/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SvelteStoreRxjsShowcase 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs-showcase/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs-showcase/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs-showcase/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs-showcase/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs-showcase/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs-showcase/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs-showcase/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs/README.md: -------------------------------------------------------------------------------- 1 | # Svelte Store RxJS 2 | 3 | **Svelte Store RxJS** provides [Svelte Stores](https://svelte.dev/docs#svelte_store) (**readable**, **writable**, **derived**) as [RxJS](https://github.com/ReactiveX/rxjs) Observables. 4 | 5 | Example: 6 | 7 | ```ts 8 | import { derived, readable, Writable, writable } from 'svelte-store-rxjs'; 9 | import { filter } from 'rxjs/operators'; 10 | import { Observable } from 'rxjs'; 11 | 12 | // Writable 13 | const a: Writable = writable(1); 14 | a.subscribe(a => console.log('writable', a)); 15 | a.set(2); 16 | a.update(state => state + 1); 17 | 18 | // Readable 19 | const b: Observable = readable(12); 20 | b.subscribe(b => console.log('readable', b)); 21 | 22 | // Derived 23 | const abSummed: Observable = derived([a, b], ([a, b]) => { 24 | return a + b; 25 | }); 26 | abSummed.subscribe(abSummed => console.log('summed', abSummed)); 27 | 28 | // All stores are true RxJS Observables, we can have RxJS fun with more than 100 RxJS operators :) 29 | abSummed.pipe( 30 | // Filter out non-even numbers 31 | filter((num: number) => num % 2 === 0) 32 | ).subscribe(sum => console.log('RxJS pipe result', sum)) 33 | ``` 34 | Readable, writable, derived are true RxJS Observables. 35 | 36 | ## RxJS 37 | - `writable()` returns a `Writable` instance. `Writable` extends RxJS BehaviorSubject. 38 | - `readable()` and `derived()` return a RxJS Observable 39 | 40 | ## References 41 | - [Svelte store github](https://github.com/sveltejs/svelte/blob/master/src/runtime/store/index.ts) 42 | - [Svelte store API docs](https://svelte.dev/docs#svelte_store) 43 | - [RxJS Github issue about "SvelteSubject"](https://github.com/ReactiveX/rxjs/issues/4740#issuecomment-490601347) 44 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage/svelte-store-rxjs'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/svelte-store-rxjs", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /projects/svelte-store-rxjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-store-rxjs", 3 | "version": "0.0.2", 4 | "author": "Florian Spier (https://florian-spier.be)", 5 | "peerDependencies": { 6 | "rxjs": ">= 6.4.0 < 7" 7 | }, 8 | "license": "MIT", 9 | "homepage": "https://github.com/spierala/svelte-store-rxjs", 10 | "readme": "https://github.com/spierala/svelte-store-rxjs/blob/master/README.md", 11 | "description": "Svelte Store RxJS: Svelte Stores (readable, writable, derived) as RxJS Observables", 12 | "keywords": [ 13 | "RxJS", 14 | "Svelte", 15 | "State Management", 16 | "Observable", 17 | "Store" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/spierala/svelte-store-rxjs.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/spierala/svelte-store-rxjs/issues" 25 | }, 26 | "dependencies": { 27 | "tslib": "^1.10.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs/src/lib/store.spec.ts: -------------------------------------------------------------------------------- 1 | // Tests originally copied from here: https://github.com/sveltejs/svelte/blob/master/test/store/index.js 2 | 3 | import { derived, readable, writable } from 'svelte-store-rxjs'; 4 | import * as assert from 'assert'; 5 | 6 | describe('store', () => { 7 | describe('writable', () => { 8 | it('creates a writable store', () => { 9 | const count = writable(0); 10 | const values = []; 11 | 12 | const sub = count.subscribe(value => { 13 | values.push(value); 14 | }); 15 | 16 | count.set(1); 17 | count.update(n => n + 1); 18 | 19 | sub.unsubscribe(); 20 | 21 | count.set(3); 22 | count.update(n => n + 1); 23 | 24 | assert.deepEqual(values, [0, 1, 2]); 25 | }); 26 | 27 | // it('calls provided subscribe handler', () => { 28 | // let called = 0; 29 | // 30 | // const store = writable(0, () => { 31 | // called += 1; 32 | // return () => called -= 1; 33 | // }); 34 | // 35 | // const unsubscribe1 = store.subscribe(() => { }); 36 | // assert.equal(called, 1); 37 | // 38 | // const unsubscribe2 = store.subscribe(() => { }); 39 | // assert.equal(called, 1); 40 | // 41 | // unsubscribe1(); 42 | // assert.equal(called, 1); 43 | // 44 | // unsubscribe2(); 45 | // assert.equal(called, 0); 46 | // }); 47 | 48 | it('does not assume immutable data', () => { 49 | const obj = {}; 50 | let called = 0; 51 | 52 | const store = writable(obj); 53 | 54 | store.subscribe(() => { 55 | called += 1; 56 | }); 57 | 58 | store.set(obj); 59 | assert.equal(called, 2); 60 | 61 | store.update(obj => obj); 62 | assert.equal(called, 3); 63 | }); 64 | 65 | // it('only calls subscriber once initially, including on resubscriptions', () => { 66 | // let num = 0; 67 | // const store = writable(num, set => set(num += 1)); 68 | // 69 | // let count1 = 0; 70 | // let count2 = 0; 71 | // 72 | // store.subscribe(() => count1 += 1)(); 73 | // assert.equal(count1, 1); 74 | // 75 | // const unsubscribe = store.subscribe(() => count2 += 1); 76 | // assert.equal(count2, 1); 77 | // 78 | // unsubscribe(); 79 | // }); 80 | }); 81 | 82 | describe('readable', () => { 83 | // it('creates a readable store', () => { 84 | // let running; 85 | // let tick; 86 | // 87 | // const store = readable(undefined, set => { 88 | // tick = set; 89 | // running = true; 90 | // 91 | // set(0); 92 | // 93 | // return () => { 94 | // tick = () => { }; 95 | // running = false; 96 | // }; 97 | // }); 98 | // 99 | // assert.ok(!running); 100 | // 101 | // const values = []; 102 | // 103 | // const unsubscribe = store.subscribe(value => { 104 | // values.push(value); 105 | // }); 106 | // 107 | // assert.ok(running); 108 | // tick(1); 109 | // tick(2); 110 | // 111 | // unsubscribe(); 112 | // 113 | // assert.ok(!running); 114 | // tick(3); 115 | // tick(4); 116 | // 117 | // assert.deepEqual(values, [0, 1, 2]); 118 | // }); 119 | }); 120 | 121 | // tslint:disable-next-line:variable-name 122 | const fake_observable = { 123 | subscribe(fn) { 124 | fn(42); 125 | return { 126 | unsubscribe: () => {} 127 | }; 128 | } 129 | }; 130 | 131 | describe('derived', () => { 132 | it('maps a single store', () => { 133 | const a = writable(1); 134 | const b = derived(a, n => n * 2); 135 | 136 | const values = []; 137 | 138 | const sub = b.subscribe(value => { 139 | values.push(value); 140 | }); 141 | 142 | a.set(2); 143 | assert.deepEqual(values, [2, 4]); 144 | 145 | sub.unsubscribe(); 146 | 147 | a.set(3); 148 | assert.deepEqual(values, [2, 4]); 149 | }); 150 | 151 | it('maps multiple stores', () => { 152 | const a = writable(2); 153 | const b = writable(3); 154 | const c = derived(([a, b]), ([a, b]) => a * b); 155 | 156 | const values = []; 157 | 158 | const sub = c.subscribe(value => { 159 | values.push(value); 160 | }); 161 | 162 | a.set(4); 163 | b.set(5); 164 | assert.deepEqual(values, [6, 12, 20]); 165 | 166 | sub.unsubscribe(); 167 | 168 | a.set(6); 169 | assert.deepEqual(values, [6, 12, 20]); 170 | }); 171 | 172 | it('passes optional set function', () => { 173 | const aNumber = writable(1); 174 | const evens = derived(aNumber, (n, set) => { 175 | if (n % 2 === 0) set(n); 176 | }, 0); 177 | 178 | const values = []; 179 | 180 | const sub = evens.subscribe(value => { 181 | values.push(value); 182 | }); 183 | 184 | aNumber.set(2); 185 | aNumber.set(3); 186 | aNumber.set(4); 187 | aNumber.set(5); 188 | assert.deepEqual(values, [0, 2, 4]); 189 | 190 | sub.unsubscribe(); 191 | 192 | aNumber.set(6); 193 | aNumber.set(7); 194 | aNumber.set(8); 195 | assert.deepEqual(values, [0, 2, 4]); 196 | }); 197 | 198 | it('prevents glitches', () => { 199 | const lastname = writable('Jekyll'); 200 | const firstname = derived(lastname, n => n === 'Jekyll' ? 'Henry' : 'Edward'); 201 | 202 | const fullname = derived([firstname, lastname], names => names.join(' ')); 203 | 204 | const values = []; 205 | 206 | const sub = fullname.subscribe(value => { 207 | values.push(value); 208 | }); 209 | 210 | lastname.set('Hyde'); 211 | 212 | assert.deepEqual(values, [ 213 | 'Henry Jekyll', 214 | 'Edward Hyde' 215 | ]); 216 | 217 | sub.unsubscribe(); 218 | }); 219 | 220 | it('prevents diamond dependency problem', () => { 221 | const count = writable(0); 222 | const values = []; 223 | 224 | const a = derived(count, $count => { 225 | return 'a' + $count; 226 | }); 227 | 228 | const b = derived(count, $count => { 229 | return 'b' + $count; 230 | }); 231 | 232 | const combined = derived([a, b], ([a, b]) => { 233 | return a + b; 234 | }); 235 | 236 | const sub = combined.subscribe(v => { 237 | values.push(v); 238 | }); 239 | 240 | assert.deepEqual(values, ['a0b0']); 241 | 242 | count.set(1); 243 | assert.deepEqual(values, ['a0b0', 'a1b1']); 244 | 245 | sub.unsubscribe(); 246 | }); 247 | 248 | it('derived dependency does not update and shared ancestor updates', () => { 249 | const root = writable({ a: 0, b:0 }); 250 | const values = []; 251 | 252 | const a = derived(root, $root => { 253 | return 'a' + $root.a; 254 | }); 255 | 256 | const b = derived([a, root], ([$a, $root]) => { 257 | return 'b' + $root.b + $a; 258 | }); 259 | 260 | const sub = b.subscribe(v => { 261 | values.push(v); 262 | }); 263 | 264 | assert.deepEqual(values, ['b0a0']); 265 | 266 | root.set({ a: 0, b: 1 }); 267 | assert.deepEqual(values, ['b0a0', 'b1a0']); 268 | 269 | sub.unsubscribe(); 270 | }); 271 | 272 | it('is updated with safe_not_equal logic', () => { 273 | const arr = [0]; 274 | 275 | const aNumber = writable(1); 276 | const numbers = derived(aNumber, $number => { 277 | arr[0] = $number; 278 | return arr; 279 | }); 280 | 281 | const concatenated = []; 282 | 283 | const sub = numbers.subscribe(value => { 284 | concatenated.push(value); 285 | }); 286 | 287 | aNumber.set(2); 288 | aNumber.set(3); 289 | 290 | assert.deepEqual(concatenated, [1, 2, 3]); 291 | 292 | sub.unsubscribe(); 293 | }); 294 | 295 | it('calls a cleanup function', () => { 296 | const num = writable(1); 297 | 298 | const values = []; 299 | // tslint:disable-next-line:variable-name 300 | const cleaned_up = []; 301 | 302 | const d = derived(num, ($num, set) => { 303 | set($num * 2); 304 | 305 | return function cleanup() { 306 | cleaned_up.push($num); 307 | }; 308 | }); 309 | 310 | num.set(2); 311 | 312 | const sub = d.subscribe(value => { 313 | values.push(value); 314 | }); 315 | 316 | num.set(3); 317 | num.set(4); 318 | 319 | assert.deepEqual(values, [4, 6, 8]); 320 | assert.deepEqual(cleaned_up, [2, 3]); 321 | 322 | sub.unsubscribe(); 323 | 324 | assert.deepEqual(cleaned_up, [2, 3, 4]); 325 | }); 326 | 327 | it('discards non-function return values', () => { 328 | const num = writable(1); 329 | 330 | const values = []; 331 | 332 | const d = derived(num, ($num, set) => { 333 | set($num * 2); 334 | return {}; 335 | }); 336 | 337 | num.set(2); 338 | 339 | const sub = d.subscribe(value => { 340 | values.push(value); 341 | }); 342 | 343 | num.set(3); 344 | num.set(4); 345 | 346 | assert.deepEqual(values, [4, 6, 8]); 347 | 348 | sub.unsubscribe(); 349 | }); 350 | 351 | // it('allows derived with different types', () => { 352 | // const a = writable('one'); 353 | // const b = writable(1); 354 | // const c = derived([a, b], ([a, b]) => `${a} ${b}`); 355 | // 356 | // assert.deepEqual(get(c), 'one 1'); 357 | // 358 | // a.set('two'); 359 | // b.set(2); 360 | // assert.deepEqual(get(c), 'two 2'); 361 | // }); 362 | // 363 | // it('works with RxJS-style observables', () => { 364 | // const d = derived(fake_observable, _ => _); 365 | // assert.equal(get(d), 42); 366 | // }); 367 | }); 368 | 369 | // describe('get', () => { 370 | // it('gets the current value of a store', () => { 371 | // const store = readable(42, () => { }); 372 | // assert.equal(get(store), 42); 373 | // }); 374 | // 375 | // it('works with RxJS-style observables', () => { 376 | // assert.equal(get(fake_observable), 42); 377 | // }); 378 | // }); 379 | }); 380 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs/src/lib/store.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject, combineLatest, EMPTY, Observable } from "rxjs"; 2 | import { distinctUntilChanged, map } from "rxjs/operators"; 3 | 4 | // Svelte store github: https://github.com/sveltejs/svelte/blob/master/src/runtime/store/index.ts 5 | // Svelte store API docs: https://svelte.dev/docs#svelte_store 6 | 7 | function safe_not_equal(a, b) { 8 | // tslint:disable-next-line:triple-equals 9 | return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function'); 10 | } 11 | 12 | type Updater = (value: T) => T; 13 | 14 | export interface IWritable extends Observable { 15 | // Extend Observable instead of Readable! 16 | set(value: T): void; 17 | update(updater: Updater): void; 18 | } 19 | 20 | // Based on "SvelteSubject" https://github.com/ReactiveX/rxjs/issues/4740#issuecomment-490601347 21 | export class Writable extends BehaviorSubject implements IWritable { 22 | constructor(initialValue: T) { 23 | super(initialValue); 24 | } 25 | 26 | set(value: T): void { 27 | if (safe_not_equal(value, this.getValue())) { 28 | super.next(value); 29 | } 30 | } 31 | 32 | update(fn: Updater): void { 33 | this.set(fn(this.getValue())); 34 | } 35 | 36 | lift(operator): Writable { 37 | const result = new Writable(operator); 38 | result.operator = operator; 39 | result.source = this; 40 | return result; 41 | } 42 | } 43 | 44 | export function readable(initialState: T): Observable { 45 | // Observable is our Readable! 46 | const stateSource: Writable = new Writable(initialState); 47 | return stateSource.asObservable(); 48 | } 49 | 50 | export function writable(initialState: T): Writable { 51 | const stateSource: Writable = new Writable(initialState); 52 | return stateSource; 53 | } 54 | 55 | type Stores = Observable | [Observable, ...Array>]; 56 | 57 | export function derived( 58 | stores: Stores, 59 | fn: Function, 60 | initial_value?: T // TODO 61 | ): Observable { 62 | const single = !Array.isArray(stores); 63 | const storesArray: Array> = single 64 | ? [stores as Observable] 65 | : (stores as Array>); 66 | 67 | return combineLatest(storesArray).pipe( 68 | distinctUntilChanged((prev, curr) => 69 | prev.every((prevItem, i) => prevItem === curr[i]) 70 | ), 71 | map(values => fn(single ? values[0] : values)) 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of svelte-store-rxjs 3 | */ 4 | 5 | export * from './lib/store'; 6 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone'; 4 | import 'zone.js/dist/zone-testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | declare const require: { 12 | context(path: string, deep?: boolean, filter?: RegExp): { 13 | keys(): string[]; 14 | (id: string): T; 15 | }; 16 | }; 17 | 18 | // First, initialize the Angular testing environment. 19 | getTestBed().initTestEnvironment( 20 | BrowserDynamicTestingModule, 21 | platformBrowserDynamicTesting() 22 | ); 23 | // Then we find all the tests. 24 | const context = require.context('./', true, /\.spec\.ts$/); 25 | // And load the modules. 26 | context.keys().map(context); 27 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "target": "es2015", 6 | "declaration": true, 7 | "inlineSources": true, 8 | "types": [], 9 | "lib": [ 10 | "dom", 11 | "es2018" 12 | ] 13 | }, 14 | "angularCompilerOptions": { 15 | "skipTemplateCodegen": true, 16 | "strictMetadataEmit": true, 17 | "enableResourceInlining": true 18 | }, 19 | "exclude": [ 20 | "src/test.ts", 21 | "**/*.spec.ts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "angularCompilerOptions": { 4 | "enableIvy": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /projects/svelte-store-rxjs/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "lib", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "lib", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "lib": [ 15 | "es2018", 16 | "dom" 17 | ], 18 | "paths": { 19 | "svelte-store-rxjs": [ 20 | "projects/svelte-store-rxjs/src/public-api" 21 | ] 22 | } 23 | }, 24 | "angularCompilerOptions": { 25 | "fullTemplateTypeCheck": true, 26 | "strictInjectionParameters": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "align": { 8 | "options": [ 9 | "parameters", 10 | "statements" 11 | ] 12 | }, 13 | "array-type": false, 14 | "arrow-return-shorthand": true, 15 | "curly": true, 16 | "deprecation": { 17 | "severity": "warning" 18 | }, 19 | "eofline": true, 20 | "import-blacklist": [ 21 | true, 22 | "rxjs/Rx" 23 | ], 24 | "import-spacing": true, 25 | "indent": { 26 | "options": [ 27 | "spaces" 28 | ] 29 | }, 30 | "max-classes-per-file": false, 31 | "max-line-length": [ 32 | true, 33 | 140 34 | ], 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-console": [ 47 | true, 48 | "debug", 49 | "info", 50 | "time", 51 | "timeEnd", 52 | "trace" 53 | ], 54 | "no-empty": false, 55 | "no-inferrable-types": [ 56 | true, 57 | "ignore-params" 58 | ], 59 | "no-non-null-assertion": true, 60 | "no-redundant-jsdoc": true, 61 | "no-switch-case-fall-through": true, 62 | "no-var-requires": false, 63 | "object-literal-key-quotes": [ 64 | true, 65 | "as-needed" 66 | ], 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "semicolon": { 72 | "options": [ 73 | "always" 74 | ] 75 | }, 76 | "space-before-function-paren": { 77 | "options": { 78 | "anonymous": "never", 79 | "asyncArrow": "always", 80 | "constructor": "never", 81 | "method": "never", 82 | "named": "never" 83 | } 84 | }, 85 | "typedef-whitespace": { 86 | "options": [ 87 | { 88 | "call-signature": "nospace", 89 | "index-signature": "nospace", 90 | "parameter": "nospace", 91 | "property-declaration": "nospace", 92 | "variable-declaration": "nospace" 93 | }, 94 | { 95 | "call-signature": "onespace", 96 | "index-signature": "onespace", 97 | "parameter": "onespace", 98 | "property-declaration": "onespace", 99 | "variable-declaration": "onespace" 100 | } 101 | ] 102 | }, 103 | "variable-name": { 104 | "options": [ 105 | "ban-keywords", 106 | "check-format", 107 | "allow-pascal-case" 108 | ] 109 | }, 110 | "whitespace": { 111 | "options": [ 112 | "check-branch", 113 | "check-decl", 114 | "check-operator", 115 | "check-separator", 116 | "check-type", 117 | "check-typecast" 118 | ] 119 | }, 120 | "component-class-suffix": true, 121 | "contextual-lifecycle": true, 122 | "directive-class-suffix": true, 123 | "no-conflicting-lifecycle": true, 124 | "no-host-metadata-property": true, 125 | "no-input-rename": true, 126 | "no-inputs-metadata-property": true, 127 | "no-output-native": true, 128 | "no-output-on-prefix": true, 129 | "no-output-rename": true, 130 | "no-outputs-metadata-property": true, 131 | "template-banana-in-box": true, 132 | "template-no-negated-async": true, 133 | "use-lifecycle-interface": true, 134 | "use-pipe-transform-interface": true 135 | } 136 | } 137 | --------------------------------------------------------------------------------