@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 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 { addRenderTargetToThread, WorkThread } from "./thread";
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
- addRenderTargetToThread(thread, consumer);
74
+ WorkThread.queueTarget(thread, consumer, tree);
75
75
  }
76
76
  }
77
77
  break;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lukekaalim/act-recon",
3
3
  "type": "module",
4
- "version": "3.0.0-alpha.2",
4
+ "version": "3.0.0-alpha.4",
5
5
  "main": "mod.ts",
6
6
  "dependencies": {
7
7
  "@lukekaalim/act": "^3.2.0"
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, WorkID } from "./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
- work: WorkID | null,
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
- work: null,
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
- state.work = scheduler.requestWork(work);
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
- for (const [,target] of pendingTargets)
60
- render(target);
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
- WorkThread.apply(completedThread, tree);
63
- events.emit({ type: 'thread:complete', thread: completedThread });
64
-
65
- // Run side effects
66
- for (const effect of completedThread.pendingEffects) {
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 start = () => {
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
- if (!state.work) {
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 = start();
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 = start();
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
- * Work provider is platform agnostic timer/callback interface
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
- requestWork(callback: () => void): WorkID,
9
- cancelWork(workId: WorkID): void,
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
- thread.pendingUpdates.push(...updates);
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
- export const addRenderTargetToThread = (thread: WorkThread, target: CommitRef): boolean => {
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(ref.id) as Commit;
221
- startWorkThreadUpdate(thread, ref, prev, prev.element);
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);