@lukekaalim/act-backstage 2.0.0 → 3.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 CHANGED
@@ -1,5 +1,36 @@
1
1
  # @lukekaalim/act-backstage
2
2
 
3
+ ## 3.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 6658c01: Internal Refactor!
8
+ - afd247e: Another major refactor! So everything is broken. Good luck!
9
+ - b3f6c49: Added debug capabilities and protocol
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies [6658c01]
14
+ - Updated dependencies [fdf1557]
15
+ - Updated dependencies [ccb3900]
16
+ - Updated dependencies [afd247e]
17
+ - Updated dependencies [2984273]
18
+ - Updated dependencies [b3f6c49]
19
+ - Updated dependencies [c5e8775]
20
+ - @lukekaalim/act-recon@3.0.0
21
+ - @lukekaalim/act@4.0.0
22
+
23
+ ## 3.0.0-alpha.0
24
+
25
+ ### Major Changes
26
+
27
+ - b3f6c49: Added debug capabilities and protocol
28
+
29
+ ### Patch Changes
30
+
31
+ - Updated dependencies [b3f6c49]
32
+ - @lukekaalim/act-recon@3.0.0-alpha.0
33
+
3
34
  ## 2.0.0
4
35
 
5
36
  ### Major Changes
package/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # @lukekaalim/act-backstage
2
+
3
+ Backstage is a collection of utilities for creating Act renderers
4
+ fairly simply. Most renderers in Act are just composed
5
+ of one or more "RenderSpace"'s, which are the systems
6
+ that keep track of which HTML/ThreeJs/SVG nodes correspond
7
+ to which commit.
8
+
9
+ ## API
10
+
11
+ <TypeDoc project="@lukekaalim/act-backstage" name="NodeBuilder" />
12
+
13
+ <TypeDoc project="@lukekaalim/act-backstage" name="RenderSpace2" />
14
+
15
+ <TypeDoc project="@lukekaalim/act-backstage" name="setPropObject" />
16
+
17
+ ## Guide
18
+
19
+ We're going to build a classic render function
package/builder.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { Element } from "@lukekaalim/act";
2
+
3
+ /**
4
+ * This type describes an implementation
5
+ * that can be provided to a RenderSpace
6
+ * in order to produce a fully-featured renderer.
7
+ */
8
+ export type NodeBuilder<TNode, TRoot = string | symbol> = {
9
+ roots: Set<TRoot>,
10
+
11
+ create: (element: Element, root: TRoot) => null | TNode,
12
+ destroy?: (el: TNode) => unknown,
13
+
14
+ linkRoot?: (child: TNode) => unknown,
15
+ unlinkRoot?: (child: TNode) => unknown,
16
+ link?: (child: TNode, parent: TNode) => unknown,
17
+ unlink?: (child: TNode, parent: TNode) => unknown,
18
+
19
+ sort?: (el: TNode, children: readonly TNode[]) => unknown,
20
+ update?: (el: TNode, next: Element, prev: null | Element) => unknown,
21
+
22
+ suspend?: (el: TNode, parent: TNode) => void,
23
+ unsuspend?: (el: TNode, parent: TNode) => void,
24
+ };
package/mod.ts CHANGED
@@ -1,3 +1,3 @@
1
- export * from './space.ts';
2
1
  export * from './props.ts';
3
- export * from './render.ts';
2
+ export * from './space2.ts';
3
+ export * from './builder.ts';
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@lukekaalim/act-backstage",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "main": "mod.ts",
5
5
  "dependencies": {
6
- "@lukekaalim/act": "^3.2.0",
7
- "@lukekaalim/act-recon": "^2.0.0"
6
+ "@lukekaalim/act": "^4.0.0",
7
+ "@lukekaalim/act-recon": "^3.0.0"
8
8
  }
9
9
  }
package/props.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Generically attempt to apply props to a target either by assigning them,
3
- * or letting a specialied function handle it.
3
+ * or letting a specialized function handle it.
4
4
  *
5
5
  * Iterate across props from as to present, setting removed props to "undefined"
6
6
  */
@@ -24,4 +24,4 @@ export const setPropObject = (
24
24
  }
25
25
  }
26
26
  }
