Skip to content

Commit 9da3a10

Browse files
committed
createStartStopNotifier
1 parent 62d370a commit 9da3a10

File tree

5 files changed

+79
-64
lines changed

5 files changed

+79
-64
lines changed

packages/svelte/src/reactivity/index-server.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,10 @@ export class MediaQuery {
1414
this.current = matches;
1515
}
1616
}
17+
18+
/**
19+
* @param {any} _
20+
*/
21+
export function createStartStopNotifier(_) {
22+
return () => {};
23+
}
Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,20 @@
1-
import { get, tick } from '../internal/client/runtime.js';
1+
import { get } from '../internal/client/runtime.js';
22
import { set, source } from '../internal/client/reactivity/sources.js';
3-
import { effect_tracking, render_effect } from '../internal/client/reactivity/effects.js';
3+
import { effect_tracking } from '../internal/client/reactivity/effects.js';
4+
import { createStartStopNotifier } from './start-stop-notifier.js';
45

56
/**
67
* Creates a media query and provides a `current` property that reflects whether or not it matches.
78
*/
89
export class MediaQuery {
910
#version = source(0);
10-
#subscribers = 0;
1111
#query;
12-
/** @type {any} */
13-
#listener;
12+
#notify;
1413

1514
get current() {
1615
if (effect_tracking()) {
1716
get(this.#version);
18-
19-
render_effect(() => {
20-
if (this.#subscribers === 0) {
21-
this.#listener = () => set(this.#version, this.#version.v + 1);
22-
this.#query.addEventListener('change', this.#listener);
23-
}
24-
25-
this.#subscribers += 1;
26-
27-
return () => {
28-
tick().then(() => {
29-
// Only count down after timeout, else we would reach 0 before our own render effect reruns,
30-
// but reach 1 again when the tick callback of the prior teardown runs. That would mean we
31-
// re-subcribe unnecessarily and create a memory leak because the old subscription is never cleaned up.
32-
this.#subscribers -= 1;
33-
34-
if (this.#subscribers === 0) {
35-
this.#query.removeEventListener('change', this.#listener);
36-
}
37-
});
38-
};
39-
});
17+
this.#notify();
4018
}
4119

4220
return this.#query.matches;
@@ -48,5 +26,10 @@ export class MediaQuery {
4826
*/
4927
constructor(query, matches) {
5028
this.#query = window.matchMedia(query);
29+
this.#notify = createStartStopNotifier(() => {
30+
const listener = () => set(this.#version, this.#version.v + 1);
31+
this.#query.addEventListener('change', listener);
32+
return () => this.#query.removeEventListener('change', listener);
33+
});
5134
}
5235
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { tick, untrack } from '../internal/client/runtime.js';
2+
import { effect_tracking, render_effect } from '../internal/client/reactivity/effects.js';
3+
4+
/**
5+
* Returns a function that, when invoked in a reactive context, calls the `start` function once,
6+
* and calls the `stop` function returned from `start` when all reactive contexts it's called in
7+
* are destroyed. This is useful for creating a notifier that starts and stops when the
8+
* "subscriber" count goes from 0 to 1 and back to 0.
9+
* @param {() => () => void} start
10+
*/
11+
export function createStartStopNotifier(start) {
12+
let subscribers = 0;
13+
/** @type {() => void} */
14+
let stop;
15+
16+
return () => {
17+
if (effect_tracking()) {
18+
render_effect(() => {
19+
if (subscribers === 0) {
20+
stop = untrack(start);
21+
}
22+
23+
subscribers += 1;
24+
25+
return () => {
26+
tick().then(() => {
27+
// Only count down after timeout, else we would reach 0 before our own render effect reruns,
28+
// but reach 1 again when the tick callback of the prior teardown runs. That would mean we
29+
// re-subcribe unnecessarily and create a memory leak because the old subscription is never cleaned up.
30+
subscribers -= 1;
31+
32+
if (subscribers === 0) {
33+
stop();
34+
}
35+
});
36+
};
37+
});
38+
}
39+
};
40+
}

packages/svelte/src/store/index-client.js

Lines changed: 15 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
/** @import { Readable, Writable } from './public.js' */
2-
import { noop } from '../internal/shared/utils.js';
32
import {
43
effect_root,
54
effect_tracking,
65
render_effect
76
} from '../internal/client/reactivity/effects.js';
87
import { source } from '../internal/client/reactivity/sources.js';
9-
import { get as get_source, tick } from '../internal/client/runtime.js';
8+
import { get as get_source } from '../internal/client/runtime.js';
109
import { increment } from '../reactivity/utils.js';
1110
import { get, writable } from './shared/index.js';
11+
import { createStartStopNotifier } from '../reactivity/start-stop-notifier.js';
1212

1313
export { derived, get, readable, readonly, writable } from './shared/index.js';
1414

@@ -110,42 +110,24 @@ export function toStore(get, set) {
110110
export function fromStore(store) {
111111
let value = /** @type {V} */ (undefined);
112112
let version = source(0);
113-
let subscribers = 0;
114113

115-
let unsubscribe = noop;
114+
const notify = createStartStopNotifier(() => {
115+
let ran = false;
116+
117+
const unsubscribe = store.subscribe((v) => {
118+
value = v;
119+
if (ran) increment(version);
120+
});
121+
122+
ran = true;
123+
124+
return unsubscribe;
125+
});
116126

117127
function current() {
118128
if (effect_tracking()) {
119129
get_source(version);
120-
121-
render_effect(() => {
122-
if (subscribers === 0) {
123-
let ran = false;
124-
125-
unsubscribe = store.subscribe((v) => {
126-
value = v;
127-
if (ran) increment(version);
128-
});
129-
130-
ran = true;
131-
}
132-
133-
subscribers += 1;
134-
135-
return () => {
136-
tick().then(() => {
137-
// Only count down after timeout, else we would reach 0 before our own render effect reruns,
138-
// but reach 1 again when the tick callback of the prior teardown runs. That would mean we
139-
// re-subcribe unnecessarily and create a memory leak because the old subscription is never cleaned up.
140-
subscribers -= 1;
141-
142-
if (subscribers === 0) {
143-
unsubscribe();
144-
}
145-
});
146-
};
147-
});
148-
130+
notify();
149131
return value;
150132
}
151133

packages/svelte/types/index.d.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1726,12 +1726,15 @@ declare module 'svelte/reactivity' {
17261726
#private;
17271727
}
17281728
/**
1729-
* Creates a media query and provides a `matches` property that reflects its current state.
1729+
* Creates a media query and provides a `current` property that reflects whether or not it matches.
17301730
*/
17311731
export class MediaQuery {
1732-
1733-
constructor(query: string);
1734-
get matches(): boolean;
1732+
/**
1733+
* @param query A media query string (don't forget the braces)
1734+
* @param matches Fallback value for the server
1735+
*/
1736+
constructor(query: string, matches?: boolean | undefined);
1737+
get current(): boolean;
17351738
#private;
17361739
}
17371740

0 commit comments

Comments
 (0)