Repository: sveltejs/svelte (JavaScript)
Date: 2019-02-17 17:20:43+00:00
Task type: Feature

Issue:
Add second argument to `writable`
I think I've found a use case that isn't served by `writable` and `readable` — namely, it's not possible for a writable store to know whether or not it has any subscribers, and it's very difficult to change the parameters of a readable store after it's been created.

This bit me in a Sapper app today — I have a component that fetches a lot of paginated data for a given profile. If the user navigates away, the store should stop fetching data if it hasn't reached the end already. If the user navigates to a sibling page, the store should fetch data for the new profile instead. (It might be simpler in that case just to create a new store instead, but #2014 prevents that, and in any case it's not a general solution.)

Proposed change:

```diff
-export function writable(value) {
+export function writable(value, start = noop) {
+  let stop;
  const subscribers = [];

  function set(newValue) {
    if (newValue === value) return;
    value = newValue;
    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() || 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 };
}
```

This would make it possible to do this sort of thing:

```js
function create_custom_store() {
  let params;
  let fetcher;

  const { set, subscribe } = writable(null, () => {
    fetcher = new Fetcher(params, set);
    return () => {
      fetcher.stop();
      fetcher = null;
    };
  });

  return {
    set_params(value) {
      params = value;
      if (fetcher) fetcher.update_params(params);
    },
    subscribe
  }
}
```

Does this seem like a good solution? Am I missing anything?


Issue discussion:
At first glance, it looks like mixing concerns a bit. At second glance, I'm not sure how you'd get separation without feeling over-engineered, so, yeah.

This would cover lazy stores nicely too, which is very helpful. A custom lazy store could start with an unresolved promise to be used in an `{#await}`, kick off value retrieval at first `subscribe`, swap out the promise after resolution, and optionally clear out the store on stop. That's a pattern I've used quite a bit in business apps that keep data synced with a central db via websockets. When a client is no longer referencing a store, it sets a timeout to stop sync release the store contents from memory.
wonder if it would be handy to just call the 2nd argument every subscription change. It might allow for other composition we haven't thought of, although it would be more work for each user since the default use case is probably what you proposed anyway

```js
function writable(value, onSubscriptionChange = noop) {
  let stop;
  const subscribers = [];

  function set(newValue) {
    if (newValue === value) return;
    value = newValue;
    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);
    onSubscriptionChange(subscribers);
    run(value);

    return () => {
      const index = subscribers.indexOf(subscriber);
      if (index !== -1) subscribers.splice(index, 1);
      onSubscriptionChange(subscribers);
    };
  }

  return { set, update, subscribe };
}

//then

function create_custom_store() {
  let params;
  let fetcher;

  const { set, subscribe } = writable(null, (subs) => {
     if (subs.length == 1) {
        fetcher = new Fetcher(params, set);
     } else if (subs.length == 0) {
        fetcher.stop();
        fetcher = null;
     }
  });

  return {
    set_params(value) {
      params = value;
      if (fetcher) fetcher.update_params(params);
    },
    subscribe
  }
}
```

Edit: after thinking about this for most of the day, I couldn't come up with a use case that wasn't the example you gave, except for maybe if you wanted to wrap each subscriber function for logging or exception handling or something.
Does `derive` cover this use case? It would have to be based on one of the derived values becoming `null`, rather than knowing about the number of subscribers, but it should avoid mixing concerns / leaking internals of the store.

For example, my app has a list of tests and a test detail page, so it's not that different from the profile pattern you described. I don't (currently) have to deal with _continuous_ fetching, but probably will need to eventually. I can imagine doing something like this:

```js
function test_results_store () {
  const testId = writable(null)
  let resultsFetcher = null
  
  const { subscribe } = derive(testId, (id) => {
    if (id === null && resultsFetcher) {
      // navigated away from the test detail page
      resultsFetcher.stop()
      resultsFetcher = null
    } else {
      if (resultsFetcher) resultsFetcher.stop()
      resultsFetcher = new ResultsFetcher(id)
    }
    return resultsFetcher
  })

  return {
    subscribe,
    set_test (id) {
      testId.set(id)
    }
  }
}
```

This might suffer from a subtle bug though, if the last subscriber unsubscribes before the `id` is set to `null` - I think that would mean the derive callback wouldn't be called?


Does it make sense to provide this as an alternative writable and use the simpler one by default?
Is there a conflict using them side-by-side?
@thgh Rich's example version can be used in the same way as the current one... The `start` param is optional.
@jches you're right about the bug, yeah — it would work in your app if you were careful about setting `testId`, but I don't think it's a general solution unfortunately.
Agreed. It makes sense to me to add this second parameter, since it gives `writable` access to those first/last subscriber events, like `readable` already has. Makes me wonder if `derive` actually might need something similar?

Solution patch:
diff --git a/store.mjs b/store.mjs
--- a/store.mjs
+++ b/store.mjs
@@ -34,7 +34,8 @@ export function readable(start, value) {
 	};
 }
 
-export function writable(value) {
+export function writable(value, start = noop) {
+	let stop;
 	const subscribers = [];
 
 	function set(newValue) {
@@ -51,11 +52,13 @@ export function writable(value) {
 	function subscribe(run, invalidate = noop) {
 		const subscriber = [run, invalidate];
 		subscribers.push(subscriber);
+		if (subscribers.length === 1) stop = start() || noop;
 		run(value);
 
 		return () => {
 			const index = subscribers.indexOf(subscriber);
 			if (index !== -1) subscribers.splice(index, 1);
+			if (subscribers.length === 0) stop();
 		};
 	}
 


Test patch:
diff --git a/test/store/index.js b/test/store/index.js
--- a/test/store/index.js
+++ b/test/store/index.js
@@ -21,6 +21,27 @@ describe('store', () => {
 
 			assert.deepEqual(values, [0, 1, 2]);
 		});
+
+		it('calls provided subscribe handler', () => {
+			let called = 0;
+
+			const store = writable(0, () => {
+				called += 1;
+				return () => called -= 1;
+			});
+
+			const unsubscribe1 = store.subscribe(() => {});
+			assert.equal(called, 1);
+
+			const unsubscribe2 = store.subscribe(() => {});
+			assert.equal(called, 1);
+
+			unsubscribe1();
+			assert.equal(called, 1);
+
+			unsubscribe2();
+			assert.equal(called, 0);
+		});
 	});
 
 	describe('readable', () => {