@lukekaalim/act-recon 3.0.0-alpha.3 → 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,31 @@
1
1
  # @lukekaalim/act-recon
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
+ - fdf1557: Fixed issue with multiple changes (where changes after first were children of first) being ignored due to "MustVisit" being misinput each time
14
+ - ccb3900: Reconciler should apply all changes in the correct order, and not skip any.
15
+ - Reconciler does send out a Render command once it completes all pending renders (called a "ThreadStack")
16
+ Scheduler has been updated to perform some updates in Sync.
17
+ - 2984273: Check if immediate child update is already handlded by some other system (avoiding double-rendering)
18
+ - c5e8775: Fix context updates not actually being pushed to the MustRender list
19
+ - Updated dependencies [6658c01]
20
+ - Updated dependencies [afd247e]
21
+ - @lukekaalim/act@4.0.0
22
+
23
+ ## 3.0.0-alpha.4
24
+
25
+ ### Patch Changes
26
+
27
+ - 2984273: Check if immediate child update is already handlded by some other system (avoiding double-rendering)
28
+
3
29
  ## 3.0.0-alpha.3
4
30
 
5
31
  ### Patch Changes
package/algorithms.ts CHANGED
@@ -1,50 +1,64 @@
1
1
  import { MagicError } from "@lukekaalim/act";
2
2
 
