# Serialization of values Serialization is the key to resumability. If we want to pick up where the server left off, we need to be able to serialize the state of the application. HTML serves as the serialization format for the DOM, but we also need to serialize: - Event handlers - Mount effects - Intersecting values Marko doesn't have an explicit API for serialization. It's supposed to _just work™️_. This is really important to get right as it's not going to be immediately clear which values get serialized and which don't. However there are a number of challenges for reliable serialization. ## Primitive values The easy/straight-forward ones (just `JSON.stringify`) - `string` - `number` - `boolean` - `null` - `undefined` ## Global constructors - `Symbol` - `Object` - `Array` - `Date` - `RegExp` - `Map` - `Set` - etc. we'll need code paths to handle all supported values, but this would be server only (unless we decide to switch to non-script based serialization method) ## Promises Generate an ID and immediately serialize a promise which goes into a lookup. Later when the promise resolves/rejects, flush a script that finds the promise by ID and resolves it. This means the serializer would need to be able to hold a connection open. ## Functions Functions can be stringified, however this only works if the function doesn't close over any non-global values and this causes [duplication]() of source code if the serialized function also appears in the bundle (though it avoids the [registration challenge]()). **Basically every polyfill tool injects random references to utilities that would break this.** For functions that do close over values, if they're part of the `.marko` template, we can rewrite the function to read from a scope object instead of a closure, then serialize the scope and associate it with the function upon deserialization (in theory we could stringify this function as well, however the current implementation uses registration). For functions that close over values and come from outside the template, we cannot serialize them. But we may be able to [replay]() them. ## Replay Values ``` import Dog from "./Dog"; <let/name = "Spot"/> <const/dog = new Dog(name)/> ``` becomes something like ``` import Dog from "./Dog"; register("Dog_32o4", Dog); <let/name = "Spot"/> <const/dog = replayable(new Dog(name), (serialize) => `(new ${serialize(Dog)}(${serialize(name)}))`))/> <const/x = replayable(dog.x, (serialize) => `${serialize(dog)}.x`)/> ``` `// (new b("Dog_32o4")("Spot")).x` The challenge with this is nearly every expression would need to be transformed this way. For most expressions we won't reliably know at compile-time whether an expression can go down the happy path (primitive/global constructors) or will need to be replayed. We could potentially leverage type information, but that's not reliable and types aren't expected to change the way your application runs. ## Static Values In many cases static values don't need to be serialized since code will just close over the value. But a static value could be passed to another template and need to be serialized there: ``` import Foo from "./foo"; <child foo=Foo/> ``` It may also need to be serializable if it's used by a replayable value and we go down the route of stringifying the replay function. By registering the value, we can allow it to be serialized: ``` import Foo from "./foo"; static register("Foo_238jd", Foo); <child foo=Foo/> ``` --- ## Registration & Treeshaking When something is registered, it's made available to the serialized _in case_ the value is serialized. This means we can't tree-shake it from the bundle. Certain patterns (static registration/replay registration/renderer registration) might mean a _lot_ of things get registered, pulling in code that should never have been sent to the browser. ### Alternative to registration Rather than serializing a registration ID and expecting the bundle to contain the registered object, serialize a chunk url and dynamically import the object only when it is needed. We should be able to serialize these urls in such a way that for each hydration script, all the urls necessary are present and can be requested in parallel before continuing deserialization. No waterfalls and only the initial deserialization is async (which should be fine because it can be async anyways if it is waiting for the bundle with the registrations). Downside: we may not discover something we need until later in the request. Could have been fetching the chunk earlier. We might be able to flush preloads in prior flushes if something is blocked by in-order streaming. We probably also want to include a small inline runtime to ensure these chunk requests get kicked off immediately upon the inline script executing without waiting for the main bundle to load. Because replayability can't be determined until runtime, we'd end up with many many entry points which might be a concern. ## Duplication between bundle & serialized data If I have `<let/count=0/>` in my bundle, it seems like the serialized data shouldn't also need `{count:0}`. ## Mutations If something is mutated after it's serialized, how do we handle that? If something is registered, but gets mutated by code that runs during rendering, how do we handle that (since we won't re-run that code or know how/when we should)? Our current answer is "we don't". What's the impact to the developer if we don't handle this? ## Side effects ```js import "setup-the-thing"; ``` ```js import {setup} from "the-thing"; static setup(); ``` ## Security One of the benefits of a full-stack solution where certain components run only on the server is that you can just make direct calls to databases or inline API keys without exposing them to the client. But in Marko 6, the boundaries are no-longer component based. Can we make it possible to mark server only sections/expressions and fail if something would cause them to get sent to the browser? What does that look like? # `<serialize>` The `<serialize>` tag is passed an expression and for pure server-rendering and pure client rendering behaves as a `<const>` tag. In the most basic form, `<serialize>` will re-execute the `value` passed to it client-side to perform deserialization. ``` <serialize/foo = new Foo() /> ``` You can also pass `encode` and `decode` methods. The encode method will be called server-side and passed the `value`. The result of `encode` (which itself must be serializable), will be passed to `decode` on the client which should return the original value. ``` <serialize/r = new Random() encode(r) { return r.seed; } decode(seed) { return new Random(seed); } /> ``` ## Or ``` <let/unserializableCount=0 replay /> <const/foo = new Foo() replay /> ``` ``` <const/foo = new Foo() serialize() {} deserialize() {} /> ``` # How to resolve serialization issues ## Unserializable `const` from import ```marko import Foo from "./Foo.js" let/count = 0 const/foo = new Foo(count) -- ${count + foo.count} button onClick() { count++ } -- ${count} ``` ### Fix with `<serialize>` ```marko import Foo from "./Foo.js" let/count = 0 serialize/foo = new Foo(count) -- ${count + foo.count} button onClick() { count++ } -- ${count} ``` ### Fix with extra attributes ```marko import Foo from "./Foo.js" let/count = 0 const/foo = new Foo(count) replay -- ${count + foo.count} button onClick() { count++ } -- ${count} ``` ## Unserializable `let` from import ```marko import Foo from "./Foo.js" let/count = 0 let/foo = new Foo(count) -- ${count + foo.count} button onClick() { count++ } -- ${count} button onClick() { foo = new Foo(count) } -- update foo ``` ## Unserializable `const` from static value ```marko static class Foo { count = 0; } let/count = 0 const/foo = new Foo(count) -- ${count + foo.count} button onClick() { count++ } -- ${count} ``` # Auto replay logic An expression is marked for replay if: It uses a non-whitelisted global/static/imported value AND... 1. The value or a value derived from it is serialized 2. There is no `BinaryExpression` or `UnaryExpression` between the value and where it is serialized ## Whitelisted values In any context: - Math.PI - Number.NaN - etc. As the `callee` in a `CallExpresssion`: - Math.random - Object.assign - Object.keys - Object.values - Array.isArray - etc. _foo.js_ ```js export const makeFoo = () => { return { s1: "hi", s2: "world", m1() { }, m2() { } } } export const makeBar = () => { return { s1: "hi", s2: "world", } } ``` ```marko import { makeFoo } from "./foo.js" <const/foo = makeFoo()/> <effect() { console.log(foo); }/> ```