Repository: sveltejs/svelte (JavaScript)
Date: 2019-07-30 21:22:27+00:00
Task type: Bug Fix

Issue:
Observable-as-store error on reassignment
**Describe the bug**

Reassigning an observable-as-store variable results in an error `$$unsubscribe_NAMEHERE is not a function`. It appears that currently observables can be used only in certain undocumented restricted ways, perhaps only as long as such variables are never reassigned? 

**To Reproduce**

https://svelte.dev/repl/c73aa3a571844702baa8538c80376603?version=3.6.9

```
<script>
  import { of } from 'rxjs';

  let myObs = of([1, 2, 3, 4, 5]);
  myObs = of([6, 7, 8, 9, 10]);
</script>

{$myObs}
```

**Expected behavior**

Runs without error.

**Severity**

It's not severe at all if you never use RxJS, but it's a pretty significant restriction if you do (!).

Fixing this seems necessary to declare reasonably observable-as-store support.





Issue discussion:
[Here](https://github.com/sveltejs/svelte/blob/b3ef4e64beb7de62eeb16fb40281c50a0aa21dea/src/compiler/compile/render_dom/index.ts#L393) when we're setting the unsubscription function to be called later, we're not accounting for the fact that the return value of `.subscribe()` might be an object with a `.unsubscribe()` method, rather than an unsubscription function directly. We can either put a check for that inline here (which would be generated for each reassigned store in each component), or we can extract that logic into its own internal helper. The logic we need is already in the `subscribe` internal helper, but it's along with some other component-specific stuff we don't want. I'm thinking we should factor some sort of 'just subscribe, and return a normalized unsubscription function' helper out of `subscribe` and `get_store_value`, and use that here as well.
Current WIP implementation:

```diff
diff --git a/src/compiler/compile/render_dom/index.ts b/src/compiler/compile/render_dom/index.ts
index c6eb1f1b..df5d1526 100644
--- a/src/compiler/compile/render_dom/index.ts
+++ b/src/compiler/compile/render_dom/index.ts
@@ -390,7 +390,7 @@ export default function dom(
 
 			const store = component.var_lookup.get(name);
 			if (store && store.reassigned) {
-				return `${$name}, $$unsubscribe_${name} = @noop, $$subscribe_${name} = () => { $$unsubscribe_${name}(); $$unsubscribe_${name} = ${name}.subscribe($$value => { ${$name} = $$value; $$invalidate('${$name}', ${$name}); }) }`;
+				return `${$name}, $$unsubscribe_${name} = @noop, $$subscribe_${name} = () => { $$unsubscribe_${name}(); $$unsubscribe_${name} = @just_subscribe(${name},$$value => { ${$name} = $$value; $$invalidate('${$name}', ${$name}); }) }`;
 			}
 
 			return $name;
diff --git a/src/runtime/internal/ssr.ts b/src/runtime/internal/ssr.ts
index 1ae1ae1d..d84efc73 100644
--- a/src/runtime/internal/ssr.ts
+++ b/src/runtime/internal/ssr.ts
@@ -1,6 +1,5 @@
 import { set_current_component, current_component } from './lifecycle';
 import { run_all, blank_object } from './utils';
-import { Readable } from 'svelte/store';
 
 export const invalid_attribute_name_character = /[\s'">/=\u{FDD0}-\u{FDEF}\u{FFFE}\u{FFFF}\u{1FFFE}\u{1FFFF}\u{2FFFE}\u{2FFFF}\u{3FFFE}\u{3FFFF}\u{4FFFE}\u{4FFFF}\u{5FFFE}\u{5FFFF}\u{6FFFE}\u{6FFFF}\u{7FFFE}\u{7FFFF}\u{8FFFE}\u{8FFFF}\u{9FFFE}\u{9FFFF}\u{AFFFE}\u{AFFFF}\u{BFFFE}\u{BFFFF}\u{CFFFE}\u{CFFFF}\u{DFFFE}\u{DFFFF}\u{EFFFE}\u{EFFFF}\u{FFFFE}\u{FFFFF}\u{10FFFE}\u{10FFFF}]/u;
 // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
@@ -121,18 +120,6 @@ export function create_ssr_component(fn) {
 	};
 }
 
-/**
- * Get the current value from a store by subscribing and immediately unsubscribing.
- * @param store readable
- */
-export function get_store_value<T>(store: Readable<T>): T | undefined {
-	let value;
-	const unsubscribe: any = store.subscribe(_ => value = _);
-	if (unsubscribe.unsubscribe) unsubscribe.unsubscribe();
-	else unsubscribe();
-	return value;
-}
-
 export function add_attribute(name, value) {
 	if (!value) return '';
 	return ` ${name}${value === true ? '' : `=${typeof value === 'string' ? JSON.stringify(value) : `"${value}"`}`}`;
diff --git a/src/runtime/internal/utils.ts b/src/runtime/internal/utils.ts
index 08410ec3..a5c8cff2 100644
--- a/src/runtime/internal/utils.ts
+++ b/src/runtime/internal/utils.ts
@@ -48,12 +48,19 @@ export function validate_store(store, name) {
 	}
 }
 
-export function subscribe(component, store, callback) {
+export function just_subscribe(store, callback) {
 	const unsub = store.subscribe(callback);
+	return unsub.unsubscribe ? () => unsub.unsubscribe() : unsub;
+}
 
-	component.$$.on_destroy.push(unsub.unsubscribe
-		? () => unsub.unsubscribe()
-		: unsub);
+export function get_store_value(store) {
+	let value;
+	just_subscribe(store, _ => value = _)();
+	return value;
+}
+
+export function subscribe(component, store, callback) {
+	component.$$.on_destroy.push(just_subscribe(store, callback));
 }
 
 export function create_slot(definition, ctx, fn) {
```

For some reason, I was getting all sorts of bizarre TypeScript errors on unrelated parts of the code when I moved `get_store_value` from ssr.ts to utils.ts. Removing its type definitions helped, but we're going to want to add those back in again probably.
Different direction of attack. I think this is doing the correct thing:

```typescript
/**
 * Get the current value from a store by subscribing and immediately unsubscribing.
 * @param store readable
 */
export const get = get_store_value as {
	<T>(store: Readable<T>): T;
};
```

This is what I'm doing now in store.ts instead of a regular re-export of `get_store_value`.

Solution patch:
diff --git a/src/compiler/compile/render_dom/index.ts b/src/compiler/compile/render_dom/index.ts
--- a/src/compiler/compile/render_dom/index.ts
+++ b/src/compiler/compile/render_dom/index.ts
@@ -267,9 +267,9 @@ export default function dom(
 				return `$$subscribe_${name}()`;
 			}
 
-			const subscribe = component.helper('subscribe');
+			const component_subscribe = component.helper('component_subscribe');
 
-			let insert = `${subscribe}($$self, ${name}, $${callback})`;
+			let insert = `${component_subscribe}($$self, ${name}, $${callback})`;
 			if (component.compile_options.dev) {
 				const validate_store = component.helper('validate_store');
 				insert = `${validate_store}(${name}, '${name}'); ${insert}`;
@@ -343,7 +343,7 @@ export default function dom(
 		})
 		.map(({ name }) => deindent`
 			${component.compile_options.dev && `@validate_store(${name.slice(1)}, '${name.slice(1)}');`}
-			@subscribe($$self, ${name.slice(1)}, $$value => { ${name} = $$value; $$invalidate('${name}', ${name}); });
+			@component_subscribe($$self, ${name.slice(1)}, $$value => { ${name} = $$value; $$invalidate('${name}', ${name}); });
 		`);
 
 	const resubscribable_reactive_store_unsubscribers = reactive_stores
@@ -390,7 +390,7 @@ export default function dom(
 
 			const store = component.var_lookup.get(name);
 			if (store && store.reassigned) {
-				return `${$name}, $$unsubscribe_${name} = @noop, $$subscribe_${name} = () => { $$unsubscribe_${name}(); $$unsubscribe_${name} = ${name}.subscribe($$value => { ${$name} = $$value; $$invalidate('${$name}', ${$name}); }) }`;
+				return `${$name}, $$unsubscribe_${name} = @noop, $$subscribe_${name} = () => { $$unsubscribe_${name}(); $$unsubscribe_${name} = @subscribe(${name}, $$value => { ${$name} = $$value; $$invalidate('${$name}', ${$name}); }) }`;
 			}
 
 			return $name;
diff --git a/src/runtime/internal/ssr.ts b/src/runtime/internal/ssr.ts
--- a/src/runtime/internal/ssr.ts
+++ b/src/runtime/internal/ssr.ts
@@ -1,6 +1,5 @@
 import { set_current_component, current_component } from './lifecycle';
 import { run_all, blank_object } from './utils';
-import { Readable } from 'svelte/store';
 
 export const invalid_attribute_name_character = /[\s'">/=\u{FDD0}-\u{FDEF}\u{FFFE}\u{FFFF}\u{1FFFE}\u{1FFFF}\u{2FFFE}\u{2FFFF}\u{3FFFE}\u{3FFFF}\u{4FFFE}\u{4FFFF}\u{5FFFE}\u{5FFFF}\u{6FFFE}\u{6FFFF}\u{7FFFE}\u{7FFFF}\u{8FFFE}\u{8FFFF}\u{9FFFE}\u{9FFFF}\u{AFFFE}\u{AFFFF}\u{BFFFE}\u{BFFFF}\u{CFFFE}\u{CFFFF}\u{DFFFE}\u{DFFFF}\u{EFFFE}\u{EFFFF}\u{FFFFE}\u{FFFFF}\u{10FFFE}\u{10FFFF}]/u;
 // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
@@ -121,18 +120,6 @@ export function create_ssr_component(fn) {
 	};
 }
 
-/**
- * Get the current value from a store by subscribing and immediately unsubscribing.
- * @param store readable
- */
-export function get_store_value<T>(store: Readable<T>): T | undefined {
-	let value;
-	const unsubscribe: any = store.subscribe(_ => value = _);
-	if (unsubscribe.unsubscribe) unsubscribe.unsubscribe();
-	else unsubscribe();
-	return value;
-}
-
 export function add_attribute(name, value) {
 	if (!value) return '';
 	return ` ${name}${value === true ? '' : `=${typeof value === 'string' ? JSON.stringify(value) : `"${value}"`}`}`;
diff --git a/src/runtime/internal/utils.ts b/src/runtime/internal/utils.ts
--- a/src/runtime/internal/utils.ts
+++ b/src/runtime/internal/utils.ts
@@ -48,12 +48,19 @@ export function validate_store(store, name) {
 	}
 }
 
-export function subscribe(component, store, callback) {
+export function subscribe(store, callback) {
 	const unsub = store.subscribe(callback);
+	return unsub.unsubscribe ? () => unsub.unsubscribe() : unsub;
+}
+
+export function get_store_value(store) {
+	let value;
+	subscribe(store, _ => value = _)();
+	return value;
+}
 
-	component.$$.on_destroy.push(unsub.unsubscribe
-		? () => unsub.unsubscribe()
-		: unsub);
+export function component_subscribe(component, store, callback) {
+	component.$$.on_destroy.push(subscribe(store, callback));
 }
 
 export function create_slot(definition, ctx, fn) {
diff --git a/src/runtime/store/index.ts b/src/runtime/store/index.ts
--- a/src/runtime/store/index.ts
+++ b/src/runtime/store/index.ts
@@ -1,5 +1,4 @@
-import { run_all, noop, safe_not_equal, is_function } from 'svelte/internal';
-export { get_store_value as get } from 'svelte/internal';
+import { run_all, noop, safe_not_equal, is_function, get_store_value } from 'svelte/internal';
 
 /** Callback to inform of a value updates. */
 type Subscriber<T> = (value: T) => void;
@@ -182,3 +181,11 @@ export function derived<T, S extends Stores>(
 		};
 	});
 }
+
+/**
+ * Get the current value from a store by subscribing and immediately unsubscribing.
+ * @param store readable
+ */
+export const get = get_store_value as {
+	<T>(store: Readable<T>): (T | undefined);
+};


Test patch:
diff --git a/test/runtime/samples/store-resubscribe-observable/_config.js b/test/runtime/samples/store-resubscribe-observable/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/store-resubscribe-observable/_config.js
@@ -0,0 +1,3 @@
+export default {
+	html: `42`,
+};
diff --git a/test/runtime/samples/store-resubscribe-observable/main.svelte b/test/runtime/samples/store-resubscribe-observable/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/store-resubscribe-observable/main.svelte
@@ -0,0 +1,11 @@
+<script>
+	import { writable } from 'svelte/store';
+	function fake_observable(store) {
+		return { subscribe: cb => ({ unsubscribe: store.subscribe(cb) }) };
+	}
+
+	let foo = fake_observable(writable(0));
+	foo = fake_observable(writable(42));
+</script>
+
+{$foo}