@lukekaalim/act-backstage 1.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/mod.ts +3 -0
- package/package.json +9 -0
- package/props.ts +27 -0
- package/render.ts +51 -0
- package/space.ts +204 -0
package/mod.ts
ADDED
package/package.json
ADDED
package/props.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generically attempt to apply props to a target either by assigning them,
|
|
3
|
+
* or letting a specialied function handle it.
|
|
4
|
+
*
|
|
5
|
+
* Iterate across props from as to present, setting removed props to "undefined"
|
|
6
|
+
*/
|
|
7
|
+
export const setPropObject = (
|
|
8
|
+
target: Record<string, unknown>,
|
|
9
|
+
next: null | Record<string, unknown>,
|
|
10
|
+
prev: null | Record<string, unknown>,
|
|
11
|
+
assign: null | ((name: string, next: unknown, prev: unknown) => boolean) = null,
|
|
12
|
+
) => {
|
|
13
|
+
const names = new Set([
|
|
14
|
+
...Object.keys(next || {}),
|
|
15
|
+
...Object.keys(prev || {})
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
for (const name of names) {
|
|
19
|
+
const nextValue = (next || {})[name];
|
|
20
|
+
const successfulAssign = assign && assign(name, nextValue, (prev || {})[name]);
|
|
21
|
+
if (!successfulAssign) {
|
|
22
|
+
if (target[name] !== nextValue) {
|
|
23
|
+
target[name] = nextValue;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
package/render.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { CommitTree, createReconciler } from "@lukekaalim/act-recon";
|
|
2
|
+
import { RenderSpace } from "./space";
|
|
3
|
+
import * as act from '@lukekaalim/act';
|
|
4
|
+
|
|
5
|
+
export type ScheduleRequestFunc<ID> = (callback: () => void) => ID;
|
|
6
|
+
export type ScheduleCancelFunc<ID> = (id: ID) => void;
|
|
7
|
+
|
|
8
|
+
export type Scheduler<ID> = {
|
|
9
|
+
duration: number,
|
|
10
|
+
|
|
11
|
+
request: ScheduleRequestFunc<ID>,
|
|
12
|
+
cancel: ScheduleCancelFunc<ID>,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const createRenderFunction = <S, T>(
|
|
16
|
+
scheduler: Scheduler<S>,
|
|
17
|
+
createSpace: (tree: CommitTree, root: T) => RenderSpace
|
|
18
|
+
) => {
|
|
19
|
+
const render = (node: act.Node, root: T) => {
|
|
20
|
+
const reconciler = createReconciler(deltas => {
|
|
21
|
+
space.create(deltas).configure();
|
|
22
|
+
}, () => {
|
|
23
|
+
scheduler.cancel(id);
|
|
24
|
+
id = scheduler.request(work)
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const space = createSpace(reconciler.tree, root);
|
|
28
|
+
|
|
29
|
+
const work = () => {
|
|
30
|
+
const start = performance.now();
|
|
31
|
+
const end = start + scheduler.duration;
|
|
32
|
+
const done = reconciler.threads.work(() => {
|
|
33
|
+
const now = performance.now();
|
|
34
|
+
return now >= end;
|
|
35
|
+
})
|
|
36
|
+
if (done) {
|
|
37
|
+
scheduler.cancel(id)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let id = scheduler.request(work);
|
|
42
|
+
|
|
43
|
+
reconciler.threads.mount(node);
|
|
44
|
+
|
|
45
|
+
return () => {
|
|
46
|
+
scheduler.cancel(id)
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return render;
|
|
51
|
+
}
|
package/space.ts
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import * as act from '@lukekaalim/act';
|
|
2
|
+
import * as recon from '@lukekaalim/act-recon';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A "RenderSpace" is a service that manages
|
|
6
|
+
* a subtree of a particular type (T). Its broken
|
|
7
|
+
* down into two phases, "create" & "configure".
|
|
8
|
+
*
|
|
9
|
+
* This needs to be done in two phases, because
|
|
10
|
+
* often configuring elements requires references to
|
|
11
|
+
* children and parents, and if they haven't been
|
|
12
|
+
* created yet (or if the order of their creation is
|
|
13
|
+
* uncertain), then these functions cant act properly.
|
|
14
|
+
*
|
|
15
|
+
* #### Create
|
|
16
|
+
* The create phase is responsible for the assignment
|
|
17
|
+
* of CommitIds to T.
|
|
18
|
+
*
|
|
19
|
+
* #### Configure
|
|
20
|
+
* This is where props are assigned, and children +
|
|
21
|
+
* heirarchial elements can be setup.
|
|
22
|
+
*/
|
|
23
|
+
export type RenderSpace = {
|
|
24
|
+
create(deltas: recon.DeltaSet): { configure: () => unknown },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type SimpleRenderSpaceArgs<T, R extends string | Symbol> = {
|
|
28
|
+
rootTypes: Set<R>,
|
|
29
|
+
|
|
30
|
+
create: (element: act.Element, rootType: R) => null | T,
|
|
31
|
+
link?: (el: T, parent: null | T) => unknown,
|
|
32
|
+
unlink?: (el: T, parent: null | T) => unknown,
|
|
33
|
+
|
|
34
|
+
sort?: (el: T, children: readonly T[]) => unknown,
|
|
35
|
+
update?: (el: T, next: act.Element, prev: null | act.Element) => unknown,
|
|
36
|
+
destroy?: (el: T) => unknown,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type NodeRef<T> = {
|
|
40
|
+
id: recon.CommitID,
|
|
41
|
+
node: T,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const createSimpleRenderSpace = <T, R extends string | Symbol>(
|
|
45
|
+
tree: recon.CommitTree,
|
|
46
|
+
args: SimpleRenderSpaceArgs<T, R>,
|
|
47
|
+
nodeByCommit: Map<recon.CommitID, T | null> = new Map(),
|
|
48
|
+
): RenderSpace => {
|
|
49
|
+
const commitByNode = new Map<T, recon.CommitID>();
|
|
50
|
+
|
|
51
|
+
const rootIds = new Set<recon.CommitID>();
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Find all the nodes that belong children (in commit order!)
|
|
55
|
+
* for a particular commit.
|
|
56
|
+
*/
|
|
57
|
+
const findChildren = (id: recon.CommitID, ignoreFirst = false): readonly T[] => {
|
|
58
|
+
const node = nodeByCommit.get(id);
|
|
59
|
+
if (node && !ignoreFirst)
|
|
60
|
+
return [node];
|
|
61
|
+
const commit = tree.commits.get(id);
|
|
62
|
+
if (!commit)
|
|
63
|
+
return [];
|
|
64
|
+
if (commit.element.type === act.primitiveNodeTypes.null)
|
|
65
|
+
return [];
|
|
66
|
+
return commit.children.map(c => findChildren(c.id)).flat(1);
|
|
67
|
+
};
|
|
68
|
+
const findParent = (ref: recon.CommitRef): null | NodeRef<T | null> => {
|
|
69
|
+
for (let i = 1; i < ref.path.length; i++) {
|
|
70
|
+
const id = ref.path[ref.path.length - i - 1];
|
|
71
|
+
|
|
72
|
+
const commit = tree.commits.get(id) as recon.Commit;
|
|
73
|
+
// Early exit out of parent lookup if someone on the path is null;
|
|
74
|
+
if (commit.element.type === act.primitiveNodeTypes.null)
|
|
75
|
+
return { id, node: null };
|
|
76
|
+
|
|
77
|
+
const node = nodeByCommit.get(id);
|
|
78
|
+
// If you find an element with a node
|
|
79
|
+
if (node)
|
|
80
|
+
return { id, node }
|
|
81
|
+
}
|
|
82
|
+
// this element has no parents - it is probably a "root" commit
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const findRootId = (ref: recon.CommitRef) => {
|
|
86
|
+
for (let i = ref.path.length - 1; i >= 0; i--) {
|
|
87
|
+
const id = ref.path[i];
|
|
88
|
+
if (rootIds.has(id))
|
|
89
|
+
return id;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const space: RenderSpace = {
|
|
95
|
+
create(deltas) {
|
|
96
|
+
const newNodes: Set<{ delta: recon.CreateDelta, node: T }> = new Set();
|
|
97
|
+
const needsReorder = new Set<recon.CommitID>();
|
|
98
|
+
|
|
99
|
+
for (const delta of deltas.created) {
|
|
100
|
+
if (delta.next.element.type === act.primitiveNodeTypes.render) {
|
|
101
|
+
// add render boundary
|
|
102
|
+
rootIds.add(delta.ref.id);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const rootId = findRootId(delta.ref);
|
|
106
|
+
const root = rootId && tree.commits.get(rootId) || null;
|
|
107
|
+
if (!root)
|
|
108
|
+
continue;
|
|
109
|
+
|
|
110
|
+
const rootType = root.element.props['type'] as R;
|
|
111
|
+
|
|
112
|
+
// test to see if this element
|
|
113
|
+
// belongs to this
|
|
114
|
+
if (args.rootTypes.has(rootType) ) {
|
|
115
|
+
// Try to create a <T> for every new commit
|
|
116
|
+
const node = args.create(delta.next.element, rootType);
|
|
117
|
+
// Not all commits have a corresponding node
|
|
118
|
+
if (node) {
|
|
119
|
+
newNodes.add({ node, delta });
|
|
120
|
+
nodeByCommit.set(delta.ref.id, node);
|
|
121
|
+
commitByNode.set(node, delta.ref.id)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
configure() {
|
|
129
|
+
if (args.link || args.sort) {
|
|
130
|
+
// Loop through newly created nodes
|
|
131
|
+
for (const { delta, node } of newNodes) {
|
|
132
|
+
const parent = findParent(delta.ref);
|
|
133
|
+
const parentNode = parent && parent.node;
|
|
134
|
+
if (parentNode) {
|
|
135
|
+
needsReorder.add(parent.id)
|
|
136
|
+
}
|
|
137
|
+
if (args.link && (!parent || parentNode)) {
|
|
138
|
+
args.link(node, parentNode);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (args.update) {
|
|
144
|
+
for (const delta of deltas.updated) {
|
|
145
|
+
const node = nodeByCommit.get(delta.ref.id);
|
|
146
|
+
if (node)
|
|
147
|
+
args.update(node, delta.next.element, delta.prev.element);
|
|
148
|
+
}
|
|
149
|
+
for (const delta of deltas.created) {
|
|
150
|
+
const node = nodeByCommit.get(delta.ref.id);
|
|
151
|
+
if (node)
|
|
152
|
+
args.update(node, delta.next.element, null);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const delta of deltas.removed) {
|
|
157
|
+
const prevResult = nodeByCommit.get(delta.ref.id);
|
|
158
|
+
if (prevResult) {
|
|
159
|
+
nodeByCommit.delete(delta.ref.id);
|
|
160
|
+
const parentId = delta.ref.path.find(id => nodeByCommit.get(id));
|
|
161
|
+
if (parentId)
|
|
162
|
+
needsReorder.add(parentId)
|
|
163
|
+
|
|
164
|
+
commitByNode.delete(prevResult);
|
|
165
|
+
if (args.destroy)
|
|
166
|
+
args.destroy(prevResult);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (args.sort) {
|
|
171
|
+
for (const id of needsReorder) {
|
|
172
|
+
const node = nodeByCommit.get(id);
|
|
173
|
+
if (node) {
|
|
174
|
+
const children = findChildren(id, true);
|
|
175
|
+
args.sort(node, children);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return space;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
export const RenderSpace = {
|
|
191
|
+
combine(subspaces: RenderSpace[]) {
|
|
192
|
+
const create = (deltas: recon.DeltaSet) => {
|
|
193
|
+
const results = subspaces.map(ss => ss.create(deltas));
|
|
194
|
+
|
|
195
|
+
const configure = () => {
|
|
196
|
+
results.map(result => result.configure());
|
|
197
|
+
}
|
|
198
|
+
return { configure }
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return { create };
|
|
202
|
+
},
|
|
203
|
+
simple: createSimpleRenderSpace
|
|
204
|
+
}
|