@lukekaalim/act-web 4.0.0 → 5.0.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 +18 -0
- package/dehydrate.ts +107 -0
- package/hast.ts +114 -0
- package/mod.ts +6 -1
- package/nodejs/TickScheduler.ts +35 -0
- package/nodejs/index.ts +1 -0
- package/package.json +8 -4
- package/rehydrate.ts +144 -0
- package/scheduler.ts +3 -3
- package/space.ts +5 -1
- package/ssr.ts +278 -0
- package/server.ts +0 -91
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @lukekaalim/act-web
|
|
2
2
|
|
|
3
|
+
## 5.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- bcbd299: SSR API for @lukekaalim/act-web
|
|
8
|
+
|
|
9
|
+
### Minor Changes
|
|
10
|
+
|
|
11
|
+
- beec21c: Fixed Web text element rehydration, remove nodejs dependencies from dehydration
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [bcbd299]
|
|
16
|
+
- Updated dependencies [beec21c]
|
|
17
|
+
- @lukekaalim/act-backstage@3.1.0
|
|
18
|
+
- @lukekaalim/act-recon@3.1.0
|
|
19
|
+
- @lukekaalim/act@4.1.0
|
|
20
|
+
|
|
3
21
|
## 4.0.0
|
|
4
22
|
|
|
5
23
|
### 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
|
+
}
|
package/hast.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { h, s } from 'hastscript';
|
|
2
|
+
import { Element, ElementContent, Nodes as HNode, Root } from 'hast';
|
|
3
|
+
import { NodeBuilder, RenderSpace2, setPropObject } from "@lukekaalim/act-backstage";
|
|
4
|
+
import { Node, primitiveNodeTypes, createElement } from '@lukekaalim/act';
|
|
5
|
+
import { CommitID, CommitTree2, Reconciler2, Scheduler } from '@lukekaalim/act-recon';
|
|
6
|
+
import { createDOMScheduler } from './scheduler';
|
|
7
|
+
import { recon } from '../three/deps';
|
|
8
|
+
import { HTML } from './space';
|
|
9
|
+
|
|
10
|
+
export const createHASTBuilder = (root: Root): NodeBuilder<HNode, 'web:html' | 'web:svg'> => ({
|
|
11
|
+
roots: new Set(['web:html', 'web:svg']),
|
|
12
|
+
|
|
13
|
+
create(element, root): HNode | null {
|
|
14
|
+
switch (element.type) {
|
|
15
|
+
case primitiveNodeTypes.string:
|
|
16
|
+
case primitiveNodeTypes.string:
|
|
17
|
+
case primitiveNodeTypes.number:
|
|
18
|
+
return { type: 'text', value: '' };
|
|
19
|
+
default: {
|
|
20
|
+
switch (typeof element.type) {
|
|
21
|
+
case 'string':
|
|
22
|
+
switch (root) {
|
|
23
|
+
case 'web:html':
|
|
24
|
+
return h(element.type);
|
|
25
|
+
case 'web:svg':
|
|
26
|
+
return s(element.type);
|
|
27
|
+
}
|
|
28
|
+
default:
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
link(child, parent) {
|
|
35
|
+
switch (child.type) {
|
|
36
|
+
case 'element':
|
|
37
|
+
case 'comment':
|
|
38
|
+
case 'text':
|
|
39
|
+
switch (parent.type) {
|
|
40
|
+
case 'element':
|
|
41
|
+
case 'root':
|
|
42
|
+
parent.children.push(child);
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
case 'doctype':
|
|
46
|
+
switch (parent.type) {
|
|
47
|
+
case 'root':
|
|
48
|
+
parent.children.push(child);
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
default:
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
linkRoot(child) {
|
|
55
|
+
switch (child.type) {
|
|
56
|
+
case 'doctype':
|
|
57
|
+
case 'element':
|
|
58
|
+
case 'comment':
|
|
59
|
+
case 'text':
|
|
60
|
+
root.children.push(child);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
update(el, next, prev, ref) {
|
|
64
|
+
switch (el.type) {
|
|
65
|
+
case 'text':
|
|
66
|
+
el.value = (next.props.value as number | string | boolean).toString()
|
|
67
|
+
return;
|
|
68
|
+
case 'element':
|
|
69
|
+
el.properties['data-commit-id'] = ref.id;
|
|
70
|
+
setPropObject(el.properties, next.props, prev?.props || {}, (prop, next, prev) => {
|
|
71
|
+
// event handlers not supported
|
|
72
|
+
if (prop.startsWith('on'))
|
|
73
|
+
return true;
|
|
74
|
+
|
|
75
|
+
switch (prop) {
|
|
76
|
+
case 'style':
|
|
77
|
+
const style = Object.entries(next as {}).map(([key, value]) => `${key}: ${value};`).join(' ');
|
|
78
|
+
el.properties['style'] = style;
|
|
79
|
+
return true;
|
|
80
|
+
default:
|
|
81
|
+
// complex objects not supported
|
|
82
|
+
switch (typeof next) {
|
|
83
|
+
case 'object':
|
|
84
|
+
case 'function':
|
|
85
|
+
case 'undefined':
|
|
86
|
+
case 'symbol':
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
el.properties[prop as string] = next as string;
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
unlink(child, parent) {
|
|
97
|
+
if ("children" in parent) {
|
|
98
|
+
parent.children = parent.children.filter(c => c !== child);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
sort(el, children) {
|
|
102
|
+
if ("children" in el) {
|
|
103
|
+
el.children.length = 0;
|
|
104
|
+
for (let i = 0; i < children.length; i++) {
|
|
105
|
+
const child = children[i];
|
|
106
|
+
(el.children as ElementContent[]).push(child as Element);
|
|
107
|
+
const nextChild = children[i + 1];
|
|
108
|
+
if (child.type === 'text' && nextChild && nextChild.type === 'text') {
|
|
109
|
+
el.children.push({ type: 'comment', value: '' })
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
})
|
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
|
-
|
|
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
|
+
}
|
package/nodejs/index.ts
ADDED
|
@@ -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": "
|
|
3
|
+
"version": "5.0.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.
|
|
8
|
-
"@lukekaalim/act-backstage": "^3.
|
|
9
|
-
"@lukekaalim/act-recon": "^3.
|
|
11
|
+
"@lukekaalim/act": "^4.1.0",
|
|
12
|
+
"@lukekaalim/act-backstage": "^3.1.0",
|
|
13
|
+
"@lukekaalim/act-recon": "^3.1.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 = reconciler.pools.commit.acquire(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/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 =
|
|
45
|
+
id = globalThis.setTimeout(onTimeout, 0);
|
|
46
46
|
}
|
|
47
47
|
},
|
|
48
48
|
cancelCallback() {
|
|
49
49
|
if (id !== null)
|
|
50
|
-
|
|
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]))
|
package/server.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { h, s } from 'hastscript';
|
|
2
|
-
import { Element, Nodes as HNode, Root } from 'hast';
|
|
3
|
-
import { NodeBuilder, RenderSpace2 } from "@lukekaalim/act-backstage";
|
|
4
|
-
import { primitiveNodeTypes } from '@lukekaalim/act';
|
|
5
|
-
import { CommitTree2 } from '@lukekaalim/act-recon';
|
|
6
|
-
|
|
7
|
-
export const createHASTBuilder = (root: Root): NodeBuilder<HNode, 'web:html' | 'web:svg'> => ({
|
|
8
|
-
roots: new Set(['web:html', 'web:svg']),
|
|
9
|
-
|
|
10
|
-
create(element, root): HNode | null {
|
|
11
|
-
switch (element.type) {
|
|
12
|
-
case primitiveNodeTypes.string:
|
|
13
|
-
case primitiveNodeTypes.string:
|
|
14
|
-
case primitiveNodeTypes.number:
|
|
15
|
-
return { type: 'text', value: '' };
|
|
16
|
-
default: {
|
|
17
|
-
switch (typeof element.type) {
|
|
18
|
-
case 'string':
|
|
19
|
-
switch (root) {
|
|
20
|
-
case 'web:html':
|
|
21
|
-
return h(element.type);
|
|
22
|
-
case 'web:svg':
|
|
23
|
-
return s(element.type);
|
|
24
|
-
}
|
|
25
|
-
default:
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
link(child, parent) {
|
|
32
|
-
switch (child.type) {
|
|
33
|
-
case 'element':
|
|
34
|
-
case 'comment':
|
|
35
|
-
case 'text':
|
|
36
|
-
switch (parent.type) {
|
|
37
|
-
case 'element':
|
|
38
|
-
case 'root':
|
|
39
|
-
parent.children.push(child);
|
|
40
|
-
}
|
|
41
|
-
return;
|
|
42
|
-
case 'doctype':
|
|
43
|
-
switch (parent.type) {
|
|
44
|
-
case 'root':
|
|
45
|
-
parent.children.push(child);
|
|
46
|
-
}
|
|
47
|
-
return;
|
|
48
|
-
default:
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
linkRoot(child) {
|
|
52
|
-
switch (child.type) {
|
|
53
|
-
case 'doctype':
|
|
54
|
-
case 'element':
|
|
55
|
-
case 'comment':
|
|
56
|
-
case 'text':
|
|
57
|
-
root.children.push(child);
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
update(el, next, prev) {
|
|
61
|
-
switch (el.type) {
|
|
62
|
-
case 'text':
|
|
63
|
-
el.value = (next.props.value as number | string | boolean).toString()
|
|
64
|
-
return;
|
|
65
|
-
case 'element':
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
unlink(child, parent) {
|
|
70
|
-
if ("children" in parent) {
|
|
71
|
-
parent.children = parent.children.filter(c => c !== child);
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
sort(el, children) {
|
|
75
|
-
if ("children" in el) {
|
|
76
|
-
el.children = children as Element[];
|
|
77
|
-
}
|
|
78
|
-
},
|
|
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
|
-
}
|