27
- }
27
+ }
package/space2.ts ADDED
@@ -0,0 +1,264 @@
1
+ import { primitiveNodeTypes, specialNodeTypes, SuspendProps } from "@lukekaalim/act";
2
+ import { Commit2, CommitID, CommitRef2, CommitTree2, Delta, ReconcilerEventBus } from "@lukekaalim/act-recon"
3
+ import { NodeBuilder } from "./builder";
4
+
5
+ type ParentSearchResult<TNode> = {
6
+ /**
7
+ * You might not have a parent - no
8
+ * commit means there are no Nodes above you - just Root.
9
+ */
10
+ commit: Commit2 | null,
11
+ /**
12
+ * Your parent might be a valid node,
13
+ * or it might be "null"
14
+ */
15
+ node: TNode | null,
16
+
17
+ attachable: boolean,
18
+ }
19
+
20
+ /**
21
+ * The RenderSpace class
22
+ */
23
+ export class RenderSpace2<TNode, TRoot extends string | symbol> {
24
+ /** The CommitTree this render space is connected to */
25
+ tree: CommitTree2;
26
+
27
+ /** A reverse map to look up Commits given just the node they represent */
28
+ nodeByCommit: Map<CommitID, TNode> = new Map();
29
+ /** A reverse map to look up Commits given just the node they represent */
30
+ commitByNode: Map<TNode, Commit2> = new Map();
31
+
32
+ /** For a given CommitID, find it's closest "real" parent (a ancestor with a non-null TNode) */
33
+ parentByNode: Map<CommitID, Commit2> = new Map();
34
+
35
+ /**
36
+ * A set of all special Root element IDs in the tree.
37
+ * A Root element helps tell a renderer what kind of element
38
+ * to render, based on it's closest Root.
39
+ */
40
+ roots: Map<CommitID, Commit2> = new Map();
41
+ bus: ReconcilerEventBus;
42
+ builder: NodeBuilder<TNode, TRoot>;
43
+
44
+ constructor(tree: CommitTree2, builder: NodeBuilder<TNode, TRoot>) {
45
+ this.tree = tree;
46
+ this.bus = {
47
+ render: (delta) => {
48
+ this.create(delta);
49
+ this.update(delta);
50
+ },
51
+ }
52
+ this.builder = builder;
53
+ }
54
+
55
+ findChildren(id: CommitID, ignoreFirst = false, ignoreSuspended = true): TNode[] {
56
+ const node = this.nodeByCommit.get(id);
57
+ if (node && !ignoreFirst)
58
+ return [node];
59
+
60
+ const commit = this.tree.commits.get(id);
61
+ if (!commit)
62
+ return [];
63
+ if (commit.element.type === primitiveNodeTypes.null)
64
+ return [];
65
+ // suspended nodes don't count as children
66
+ if (ignoreSuspended && commit.isSuspended())
67
+ return [];
68
+
69
+ return commit.children.map(c => this.findChildren(c.id)).flat(1);
70
+ }
71
+
72
+ /**
73
+ * "find parent" can return 3 types of results:
74
+ * - An ancestor has a node! we return that.
75
+ * - An ancestor is a "null" node - we return the special form of "commit but no node"
76
+ * - No ancestor has a node - you might be near the root
77
+ * @param ref
78
+ * @returns
79
+ */
80
+ findParent(ref: CommitRef2): ParentSearchResult<TNode> {
81
+ let ancestor: CommitRef2 | null = ref;
82
+ let attachable = true;
83
+
84
+ while (ancestor) {
85
+ if (ancestor.id !== ref.id) {
86
+ const commit = this.tree.commits.get(ancestor.id) || null;
87
+
88
+ // Early exit out of parent lookup if someone on the path is null;
89
+ if (commit && commit.element.type === primitiveNodeTypes.null)
90
+ return { commit, node: null, attachable: false };
91
+
92
+ // maybe a bad idea... we'll see
93
+ if (commit && commit.isSuspended())
94
+ attachable = false;
95
+
96
+ const node = this.nodeByCommit.get(ancestor.id);
97
+ // If you find an element with a node
98
+ if (node)
99
+ return { commit, node, attachable }
100
+ }
101
+ ancestor = ancestor.parent;
102
+ }
103
+
104
+ // this element has no "node" parents - it is probably a "root" commit
105
+ return { commit: null, node: null, attachable };
106
+ }
107
+
108
+ findRoot(ref: CommitRef2) {
109
+ let ancestor: CommitRef2 | null = ref;
110
+
111
+ while (ancestor) {
112
+ const root = this.roots.get(ancestor.id);
113
+ if (root)
114
+ return root;
115
+ ancestor = ancestor.parent;
116
+ }
117
+
118
+ return null;
119
+ }
120
+
121
+ newNodes: Set<[Commit2, TNode]> = new Set();
122
+ needsReorder: Set<CommitID> = new Set();
123
+
124
+ create(deltas: Delta) {
125
+ this.newNodes.clear();
126
+ this.needsReorder.clear();
127
+
128
+ for (const next of deltas.fresh.values()) {
129
+ if (next.element.type === specialNodeTypes.render) {
130
+ // add render boundary
131
+ this.roots.set(next.ref.id, next);
132
+ continue;
133
+ }
134
+ const root = this.findRoot(next.ref);
135
+ if (!root)
136
+ continue;
137
+
138
+ const rootType = root.element.props['type'] as TRoot;
139
+
140
+ // test to see if this element
141
+ // belongs to this
142
+ if (this.builder.roots.has(rootType) ) {
143
+ // Try to create a <T> for every new commit
144
+ const node = this.builder.create(next.element, rootType);
145
+ // Not all commits have a corresponding node
146
+ if (node) {
147
+ this.newNodes.add([next, node]);
148
+ this.nodeByCommit.set(next.ref.id, node);
149
+ this.commitByNode.set(node, next);
150
+ }
151
+ }
152
+ }
153
+ }
154
+
155
+ update(deltas: Delta) {
156
+ const {
157
+ link,
158
+ unlink,
159
+ linkRoot,
160
+ sort,
161
+ update,
162
+ destroy,
163
+ unlinkRoot,
164
+ suspend = unlink,
165
+ unsuspend = link
166
+ } = this.builder;
167
+
168
+ if (link || sort) {
169
+ // Loop through newly created nodes
170
+ for (const [next, node] of this.newNodes) {
171
+ const result = this.findParent(next.ref);
172
+
173
+ if (next.element.type === specialNodeTypes.suspend)
174
+ console.log(`Creating suspense node`, result)
175
+
176
+ if (result.commit && result.node && result.attachable) {
177
+ this.needsReorder.add(result.commit.ref.id)
178
+
179
+ if (link)
180
+ link(node, result.node);
181
+ }
182
+
183
+ if (linkRoot && !result.commit && result.attachable)
184
+ linkRoot(node);
185
+ }
186
+ }
187
+
188
+ if (update) {
189
+ for (const { prev, next, moved } of deltas.changed.values()) {
190
+
191
+ // suspense code
192
+ if (next.element.type === specialNodeTypes.suspend) {
193
+ const result = this.findParent(next.ref);
194
+
195
+ const wasSuspended = !!prev.props.suspended;
196
+ const isSuspended = !!next.element.props.suspended;
197
+
198
+ const suspenseChanged = wasSuspended !== isSuspended;
199
+ if (suspenseChanged && result.commit && result.node) {
200
+ this.needsReorder.add(result.commit.ref.id);
201
+
202
+ const children = this.findChildren(next.ref.id, true, false);
203
+ for (const child of children) {
204
+ if (isSuspended && suspend) {
205
+ suspend(child, result.node);
206
+ }
207
+
208
+ if (!isSuspended && unsuspend) {
209
+ unsuspend(child, result.node);
210
+ }
211
+ }
212
+ }
213
+ continue;
214
+ }
215
+
216
+ const node = this.nodeByCommit.get(next.ref.id);
217
+ if (!node)
218
+ continue;
219
+
220
+ update(node, next.element, prev);
221
+
222
+ if (moved) {
223
+ const result = this.findParent(next.ref);
224
+
225
+ if (result.commit) {
226
+ this.needsReorder.add(result.commit.ref.id);
227
+ }
228
+ }
229
+ }
230
+ for (const next of deltas.fresh.values()) {
231
+ const node = this.nodeByCommit.get(next.ref.id);
232
+ if (node)
233
+ update(node, next.element, null);
234
+ }
235
+ }
236
+ for (const prev of deltas.removed.values()) {
237
+ const node = this.nodeByCommit.get(prev.ref.id);
238
+ if (node) {
239
+ this.nodeByCommit.delete(prev.ref.id);
240
+ const parent = this.findParent(prev.ref);
241
+ if (parent.commit && parent.node)
242
+ this.needsReorder.add(parent.commit.ref.id)
243
+
244
+ this.commitByNode.delete(node);
245
+ if (unlink && parent.node)
246
+ unlink(node, parent.node);
247
+ if (unlinkRoot && !parent.commit)
248
+ unlinkRoot(node)
249
+ if (destroy)
250
+ destroy(node);
251
+ }
252
+ }
253
+
254
+ if (sort) {
255
+ for (const id of this.needsReorder) {
256
+ const node = this.nodeByCommit.get(id);
257
+ if (node) {
258
+ const children = this.findChildren(id, true, true);
259
+ sort(node, children);
260
+ }
261
+ }
262
+ }
263
+ }
264
+ }
package/render.ts DELETED
@@ -1,32 +0,0 @@
1
- import { CommitTree, createReconciler, Scheduler, WorkThread } from "@lukekaalim/act-recon";
2
- import { RenderSpace } from "./space";
3
- import * as act from '@lukekaalim/act';
4
-
5
- export type RenderFunction<T> = (node: act.Node, root: T) => { stop: () => void }
6
-
7
- export const createRenderFunction = <T>(
8
- scheduler: Scheduler,
9
- createSpace: (tree: CommitTree, root: T) => RenderSpace
10
- ): RenderFunction<T> => {
11
-
12
- const render: RenderFunction<T> = (node: act.Node, root: T) => {
13
- const onThreadComplete = (thread: WorkThread) => {
14
- space.create(thread.deltas).configure();
15
- };
16
-
17
- const reconciler = createReconciler(scheduler);
18
- const threadCompleteSub = reconciler.on('on-thread-complete', onThreadComplete);
19
-
20
- const space = createSpace(reconciler.tree, root);
21
-
22
- reconciler.mount(node);
23
-
24
- return {
25
- stop() {
26
- threadCompleteSub.cancel();
27
- },
28
- }
29
- };
30
-
31
- return render;
32
- }
package/space.ts DELETED
@@ -1,222 +0,0 @@
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
- /**
45
- * A "simple" render space is just a conventional
46
- * render space where we keep track of each commit
47
- * in a map - and take a "SimpleRenderSpaceArgs"
48
- * args object that tells us how we make, link,
49
- * sort, and update our nodes.
50
- *
51
- * @param tree
52
- * @param args
53
- * @param nodeByCommit
54
- * @returns
55
- */
56
- export const createSimpleRenderSpace = <T, R extends string | Symbol>(
57
- tree: recon.CommitTree,
58
- args: SimpleRenderSpaceArgs<T, R>,
59
- nodeByCommit: Map<recon.CommitID, T | null> = new Map(),
60
- ): RenderSpace => {
61
- const commitByNode = new Map<T, recon.CommitID>();
62
-
63
- const rootIds = new Set<recon.CommitID>();
64
-
65
- /**
66
- * Find all the nodes that belong children (in commit order!)
67
- * for a particular commit.
68
- */
69
- const findChildren = (id: recon.CommitID, ignoreFirst = false): readonly T[] => {
70
- const node = nodeByCommit.get(id);
71
- if (node && !ignoreFirst)
72
- return [node];
73
- const commit = tree.commits.get(id);
74
- if (!commit)
75
- return [];
76
- if (commit.element.type === act.primitiveNodeTypes.null)
77
- return [];
78
- return commit.children.map(c => findChildren(c.id)).flat(1);
79
- };
80
- const findParent = (ref: recon.CommitRef): null | NodeRef<T | null> => {
81
- for (let i = 1; i < ref.path.length; i++) {
82
- const id = ref.path[ref.path.length - i - 1];
83
-
84
- const commit = tree.commits.get(id) as recon.Commit;
85
- // Early exit out of parent lookup if someone on the path is null;
86
- if (commit.element.type === act.primitiveNodeTypes.null)
87
- return { id, node: null };
88
-
89
- const node = nodeByCommit.get(id);
90
- // If you find an element with a node
91
- if (node)
92
- return { id, node }
93
- }
94
- // this element has no "node" parents - it is probably a "root" commit
95
- return null;
96
- }
97
- const findRootId = (ref: recon.CommitRef) => {
98
- for (let i = ref.path.length - 1; i >= 0; i--) {
99
- const id = ref.path[i];
100
- if (rootIds.has(id))
101
- return id;
102
- }
103
- return null;
104
- }
105
-
106
- const space: RenderSpace = {
107
- create(deltas) {
108
- const newNodes: Set<{ delta: recon.CreateDelta, node: T }> = new Set();
109
- const needsReorder = new Set<recon.CommitID>();
110
-
111
- for (const delta of deltas.created) {
112
- if (delta.next.element.type === act.renderNodeType) {
113
- // add render boundary
114
- rootIds.add(delta.ref.id);
115
- continue;
116
- }
117
- const rootId = findRootId(delta.ref);
118
- const root = rootId && tree.commits.get(rootId) || null;
119
- if (!root)
120
- continue;
121
-
122
- const rootType = root.element.props['type'] as R;
123
-
124
- // test to see if this element
125
- // belongs to this
126
- if (args.rootTypes.has(rootType) ) {
127
- // Try to create a <T> for every new commit
128
- const node = args.create(delta.next.element, rootType);
129
- // Not all commits have a corresponding node
130
- if (node) {
131
- newNodes.add({ node, delta });
132
- nodeByCommit.set(delta.ref.id, node);
133
- commitByNode.set(node, delta.ref.id)
134
- }
135
- }
136
- }
137
-
138
- return {
139
- configure() {
140
- if (args.link || args.sort) {
141
- // Loop through newly created nodes
142
- for (const { delta, node } of newNodes) {
143
- const parent = findParent(delta.ref);
144
- const parentNode = parent && parent.node;
145
- if (parentNode) {
146
- needsReorder.add(parent.id)
147
- }
148
- if (args.link && (!parent || parentNode)) {
149
- args.link(node, parentNode);
150
- }
151
- }
152
- }
153
-
154
- if (args.update) {
155
- for (const delta of deltas.updated) {
156
- const node = nodeByCommit.get(delta.ref.id);
157
- if (!node)
158
- continue;
159
- args.update(node, delta.next.element, delta.prev.element);
160
- if (delta.moved) {
161
- const parent = findParent(delta.ref);
162
- const parentNode = parent && parent.node;
163
- if (parentNode) {
164
- needsReorder.add(parent.id)
165
- }
166
- }
167
- }
168
- for (const delta of deltas.created) {
169
- const node = nodeByCommit.get(delta.ref.id);
170
- if (node)
171
- args.update(node, delta.next.element, null);
172
- }
173
- }
174
- for (const delta of deltas.removed) {
175
- const prevResult = nodeByCommit.get(delta.ref.id);
176
- if (prevResult) {
177
- nodeByCommit.delete(delta.ref.id);
178
- const parentId = delta.ref.path.find(id => nodeByCommit.get(id));
179
- if (parentId)
180
- needsReorder.add(parentId)
181
-
182
- commitByNode.delete(prevResult);
183
- if (args.destroy)
184
- args.destroy(prevResult);
185
- }
186
- }
187
-
188
- if (args.sort) {
189
- for (const id of needsReorder) {
190
- const node = nodeByCommit.get(id);
191
- if (node) {
192
- const children = findChildren(id, true);
193
- args.sort(node, children);
194
- }
195
- }
196
- }
197
- },
198
- }
199
- },
200
- }
201
-
202
- return space;
203
- };
204
-
205
-
206
-
207
-
208
- export const RenderSpace = {
209
- combine(subspaces: RenderSpace[]) {
210
- const create = (deltas: recon.DeltaSet) => {
211
- const results = subspaces.map(ss => ss.create(deltas));
212
-
213
- const configure = () => {
214
- results.map(result => result.configure());
215
- }
216
- return { configure }
217
- };
218
-
219
- return { create };
220
- },
221
- simple: createSimpleRenderSpace
222
- }