3
- export type ChangeReport = {
4
- /** Index for Next that didnt exist in Prev */
5
- created: number[],
6
- /** Index for Prev that didnt exist in Next */
7
- removed: number[],
8
-
9
- /** Index for elements that are the same previously and next */
10
- nextToPrev: number[],
11
- };
3
+ /**
4
+ * ChangeReport
5
+ */
6
+ export class ChangeReport2 {
7
+ /**
8
+ * The indices of elements that were removed
9
+ */
10
+ removed: number[] = [];
11
+ /**
12
+ * The indices of the previous position that an element
13
+ * was in, or -1 if it didn't exist in the "prevs" array.
14
+ */
15
+ transform: number[] = [];
12
16
 
13
- export type ChangeEqualityTest<Prev, Next> = (prev: Prev, next: Next, prevIndex: number, nextIndex: number) => boolean;
17
+ /**
18
+ * A (hopefully) faster single-entry report generator
19
+ * @param prev
20
+ * @param next
21
+ * @param equalityTest
22
+ * @returns
23
+ */
24
+ static generateSingles<Prev, Next>(prev: Prev, next: Next, equalityTest: ChangeEqualityTest<Prev, Next>) {
25
+ const report = new ChangeReport2();
26
+
27
+ if (equalityTest(prev, next, 0, 0)) {
28
+ report.transform.push(0);
29
+ } else {
30
+ report.transform.push(-1);
31
+ report.removed.push(0);
32
+ }
14
33
 
15
- export const calculateChangedElements = <Prev, Next>(
16
- prevs: Prev[],
17
- nexts: Next[],
18
- isEqual: ChangeEqualityTest<Prev, Next>,
19
- ): ChangeReport => {
20
- const report: ChangeReport = {
21
- created: [],
22
- removed: [],
23
- nextToPrev: [],
34
+ return report;
24
35
  }
25
- report.nextToPrev = nexts.map((next, nextIndex) => {
26
- const prevIndex = prevs.findIndex((prev, prevIndex) => isEqual(prev, next, prevIndex, nextIndex));
27
- if (prevIndex === -1)
28
- report.created.push(nextIndex);
29
- return prevIndex;
30
- });
31
- report.removed = prevs
32
- .map((_, index) => {
33
- return report.nextToPrev.indexOf(index) !== -1 ? -1 : index;
34
- })
35
- .filter((index) => index !== -1)
36
-
37
- return report;
38
- }
39
36
 
40
- export type SortedChangeReport = ChangeReport & {
41
- /** Index for elements that moved around, but were equal */
42
- moved: [number, number][],
43
- };
44
- export const calculateSortedChangedElements = (): SortedChangeReport => {
45
- throw new MagicError();
37
+ static generate<Prev, Next>(prevs: Prev[], nexts: Next[], equalityTest: ChangeEqualityTest<Prev, Next>) {
38
+ if (prevs.length === 0 && nexts.length === 0)
39
+ return ChangeReport2.generateSingles(prevs[0], nexts[0], equalityTest);
40
+
41
+ const report = new ChangeReport2();
42
+ const visited = new Set();
43
+
44
+ for (let nextIndex = 0; nextIndex < nexts.length; nextIndex++) {
45
+ const next = nexts[nextIndex];
46
+ const prevIndex = prevs.findIndex((prev, prevIndex) => equalityTest(prev, next, prevIndex, nextIndex));
47
+ report.transform.push(prevIndex);
48
+ if (prevIndex !== -1)
49
+ visited.add(prevIndex);
50
+ }
51
+ for (let i = 0; i < prevs.length; i++) {
52
+ if (!visited.has(i))
53
+ report.removed.push(i);
54
+ }
55
+
56
+ return report;
57
+ }
46
58
  }
47
59
 
60
+ export type ChangeEqualityTest<Prev, Next> = (prev: Prev, next: Next, prevIndex: number, nextIndex: number) => boolean;
61
+
48
62
  export const first = <X, Y>(array: ReadonlyArray<X>, func: (value: X, index: number) => Y | null): Y | null => {
49
63
  for (let i = 0; i < array.length; i++) {
50
64
  const value = array[i];
@@ -55,11 +69,11 @@ export const first = <X, Y>(array: ReadonlyArray<X>, func: (value: X, index: num
55
69
  return null;
56
70
  }
57
71
 
58
- export const last = <X, Y>(array: ReadonlyArray<X>, func: (value: X, index: number) => Y | null): Y | null => {
72
+ export const last = <X, Y extends {}>(array: ReadonlyArray<X>, func: (value: X, index: number) => Y | null | false | undefined | 0): Y | null => {
59
73
  for (let i = array.length - 1; i > 0; i--) {
60
74
  const value = array[i];
61
75
  const result = func(value, i);
62
- if (result !== null)
76
+ if (result)
63
77
  return result;
64
78
  }
65
79
  return null;
package/commit.ts CHANGED
@@ -1,17 +1,11 @@
1
- import { createId, Element, OpaqueID } from "@lukekaalim/act";
1
+ import { createId, Element, OpaqueID, specialNodeTypes, SuspendProps } from "@lukekaalim/act";
2
+ import { createObjectPool } from "./pool";
2
3
 
3
4
  /**
4
5
  * A single consistent id representing a commit in the act tree.
5
6
  * Does not change.
6
7
  */
7
8
  export type CommitID = OpaqueID<"CommitID">;
8
- /**
9
- * A array of **CommitID**'s, starting at the "root" id and "descending"
10
- * until reaching (including) the subject's ID. Useful for efficiently
11
- * descending the tree to find a specific change.
12
- * Does not change.
13
- */
14
- export type CommitPath = readonly CommitID[];
15
9
  /**
16
10
  * A ID for a particular _state_ a **Commit** is in - every time it or its
17
11
  * children change, a commit with the same Id but a new CommitVersion
@@ -19,61 +13,110 @@ export type CommitPath = readonly CommitID[];
19
13
  */
20
14
  export type CommitVersion = OpaqueID<"CommitVersion">;
21
15
 
22
- /**
23
- * Structure for quick lookup and identification of a commit
24
- */
25
- export type CommitRef = {
16
+ export class CommitRef2 {
26
17
  id: CommitID;
27
- path: CommitPath;
28
- };
29
- export const CommitRef = {
30
- from(path: CommitPath) {
31
- return {
32
- path,
33
- id: path[path.length - 1],
18
+ parent: null | CommitRef2;
19
+ length: number;
20
+
21
+ private constructor(id: CommitID, parent: CommitRef2 | null) {
22
+ this.id = id;
23
+ this.parent = parent;
24
+ if (parent)
25
+ this.length = parent.length + 1;
26
+ else
27
+ this.length = 1;
28
+ }
29
+
30
+ /*
31
+ [Symbol.iterator]() {
32
+ return this.ancestors();
33
+ }
34
+ */
35
+
36
+ /**
37
+ * Iterate though all "parent" commit refs,
38
+ * including itself as the first entry.
39
+ *
40
+ * @returns Iterator<CommitRef2>
41
+ */
42
+ *ancestors() {
43
+ let ref: CommitRef2 | null = this;
44
+
45
+ while (ref) {
46
+ yield ref;
47
+ ref = ref.parent;
34
48
  }
35
- },
36
- new(path: CommitPath = []) {
37
- const id = createId<'CommitID'>();
38
- return {
39
- path: [...path, id],
40
- id,
49
+ }
50
+
51
+ /**
52
+ *
53
+ * @param climber A function that receives every ancestor commit ref,
54
+ * including this one. Return "true" to stop climbing early.
55
+ */
56
+ climb(climber: (ref: CommitRef2) => boolean | void) {
57
+ let ref: CommitRef2 | null = this;
58
+ while (ref) {
59
+ if (climber(ref))
60
+ return;
61
+
62
+ ref = ref.parent;
41
63
  }
42
- },
64
+ }
65
+
66
+ find<T>(test: (id: CommitRef2) => T | null | undefined | false): T | null {
67
+ let result: T | null = null;
68
+ this.climb(ref => {
69
+ const currentResult = test(ref);
70
+ if (currentResult) {
71
+ result = currentResult
72
+ return true;
73
+ }
74
+ })
75
+ return result;
76
+ }
77
+
78
+ static fresh(parent: CommitRef2 | null) {
79
+ return new CommitRef2(createId('CommitID'), parent);
80
+ }
43
81
  }
44
82
 
45
- /**
46
- * Representing an entry in the act "Tree"
47
- */
48
- export type Commit = CommitRef & {
49
- version: CommitVersion;
50
- element: Element;
51
- children: CommitRef[];
52
- };
53
-
54
- export const updateCommit = (
55
- ref: CommitRef,
56
- element: Element,
57
- children: CommitRef[]
58
- ): Commit => ({
59
- ...ref,
60
- element,
61
- children,
62
- version: createId(),
63
- });
64
-
65
- export const Commit = {
66
- new(element: Element, path: CommitPath = [], children: CommitRef[] = []): Commit {
67
- return {
68
- ...CommitRef.new(path),
69
- version: createId(),
70
- children,
71
- element,
83
+ export class Commit2 {
84
+ static pool = () => createObjectPool<Commit2, ConstructorParameters<typeof Commit2>>(
85
+ function alloc (ref, el, ch) { return new Commit2(ref, el, ch) },
86
+ function reassign(c, ref, el, ch) {
87
+ c.ref = ref;
88
+ c.element = el;
89
+ c.children = ch;
90
+ c.version = createId('CommitVersion');
72
91
  }
73
- },
74
- update: updateCommit,
75
- version: (commit: Commit): Commit => ({
76
- ...commit,
77
- version: createId(),
78
- }),
79
- }
92
+ )
93
+
94
+ ref: CommitRef2;
95
+
96
+ element: Element;
97
+ children: CommitRef2[];
98
+
99
+ version: CommitVersion = createId('CommitVersion');
100
+
101
+ constructor(ref: CommitRef2, element: Element, children: CommitRef2[]) {
102
+ this.ref = ref;
103
+ this.element = element;
104
+ this.children = children;
105
+ }
106
+
107
+ update(element: null | Element = null, children: null | CommitRef2[] = null) {
108
+ this.version = createId('CommitVersion');
109
+
110
+ if (element)
111
+ this.element = element;
112
+ if (children)
113
+ this.children = children;
114
+ }
115
+
116
+ isSuspended() {
117
+ return (
118
+ this.element.type === specialNodeTypes.suspend
119
+ && (this.element.props as SuspendProps).suspended
120
+ );
121
+ }
122
+ }
package/delta.ts CHANGED
@@ -1,47 +1,77 @@
1
- import { Commit, CommitRef } from "./commit.ts";
2
- import { CommitTree } from "./tree.ts";
3
-
4
- export type CreateDelta = { ref: CommitRef, next: Commit };
5
- export type UpdateDelta = { ref: CommitRef, next: Commit, prev: Commit, moved: boolean };
6
- export type RemoveDelta = { ref: CommitRef, prev: Commit };
7
- export type SkipDelta = { next: Commit };
8
-
9
- export type DeltaSet = {
10
- created: CreateDelta[],
11
- updated: UpdateDelta[],
12
- skipped: SkipDelta[],
13
- removed: RemoveDelta[],
14
- };
1
+ import { Element } from "@lukekaalim/act";
2
+ import { Commit2, CommitID } from "./commit.ts";
3
+ import { EffectID, EffectTask } from "./state.ts";
15
4
 
16
5
  /**
17
- * Apply a deltaset to a tree, modifying it's commit list
18
- * to match the changes produced by the thread.
6
+ * The Delta class represents an accumulation
7
+ * of changes over time.
8
+ *
9
+ * A WorkThread may do several "passes" over the CommitTree,
10
+ * but all of those changes are written to the same Delta.
11
+ *
12
+ * The Delta keeps track of only the immediately prior state (the
13
+ * last one that was sent to the Renderer), and the final state.
14
+ *
15
+ * If a pass causes a component to be rendered/updated several times,
16
+ * it will only be recorded in the delta once for it's final state. Similarly,
17
+ * if an element is create in one pass, but removed in a another, then it will
18
+ * be entirely excluded from the delta - and the renderer will never know it existed.
19
19
  *
20
- * @param thread
21
- * @param tree
20
+ * The Delta records Commits as well as Effects this way.
22
21
  */
23
- const applyDeltaSet = (deltas: DeltaSet, tree: CommitTree) => {
24
- for (const delta of deltas.created)
25
- tree.commits.set(delta.ref.id, delta.next);
26
-
27
- for (const delta of deltas.skipped)
28
- tree.commits.set(delta.next.id, delta.next);
29
-
30
- for (const delta of deltas.updated)
31
- tree.commits.set(delta.ref.id, delta.next);
32
-
33
- for (const delta of deltas.removed)
34
- tree.commits.delete(delta.ref.id);
35
- };
22
+ export class Delta {
23
+ fresh: Map<CommitID, Commit2> = new Map();
24
+ changed: Map<CommitID, { prev: Element, next: Commit2, moved: boolean }> = new Map();
25
+ removed: Map<CommitID, Commit2> = new Map();
26
+
27
+ effects: Map<EffectID, EffectTask> = new Map();
28
+ cleanups: Map<EffectID, EffectTask> = new Map();
29
+
30
+ get size() {
31
+ return (
32
+ + this.fresh.size
33
+ + this.changed.size
34
+ + this.removed.size
35
+ )
36
+ }
37
+
38
+ add(commit: Commit2) {
39
+ this.fresh.set(commit.ref.id, commit)
40
+ }
41
+ update(prev: Element, next: Commit2, moved: boolean) {
42
+ if (this.fresh.has(next.ref.id)) {
43
+ this.fresh.set(next.ref.id, next);
44
+ } else {
45
+ const change = this.changed.get(next.ref.id);
46
+ if (change) {
47
+ change.next = next;
48
+ } else {
49
+ this.changed.set(next.ref.id, { prev, next, moved });
50
+ }
51
+ }
52
+ }
53
+ delete(commit: Commit2) {
54
+ if (this.fresh.has(commit.ref.id)) {
55
+ this.fresh.delete(commit.ref.id);
56
+ }
57
+ else {
58
+ if (this.changed.has(commit.ref.id))
59
+ this.changed.delete(commit.ref.id);
60
+
61
+ this.removed.set(commit.ref.id, commit);
62
+ }
63
+ }
36
64
 
37
- export const DeltaSet = {
38
- create: (): DeltaSet => ({ created: [], updated: [], skipped: [], removed: [] }),
39
- clone: (deltas: DeltaSet): DeltaSet => ({
40
- created: [...deltas.created],
41
- updated: [...deltas.updated],
42
- skipped: [...deltas.skipped],
43
- removed: [...deltas.removed],
44
- }),
45
- apply: applyDeltaSet,
46
- }
65
+ addEffects(tasks: EffectTask[]) {
66
+ for (const task of tasks) {
67
+ this.effects.set(task.id, task);
68
+ }
69
+ }
47
70
 
71
+ addCleanups(tasks: EffectTask[]) {
72
+ for (const task of tasks) {
73
+ this.effects.delete(task.id);
74
+ this.cleanups.set(task.id, task);
75
+ }
76
+ }
77
+ }