Skip to content

Commit 40b47cf

Browse files
committed
Add, document, and test new store implementation
1 parent ce7c472 commit 40b47cf

File tree

10 files changed

+913
-298
lines changed

10 files changed

+913
-298
lines changed

documentation/docs/03-runtime/02-svelte-store.md

Lines changed: 120 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ title: 'svelte/store'
44

55
The `svelte/store` module exports functions for creating [readable](/docs/svelte-store#readable), [writable](/docs/svelte-store#writable) and [derived](/docs/svelte-store#derived) stores.
66

7-
Keep in mind that you don't _have_ to use these functions to enjoy the [reactive `$store` syntax](/docs/svelte-components#script-4-prefix-stores-with-$-to-access-their-values) in your components. Any object that correctly implements `.subscribe`, unsubscribe, and (optionally) `.set` is a valid store, and will work both with the special syntax, and with Svelte's built-in [`derived` stores](/docs/svelte-store#derived).
7+
Keep in mind that you don't _have_ to use these functions to enjoy the [reactive `$store` syntax](/docs/svelte-components#script-4-prefix-stores-with-$-to-access-their-values) in your components. Any object that correctly implements the `subscribe` (including returning unsubscribe functions) and (optionally) `set` methods is a valid store, and will work both with the special syntax and with Svelte's built-in [derived stores](/docs/svelte-store#derived).
88

99
This makes it possible to wrap almost any other reactive state handling library for use in Svelte. Read more about the [store contract](/docs/svelte-components#script-4-prefix-stores-with-$-to-access-their-values) to see what a correct implementation looks like.
1010

@@ -18,8 +18,7 @@ Function that creates a store which has values that can be set from 'outside' co
1818

1919
`update` is a method that takes one argument which is a callback. The callback takes the existing store value as its argument and returns the new value to be set to the store.
2020

21-
```js
22-
/// file: store.js
21+
```ts
2322
import { writable } from 'svelte/store';
2423

2524
const count = writable(0);
@@ -35,8 +34,7 @@ count.update((n) => n + 1); // logs '2'
3534

3635
If a function is passed as the second argument, it will be called when the number of subscribers goes from zero to one (but not from one to two, etc). That function will be passed a `set` function which changes the value of the store, and an `update` function which works like the `update` method on the store, taking a callback to calculate the store's new value from its old value. It must return a `stop` function that is called when the subscriber count goes from one to zero.
3736

38-
```js
39-
/// file: store.js
37+
```ts
4038
import { writable } from 'svelte/store';
4139

4240
const count = writable(0, () => {
@@ -59,27 +57,61 @@ Note that the value of a `writable` is lost when it is destroyed, for example wh
5957

6058
> EXPORT_SNIPPET: svelte/store#readable
6159
62-
Creates a store whose value cannot be set from 'outside', the first argument is the store's initial value, and the second argument to `readable` is the same as the second argument to `writable`.
60+
Creates a store which can be subscribed to, but does not have the `set` and `update` methods that a writable store has. The value of a readable store is instead set by the `initial_value` argument at creation and then updated internally by an `on_start` function. This function is called when the store receives its first subscriber.
61+
62+
The `on_start` function allows the creation of stores whose value changes automatically based on application-specific logic. It's passed `set` and `update` functions that behave like the methods available on writable stores.
63+
64+
The `on_start` function can optionally return an `on_stop` function which will be called when the store loses its last subscriber. This allows stores to go dormant when not being used by any other code.
6365

6466
```ts
6567
import { readable } from 'svelte/store';
6668

67-
const time = readable(new Date(), (set) => {
68-
set(new Date());
69-
70-
const interval = setInterval(() => {
71-
set(new Date());
72-
}, 1000);
69+
const lastPressedSimpleKey = readable('', (set) => {
70+
const handleEvent = (event: KeyboardEvent) => {
71+
if (event.key.length === 1) {
72+
set(event.key);
73+
}
74+
};
75+
76+
window.addEventListener('keypress', handleEvent, { passive: true });
77+
78+
return function onStop() {
79+
window.removeEventListener('keypress', handleEvent);
80+
};
81+
});
7382

74-
return () => clearInterval(interval);
83+
lastPressedSimpleKey.subscribe((value) => {
84+
console.log(`Most recently pressed simple key is "${value}".`);
7585
});
86+
```
87+
88+
`on_start` could also set up a timer to poll an API for data which changes frequently, or even establish a WebSocket connection. The following example creates a store which polls [Open Notify's public ISS position API](http://open-notify.org/Open-Notify-API/ISS-Location-Now/) every 3 seconds.
7689

77-
const ticktock = readable('tick', (set, update) => {
78-
const interval = setInterval(() => {
79-
update((sound) => (sound === 'tick' ? 'tock' : 'tick'));
80-
}, 1000);
90+
> If `set` or `update` are called after the store has lost its last subscriber, they will have no effect. You should still take care to clean up any asynchronous callbacks registered in `on_start` by providing a suitable `on_stop` function, but a few accidental late calls will not negatively affect the store.
91+
>
92+
> For instance, in the example below `set` may be called late if the `issPosition` store loses its last subscriber after a `fetch` call is made but before the corresponding HTTP response is received.
8193
82-
return () => clearInterval(interval);
94+
```ts
95+
import { readable } from 'svelte/store';
96+
97+
const issPosition = readable(
98+
{ latitude: 0, longitude: 0 },
99+
(set) => {
100+
const interval = setInterval(() => {
101+
fetch('http://api.open-notify.org/iss-now.json')
102+
.then(response => response.json())
103+
.then(payload => set(payload.iss_position));
104+
}, 3000);
105+
106+
return function onStop() {
107+
clearInterval(interval);
108+
};
109+
}
110+
);
111+
112+
issPosition.subscribe(({latitude, longitude}) => {
113+
console.log(`The ISS is currently above ${latitude}°, ${longitude`
114+
+ ` in the ${latitude > 0 ? 'northern' : 'southern'} hemisphere.`);
83115
});
84116
```
85117

@@ -108,7 +140,9 @@ import { derived } from 'svelte/store';
108140
const doubled = derived(a, ($a) => $a * 2);
109141
```
110142

111-
The callback can set a value asynchronously by accepting a second argument, `set`, and an optional third argument, `update`, calling either or both of them when appropriate.
143+
The `derive_value` function can set values asynchronously by accepting a second argument, `set`, and an optional third argument, `update`, and calling either or both of these functions when appropriate.
144+
145+
> If `set` and `update` are, in combination, called multiple times synchronously, only the last change will cause the store's subscribers to be notified. For instance, calling `update` and then `set` synchronously in a `derive_value` function will only cause the value passed to `set` to be sent to subscribers.
112146
113147
In this case, you can also pass a third argument to `derived` — the initial value of the derived store before `set` or `update` is first called. If no initial value is specified, the store's initial value will be `undefined`.
114148

@@ -123,7 +157,7 @@ declare global {
123157
export {};
124158

125159
// @filename: index.ts
126-
// @errors: 18046 2769 7006
160+
// @errors: 18046 2769 7006 2722
127161
// ---cut---
128162
import { derived } from 'svelte/store';
129163

@@ -143,7 +177,7 @@ const delayedIncrement = derived(a, ($a, set, update) => {
143177
});
144178
```
145179

146-
If you return a function from the callback, it will be called when a) the callback runs again, or b) the last subscriber unsubscribes.
180+
If you return a function from the `derive_value` function, it will be called a) before the function runs again, or b) after the last subscriber unsubscribes.
147181

148182
```ts
149183
// @filename: ambient.d.ts
@@ -188,7 +222,6 @@ declare global {
188222
export {};
189223

190224
// @filename: index.ts
191-
192225
// ---cut---
193226
import { derived } from 'svelte/store';
194227

@@ -199,13 +232,76 @@ const delayed = derived([a, b], ([$a, $b], set) => {
199232
});
200233
```
201234

235+
### TypeScript type inference
236+
237+
If a multi-argument `derive_value` function is passed to `derive`, TypeScript may not be able to infer the type of the derived store, yielding a store of type `Readable<unknown>`. Set an initial value for the store to resolve this; `undefined` with a type assertion is sufficient. Alternatively, you may use type arguments, although this requires specifying the types of the dependency stores, too.
238+
239+
```ts
240+
// @filename: ambient.d.ts
241+
import { type Writable } from 'svelte/store';
242+
243+
declare global {
244+
const a: Writable<number>;
245+
}
246+
247+
export {};
248+
249+
// @filename: index.ts
250+
// ---cut---
251+
import { derived } from 'svelte/store';
252+
253+
// @errors: 2769
254+
const aInc = derived(
255+
a,
256+
($a, set) => setTimeout(() => set($a + 1), 1000),
257+
undefined as unknown as number
258+
);
259+
260+
const concatenated = derived<[number, number], string>(
261+
[a, aInc],
262+
([$a, $aInc], set) => setTimeout(() => set(`${$a}${$aInc}`), 1000)
263+
);
264+
```
265+
266+
`derived` can derive new stores from stores not created by Svelte, including from RxJS `Observable`s. In this case, TypeScript may not be able to infer the types of data held by the dependency stores. Use a type assertion to `ExternalStore` or a type argument to provide the missing context.
267+
268+
Until TypeScript gains support for [partial type argument inference](https://github.com/microsoft/TypeScript/issues/26242), the latter option requires also specifying the return type of `derive_store`.
269+
270+
```ts
271+
// @filename: ambient.d.ts
272+
import { type Writable } from 'svelte/store';
273+
274+
declare global {
275+
const a: Writable<number>;
276+
const observable: {
277+
subscribe: (fn: (value: unknown) => void) => { unsubscribe: () => void };
278+
};
279+
}
280+
281+
export {};
282+
283+
// @filename: index.ts
284+
// ---cut---
285+
import { derived, type ExternalStore } from 'svelte/store';
286+
287+
const sum = derived(
288+
[a, observable as ExternalStore<number>],
289+
([$a, $observable]) => $a + $observable
290+
);
291+
292+
const sumMore = derived<[number, number], number>(
293+
[sum, observable],
294+
([$sum, $observable]) => $sum + $observable
295+
);
296+
```
297+
202298
## `readonly`
203299

204300
> EXPORT_SNIPPET: svelte/store#readonly
205301
206302
This simple helper function makes a store readonly. You can still subscribe to the changes from the original one using this new readable store.
207303

208-
```js
304+
```ts
209305
import { readonly, writable } from 'svelte/store';
210306

211307
const writableStore = writable(1);
@@ -224,7 +320,7 @@ readableStore.set(2); // ERROR
224320
225321
Generally, you should read the value of a store by subscribing to it and using the value as it changes over time. Occasionally, you may need to retrieve the value of a store to which you're not subscribed. `get` allows you to do so.
226322

227-
> This works by creating a subscription, reading the value, then unsubscribing. It's therefore not recommended in hot code paths.
323+
> By default, `get` subscribes to the given store, makes note of its value, then unsubscribes again. Passing `true` as a second argument causes `get` to directly read the internal state of the store instead, which, in the case of a derived store, may be outdated or `undefined`. Where performance is important, it's recommended to set `allow_stale` to `true` or not use `get`.
228324
229325
```ts
230326
// @filename: ambient.d.ts

packages/svelte/src/internal/client/runtime.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { subscribe_to_store } from '../../store/utils.js';
1+
import { subscribe_to_store } from '../../store/index.js';
22
import { EMPTY_FUNC } from '../common.js';
33
import { unwrap } from './render.js';
44
import { map_delete, map_get, map_set } from './operations.js';

packages/svelte/src/internal/server/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as $ from '../client/runtime.js';
22
import { set_is_ssr } from '../client/runtime.js';
33
import { is_promise } from '../common.js';
4-
import { subscribe_to_store } from '../../store/utils.js';
4+
import { subscribe_to_store } from '../../store/index.js';
55

66
export * from '../client/validate.js';
77

packages/svelte/src/motion/tweened.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,12 @@ export function tweened(value, defaults = {}) {
9090
* @param {import('./private').TweenedOptions<T>} [opts]
9191
*/
9292
function set(new_value, opts) {
93+
target_value = new_value;
94+
9395
if (value == null) {
9496
store.set((value = new_value));
9597
return Promise.resolve();
9698
}
97-
target_value = new_value;
9899

99100
/** @type {import('../internal/client/private').Task | null} */
100101
let previous_task = task;
@@ -124,8 +125,9 @@ export function tweened(value, defaults = {}) {
124125
if (now < start) return true;
125126
if (!started) {
126127
fn = interpolate(/** @type {any} */ (value), new_value);
127-
if (typeof duration === 'function')
128+
if (typeof duration === 'function') {
128129
duration = duration(/** @type {any} */ (value), new_value);
130+
}
129131
started = true;
130132
}
131133
if (previous_task) {

0 commit comments

Comments
 (0)