├── .gitignore ├── .editorconfig ├── src ├── index.d.ts └── index.js ├── CHANGELOG.md ├── rollup.config.js ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | node_modules 4 | *.log 5 | .idea/ 6 | .sessions/ 7 | .vscode/ 8 | dist -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,css,html,svelte}] 8 | charset = utf-8 9 | indent_style = tab 10 | indent_size = 4 11 | trim_trailing_whitespace = true 12 | max_line_length = 100 -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { Readable, Unsubscriber } from 'svelte/store'; 2 | 3 | export type Format = 'json' | 'base64' | 'urlencoded' | 'raw'; 4 | 5 | export interface Config { 6 | url: string; 7 | event?: string; 8 | format?: Format; 9 | withCredentials?: boolean; 10 | } 11 | 12 | export declare function streamable( 13 | config: Config, 14 | callback?: ( 15 | data: T | void, 16 | set: (value: U | T) => void 17 | ) => Unsubscriber | U | T | void, 18 | defaultValue?: T 19 | ): Readable>; 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # svelte-streamable changelog 2 | 3 | ## 2.3.0 4 | * Opitimize number of SSE connections. 5 | * Improve docs. 6 | 7 | ## 2.2.0 8 | * Add an argument to `cleanup` function to detect `lastSubscriber` case. 9 | 10 | ## 2.1.2 11 | * Improved types. 12 | 13 | ## 2.1.1 14 | * Improved types. 15 | 16 | ## 2.1.0 17 | * Support `base64` and `urlencoded` formats of payload. 18 | * Improved types. 19 | 20 | ## 2.0.0 21 | 22 | * (breaking change): now `streamable` store is always contain `Promise` to control different async statuses. 23 | * (breaking change): `streamable` store initially have `pending` status if `defaultValue` is not provided. 24 | 25 | ## 1.1.0 26 | 27 | * Call callback with `undefined` data on int. 28 | * Fallback to default value every time when result data is `undefined`. 29 | * Remove superfluous logging and event listeners. 30 | * Few improvements in README. 31 | * Improved types. 32 | 33 | ## 1.0.0 34 | 35 | * First release 36 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { readable } from 'svelte/store'; 2 | 3 | const esx = {}; 4 | 5 | export function streamable( 6 | { url, event = 'message', format = 'json', ...options }, 7 | callback, 8 | defaultValue 9 | ) { 10 | const auto = !callback || callback.length < 2; 11 | const initial = defaultValue ? Promise.resolve(defaultValue) : new Promise(noop); 12 | return readable(initial, (set) => { 13 | let cleanup = noop; 14 | 15 | function resolve(value) { 16 | set(typeof value !== 'undefined' ? Promise.resolve(value) : initial); 17 | } 18 | 19 | function update(e) { 20 | cleanup(false); 21 | 22 | let data; 23 | 24 | if (e && e.data) { 25 | if (format === 'json') { 26 | data = JSON.parse(e.data); 27 | } else if (format === 'base64') { 28 | data = atob(e.data); 29 | } else if (format === 'urlencoded') { 30 | data = Object.fromEntries(new URLSearchParams(e.data)); 31 | } else { 32 | data = e.data; 33 | } 34 | } 35 | 36 | const result = callback ? callback(data, resolve) : data; 37 | 38 | if (auto) { 39 | resolve(result); 40 | } else { 41 | cleanup = typeof result === 'function' ? result : noop; 42 | } 43 | } 44 | 45 | function error(e) { 46 | set(Promise.reject(e)); 47 | } 48 | 49 | const keypath = Object.entries(options) 50 | .sort() 51 | .reduce((k, [, v]) => `${k}/${v}`, url); 52 | 53 | let es = esx[keypath]; 54 | 55 | if (!es) { 56 | es = new EventSource(url, options); 57 | es.subscribers = 0; 58 | esx[keypath] = es; 59 | } 60 | 61 | es.addEventListener('error', error); 62 | es.addEventListener(event, update); 63 | 64 | callback && setTimeout(update); 65 | 66 | es.subscribers++; 67 | 68 | return () => { 69 | es.removeEventListener('error', error); 70 | es.removeEventListener(event, update); 71 | 72 | if (!--es.subscribers) { 73 | es.close(); 74 | delete esx[keypath]; 75 | } 76 | 77 | cleanup(true); 78 | }; 79 | }); 80 | } 81 | 82 | function noop() {} 83 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import babel from '@rollup/plugin-babel'; 4 | import { terser } from 'rollup-plugin-terser'; 5 | import sourceMaps from "rollup-plugin-sourcemaps"; 6 | 7 | import pkg from './package.json'; 8 | 9 | const extensions = ['.js', '.mjs',]; 10 | 11 | const pkgname = pkg.name.replace(/^(@\S+\/)?(svelte-)?(\S+)/, '$3'); 12 | const clsname = pkgname.replace(/-\w/g, m => m[1].toUpperCase()); 13 | 14 | const external = [ 15 | ...Object.keys(pkg.dependencies || {}), 16 | ...Object.keys(pkg.peerDependencies || {}), 17 | ]; 18 | 19 | const appendMin = file => file.replace(/(\.[\w\d_-]+)$/i, '.min$1'); 20 | const replaceFilename = (file, name) => file.replace(/(.*)\/.*(\.[\w\d_-]+)/i, `$1/${name}$2`); 21 | 22 | const pkgMain = replaceFilename(pkg.main, pkgname); 23 | const pkgModule = replaceFilename(pkg.module, pkgname); 24 | 25 | const input = 'src/index.js'; 26 | 27 | const files = { 28 | npm: { 29 | esm: { file: pkg.module, format: 'esm', }, 30 | umd: { file: pkg.main, format: 'umd', name: clsname, }, 31 | esmMin: { 32 | file: appendMin(pkg.module), 33 | format: 'esm', 34 | sourcemap: true, 35 | }, 36 | umdMin: { 37 | file: appendMin(pkg.main), 38 | format: 'umd', 39 | sourcemap: true, 40 | name: clsname, 41 | }, 42 | }, 43 | cdn: { 44 | esm: { file: pkgModule, format: 'esm', }, 45 | iife: { file: pkgMain, format: 'iife', name: clsname, }, 46 | esmMin: { 47 | file: appendMin(pkgModule), 48 | format: 'esm', 49 | sourcemap: true, 50 | }, 51 | iifeMin: { 52 | file: appendMin(pkgMain), 53 | format: 'iife', 54 | sourcemap: true, 55 | name: clsname, 56 | }, 57 | } 58 | }; 59 | 60 | const terserConfig = { 61 | format: { 62 | comments: false, 63 | }, 64 | }; 65 | 66 | const cdnPlugins = [ 67 | resolve({ browser: true, extensions, }), 68 | commonjs({ include: 'node_modules/**', extensions, }), 69 | ]; 70 | 71 | export default [{ // esm && umd bundles for npm/yarn 72 | input, 73 | output: [ 74 | files.npm.esm, 75 | files.npm.umd, 76 | ], 77 | external, 78 | }, { // esm && umd bundles for npm/yarn (min) 79 | input, 80 | output: [ 81 | files.npm.esmMin, 82 | files.npm.umdMin, 83 | ], 84 | external, 85 | plugins: [ 86 | sourceMaps(), 87 | terser(terserConfig), 88 | ], 89 | }, { // esm bundle for cdn/unpkg 90 | input, 91 | output: files.cdn.esm, 92 | plugins: cdnPlugins, 93 | }, { // iife bundle for cdn/unpkg 94 | input, 95 | output: files.cdn.iife, 96 | plugins: [ 97 | ...cdnPlugins, 98 | babel({ babelHelpers: 'bundled' }), 99 | ], 100 | }, { // esm bundle from cdn/unpkg (min) 101 | input, 102 | output: files.cdn.esmMin, 103 | plugins: [ 104 | ...cdnPlugins, 105 | sourceMaps(), 106 | terser(terserConfig), 107 | ], 108 | }, { // iife bundle for cdn/unpkg (min) 109 | input, 110 | output: files.cdn.iifeMin, 111 | plugins: [ 112 | ...cdnPlugins, 113 | sourceMaps(), 114 | babel({ babelHelpers: 'bundled' }), 115 | terser(terserConfig), 116 | ], 117 | },]; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Super tiny, simple to use SvelteJS store for real-time updates from server via SSE. 2 | 3 | [![NPM version](https://img.shields.io/npm/v/svelte-streamable.svg?style=flat)](https://www.npmjs.com/package/svelte-streamable) [![NPM downloads](https://img.shields.io/npm/dm/svelte-streamable.svg?style=flat)](https://www.npmjs.com/package/svelte-streamable) 4 | 5 | 6 | ## Install 7 | 8 | ```bash 9 | npm i svelte-streamable --save-dev 10 | ``` 11 | 12 | ```bash 13 | yarn add svelte-streamable 14 | ``` 15 | 16 | CDN: [UNPKG](https://unpkg.com/svelte-streamable/) | [jsDelivr](https://cdn.jsdelivr.net/npm/svelte-streamable/) (available as `window.Streamable`) 17 | 18 | If you are **not** using ES6, instead of importing add 19 | 20 | ```html 21 | 22 | ``` 23 | 24 | just before closing body tag. 25 | 26 | ## Usage 27 | 28 | ### Store for any server updates in JSON format 29 | 30 | Just provide Server-sent event endpoint as `url` property in config object. 31 | 32 | ```javascript 33 | import { streamable } from 'svelte-streamable'; 34 | 35 | const updatesAsync = streamable({ 36 | url: 'http://xxx.xxx.xxx:xxx/updates' 37 | }); 38 | ``` 39 | 40 | ### Store for specific server event and allow credentials if needed: 41 | 42 | Just provide event name as `event` and `withCredentials` properties in config object. 43 | 44 | ```javascript 45 | import { streamable } from 'svelte-streamable'; 46 | 47 | const postsAsync = streamable({ 48 | url: 'http://xxx.xxx.xxx:xxx/updates', 49 | event: 'posts', 50 | withCredentials: true, 51 | }); 52 | ``` 53 | 54 | ### Pre-process recieved data with custom logic: 55 | 56 | Just provide callback handler as second argument of `streamable` constructor and return the value: 57 | 58 | ```javascript 59 | import { streamable } from 'svelte-streamable'; 60 | 61 | const postsAsync = streamable({ 62 | url: 'http://xxx.xxx.xxx:xxx/updates', 63 | event: 'posts' 64 | }, ($posts) => { 65 | return $posts.reduce(...); 66 | }); 67 | ``` 68 | 69 | ### Asynchronous callback with cleanup logic: 70 | 71 | This sematic formly looks like Svelte's [derived](https://svelte.dev/docs#derived) store: 72 | 73 | ```javascript 74 | import { streamable } from 'svelte-streamable'; 75 | 76 | const postsAsync = streamable({ 77 | url: 'http://xxx.xxx.xxx:xxx/updates', 78 | event: 'posts' 79 | }, ($posts, set) => { 80 | 81 | // some async logic 82 | 83 | setTimeout(() => { 84 | set($posts); 85 | }, 1000); 86 | 87 | return (lastSubscriber) => { 88 | // cleanup logic 89 | console.log(lastSubscriber ? 'no more subscribers' : 'new update cleanup'); 90 | }; 91 | }); 92 | ``` 93 | 94 | ### Supporting several formats of the data: 95 | 96 | Use `format` option with one of the folowing value: `json` (default), `base64`, `urlencoded` or `raw`. 97 | 98 | ```javascript 99 | import { streamable } from 'svelte-streamable'; 100 | 101 | const csvAsync = streamable({ 102 | url: 'http://xxx.xxx.xxx:xxx/updates', 103 | format: 'urlencoded', 104 | }); 105 | ``` 106 | 107 | ### Get value in `raw` format instead of `json` (default) with custom preprocessing: 108 | 109 | ```javascript 110 | import { streamable } from 'svelte-streamable'; 111 | 112 | const csvAsync = streamable({ 113 | url: 'http://xxx.xxx.xxx:xxx/updates', 114 | event: 'csv', 115 | format: 'raw', 116 | }, ($csv) => { 117 | return CSVToArray($csv); 118 | }); 119 | ``` 120 | 121 | ### Using with `svelte-asyncable` 122 | 123 | Streamable store contains a Promise to control async statuses (pending, fullfilled, rejected). To use the data in synchronous-way, you can use `syncable` store from [svelte-asyncable](https://www.npmjs.com/package/svelte-asyncable) package: 124 | 125 | 126 | ```javascript 127 | import { streamable } from 'svelte-streamable'; 128 | import { syncable } from 'svelte-asyncable'; 129 | 130 | const postsAsync = streamable(...); // contains Promise 131 | const posts = syncable(postsAsync, []); // contains value 132 | ``` 133 | 134 | ## License 135 | 136 | MIT © [PaulMaly](https://github.com/PaulMaly) 137 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-streamable", 3 | "version": "2.3.0", 4 | "description": "Super tiny, simple to use SvelteJS store for real-time updates from server via SSE.", 5 | "main": "dist/index.js", 6 | "module": "dist/index.mjs", 7 | "cdn": "dist/streamable.min.js", 8 | "unpkg": "dist/streamable.min.js", 9 | "svelte": "src/index.js", 10 | "types": "src/index.d.ts", 11 | "engines": { 12 | "node": ">=10.0.0" 13 | }, 14 | "scripts": { 15 | "build": "npm run format && npm run lint && npm run clean && rollup -c", 16 | "prepublishOnly": "npm run build && npm run size", 17 | "format": "prettier --write src", 18 | "lint": "eslint src", 19 | "lint:fix": "eslint src --fix", 20 | "test": "jest src", 21 | "size": "size-limit", 22 | "clean": "rm -rf ./dist" 23 | }, 24 | "babel": { 25 | "presets": [ 26 | [ 27 | "@babel/preset-env" 28 | ] 29 | ] 30 | }, 31 | "browserslist": [ 32 | "extends browserslist-config-google" 33 | ], 34 | "size-limit": [ 35 | { 36 | "name": "UMD output", 37 | "limit": "1 KB", 38 | "path": "./dist/index.js" 39 | }, 40 | { 41 | "name": "ESM output", 42 | "limit": "900 B", 43 | "path": "./dist/index.mjs" 44 | }, 45 | { 46 | "name": "UMD output (minified)", 47 | "limit": "800 B", 48 | "path": "./dist/index.min.js" 49 | }, 50 | { 51 | "name": "ESM output (minified)", 52 | "limit": "700 B", 53 | "path": "./dist/index.min.mjs" 54 | }, 55 | { 56 | "name": "IIFE bundle", 57 | "limit": "3 KB", 58 | "path": "./dist/streamable.js" 59 | }, 60 | { 61 | "name": "ESM bundle", 62 | "limit": "1.5 KB", 63 | "path": "./dist/streamable.mjs" 64 | }, 65 | { 66 | "name": "IIFE bundle (minified)", 67 | "limit": "1.5 KB", 68 | "path": "./dist/streamable.min.js" 69 | }, 70 | { 71 | "name": "ESM bundle (minified)", 72 | "limit": "1 KB", 73 | "path": "./dist/streamable.min.mjs" 74 | } 75 | ], 76 | "prettier": { 77 | "tabWidth": 4, 78 | "semi": true, 79 | "singleQuote": true 80 | }, 81 | "eslintConfig": { 82 | "extends": [ 83 | "eslint:recommended", 84 | "prettier" 85 | ], 86 | "parserOptions": { 87 | "ecmaVersion": 2019, 88 | "sourceType": "module" 89 | }, 90 | "env": { 91 | "es6": true, 92 | "browser": true 93 | } 94 | }, 95 | "files": [ 96 | "src", 97 | "dist" 98 | ], 99 | "repository": { 100 | "type": "git", 101 | "url": "git+https://github.com/sveltetools/svelte-streamable.git" 102 | }, 103 | "keywords": [ 104 | "svelte", 105 | "svelte store" 106 | ], 107 | "author": "PaulMaly", 108 | "license": "MIT", 109 | "bugs": { 110 | "url": "https://github.com/sveltetools/svelte-streamable/issues" 111 | }, 112 | "homepage": "https://github.com/sveltetools/svelte-streamable#readme", 113 | "devDependencies": { 114 | "@babel/core": "^7.13.8", 115 | "@babel/preset-env": "^7.13.8", 116 | "@rollup/plugin-babel": "^5.3.0", 117 | "@rollup/plugin-commonjs": "^17.1.0", 118 | "@rollup/plugin-node-resolve": "^11.2.0", 119 | "@size-limit/preset-app": "^4.9.2", 120 | "browserslist-config-google": "^2.0.0", 121 | "core-js": "^3.9.0", 122 | "eslint": "^7.20.0", 123 | "eslint-config-prettier": "^8.1.0", 124 | "jest": "^26.6.3", 125 | "prettier": "^2.2.1", 126 | "rollup": "^2.40.0", 127 | "rollup-plugin-sourcemaps": "^0.6.3", 128 | "rollup-plugin-terser": "^7.0.2", 129 | "size-limit": "^4.9.2", 130 | "svelte": "^3.0.0" 131 | }, 132 | "peerDependencies": { 133 | "svelte": "3.x" 134 | } 135 | } 136 | --------------------------------------------------------------------------------