@lukekaalim/act-web 4.0.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # @lukekaalim/act-web
2
2
 
3
+ ## 5.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Improve debugger support
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies
12
+ - @lukekaalim/act-recon@4.0.0
13
+ - @lukekaalim/act@4.2.0
14
+ - @lukekaalim/act-backstage@3.1.1
15
+
16
+ ## 5.0.0
17
+
18
+ ### Major Changes
19
+
20
+ - bcbd299: SSR API for @lukekaalim/act-web
21
+
22
+ ### Minor Changes
23
+
24
+ - beec21c: Fixed Web text element rehydration, remove nodejs dependencies from dehydration
25
+
26
+ ### Patch Changes
27
+
28
+ - Updated dependencies [bcbd299]
29
+ - Updated dependencies [beec21c]
30
+ - @lukekaalim/act-backstage@3.1.0
31
+ - @lukekaalim/act-recon@3.1.0
32
+ - @lukekaalim/act@4.1.0
33
+
3
34
  ## 4.0.0
4
35
 
5
36
  ### Major Changes
package/dehydrate.ts ADDED
@@ -0,0 +1,107 @@
1
+ import { Reconciler2, Scheduler } from '@lukekaalim/act-recon';
2
+ import { Root } from 'hast';
3
+ import { createHASTBuilder } from './hast';
4
+ import { RenderSpace2 } from '@lukekaalim/act-backstage';
5
+ import { ContextID, createElement, Element, h, Node, primitiveNodeTypes, specialNodeTypes } from '@lukekaalim/act';
6
+ import { HTML } from './space';
7
+ import { createTickScheduler } from './nodejs/TickScheduler';
8
+ import {
9
+ DehydratedCommit, RehydratableComponent, RehydratableProps,
10
+ serializeSSRContext, SSRContext, ssrSymbolToStringMap
11
+ } from './ssr';
12
+
13
+ export const dehydrate = async (
14
+ node: Node,
15
+ scheduler: Scheduler,
16
+ targets: { [key: string]: RehydratableComponent }
17
+ ) => {
18
+ const reverseTargetMap = new Map<RehydratableComponent, string>(
19
+ Object.entries(targets).map(([key, component]) => [component, key])
20
+ )
21
+
22
+ const root: Root = { type: 'root', children: [] }
23
+
24
+ const reconciler = new Reconciler2(scheduler);
25
+ const space = new RenderSpace2(reconciler.tree, createHASTBuilder(root));
26
+ reconciler.bus = space.bus;
27
+
28
+ const ssrContext: SSRContext = {
29
+ components: new Map(),
30
+ contexts: new Map(),
31
+ commits: new Map(),
32
+ commitIdRemap: new Map(),
33
+ mounts: [],
34
+
35
+ mode: 'server',
36
+ contextCommitID: null,
37
+ readyForServer() {}
38
+ };
39
+ const readyPromise = new Promise<void>(resolve => {
40
+ ssrContext.readyForServer = resolve
41
+ });
42
+ reconciler.mount(createElement(HTML, {}, h(SSRContext.Provider, { value: ssrContext }, node)));
43
+
44
+ // Either use the user-provided promise,
45
+ // or just wait until we finish rendering
46
+ await readyPromise;
47
+
48
+ const serializeElementType = (element: Element) => {
49
+ switch (typeof element.type) {
50
+ case 'symbol':
51
+ return ssrSymbolToStringMap[element.type] || 'special:unknown';
52
+ case 'function':
53
+ if (reverseTargetMap.has(element.type as RehydratableComponent))
54
+ return 'special:target:' + reverseTargetMap.get(element.type as RehydratableComponent) as string;
55
+ return 'special:placeholder';
56
+ case 'string':
57
+ return element.type;
58
+ default:
59
+ throw new Error(`Cannot serialize element type "${typeof element.type}"`);
60
+ }
61
+ }
62
+
63
+ for (const commit of reconciler.tree.commits.values()) {
64
+ const dehydrated: DehydratedCommit = {
65
+ id: commit.ref.id,
66
+ elementType: serializeElementType(commit.element),
67
+ parent: commit.ref.parent && commit.ref.parent.id || null,
68
+ key: commit.element.props['key'] as string || null,
69
+ children: commit.children.map(ref => ref.id),
70
+ distance: commit.ref.length,
71
+ props: [],
72
+ };
73
+ if (commit.element.type === specialNodeTypes.render) {
74
+ dehydrated.props.push(['type', commit.element.props.type as string])
75
+ }
76
+ switch (commit.element.type) {
77
+ case primitiveNodeTypes.number:
78
+ case primitiveNodeTypes.string:
79
+ case primitiveNodeTypes.null:
80
+ case primitiveNodeTypes.boolean:
81
+ dehydrated.props.push(['value', commit.element.props.value as string])
82
+ break;
83
+ default:
84
+ break;
85
+ }
86
+ if (commit.element.type === specialNodeTypes.provider) {
87
+ if (commit.element.props.id as ContextID === SSRContext.id) {
88
+ ssrContext.contextCommitID = commit.ref.id;
89
+ }
90
+ }
91
+ if (reverseTargetMap.has(commit.element.type as RehydratableComponent)) {
92
+ const name = reverseTargetMap.get(commit.element.type as RehydratableComponent) as string;
93
+
94
+ dehydrated.elementType = `special:mount:${name}`
95
+ const props = commit.element.props as RehydratableProps;
96
+ for (const [key, value] of Object.entries(props)) {
97
+ dehydrated.props.push([key, value])
98
+ }
99
+ }
100
+
101
+ ssrContext.commits.set(dehydrated.id, dehydrated);
102
+ }
103
+
104
+ const payload = serializeSSRContext(ssrContext);
105
+
106
+ return { payload, root };
107
+ }
@@ -1,8 +1,7 @@
1
1
  import { h, s } from 'hastscript';
