diff --git a/.gitignore b/.gitignore index 06671edc2bf1..7aa75b29f44c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ node_modules /compiler.js /index.js /internal.* -/store.js +/store.* /easing.js /motion.* /transition.js diff --git a/package.json b/package.json index 437eef8d0e27..bbe8f5389879 100644 --- a/package.json +++ b/package.json @@ -27,11 +27,12 @@ "precodecov": "npm run coverage", "lint": "eslint src test/*.js", "build": "rollup -c", - "prepare": "npm run build", + "prepare": "npm run build && npm run tsd", "dev": "rollup -cw", "pretest": "npm run build", "posttest": "agadoo src/internal/index.js", - "prepublishOnly": "export PUBLISH=true && npm run lint && npm test" + "prepublishOnly": "export PUBLISH=true && npm run lint && npm test", + "tsd": "tsc -d src/store.ts --outDir ." }, "repository": { "type": "git", diff --git a/rollup.config.js b/rollup.config.js index f7b2d07d4bed..0d19e59d4ade 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -87,8 +87,37 @@ export default [ external: id => id.startsWith('svelte/') }, + /* store.mjs */ + { + input: `src/store.ts`, + output: [ + { + file: `store.mjs`, + format: 'esm', + paths: id => id.startsWith('svelte/') && id.replace('svelte', '.') + }, + { + file: `store.js`, + format: 'cjs', + paths: id => id.startsWith('svelte/') && id.replace('svelte', '.') + } + ], + plugins: [ + is_publish + ? typescript({ + include: 'src/**', + exclude: 'src/internal/**', + typescript: require('typescript') + }) + : sucrase({ + transforms: ['typescript'] + }) + ], + external: id => id.startsWith('svelte/') + }, + // everything else - ...['index', 'store', 'easing', 'transition', 'animate'].map(name => ({ + ...['index', 'easing', 'transition', 'animate'].map(name => ({ input: `${name}.mjs`, output: { file: `${name}.js`, diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 000000000000..462f155c2ada --- /dev/null +++ b/src/store.ts @@ -0,0 +1,136 @@ +import { run_all, noop, safe_not_equal } from './internal/utils'; + +type Subscriber = (value: T) => void; + +type Unsubscriber = () => void; + +type Updater = (value: T) => T; + +type Invalidater = (value?: T) => void; + +type StartStopNotifier = (set: Subscriber) => Unsubscriber | void; + +export interface Readable { + subscribe(run: Subscriber, invalidate?: Invalidater): Unsubscriber; +} + +export interface Writable extends Readable { + set(value: T): void; + update(updater: Updater): void; +} + +type SubscribeInvalidateTuple = [Subscriber, Invalidater]; + +export function readable(value: T, start: StartStopNotifier): Readable { + return { + subscribe: writable(value, start).subscribe, + }; +} + +export function writable(value: T, start: StartStopNotifier = noop): Writable { + let stop: Unsubscriber; + const subscribers: Array> = []; + + function set(new_value: T): void { + if (safe_not_equal(value, new_value)) { + value = new_value; + if (!stop) { + return; // not ready + } + subscribers.forEach((s) => s[1]()); + subscribers.forEach((s) => s[0](value)); + } + } + + function update(fn: Updater): void { + set(fn(value)); + } + + function subscribe(run: Subscriber, invalidate: Invalidater = noop): Unsubscriber { + const subscriber: SubscribeInvalidateTuple = [run, invalidate]; + subscribers.push(subscriber); + if (subscribers.length === 1) { + stop = start(set) || noop; + } + run(value); + + return () => { + const index = subscribers.indexOf(subscriber); + if (index !== -1) { + subscribers.splice(index, 1); + } + if (subscribers.length === 0) { + stop(); + } + }; + } + + return { set, update, subscribe }; +} + +type Stores = Readable | [Readable, ...Array>]; + +type StoresValues = T extends Readable ? U : + { [K in keyof T]: T[K] extends Readable ? U : never }; + +export function derived( + stores: S, + fn: (values: StoresValues, set?: Subscriber) => T | Unsubscriber | void, + initial_value?: T, +): Readable { + + const single = !Array.isArray(stores); + const stores_array: Array> = single + ? [stores as Readable] + : stores as Array>; + + const auto = fn.length < 2; + + return readable(initial_value, (set) => { + let inited = false; + const values: StoresValues = [] as StoresValues; + + let pending = 0; + let cleanup = noop; + + const sync = () => { + if (pending) { + return; + } + cleanup(); + const result = fn(single ? values[0] : values, set); + if (auto) { + set(result as T); + } else { + cleanup = result as Unsubscriber || noop; + } + }; + + const unsubscribers = stores_array.map((store, i) => store.subscribe( + (value) => { + values[i] = value; + pending &= ~(1 << i); + if (inited) { + sync(); + } + }, + () => { + pending |= (1 << i); + }), + ); + + inited = true; + sync(); + + return function stop() { + run_all(unsubscribers); + cleanup(); + }; + }); +} + +export function get(store: Readable): T { + let value: T | undefined; + store.subscribe((_: T) => value = _)(); + return value as T; +} diff --git a/store.mjs b/store.mjs deleted file mode 100644 index 624ede3dde64..000000000000 --- a/store.mjs +++ /dev/null @@ -1,85 +0,0 @@ -import { run_all, noop, get_store_value, safe_not_equal } from './internal'; - -export function readable(value, start) { - return { - subscribe: writable(value, start).subscribe - }; -} - -export function writable(value, start = noop) { - let stop; - const subscribers = []; - - function set(new_value) { - if (safe_not_equal(value, new_value)) { - value = new_value; - if (!stop) return; // not ready - subscribers.forEach(s => s[1]()); - subscribers.forEach(s => s[0](value)); - } - } - - function update(fn) { - set(fn(value)); - } - - function subscribe(run, invalidate = noop) { - const subscriber = [run, invalidate]; - subscribers.push(subscriber); - if (subscribers.length === 1) stop = start(set) || noop; - run(value); - - return () => { - const index = subscribers.indexOf(subscriber); - if (index !== -1) subscribers.splice(index, 1); - if (subscribers.length === 0) stop(); - }; - } - - return { set, update, subscribe }; -} - -export function derived(stores, fn, initial_value) { - const single = !Array.isArray(stores); - if (single) stores = [stores]; - - const auto = fn.length < 2; - let value = {}; - - return readable(initial_value, set => { - let inited = false; - const values = []; - - let pending = 0; - let cleanup = noop; - - const sync = () => { - if (pending) return; - cleanup(); - const result = fn(single ? values[0] : values, set); - if (auto) set(result); - else cleanup = result || noop; - }; - - const unsubscribers = stores.map((store, i) => store.subscribe( - value => { - values[i] = value; - pending &= ~(1 << i); - if (inited) sync(); - }, - () => { - pending |= (1 << i); - }) - ); - - inited = true; - sync(); - - return function stop() { - run_all(unsubscribers); - cleanup(); - }; - }); -} - -export { get_store_value as get }; diff --git a/test/store/index.js b/test/store.ts similarity index 89% rename from test/store/index.js rename to test/store.ts index 5f9176cffb29..f931a841d741 100644 --- a/test/store/index.js +++ b/test/store.ts @@ -1,5 +1,5 @@ import * as assert from 'assert'; -import { readable, writable, derived, get } from '../../store.js'; +import { readable, writable, derived, get } from '../store'; describe('store', () => { describe('writable', () => { @@ -30,10 +30,10 @@ describe('store', () => { return () => called -= 1; }); - const unsubscribe1 = store.subscribe(() => {}); + const unsubscribe1 = store.subscribe(() => { }); assert.equal(called, 1); - const unsubscribe2 = store.subscribe(() => {}); + const unsubscribe2 = store.subscribe(() => { }); assert.equal(called, 1); unsubscribe1(); @@ -73,7 +73,7 @@ describe('store', () => { set(0); return () => { - tick = () => {}; + tick = () => { }; running = false; }; }); @@ -242,11 +242,23 @@ describe('store', () => { assert.deepEqual(cleaned_up, [2, 3, 4]); }); + + it('allows derived with different types', () => { + const a = writable('one'); + const b = writable(1); + const c = derived([a, b], ([a, b]) => `${a} ${b}`); + + assert.deepEqual(get(c), 'one 1'); + + a.set('two'); + b.set(2); + assert.deepEqual(get(c), 'two 2'); + }); }); describe('get', () => { it('gets the current value of a store', () => { - const store = readable(42, () => {}); + const store = readable(42, () => { }); assert.equal(get(store), 42); }); });