@lukekaalim/act-recon 3.0.0-alpha.4 → 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 +20 -0
- package/algorithms.ts +55 -41
- package/commit.ts +103 -60
- package/delta.ts +71 -41
- package/docs/lifecycle_of_an_update.md +465 -0
- package/element.ts +153 -143
- package/hooks.ts +49 -32
- package/mod.ts +9 -7
- package/package.json +2 -2
- package/pool.ts +48 -0
- package/readme.md +52 -2
- package/reconciler.ts +83 -108
- package/state.ts +82 -9
- package/thread.ts +255 -264
- package/tree.ts +196 -54
- package/update.ts +48 -109
- package/context.ts +0 -19
- package/event.ts +0 -30
- package/work.ts +0 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
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
|
+
|
|
3
23
|
## 3.0.0-alpha.4
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
package/algorithms.ts
CHANGED
|
@@ -1,50 +1,64 @@
|
|
|
1
1
|
import { MagicError } from "@lukekaalim/act";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
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
|
-
*
|
|
18
|
-
*
|
|
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
|
-
*
|
|
21
|
-
* @param tree
|
|
20
|
+
* The Delta records Commits as well as Effects this way.
|
|
22
21
|
*/
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
}
|