@lukekaalim/act-recon 3.0.0-alpha.2 → 3.0.0-alpha.4
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 +2 -2
- package/package.json +1 -1
- package/reconciler.ts +35 -22
- package/scheduler.ts +15 -6
- package/thread.ts +15 -26
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @lukekaalim/act-recon
|
|
2
2
|
|
|
3
|
+
## 3.0.0-alpha.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 2984273: Check if immediate child update is already handlded by some other system (avoiding double-rendering)
|
|
8
|
+
|
|
9
|
+
## 3.0.0-alpha.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- ccb3900: Reconciler should apply all changes in the correct order, and not skip any.
|
|
14
|
+
- Reconciler does send out a Render command once it completes all pending renders (called a "ThreadStack")
|
|
15
|
+
Scheduler has been updated to perform some updates in Sync.
|
|
16
|
+
|
|
3
17
|
## 3.0.0-alpha.2
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/element.ts
CHANGED
|
@@ -7,7 +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 {
|
|
10
|
+
import { WorkThread } from "./thread";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* When processing an element, it may produce additional
|
|
@@ -71,7 +71,7 @@ export const createElementService = (
|
|
|
71
71
|
// there should be no way for the children of the
|
|
72
72
|
// provider to already have been renderer,
|
|
73
73
|
// so we don't check the return value.
|
|
74
|
-
|
|
74
|
+
WorkThread.queueTarget(thread, consumer, tree);
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
break;
|
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
|
@@ -109,7 +109,12 @@ const updateWorkThread = (thread: WorkThread, update: Update, tree: CommitTree,
|
|
|
109
109
|
const [childRefs, updates] = calculateUpdates(ref, prevChildren, output.child);
|
|
110
110
|
|
|
111
111
|
thread.pendingEffects.push(...output.effects);
|
|
112
|
-
|
|
112
|
+
for (const update of updates) {
|
|
113
|
+
// if someone has already marked the update as needing rendering, assume
|
|
114
|
+
// that there is already an update in the stack to handle it.
|
|
115
|
+
if (!thread.mustRender.has(update.ref.id))
|
|
116
|
+
thread.pendingUpdates.push(update);
|
|
117
|
+
}
|
|
113
118
|
|
|
114
119
|
const commit = Commit.update(ref, next, childRefs);
|
|
115
120
|
|
|
@@ -168,17 +173,17 @@ const startWorkThreadUpdate = (thread: WorkThread, ref: CommitRef, prev: Commit
|
|
|
168
173
|
* or `false` if it could not be added for some reason, such as:
|
|
169
174
|
* - The thread has already visited the Commit (a thread will never backtrack)
|
|
170
175
|
*/
|
|
171
|
-
|
|
172
|
-
// If the thread _already_ has this ref as a target,
|
|
173
|
-
// do nothing
|
|
174
|
-
if (thread.mustRender.has(target.id))
|
|
175
|
-
return true;
|
|
176
|
-
|
|
176
|
+
const queueWorkThreadTarget = (thread: WorkThread, target: CommitRef, tree: CommitTree): boolean => {
|
|
177
177
|
// We cant do work on a commit that has
|
|
178
178
|
// already been visited
|
|
179
179
|
if (thread.visited.has(target.id))
|
|
180
180
|
return false;
|
|
181
181
|
|
|
182
|
+
// If the thread _already_ has this ref as a target,
|
|
183
|
+
// do nothing
|
|
184
|
+
if (thread.mustRender.has(target.id))
|
|
185
|
+
return true;
|
|
186
|
+
|
|
182
187
|
thread.reasons.push({ type: 'target', ref: target });
|
|
183
188
|
thread.mustRender.set(target.id, target);
|
|
184
189
|
|
|
@@ -199,28 +204,12 @@ export const addRenderTargetToThread = (thread: WorkThread, target: CommitRef):
|
|
|
199
204
|
}
|
|
200
205
|
}
|
|
201
206
|
|
|
202
|
-
return true;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Request that a commit be re-rendered
|
|
207
|
-
*
|
|
208
|
-
* If returns false, the update cannot be queued in the current
|
|
209
|
-
* thread (maybe it already re-rendered?).
|
|
210
|
-
* @param thread
|
|
211
|
-
* @param ref
|
|
212
|
-
* @returns
|
|
213
|
-
*/
|
|
214
|
-
const queueWorkThreadTarget = (thread: WorkThread, ref: CommitRef, tree: CommitTree): boolean => {
|
|
215
|
-
// Try to add to the thread
|
|
216
|
-
if (!addRenderTargetToThread(thread, ref)) {
|
|
217
|
-
return false;
|
|
218
|
-
}
|
|
219
207
|
// otherwise, start a new update from the root
|
|
220
|
-
const prev = tree.commits.get(
|
|
221
|
-
startWorkThreadUpdate(thread,
|
|
208
|
+
const prev = tree.commits.get(target.id) as Commit;
|
|
209
|
+
startWorkThreadUpdate(thread, target, prev, prev.element);
|
|
222
210
|
return true;
|
|
223
211
|
}
|
|
212
|
+
|
|
224
213
|
const queueWorkThreadMount = (thread: WorkThread, ref: CommitRef, element: Element) => {
|
|
225
214
|
thread.reasons.push({ type: 'mount', element, ref });
|
|
226
215
|
startWorkThreadUpdate(thread, ref, null, element);
|