@lukekaalim/act-recon 3.0.0-alpha.2 → 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 +8 -0
- package/element.ts +2 -2
- package/package.json +1 -1
- package/reconciler.ts +35 -22
- package/scheduler.ts +15 -6
- package/thread.ts +9 -25
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
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
|
+
|
|
3
11
|
## 3.0.0-alpha.2
|
|
4
12
|
|
|
5
13
|
### 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
|
@@ -168,17 +168,17 @@ const startWorkThreadUpdate = (thread: WorkThread, ref: CommitRef, prev: Commit
|
|
|
168
168
|
* or `false` if it could not be added for some reason, such as:
|
|
169
169
|
* - The thread has already visited the Commit (a thread will never backtrack)
|
|
170
170
|
*/
|
|
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
|
-
|
|
171
|
+
const queueWorkThreadTarget = (thread: WorkThread, target: CommitRef, tree: CommitTree): boolean => {
|
|
177
172
|
// We cant do work on a commit that has
|
|
178
173
|
// already been visited
|
|
179
174
|
if (thread.visited.has(target.id))
|
|
180
175
|
return false;
|
|
181
176
|
|
|
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
182
|
thread.reasons.push({ type: 'target', ref: target });
|
|
183
183
|
thread.mustRender.set(target.id, target);
|
|
184
184
|
|
|
@@ -199,28 +199,12 @@ export const addRenderTargetToThread = (thread: WorkThread, target: CommitRef):
|
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
|
|
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
202
|
// otherwise, start a new update from the root
|
|
220
|
-
const prev = tree.commits.get(
|
|
221
|
-
startWorkThreadUpdate(thread,
|
|
203
|
+
const prev = tree.commits.get(target.id) as Commit;
|
|
204
|
+
startWorkThreadUpdate(thread, target, prev, prev.element);
|
|
222
205
|
return true;
|
|
223
206
|
}
|
|
207
|
+
|
|
224
208
|
const queueWorkThreadMount = (thread: WorkThread, ref: CommitRef, element: Element) => {
|
|
225
209
|
thread.reasons.push({ type: 'mount', element, ref });
|
|
226
210
|
startWorkThreadUpdate(thread, ref, null, element);
|