@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 ADDED
@@ -0,0 +1,3 @@
1
+ export * from './space.ts';
2
+ export * from './props.ts';
3
+ export * from './render.ts';
package/package.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "@lukekaalim/act-backstage",
3
+ "version": "1.0.0",
4
+ "main": "mod.ts",
5
+ "dependencies": {
6
+ "@lukekaalim/act": "^3.0.0",
7
+ "@lukekaalim/act-recon": "^1.0.0"
8
+ }
9
+ }
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
+ }