@lukekaalim/act-recon 3.0.0-alpha.1 → 3.0.0-alpha.3
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 +14 -0
- package/element.ts +10 -4
- package/package.json +1 -1
- package/reconciler.ts +35 -22
- package/scheduler.ts +15 -6
- package/thread.ts +24 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @lukekaalim/act-recon
|
|
2
2
|
|
|
3
|
+
## 3.0.0-alpha.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ccb3900: Reconciler should apply all changes in the correct order, and not skip any.
|
|
8
|
+
- Reconciler does send out a Render command once it completes all pending renders (called a "ThreadStack")
|
|
9
|
+
Scheduler has been updated to perform some updates in Sync.
|
|
10
|
+
|
|
11
|
+
## 3.0.0-alpha.2
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- c5e8775: Fix context updates not actually being pushed to the MustRender list
|
|
16
|
+
|
|
3
17
|
## 3.0.0-alpha.1
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/element.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { loadHooks } from "./hooks";
|
|
|
7
7
|
import { ContextState } from "./context";
|
|
8
8
|
import { ComponentState, EffectID, EffectTask } from "./state";
|
|
9
9
|
import { CommitTree } from "./tree";
|
|
10
|
+
import { WorkThread } from "./thread";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* When processing an element, it may produce additional
|
|
@@ -17,19 +18,17 @@ export type ElementOutput = {
|
|
|
17
18
|
child: Node,
|
|
18
19
|
reject: null | unknown,
|
|
19
20
|
effects: EffectTask[],
|
|
20
|
-
targets: CommitRef[],
|
|
21
21
|
};
|
|
22
22
|
export const ElementOutput = {
|
|
23
23
|
new: (child: Node): ElementOutput => ({
|
|
24
24
|
child,
|
|
25
25
|
reject: null,
|
|
26
26
|
effects: [],
|
|
27
|
-
targets: [],
|
|
28
27
|
})
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
export type ElementService = {
|
|
32
|
-
render(element: Element, ref: CommitRef): ElementOutput,
|
|
31
|
+
render(element: Element, ref: CommitRef, thread: WorkThread): ElementOutput,
|
|
33
32
|
clear(ref: Commit): ElementOutput,
|
|
34
33
|
|
|
35
34
|
boundary: Map<CommitID, unknown>,
|
|
@@ -45,6 +44,7 @@ export const createElementService = (
|
|
|
45
44
|
const render = (
|
|
46
45
|
element: Element,
|
|
47
46
|
ref: CommitRef,
|
|
47
|
+
thread: WorkThread,
|
|
48
48
|
): ElementOutput => {
|
|
49
49
|
const output = ElementOutput.new(element.children);
|
|
50
50
|
|
|
@@ -66,7 +66,13 @@ export const createElementService = (
|
|
|
66
66
|
}
|
|
67
67
|
if (state.value !== element.props.value) {
|
|
68
68
|
state.value = element.props.value;
|
|
69
|
-
|
|
69
|
+
|
|
70
|
+
for (const [, consumer] of state.consumers) {
|
|
71
|
+
// there should be no way for the children of the
|
|
72
|
+
// provider to already have been renderer,
|
|
73
|
+
// so we don't check the return value.
|
|
74
|
+
WorkThread.queueTarget(thread, consumer, tree);
|
|
75
|
+
}
|
|
70
76
|
}
|
|
71
77
|
break;
|
|
72
78
|
}
|
package/package.json
CHANGED
package/reconciler.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { CommitID, CommitRef } from "./commit";
|
|
|
3
3
|
import { WorkThread } from "./thread"
|
|
4
4
|
import { CommitTree } from "./tree";
|
|
5
5
|
import { ElementService } from "./element";
|
|
6
|
-
import { Scheduler
|
|
6
|
+
import { Scheduler } from "./scheduler";
|
|
7
7
|
import { createEventEmitter, EventEmitter } from "./event";
|
|
8
8
|
|
|
9
9
|
export type Reconciler = {
|
|
@@ -18,7 +18,7 @@ export type Reconciler = {
|
|
|
18
18
|
|
|
19
19
|
export type ReconcilerState = {
|
|
20
20
|
thread: WorkThread | null,
|
|
21
|
-
|
|
21
|
+
pendingThreadStack: WorkThread[],
|
|
22
22
|
/**
|
|
23
23
|
* These are targets that can't be fulfilled with the current thread
|
|
24
24
|
* */
|
|
@@ -36,56 +36,66 @@ export const createReconciler = (scheduler: Scheduler): Reconciler => {
|
|
|
36
36
|
const events = createEventEmitter<ReconcilerEvent>();
|
|
37
37
|
const state: ReconcilerState = {
|
|
38
38
|
thread: null,
|
|
39
|
-
|
|
39
|
+
pendingThreadStack: [],
|
|
40
40
|
pendingTargets: new Map(),
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
const work = () => {
|
|
44
|
-
state.work = null;
|
|
45
44
|
if (!state.thread)
|
|
46
45
|
return;
|
|
47
46
|
|
|
48
47
|
const update = state.thread.pendingUpdates.pop();
|
|
49
48
|
if (update) {
|
|
50
49
|
WorkThread.update(state.thread, update, tree, elements);
|
|
51
|
-
|
|
50
|
+
|
|
51
|
+
scheduler.requestCallback();
|
|
52
52
|
} else {
|
|
53
53
|
const completedThread = state.thread;
|
|
54
54
|
state.thread = null;
|
|
55
55
|
|
|
56
56
|
const pendingTargets = [...state.pendingTargets]
|
|
57
57
|
state.pendingTargets.clear();
|
|
58
|
+
state.pendingThreadStack.push(completedThread);
|
|
59
|
+
WorkThread.apply(completedThread, tree);
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
if (pendingTargets.length === 0) {
|
|
62
|
+
for (const thread of state.pendingThreadStack) {
|
|
63
|
+
// fire off all renders
|
|
64
|
+
events.emit({ type: 'thread:complete', thread });
|
|
65
|
+
|
|
66
|
+
// Run side effects
|
|
67
|
+
for (const effect of thread.pendingEffects) {
|
|
68
|
+
try {
|
|
69
|
+
effect.func();
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error(error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
61
75
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
effect.func();
|
|
69
|
-
} catch (error) {
|
|
70
|
-
console.error(error);
|
|
76
|
+
state.pendingThreadStack = [];
|
|
77
|
+
} else {
|
|
78
|
+
const nextThread = getOrStartThread();
|
|
79
|
+
for (const [,target] of pendingTargets) {
|
|
80
|
+
render(target);
|
|
71
81
|
}
|
|
82
|
+
nextThread.pendingEffects.push(...completedThread.pendingEffects);
|
|
72
83
|
}
|
|
73
84
|
}
|
|
74
85
|
}
|
|
75
86
|
|
|
76
|
-
const
|
|
87
|
+
const getOrStartThread = () => {
|
|
77
88
|
if (!state.thread) {
|
|
78
89
|
state.thread = WorkThread.new();
|
|
79
90
|
events.emit({ type: 'thread:start', thread: state.thread });
|
|
91
|
+
} else {
|
|
80
92
|
}
|
|
81
|
-
|
|
82
|
-
state.work = scheduler.requestWork(work);
|
|
83
|
-
}
|
|
93
|
+
scheduler.requestCallback();
|
|
84
94
|
return state.thread;
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
const mount = (node: Node) => {
|
|
88
|
-
const thread =
|
|
98
|
+
const thread = getOrStartThread();
|
|
89
99
|
const elements = convertNodeToElements(node)
|
|
90
100
|
|
|
91
101
|
for (const element of elements) {
|
|
@@ -96,7 +106,7 @@ export const createReconciler = (scheduler: Scheduler): Reconciler => {
|
|
|
96
106
|
events.emit({ type: 'thread:new-root', thread });
|
|
97
107
|
};
|
|
98
108
|
const render = (ref: CommitRef) => {
|
|
99
|
-
const thread =
|
|
109
|
+
const thread = getOrStartThread();
|
|
100
110
|
|
|
101
111
|
if (WorkThread.queueTarget(thread, ref, tree)) {
|
|
102
112
|
events.emit({ type: 'thread:new-target', thread });
|
|
@@ -108,5 +118,8 @@ export const createReconciler = (scheduler: Scheduler): Reconciler => {
|
|
|
108
118
|
const tree = CommitTree.new();
|
|
109
119
|
const elements = ElementService.create(tree, render);
|
|
110
120
|
|
|
121
|
+
|
|
122
|
+
scheduler.setCallbackFunc(work);
|
|
123
|
+
|
|
111
124
|
return { mount, render, state, tree, elements, subscribe: events.subscribe };
|
|
112
125
|
}
|
package/scheduler.ts
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import { OpaqueID } from "@lukekaalim/act";
|
|
2
|
-
|
|
3
|
-
export type WorkID = OpaqueID<"WorkID">;
|
|
4
1
|
/**
|
|
5
|
-
*
|
|
2
|
+
* The Scheduler is an agnostic interface for a very simple
|
|
3
|
+
* request/cancel interface for async work.
|
|
4
|
+
*
|
|
5
|
+
* In practice, this will be backed by something
|
|
6
|
+
* as simple as setTimeout, requestAnimationFrame
|
|
7
|
+
* or even idleCallback.
|
|
8
|
+
*
|
|
9
|
+
* It should have a bit of internal state - only
|
|
10
|
+
* a single callback can be queued at once.
|
|
6
11
|
*/
|
|
7
12
|
export type Scheduler = {
|
|
8
|
-
|
|
9
|
-
|
|
13
|
+
setCallbackFunc(callback: () => void): void,
|
|
14
|
+
|
|
15
|
+
requestCallback(): void,
|
|
16
|
+
cancelCallback(): void,
|
|
17
|
+
|
|
18
|
+
isCallbackPending(): boolean,
|
|
10
19
|
};
|
package/thread.ts
CHANGED
|
@@ -83,7 +83,7 @@ const updateWorkThread = (thread: WorkThread, update: Update, tree: CommitTree,
|
|
|
83
83
|
thread.visited.set(ref.id, ref);
|
|
84
84
|
|
|
85
85
|
if (next) {
|
|
86
|
-
const output = element.render(next, ref);
|
|
86
|
+
const output = element.render(next, ref, thread);
|
|
87
87
|
if (output.reject) {
|
|
88
88
|
const errorBoundary = WorkThread.findClosestBoundary(thread, tree, ref);
|
|
89
89
|
if (errorBoundary) {
|
|
@@ -158,35 +158,37 @@ const startWorkThreadUpdate = (thread: WorkThread, ref: CommitRef, prev: Commit
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
/**
|
|
161
|
-
*
|
|
161
|
+
* For a running thread, add an additional "target" to render,
|
|
162
|
+
* which forces the thread to VISIT each commit on the path to the target,
|
|
163
|
+
* and to RENDER the target specifically.
|
|
162
164
|
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* @
|
|
166
|
-
*
|
|
167
|
-
*
|
|
165
|
+
* @param thread
|
|
166
|
+
* @param target
|
|
167
|
+
* @returns `true` if the target was added successfully (or was already in the thread),
|
|
168
|
+
* or `false` if it could not be added for some reason, such as:
|
|
169
|
+
* - The thread has already visited the Commit (a thread will never backtrack)
|
|
168
170
|
*/
|
|
169
|
-
const queueWorkThreadTarget = (thread: WorkThread,
|
|
170
|
-
// If the thread _already_ has this ref as a target,
|
|
171
|
-
// do nothing
|
|
172
|
-
if (thread.mustRender.has(ref.id))
|
|
173
|
-
return true;
|
|
174
|
-
|
|
171
|
+
const queueWorkThreadTarget = (thread: WorkThread, target: CommitRef, tree: CommitTree): boolean => {
|
|
175
172
|
// We cant do work on a commit that has
|
|
176
173
|
// already been visited
|
|
177
|
-
if (thread.visited.has(
|
|
174
|
+
if (thread.visited.has(target.id))
|
|
178
175
|
return false;
|
|
179
176
|
|
|
180
|
-
thread
|
|
181
|
-
|
|
177
|
+
// If the thread _already_ has this ref as a target,
|
|
178
|
+
// do nothing
|
|
179
|
+
if (thread.mustRender.has(target.id))
|
|
180
|
+
return true;
|
|
181
|
+
|
|
182
|
+
thread.reasons.push({ type: 'target', ref: target });
|
|
183
|
+
thread.mustRender.set(target.id, target);
|
|
182
184
|
|
|
183
185
|
// Search through all the parents, looking to see if
|
|
184
186
|
// there are any pendingUpdates that might
|
|
185
187
|
// lead to this commit. If so, make sure ancestor commit
|
|
186
188
|
// is on the MustVisit so they should make their way down
|
|
187
189
|
// eventually
|
|
188
|
-
for (let i =
|
|
189
|
-
const id =
|
|
190
|
+
for (let i = target.path.length - 1; i >= 0; i--) {
|
|
191
|
+
const id = target.path[i];
|
|
190
192
|
thread.mustVisit.add(id);
|
|
191
193
|
|
|
192
194
|
for (const update of thread.pendingUpdates) {
|
|
@@ -196,11 +198,13 @@ const queueWorkThreadTarget = (thread: WorkThread, ref: CommitRef, tree: CommitT
|
|
|
196
198
|
return true;
|
|
197
199
|
}
|
|
198
200
|
}
|
|
201
|
+
|
|
199
202
|
// otherwise, start a new update from the root
|
|
200
|
-
const prev = tree.commits.get(
|
|
201
|
-
startWorkThreadUpdate(thread,
|
|
203
|
+
const prev = tree.commits.get(target.id) as Commit;
|
|
204
|
+
startWorkThreadUpdate(thread, target, prev, prev.element);
|
|
202
205
|
return true;
|
|
203
206
|
}
|
|
207
|
+
|
|
204
208
|
const queueWorkThreadMount = (thread: WorkThread, ref: CommitRef, element: Element) => {
|
|
205
209
|
thread.reasons.push({ type: 'mount', element, ref });
|
|
206
210
|
startWorkThreadUpdate(thread, ref, null, element);
|