2
- import { Element, Nodes as HNode, Root } from 'hast';
3
- import { NodeBuilder, RenderSpace2 } from "@lukekaalim/act-backstage";
2
+ import { Element, ElementContent, Nodes as HNode, Root } from 'hast';
3
+ import { NodeBuilder, setPropObject } from "@lukekaalim/act-backstage";
4
4
  import { primitiveNodeTypes } from '@lukekaalim/act';
5
- import { CommitTree2 } from '@lukekaalim/act-recon';
6
5
 
7
6
  export const createHASTBuilder = (root: Root): NodeBuilder<HNode, 'web:html' | 'web:svg'> => ({
8
7
  roots: new Set(['web:html', 'web:svg']),
@@ -57,13 +56,37 @@ export const createHASTBuilder = (root: Root): NodeBuilder<HNode, 'web:html' | '
57
56
  root.children.push(child);
58
57
  }
59
58
  },
60
- update(el, next, prev) {
59
+ update(el, next, prev, ref) {
61
60
  switch (el.type) {
62
61
  case 'text':
63
62
  el.value = (next.props.value as number | string | boolean).toString()
64
63
  return;
65
64
  case 'element':
65
+ el.properties['data-commit-id'] = ref.id;
66
+ setPropObject(el.properties, next.props, prev?.props || {}, (prop, next, prev) => {
67
+ // event handlers not supported
68
+ if (prop.startsWith('on'))
69
+ return true;
66
70
 
71
+ switch (prop) {
72
+ case 'style':
73
+ const style = Object.entries(next as {}).map(([key, value]) => `${key}: ${value};`).join(' ');
74
+ el.properties['style'] = style;
75
+ return true;
76
+ default:
77
+ // complex objects not supported
78
+ switch (typeof next) {
79
+ case 'object':
80
+ case 'function':
81
+ case 'undefined':
82
+ case 'symbol':
83
+ return true;
84
+ }
85
+ el.properties[prop as string] = next as string;
86
+ return true;
87
+ }
88
+ })
89
+ return;
67
90
  }
68
91
  },
69
92
  unlink(child, parent) {
@@ -73,19 +96,15 @@ export const createHASTBuilder = (root: Root): NodeBuilder<HNode, 'web:html' | '
73
96
  },
74
97
  sort(el, children) {
75
98
  if ("children" in el) {
76
- el.children = children as Element[];
99
+ el.children.length = 0;
100
+ for (let i = 0; i < children.length; i++) {
101
+ const child = children[i];
102
+ (el.children as ElementContent[]).push(child as Element);
103
+ const nextChild = children[i + 1];
104
+ if (child.type === 'text' && nextChild && nextChild.type === 'text') {
105
+ el.children.push({ type: 'comment', value: '' })
106
+ }
107
+ }
77
108
  }
78
109
  },
79
- })
80
-
81
- type DehydratedRender = {
82
-
83
- }
84
-
85
- export const dehydrate = (tree: CommitTree2, renderer: RenderSpace2<HNode, string | symbol>) => {
86
-
87
- }
88
-
89
- export const rehydrate = () => {
90
-
91
- }
110
+ })
package/mod.ts CHANGED
@@ -3,4 +3,9 @@ export * from './element.ts';
3
3
  export * from './props.ts';
