svelte icon indicating copy to clipboard operation
svelte copied to clipboard

Svelte 5: structuredClone tries to clone proxy object instead of its contents

Open melindatrace opened this issue 1 year ago • 15 comments

Describe the bug

Attempting to use structuredClone on any stateful object will result in an error with zero indication of a problem before running the code in both JavaScript and TypeScript.

Reproduction

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACm2QwUrEQAyGXyVEobtQ2nu3LojiwYvHPTgeujPpOjqblJmMq5S-uwwLHoq35OP7_0BmHH2ghN3rjDycCTu8nyasUX-msqQvCkpYY5IcbSF9stFPujdsNJBCThThDm6TDkqbuWCjpaoDg8_yzvAoZLDwZbszXAYrnCRQE-S0KfntDtoWDhI_E4ye6T-nKZ1rEYYEFwphHUgas9UcyT0EYboeuYafBh_IgQrQN9msBNXKrkAYqoNnJ5eqg5v-5fhBVvdgJQcHLApHAltU1xju27-PYI1ncX705LDTmGl5W34BHhXdL2IBAAA=

Logs

No response

System Info

System:
    OS: Windows 11 10.0.22631
    CPU: (20) x64 12th Gen Intel(R) Core(TM) i7-12700KF
    Memory: 48.76 GB / 63.85 GB
  Binaries:
    Node: 20.17.0 - C:\Program Files\nodejs\node.EXE
    npm: 10.8.2 - C:\Program Files\nodejs\npm.CMD
    pnpm: 9.3.0 - ~\AppData\Local\pnpm\pnpm.EXE
    bun: 1.1.30 - ~\.bun\bin\bun.EXE
  Browsers:
    Edge: Chromium (127.0.2651.74)
  npmPackages:
    svelte: ^5.0.0-next.264 => 5.0.0-next.264

Severity

annoyance

melindatrace avatar Oct 10 '24 19:10 melindatrace

This is what https://svelte-5-preview.vercel.app/docs/runes#$state-snapshot is for. I don't know whether it makes sense to patch structuredClone in dev to print a warning about this. There's going to be a never-ending list of libraries and APIs that don't expect proxies.

Conduitry avatar Oct 10 '24 19:10 Conduitry

I'm aware of that, but there is zero indication of a problem in code editor.

Code_V0pzgiunKW

Svelte lies to TypeScript that $state returns the value it holds, but in reality it returns the value wrapped in a proxy. New developers won't be aware of this, and tools like TypeScript will never know. Developer can easily introduce a bug in their code and the tools that suppose to catch it are unable to because they are being lied to.

Code_XVmSHVKp2G

I don't know what will Svelte team do regarding this issue, but this definitely should be fixed one way or another. I would suggest a fix but I am by no means qualified to talk about tool design (especially given the scale of Svelte).

melindatrace avatar Oct 10 '24 19:10 melindatrace

I would really love the addition of Boxed<T>.

FoHoOV avatar Oct 10 '24 20:10 FoHoOV

We can probably monkey patch structuredClone in DEV and warn to use $state.snapshot if encountering a Svelte proxied object. However, that can wait till 5.x as this isn't urgent.

trueadm avatar Oct 10 '24 22:10 trueadm

Just one more thing I wanna say here, If a function returns an object that is something like:

{
  name: string
}

Just by seeing the type, you have no idea if this is reactive or not, You have no idea that if you do, const somehting = $derived(x.name + " something"), this is actually reactive or not. In most cases you have to check the implementation(or docs) to see if its a signal or just a normal property/getter.

FoHoOV avatar Oct 11 '24 10:10 FoHoOV

Just one more thing I wanna say here, If a function returns an object that is something like:

{
  name: string
}

Just by seeing the type, you have no idea if this is reactive or not, You have no idea that if you do, const somehting = $derived(x.name + " something"), this is actually reactive or not. In most cases you have to check the implementation(or docs) to see if its a signal or just a normal property/getter.

Not much we can do about this with today's tooling. The same issue applies with any proxy reactivity library.

trueadm avatar Oct 11 '24 10:10 trueadm

If the return type of signals were Boxed<T>, which is just an alias for T wouldn't it help (in type level I have some idea what I'm dealing with)? Also it wouldn't break current code if I'm correct.

FoHoOV avatar Oct 11 '24 11:10 FoHoOV

@FoHoOV No, that wouldn't help at all. Boxing things doesn't solve anything – it just moves the problem to another area and in this case doesn't solve the problem of proxies. Unless you expected each property of the object/array to also be boxed – but that's terrible ergonomics.

trueadm avatar Oct 11 '24 11:10 trueadm

I must be using the wrong terms here, Boxed<T> is just an alias for T. They are basically the same but have different semantics. All siganls should be returning Boxed<T> (again just an alias for T ie Boxed<T> = T)

FoHoOV avatar Oct 11 '24 11:10 FoHoOV

You would need to add a property or symbol to the type, otherwise the wrapper type gets erased. And if you do that, you are no longer allowed to assign to the variable, because the added property will be missing from the new value.

const stateKey = Symbol('state-key');
type StateProxy<T> = T & { [stateKey]: unknown };

declare function $state(value: number): number;
declare function $state(value: string): string;
// ...
declare function $state<T>(value: T): StateProxy<T>;

let a = $state(5); // number
let b = $state({ value: 3 }); // StateProxy<{ value: number }>

b = { value: 5 }; // Error

Playground

brunnerh avatar Oct 11 '24 12:10 brunnerh

You need to make the symbol optional { [stateKey]?: unknown } but the real problem is that you cannot rid of the marker:

let a =  $state(5); // StateProxy<number & { [stateKey]?: unknown }>
let b = a; // b will have the same reactive type while, in fact, it is static

7nik avatar Oct 11 '24 17:10 7nik

you cannot rid of the marker

That is fine, it's a statement about the object, not the variable. (And the type of primitives will not be affected.)

brunnerh avatar Oct 11 '24 19:10 brunnerh

It should be recursive (it's possible) but only for POJOs. In TS, a class instance and a POJO with the same fields are the same thing, aren't they?

7nik avatar Oct 11 '24 20:10 7nik

It should be recursive (it's possible) but only for POJOs. In TS, a class instance and a POJO with the same fields are the same thing, aren't they?

Their prototypes also have to be the same.

trueadm avatar Oct 11 '24 21:10 trueadm

A mean this case which results in a wrong type

7nik avatar Oct 11 '24 21:10 7nik

Just ran into this issue. We're using Svelte 5, and have svelte 4 style stores in different files. Within those stores files, we have various exported functions that call the stores update function. Within those functions, we clone the current store using structuredClone, make the needed changes, and return the result for update to set. e.g.

export const setImmediate = () => {
  update((store: ApplicationContext) => {
    return {
      ...structuredClone(store),
      _immediate: true
    }
  })
}

It seems that if a store holds a reference to a reactive svelte object, structuredClone fails because of the whole proxy thing mentioned earlier in the ticket.

Unfortunately, we are unable to use $state.snapshot because these are not .svelte.ts files, so runes don't work in them. Is there a way to access this through an import instead? e.g.

import { snapshot } from 'svelte/state'

KieranP avatar Aug 04 '25 02:08 KieranP

I think you should be able to create a .svelte.js exporting a function that uses $state.snapshot() and then import and call that function from elsewhere in regular .js files.

Conduitry avatar Aug 04 '25 03:08 Conduitry

@Conduitry Interesting. I'll look into that, thanks

KieranP avatar Aug 04 '25 05:08 KieranP