4
4
  export * from './render.ts';
5
5
  export * from './scheduler.ts';
6
- export * from './server.ts';
6
+
7
+ export * from './rehydrate.ts';
8
+ export * from './dehydrate.ts';
9
+
10
+ export * from './hast.ts';
11
+ export * from './ssr.ts';
@@ -0,0 +1,35 @@
1
+ import { Scheduler } from '@lukekaalim/act-recon';
2
+ import { nextTick } from 'node:process';
3
+
4
+ export const createTickScheduler = (): Scheduler => {
5
+ let needs_callback = false;
6
+ let callback_pending = false;
7
+ let callback = () => {};
8
+
9
+ const run = () => {
10
+ while (needs_callback) {
11
+ needs_callback = false;
12
+ callback();
13
+ }
14
+ callback_pending = false
15
+ }
16
+
17
+ return {
18
+ setCallbackFunc(nextCallback) {
19
+ callback = nextCallback;
20
+ },
21
+ requestCallback() {
22
+ needs_callback = true;
23
+ if (!callback_pending) {
24
+ callback_pending = true;
25
+ nextTick(run)
26
+ }
27
+ },
28
+ cancelCallback() {
29
+ needs_callback = false;
30
+ },
31
+ isCallbackPending() {
32
+ return needs_callback;
33
+ },
34
+ }
35
+ }
@@ -0,0 +1 @@
1
+ export * from './TickScheduler.ts';
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@lukekaalim/act-web",
3
- "version": "4.0.0",
3
+ "version": "5.1.0",
4
4
  "type": "module",
5
5
  "main": "mod.ts",
6
+ "exports": {
7
+ ".": "./mod.ts",
8
+ "./node": "./nodejs/index.ts"
9
+ },
6
10
  "dependencies": {
7
- "@lukekaalim/act": "^4.0.0",
8
- "@lukekaalim/act-backstage": "^3.0.0",
9
- "@lukekaalim/act-recon": "^3.0.0",
11
+ "@lukekaalim/act": "^4.2.0",
12
+ "@lukekaalim/act-backstage": "^3.1.1",
13
+ "@lukekaalim/act-recon": "^4.0.0",
10
14
  "@types/hast": "^3.0.4",
11
15
  "hast-util-to-html": "^9.0.5",
12
16
  "hastscript": "^9.0.1",
package/rehydrate.ts ADDED
@@ -0,0 +1,144 @@
1
+ import { createId, Element, ElementType, h, primitiveNodeTypes, specialNodeTypes } from "@lukekaalim/act";
2
+ import { Commit2, CommitID, CommitRef2, Reconciler2 } from "@lukekaalim/act-recon";
3
+ import { deserializeSSRPayload, RehydratableComponent, SSRContext, SSRPayload, ssrStringToSymbolMap } from "./ssr";
4
+ import { RenderSpace2 } from "@lukekaalim/act-backstage";
5
+ import { recon } from "../three/deps";
6
+
7
+
8
+ export const rehydrate = (
9
+ targets: { [key: string]: RehydratableComponent },
10
+ reconciler: Reconciler2,
11
+ space: RenderSpace2<HTMLElement | SVGElement | Text, 'web:html' | 'web:svg'>,
12
+ payload: SSRPayload
13
+ ) => {
14
+ const context = deserializeSSRPayload(payload, "client", () => {});
15
+
16
+ const refs = new Map<CommitID, CommitRef2>();
17
+ const elements = new Map<CommitID, Element>();
18
+ const roots = [] as CommitRef2[]
19
+ const targetRefs = [] as CommitRef2[]
20
+
21
+ for (const commit of context.commits.values()) {
22
+ const ref = CommitRef2.rehydrate(createId("CommitID"), commit.distance);
23
+ refs.set(commit.id, ref);
24
+ context.commitIdRemap.set(ref.id, commit.id);
25
+
26
+ if (commit.elementType.startsWith('special:mount:')) {
27
+ const targetName = commit.elementType.slice('special:mount:'.length);
28
+ const target = targets[targetName];
29
+
30
+ const props = Object.fromEntries(commit.props);
31
+ const element = h(target, props);
32
+ elements.set(commit.id, element);
33
+
34
+ targetRefs.push(ref);
35
+ } else {
36
+ const elementType = (ssrStringToSymbolMap[commit.elementType]
37
+ || commit.elementType
38
+ || specialNodeTypes.placeholder) as string | symbol
39
+
40
+ switch (elementType) {
41
+ case specialNodeTypes.provider:
42
+ if (commit.id === context.contextCommitID) {
43
+ elements.set(commit.id, h(specialNodeTypes.provider, { id: SSRContext.id, value: context }));
44
+ reconciler.tree.contexts.set(ref.id, {
45
+ id: ref.id,
46
+ contextId: SSRContext.id,
47
+ value: context,
48
+ consumers: new Map()
49
+ });
50
+ continue;
51
+ }
52
+ default:
53
+ const props = Object.fromEntries(commit.props);
54
+ elements.set(commit.id, h(elementType, props));
55
+ continue;
56
+ }
57
+ }
58
+ }
59
+
60
+ const primitiveCommits: Commit2[] = []
61
+
62
+ for (const dehydratedCommit of context.commits.values()) {
63
+ const ref = refs.get(dehydratedCommit.id) as CommitRef2;
64
+ ref.parent = (dehydratedCommit.parent && refs.get(dehydratedCommit.parent)) || null;
65
+
66
+ const children = dehydratedCommit.children.map(c => refs.get(c)).filter(x => !!x);
67
+ const element = elements.get(dehydratedCommit.id) as Element;
68
+ element.children = children.map(c => elements.get(c.id) as Element)
69
+
70
+ if (!ref.parent) {
71
+ roots.push(ref);
72
+ }
73
+
74
+ const commit = new Commit2(ref,
75
+ element,
76
+ children
77
+ );
78
+
79
+ switch (commit.element.type) {
80
+ case primitiveNodeTypes.string:
81
+ case primitiveNodeTypes.boolean:
82
+ case primitiveNodeTypes.number:
83
+ primitiveCommits.push(commit);
84
+ }
85
+
86
+ const node = document.querySelector(`[data-commit-id="${dehydratedCommit.id}"]`)
87
+ if (node instanceof HTMLElement) {
88
+ space.nodeByCommit.set(commit.ref.id, node);
89
+ space.commitByNode.set(node, commit);
90
+ }
91
+ if (commit.element.type === specialNodeTypes.render) {
92
+ space.roots.set(commit.ref.id, commit);
93
+ }
94
+
95
+ reconciler.tree.commits.set(commit.ref.id, commit)
96
+ }
97
+
98
+ const primitiveParent = new Map<CommitID, Commit2>();
99
+
100
+ for (const primitiveCommit of primitiveCommits) {
101
+ const parent = space.findParent(primitiveCommit.ref)
102
+ if (parent.node && parent.commit) {
103
+ primitiveParent.set(parent.commit.ref.id, parent.commit)
104
+ }
105
+ }
106
+
107
+ for (const [id, commit] of primitiveParent) {
108
+ const node = space.nodeByCommit.get(id);
109
+ if (!node)
110
+ continue;
111
+
112
+ const textElements = [...node.childNodes]
113
+ .filter((x): x is Text => x instanceof Text)
114
+
115
+
116
+ let queue = [...commit.children];
117
+ let index = 0;
118
+
119
+ let subject: CommitRef2 | undefined;
120
+ while (subject = queue.shift()) {
121
+ const commit = reconciler.tree.commits.get(subject.id) as Commit2;
122
+ if (space.nodeByCommit.has(commit.ref.id))
123
+ continue;
124
+
125
+ switch (commit.element.type) {
126
+ case primitiveNodeTypes.string:
127
+ case primitiveNodeTypes.boolean:
128
+ case primitiveNodeTypes.number:
129
+ space.nodeByCommit.set(commit.ref.id, textElements[index]);
130
+ index++;
131
+ continue;
132
+ }
133
+ queue.unshift(...commit.children)
134
+ }
135
+ }
136
+
137
+ reconciler.bus.render = (delta) => {
138
+ space.bus.render(delta)
139
+ };
140
+
141
+ for (const ref of targetRefs.sort((left, right) => left.length - right.length)) {
142
+ reconciler.render(ref);
143
+ }
144
+ }
package/render.ts CHANGED
@@ -9,6 +9,7 @@ import { Reconciler2 } from "@lukekaalim/act-recon";
9
9
  */
10
10
  type Options = {
11
11
  window?: Window;
12
+ Reconciler?: typeof Reconciler2,
12
13
  }
13
14
 
14
15
  /**
@@ -22,7 +23,7 @@ type Options = {
22
23
  */
23
24
  export const render = (node: Node, root: HTMLElement, options: Options = {}) => {
24
25
  const scheduler = createDOMScheduler();
25
- const reconciler = new Reconciler2(scheduler);
26
+ const reconciler = new (options.Reconciler || Reconciler2)(scheduler);
26
27
  const space = new RenderSpace2(reconciler.tree, createWebNodeBuilder(root, options.window));
27
28
 
28
29
  reconciler.bus = space.bus;
package/scheduler.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Scheduler } from "@lukekaalim/act-recon";
2
2
 
3
3
  export const createDOMScheduler = (): Scheduler => {
4
- let id: number | null = null;
4
+ let id: number | null | NodeJS.Timeout = null;
5
5
  let callbackFunc = () => console.error(`DOMScheduler got callback before callback function was configured`)
6
6
  let synccall_available = false;
7
7
  let synccall_requested = false;
@@ -42,12 +42,12 @@ export const createDOMScheduler = (): Scheduler => {
42
42
  synccall_requested = true;
43
43
  }
44
44
  else if (!id) {
45
- id = window.setTimeout(onTimeout, 0);
45
+ id = globalThis.setTimeout(onTimeout, 0);
46
46
  }
47
47
  },
48
48
  cancelCallback() {
49
49
  if (id !== null)
50
- window.clearTimeout(id);
50
+ globalThis.clearTimeout(id);
51
51
  },
52
52
  }
53
53
  }
package/space.ts CHANGED
@@ -6,7 +6,7 @@ import { NodeBuilder } from '@lukekaalim/act-backstage';
6
6
  export const HTML: act.Component = ({ children }) => act.h(act.renderNodeType, { type: 'web:html' }, children);
7
7
  export const SVG: act.Component = ({ children }) => act.h(act.renderNodeType, { type: 'web:svg' }, children);
8
8
 
9
- const defaultWindow = window;
9
+ const defaultWindow = (globalThis.window);
10
10
 
11
11
  export const createWebNodeBuilder = (
12
12
  root: HTMLElement,
@@ -67,6 +67,10 @@ export const createWebNodeBuilder = (
67
67
  sort(el, newChildren) {
68
68
  if (el instanceof Text)
69
69
  return;
70
+
71
+ for (const child of el.childNodes)
72
+ if (!newChildren.includes(child as HTMLElement))
73
+
70
74
  if (newChildren.length < 2)
71
75
  return;
72
76
 
package/ssr.ts ADDED
@@ -0,0 +1,278 @@
1
+ import {
2
+ calculateDepsChange,
3
+ Component, Context, createContext, EffectConstructor, h, primitiveNodeTypes, runUpdater, specialNodeTypes, StateSetter, Updater,
4
+ useContext, useEffect, useMemo, useRef, useState, ValueOrCalculator
5
+ } from "@lukekaalim/act";
6
+
7
+ import { CommitID, ComponentState, HookID, useInternalComponentState } from "@lukekaalim/act-recon";
8
+
9
+ export type SSRComponentData = {
10
+ id: CommitID,
11
+ values: Map<HookID, JSONValue>,
12
+ deps: Map<HookID, PrimitiveJSONValue[]>,
13
+ };
14
+
15
+ export type SSRContext = {
16
+ components: Map<CommitID, SSRComponentData>,
17
+ contexts: Map<CommitID, JSONValue>,
18
+
19
+ commits: Map<CommitID, DehydratedCommit>,
20
+ commitIdRemap: Map<CommitID, CommitID>,
21
+
22
+ mounts: CommitID[],
23
+
24
+ contextCommitID: CommitID | null,
25
+
26
+ mode: 'server' | 'client',
27
+
28
+ readyForServer(): void
29
+ };
30
+ // the serializable form of the SSR context
31
+ export type SSRPayload = {
32
+ contextCommitID: CommitID | null,
33
+ commits: DehydratedCommit[],
34
+ mounts: CommitID[],
35
+ components: SSRComponentPayload[],
36
+ contexts: [CommitID, JSONValue][],
37
+ commitIdRemap: [CommitID, CommitID][]
38
+ }
39
+ export type SSRComponentPayload = {
40
+ id: CommitID,
41
+ values: [HookID, JSONValue][],
42
+ deps: [HookID, PrimitiveJSONValue[]][],
43
+ }
44
+ export const serializeSSRContext = (context: SSRContext): SSRPayload => {
45
+ return {
46
+ contextCommitID: context.contextCommitID,
47
+ commits: [...context.commits.values()],
48
+ mounts: context.mounts,
49
+ commitIdRemap: [...context.commitIdRemap.entries()],
50
+ components: [...context.components.values()].map(component => {
51
+ return {
52
+ id: component.id,
53
+ values: [...component.values.entries()],
54
+ deps: [...component.deps.entries()],
55
+ } as SSRComponentPayload
56
+ }),
57
+ contexts: [...context.contexts.entries()]
58
+ }
59
+ }
60
+
61
+ export const deserializeSSRPayload = (payload: SSRPayload, mode: 'server' | 'client', readyForServer: () => void): SSRContext => {
62
+ return {
63
+ contextCommitID: payload.contextCommitID,
64
+ mode,
65
+ readyForServer,
66
+ commitIdRemap: new Map(payload.commitIdRemap),
67
+ commits: new Map(payload.commits.map(c => [c.id, c])),
68
+ mounts: payload.mounts,
69
+ components: new Map(payload.components.map(c => ([c.id, {
70
+ id: c.id,
71
+ values: new Map(c.values),
72
+ deps: new Map(c.deps),
73
+ } as SSRComponentData]))),
74
+ contexts: new Map(payload.contexts)
75
+ }
76
+ }
77
+
78
+ export const SSRContext = createContext<SSRContext | null>(null);
79
+
80
+ const useSSRComponentState = (data: SSRContext, state: ComponentState) => {
81
+ return useMemo(() => {
82
+ let id: CommitID;
83
+ if (data.mode === 'client') {
84
+ const remappedId = data.commitIdRemap.get(state.ref.id);
85
+ if (!remappedId)
86
+ throw new Error(`Missing map from Server CommitID => Client CommitID for ${state.ref.id}`);
87
+ id = remappedId;
88
+ } else {
89
+ id = state.ref.id;
90
+ }
91
+
92
+ let ssrComponentState = data.components.get(id);
93
+ if (!ssrComponentState) {
94
+ ssrComponentState = {
95
+ id,
96
+ values: new Map(),
97
+ deps: new Map(),
98
+ }
99
+ data.components.set(id, ssrComponentState);
100
+ }
101
+
102
+ return ssrComponentState;
103
+ }, []);
104
+ }
105
+
106
+ export const ssr = {
107
+ useState<T extends JSONValue>(initialValue: ValueOrCalculator<T>): [value: T, setter: StateSetter<T>] {
108
+ const state = useInternalComponentState();
109
+ const ssrData = ssr.useSSRContext();
110
+ const ssrComponentState = useSSRComponentState(ssrData, state);
111
+
112
+ // this value will literally change
113
+ // as we go down these other hooks,
114
+ // but should stay "relatively" stable
115
+ const hookIndex = state.hookIndex;
116
+
117
+ // If we are a client, try to get the server's copy of the value
118
+ const initialServerValue = useMemo(() => {
119
+ if (ssrData.mode === 'server')
120
+ return initialValue;
121
+
122
+ if (!ssrComponentState.values.has(hookIndex))
123
+ return initialValue;
124
+
125
+ return ssrComponentState.values.get(hookIndex) as ValueOrCalculator<T>;
126
+ }, [])
127
+
128
+ // The inner setState
129
+ const [value, innerSetter] = useState<T>(initialServerValue);
130
+
131
+ const setter = useMemo(() => {
132
+ if (ssrData.mode === 'client')
133
+ return innerSetter;
134
+
135
+ // intercept updates on server to write to SSRComponentData
136
+ return (updater: Updater<T>) => {
137
+ innerSetter(prevValue => {
138
+ const nextValue = runUpdater<T>(prevValue, updater);
139
+ ssrComponentState.values.set(hookIndex, nextValue);
140
+ return nextValue;
141
+ });
142
+ }
143
+ }, []);
144
+
145
+ return [value, setter]
146
+ },
147
+ useEffect(effect: EffectConstructor, deps: PrimitiveJSONValue[]) {
148
+ const liveState = useInternalComponentState();
149
+ const ssrData = ssr.useSSRContext();
150
+ const ssrComponentState = useSSRComponentState(ssrData, liveState);
151
+
152
+ const hookIndex = liveState.hookIndex;
153
+
154
+ let firstRun = useRef(true);
155
+
156
+ useMemo(() => {
157
+ if (ssrData.mode === 'client')
158
+ return;
159
+
160
+ // record the last server set of deps
161
+ ssrComponentState.deps.set(hookIndex, deps);
162
+ }, deps)
163
+
164
+ useEffect(() => {
165
+ if (!firstRun.current || ssrData.mode === 'server')
166
+ return effect();
167
+
168
+ firstRun.current = false;
169
+
170
+ // for the very first run on a client, check against
171
+ // the server props to see if we need to re-render
172
+ const prevDeps = ssrComponentState.deps.get(hookIndex);
173
+
174
+ // only run the effect if the deps change (or if we don't have a copy of past deps anyway)
175
+ if (!prevDeps || calculateDepsChange(prevDeps, deps)) {
176
+ return effect();
177
+ }
178
+ }, deps);
179
+
180
+ },
181
+ // This needs special rehydration efforts to restore
182
+ createContext<T extends JSONValue>(defaultValue: T): Context<T> {
183
+ const innerContext = createContext(defaultValue);
184
+
185
+ return {
186
+ ...innerContext,
187
+ Provider({ value, children }) {
188
+ const liveState = useInternalComponentState();
189
+ const ssrData = ssr.useSSRContext();
190
+
191
+ useMemo(() => {
192
+ if (ssrData.mode === "client")
193
+ return;
194
+ // record the value
195
+ ssrData.contexts.set(liveState.ref.id, value);
196
+ }, [value]);
197
+
198
+ return h(specialNodeTypes.provider, { id: innerContext.id, value }, children);
199
+ },
200
+ }
201
+ },
202
+ useSSRReady() {
203
+ const ssrData = ssr.useSSRContext();
204
+
205
+ return ssrData.readyForServer;
206
+ },
207
+ useSSRContext() {
208
+ const ssrContext = useContext(SSRContext);
209
+ if (!ssrContext)
210
+ throw new Error('Missing SSR Bundle Context!')
211
+ return ssrContext;
212
+ },
213
+ };
214
+
215
+
216
+ export type DehydratedCommit = {
217
+ id: CommitID,
218
+ elementType: string,
219
+ parent: CommitID | null,
220
+ distance: number,
221
+ key: string | null,
222
+ children: CommitID[],
223
+ props: [string, JSONValue][],
224
+ }
225
+
226
+ export type DehydratedTree = {
227
+ commits: DehydratedCommit[],
228
+ mounts: CommitID[],
229
+ }
230
+
231
+ export type PrimitiveJSONValue =
232
+ | string
233
+ | number
234
+ | boolean
235
+ | null
236
+
237
+ export type CompoundJSONValue =
238
+ | ReadonlyArray<JSONValue>
239
+ | { readonly [key: string]: JSONValue }
240
+
241
+ export type JSONValue =
242
+ | PrimitiveJSONValue
243
+ | CompoundJSONValue;
244
+
245
+ export type RehydratableProps = { [key: string]: JSONValue };
246
+
247
+ export type RehydratableComponent = Component<RehydratableProps>;
248
+
249
+ export const ssrStringToSymbolMap: Record<string, symbol> = {
250
+ 'primitive:string': primitiveNodeTypes.string,
251
+ 'primitive:number': primitiveNodeTypes.number,
252
+ 'primitive:null': primitiveNodeTypes.null,
253
+ 'primitive:boolean': primitiveNodeTypes.boolean,
254
+ 'primitive:array': primitiveNodeTypes.array,
255
+
256
+
257
+ [primitiveNodeTypes.string]: 'primitive:string',
258
+ [primitiveNodeTypes.number]: 'primitive:number',
259
+ [primitiveNodeTypes.null]: 'primitive:null',
260
+ [primitiveNodeTypes.boolean]: 'primitive:boolean',
261
+ [primitiveNodeTypes.array]: 'primitive:array',
262
+
263
+ 'special:placeholder': specialNodeTypes.placeholder,
264
+ 'special:boundary': specialNodeTypes.boundary,
265
+ 'special:fallback': specialNodeTypes.fallback,
266
+ 'special:provider': specialNodeTypes.provider,
267
+ 'special:suspend': specialNodeTypes.suspend,
268
+ 'special:render': specialNodeTypes.render,
269
+
270
+ [specialNodeTypes.placeholder]: 'special:placeholder',
271
+ [specialNodeTypes.boundary]: 'special:boundary',
272
+ [specialNodeTypes.fallback]: 'special:fallback',
273
+ [specialNodeTypes.provider]: 'special:provider',
274
+ [specialNodeTypes.suspend]: 'special:suspend',
275
+ [specialNodeTypes.render]: 'special:render',
276
+ }
277
+ export const ssrSymbolToStringMap: Record<symbol, string> = Object.fromEntries(Object.entries(ssrStringToSymbolMap)
278
+ .map(([key, value]) => [value, key]))