@lukekaalim/act-recon 3.1.0 → 4.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 +11 -0
- package/algorithms.ts +1 -23
- package/commit.ts +0 -11
- package/delta.ts +3 -11
- package/element.ts +3 -9
- package/events.ts +25 -0
- package/hooks.ts +10 -19
- package/mod.ts +0 -1
- package/package.json +2 -2
- package/reconciler.ts +12 -34
- package/scheduler.ts +18 -1
- package/state.ts +19 -4
- package/thread.ts +107 -27
- package/tree.ts +45 -15
- package/update.ts +6 -20
- package/pool.ts +0 -48
package/CHANGELOG.md
CHANGED
package/algorithms.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
export type ChangeEqualityTest<Prev, Next> = (prev: Prev, next: Next, prevIndex: number, nextIndex: number) => boolean;
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* ChangeReport
|
|
@@ -56,25 +56,3 @@ export class ChangeReport2 {
|
|
|
56
56
|
return report;
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
-
|
|
60
|
-
export type ChangeEqualityTest<Prev, Next> = (prev: Prev, next: Next, prevIndex: number, nextIndex: number) => boolean;
|
|
61
|
-
|
|
62
|
-
export const first = <X, Y>(array: ReadonlyArray<X>, func: (value: X, index: number) => Y | null): Y | null => {
|
|
63
|
-
for (let i = 0; i < array.length; i++) {
|
|
64
|
-
const value = array[i];
|
|
65
|
-
const result = func(value, i);
|
|
66
|
-
if (result !== null)
|
|
67
|
-
return result;
|
|
68
|
-
}
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export const last = <X, Y extends {}>(array: ReadonlyArray<X>, func: (value: X, index: number) => Y | null | false | undefined | 0): Y | null => {
|
|
73
|
-
for (let i = array.length - 1; i > 0; i--) {
|
|
74
|
-
const value = array[i];
|
|
75
|
-
const result = func(value, i);
|
|
76
|
-
if (result)
|
|
77
|
-
return result;
|
|
78
|
-
}
|
|
79
|
-
return null;
|
|
80
|
-
}
|
package/commit.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createId, Element, OpaqueID, specialNodeTypes, SuspendProps } from "@lukekaalim/act";
|
|
2
|
-
import { createObjectPool } from "./pool";
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* A single consistent id representing a commit in the act tree.
|
|
@@ -86,16 +85,6 @@ export class CommitRef2 {
|
|
|
86
85
|
}
|
|
87
86
|
|
|
88
87
|
export class Commit2 {
|
|
89
|
-
static pool = () => createObjectPool<Commit2, ConstructorParameters<typeof Commit2>>(
|
|
90
|
-
function alloc (ref, el, ch) { return new Commit2(ref, el, ch) },
|
|
91
|
-
function reassign(c, ref, el, ch) {
|
|
92
|
-
c.ref = ref;
|
|
93
|
-
c.element = el;
|
|
94
|
-
c.children = ch;
|
|
95
|
-
c.version = createId('CommitVersion');
|
|
96
|
-
}
|
|
97
|
-
)
|
|
98
|
-
|
|
99
88
|
ref: CommitRef2;
|
|
100
89
|
|
|
101
90
|
element: Element;
|
package/delta.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Element } from "@lukekaalim/act";
|
|
2
2
|
import { Commit2, CommitID } from "./commit.ts";
|
|
3
|
-
import { EffectID, EffectTask } from "./state.ts";
|
|
3
|
+
import { EffectID, EffectTask, EffectTask2 } from "./state.ts";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The Delta class represents an accumulation
|
|
@@ -24,8 +24,7 @@ export class Delta {
|
|
|
24
24
|
changed: Map<CommitID, { prev: Element, next: Commit2, moved: boolean }> = new Map();
|
|
25
25
|
removed: Map<CommitID, Commit2> = new Map();
|
|
26
26
|
|
|
27
|
-
effects: Map<EffectID,
|
|
28
|
-
cleanups: Map<EffectID, EffectTask> = new Map();
|
|
27
|
+
effects: Map<EffectID, EffectTask2> = new Map();
|
|
29
28
|
|
|
30
29
|
get size() {
|
|
31
30
|
return (
|
|
@@ -62,16 +61,9 @@ export class Delta {
|
|
|
62
61
|
}
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
addEffects(tasks:
|
|
64
|
+
addEffects(tasks: EffectTask2[]) {
|
|
66
65
|
for (const task of tasks) {
|
|
67
66
|
this.effects.set(task.id, task);
|
|
68
67
|
}
|
|
69
68
|
}
|
|
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
69
|
}
|
package/element.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "@lukekaalim/act";
|
|
10
10
|
import { Commit2, CommitRef2 } from "./commit";
|
|
11
11
|
import { loadHooks2 } from "./hooks";
|
|
12
|
-
import { BoundaryState, ComponentState, ContextState, EffectTask } from "./state";
|
|
12
|
+
import { BoundaryState, ComponentState, ContextState, EffectID, EffectTask, EffectTask2 } from "./state";
|
|
13
13
|
import { keyedElementEqualityTest2, WorkTask } from "./update";
|
|
14
14
|
import { ChangeReport2 } from "./algorithms";
|
|
15
15
|
import { CommitTree2 } from "./tree";
|
|
@@ -40,8 +40,7 @@ export class ElementOutput2 {
|
|
|
40
40
|
*/
|
|
41
41
|
updates: WorkTask[] = []
|
|
42
42
|
|
|
43
|
-
effects: null |
|
|
44
|
-
cleanups: null | EffectTask[] = null;
|
|
43
|
+
effects: null | EffectTask2[] = null;
|
|
45
44
|
|
|
46
45
|
extraTargets: null | CommitRef2[] = null;
|
|
47
46
|
|
|
@@ -51,12 +50,9 @@ export class ElementOutput2 {
|
|
|
51
50
|
|
|
52
51
|
processComponent(component: Component<{}>, element: Element, tree: CommitTree2, state: ComponentState) {
|
|
53
52
|
this.element = element;
|
|
54
|
-
state.effectTasks = null;
|
|
55
53
|
|
|
56
54
|
state.hookIndex = 0;
|
|
57
|
-
|
|
58
|
-
state.hooks = loadHooks2(tree.reconciler, state, this.ref);
|
|
59
|
-
|
|
55
|
+
state.hooks = loadHooks2(tree.reconciler, state, this);
|
|
60
56
|
|
|
61
57
|
hookImplementation.useContext = state.hooks.useContext;
|
|
62
58
|
hookImplementation.useEffect = state.hooks.useEffect;
|
|
@@ -76,10 +72,8 @@ export class ElementOutput2 {
|
|
|
76
72
|
if (state.boundary)
|
|
77
73
|
state.boundary.clearThrow(this.ref);
|
|
78
74
|
}
|
|
79
|
-
this.effects = state.effectTasks;
|
|
80
75
|
this.calculateDiff();
|
|
81
76
|
} catch (thrownValue) {
|
|
82
|
-
|
|
83
77
|
if (!state.boundary) {
|
|
84
78
|
const boundary = tree.findClosestBoundary(this.ref);
|
|
85
79
|
if (!boundary)
|
package/events.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A convenience interface to describe a series of functions that represent
|
|
3
|
+
* event handlers.
|
|
4
|
+
*
|
|
5
|
+
* Only one function exists per "event" type, so this is simpler and more
|
|
6
|
+
* direction approach than an pub/sub architecture.
|
|
7
|
+
*
|
|
8
|
+
* Often used in recon to avoid circular reference issues when initializing,
|
|
9
|
+
* as assigning the event bus can occur after all associated resources init properly.
|
|
10
|
+
*
|
|
11
|
+
* A system that has a (writable) event bus often fills it with a stub implementation
|
|
12
|
+
* initially. The individual functions are readonly, so you should endeavour to replace
|
|
13
|
+
* the whole bus.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* You have something that wants to listen for events, and something that
|
|
17
|
+
* emits them.
|
|
18
|
+
* ```ts
|
|
19
|
+
* const listener = new Listener();
|
|
20
|
+
* const emitter = new Emitter();
|
|
21
|
+
*
|
|
22
|
+
* emitter.bus = listener.bus;
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export type EventBus<T extends { [K: string]: (...args: any[]) => void }> = { readonly [key in keyof T]: T[key] };
|
package/hooks.ts
CHANGED
|
@@ -11,29 +11,27 @@ import {
|
|
|
11
11
|
import { ComponentState, EffectID, EffectTask } from "./state";
|
|
12
12
|
import { CommitRef2 } from "./commit";
|
|
13
13
|
import { Reconciler2 } from "./reconciler";
|
|
14
|
-
import {
|
|
14
|
+
import { ElementOutput2 } from "./element";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* A fresh set of hook functions is created per component run.
|
|
18
18
|
*/
|
|
19
19
|
export const loadHooks2 = (
|
|
20
20
|
reconciler: Reconciler2,
|
|
21
|
-
|
|
22
21
|
state: ComponentState,
|
|
23
|
-
|
|
22
|
+
output: ElementOutput2
|
|
24
23
|
): HookImplementation => {
|
|
25
|
-
|
|
26
24
|
function useContext<T>(context: Context<T>): T {
|
|
27
25
|
const stateIndex = state.hookIndex++;
|
|
28
26
|
|
|
29
27
|
if (!state.providers.has(stateIndex)) {
|
|
30
|
-
const provider = ref.find(ref => {
|
|
28
|
+
const provider = output.ref.find(ref => {
|
|
31
29
|
const provider = reconciler.tree.contexts.get(ref.id)
|
|
32
30
|
if (provider && provider.contextId === context.id)
|
|
33
31
|
return provider;
|
|
34
32
|
})
|
|
35
33
|
if (provider) {
|
|
36
|
-
provider.consumers.set(ref.id, ref);
|
|
34
|
+
provider.consumers.set(output.ref.id, output.ref);
|
|
37
35
|
}
|
|
38
36
|
state.providers.set(stateIndex, provider);
|
|
39
37
|
}
|
|
@@ -58,7 +56,7 @@ export const loadHooks2 = (
|
|
|
58
56
|
return;
|
|
59
57
|
|
|
60
58
|
state.values.set(stateIndex, nextValue);
|
|
61
|
-
reconciler.render(ref);
|
|
59
|
+
reconciler.render(output.ref);
|
|
62
60
|
};
|
|
63
61
|
return [value, setValue];
|
|
64
62
|
}
|
|
@@ -74,20 +72,13 @@ export const loadHooks2 = (
|
|
|
74
72
|
const depsChanges = calculateDepsChange(prevDeps, deps)
|
|
75
73
|
|
|
76
74
|
if (depsChanges) {
|
|
77
|
-
if (!
|
|
78
|
-
|
|
75
|
+
if (!output.effects)
|
|
76
|
+
output.effects = [];
|
|
79
77
|
|
|
80
|
-
|
|
78
|
+
output.effects.push({
|
|
81
79
|
id: effectId,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const prevCleanup = state.cleanups.get(effectId);
|
|
85
|
-
if (prevCleanup) {
|
|
86
|
-
state.cleanups.delete(effectId);
|
|
87
|
-
prevCleanup();
|
|
88
|
-
}
|
|
89
|
-
state.cleanups.set(effectId, effect());
|
|
90
|
-
}
|
|
80
|
+
effect,
|
|
81
|
+
ref: output.ref,
|
|
91
82
|
});
|
|
92
83
|
}
|
|
93
84
|
}
|
package/mod.ts
CHANGED
package/package.json
CHANGED
package/reconciler.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { CommitTree2 } from "./tree";
|
|
|
5
5
|
import { Scheduler } from "./scheduler";
|
|
6
6
|
import { Delta } from "./delta";
|
|
7
7
|
import { WorkTask } from "./update";
|
|
8
|
+
import { EffectTask2 } from "./state";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* The Reconciler Event Bus is a structure that contains callbacks
|
|
@@ -35,49 +36,26 @@ export class Reconciler2 {
|
|
|
35
36
|
// in the future - maybe more than one thread?
|
|
36
37
|
thread: WorkThread2;
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
commit: Commit2.pool(),
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
constructor(scheduler: Scheduler) {
|
|
39
|
+
constructor(scheduler: Scheduler, { WorkThread = WorkThread2 }: { WorkThread?: typeof WorkThread2} = {}) {
|
|
43
40
|
this.scheduler = scheduler;
|
|
44
41
|
this.tree = new CommitTree2(this);
|
|
45
|
-
this.thread = new
|
|
42
|
+
this.thread = new WorkThread(this.tree);
|
|
46
43
|
|
|
47
|
-
this.
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
this.thread.bus = {
|
|
45
|
+
render: (delta) => this.bus.render(delta)
|
|
46
|
+
}
|
|
50
47
|
|
|
51
|
-
|
|
52
|
-
this.thread = new WorkThread2(this.tree);
|
|
48
|
+
this.scheduler.setCallbackFunc(() => this.work());
|
|
53
49
|
}
|
|
54
50
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
this.startNewThread();
|
|
59
|
-
|
|
60
|
-
// send delta ready
|
|
61
|
-
this.bus.render(currentThread.delta);
|
|
62
|
-
|
|
63
|
-
// run effects
|
|
64
|
-
for (const cleanup of currentThread.delta.cleanups.values())
|
|
65
|
-
cleanup.func();
|
|
66
|
-
for (const effect of currentThread.delta.effects.values())
|
|
67
|
-
effect.func();
|
|
51
|
+
work() {
|
|
52
|
+
this.thread.work();
|
|
68
53
|
|
|
69
|
-
|
|
70
|
-
this.
|
|
71
|
-
}
|
|
54
|
+
if (this.thread.done)
|
|
55
|
+
this.thread.reset();
|
|
72
56
|
|
|
73
|
-
|
|
74
|
-
if (!this.thread.done) {
|
|
75
|
-
// do some work
|
|
76
|
-
this.thread.work();
|
|
57
|
+
if (this.thread.hasWork)
|
|
77
58
|
this.scheduler.requestCallback();
|
|
78
|
-
} else {
|
|
79
|
-
this.submitThread()
|
|
80
|
-
}
|
|
81
59
|
}
|
|
82
60
|
|
|
83
61
|
mount(node: Node): CommitRef2 {
|
package/scheduler.ts
CHANGED
|
@@ -4,14 +4,31 @@
|
|
|
4
4
|
*
|
|
5
5
|
* In practice, this will be backed by something
|
|
6
6
|
* as simple as setTimeout, requestAnimationFrame
|
|
7
|
-
* or even idleCallback
|
|
7
|
+
* or even idleCallback - by the renderer, who
|
|
8
|
+
* knows a bit more about the current runtime
|
|
9
|
+
* environment (aka which callbacks are more
|
|
10
|
+
* suited for the task.)
|
|
8
11
|
*
|
|
9
12
|
* It should have a bit of internal state - only
|
|
10
13
|
* a single callback can be queued at once.
|
|
11
14
|
*/
|
|
12
15
|
export type Scheduler = {
|
|
16
|
+
/**
|
|
17
|
+
* The scheduler will call a specific function when
|
|
18
|
+
* requested. This method provides that function.
|
|
19
|
+
*
|
|
20
|
+
* If not set before the a callback is requested, nothing will
|
|
21
|
+
* happen (a noop is expected by default).
|
|
22
|
+
*
|
|
23
|
+
* @param callback The function that the Scheduler will call when requested.
|
|
24
|
+
*/
|
|
13
25
|
setCallbackFunc(callback: () => void): void,
|
|
14
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Request that (using whatever mechanism the implementer wants)
|
|
29
|
+
* the Callback Function be executed. It should not be called
|
|
30
|
+
* _immediately_, but after some amount of time.
|
|
31
|
+
*/
|
|
15
32
|
requestCallback(): void,
|
|
16
33
|
cancelCallback(): void,
|
|
17
34
|
|
package/state.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CommitID, CommitRef2 } from "./commit.ts";
|
|
2
|
-
import { OpaqueID, Deps, EffectCleanup, ContextID, HookImplementation, createId, BoundaryProps } from '@lukekaalim/act';
|
|
2
|
+
import { OpaqueID, Deps, EffectCleanup, ContextID, HookImplementation, createId, BoundaryProps, EffectConstructor } from '@lukekaalim/act';
|
|
3
3
|
import { CommitTree2 } from "./tree.ts";
|
|
4
4
|
|
|
5
5
|
export type HookID = number;
|
|
@@ -7,7 +7,24 @@ export type EffectID = OpaqueID<"EffectID">;
|
|
|
7
7
|
export type EffectTask = {
|
|
8
8
|
ref: CommitRef2,
|
|
9
9
|
id: EffectID,
|
|
10
|
-
func:
|
|
10
|
+
func: EffectConstructor,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* An instruction to perform the declared side effect,
|
|
15
|
+
* made by the referenced commit. If there are cleanups associated
|
|
16
|
+
* with this effect in the tree, they should be run first.
|
|
17
|
+
*
|
|
18
|
+
* If the effect produces any cleanups, they should be added to the tree.
|
|
19
|
+
*/
|
|
20
|
+
export type EffectTask2 = {
|
|
21
|
+
id: EffectID,
|
|
22
|
+
ref: CommitRef2,
|
|
23
|
+
/**
|
|
24
|
+
* when "Effect" is null, the task should
|
|
25
|
+
* only run any cleanups associated with the effect.
|
|
26
|
+
*/
|
|
27
|
+
effect: EffectConstructor | null,
|
|
11
28
|
}
|
|
12
29
|
|
|
13
30
|
export type ComponentState = {
|
|
@@ -17,12 +34,10 @@ export type ComponentState = {
|
|
|
17
34
|
|
|
18
35
|
hookIndex: HookID,
|
|
19
36
|
hooks: null | HookImplementation,
|
|
20
|
-
effectTasks: null | EffectTask[],
|
|
21
37
|
|
|
22
38
|
values: Map<HookID, unknown>;
|
|
23
39
|
deps: Map<HookID, Deps>;
|
|
24
40
|
effects: Map<HookID, EffectID>;
|
|
25
|
-
cleanups: Map<HookID, EffectCleanup>;
|
|
26
41
|
|
|
27
42
|
rejection: null | { value: unknown };
|
|
28
43
|
boundary: null | BoundaryState;
|
package/thread.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { createId, Element } from "@lukekaalim/act";
|
|
1
|
+
import { createId, Element, OpaqueID, primitiveNodeTypes } from "@lukekaalim/act";
|
|
2
2
|
import { Commit2, CommitID, CommitRef2 } from "./commit.ts";
|
|
3
3
|
import { Delta } from "./delta.ts";
|
|
4
4
|
import { CommitTree2 } from "./tree.ts";
|
|
5
5
|
import { WorkTask } from "./update.ts";
|
|
6
6
|
import { EffectTask } from "./state.ts";
|
|
7
|
+
import { ReconcilerEventBus } from "./reconciler.ts";
|
|
7
8
|
|
|
8
|
-
export type
|
|
9
|
+
export type WorkRequest =
|
|
9
10
|
| { type: 'mount', element: Element, ref: CommitRef2 }
|
|
10
11
|
| { type: 'unmount', ref: CommitRef2 }
|
|
11
12
|
| { type: 'target', ref: CommitRef2 }
|
|
@@ -16,6 +17,26 @@ export type QueueResult =
|
|
|
16
17
|
| 'existing-target'
|
|
17
18
|
| 'existing-task'
|
|
18
19
|
|
|
20
|
+
export type ThreadState = {
|
|
21
|
+
requests: WorkRequest[];
|
|
22
|
+
missedRequests: WorkRequest[];
|
|
23
|
+
|
|
24
|
+
mustRender: Set<CommitID>;
|
|
25
|
+
mustVisit: Set<CommitID>;
|
|
26
|
+
pendingTasks: WorkTask[];
|
|
27
|
+
|
|
28
|
+
missed: Set<CommitID>;
|
|
29
|
+
missedUnmount: Set<CommitID>;
|
|
30
|
+
|
|
31
|
+
visited: Set<CommitID>;
|
|
32
|
+
|
|
33
|
+
started: boolean;
|
|
34
|
+
submitted: boolean;
|
|
35
|
+
|
|
36
|
+
pass: number;
|
|
37
|
+
id: OpaqueID<"ThreadID">;
|
|
38
|
+
}
|
|
39
|
+
|
|
19
40
|
/**
|
|
20
41
|
* A temporary data structure that carries the state of a
|
|
21
42
|
* work-in-progress update to the tree.
|
|
@@ -31,7 +52,7 @@ export class WorkThread2 {
|
|
|
31
52
|
* they record the "reason", so you can trace which effects
|
|
32
53
|
* cause/contributed to this thread.
|
|
33
54
|
*/
|
|
34
|
-
|
|
55
|
+
requests: WorkRequest[] = [];
|
|
35
56
|
/**
|
|
36
57
|
* A Map of every commit that NEEDS to be rendered if you visit them.
|
|
37
58
|
* This is often for commits that explicitly need a re-render because
|
|
@@ -65,9 +86,7 @@ export class WorkThread2 {
|
|
|
65
86
|
|
|
66
87
|
unmountMissed: Set<CommitID> = new Set();
|
|
67
88
|
|
|
68
|
-
|
|
69
|
-
errorNotifications: Map<CommitID, CommitRef2> = new Map();
|
|
70
|
-
|
|
89
|
+
requestsMissed: WorkRequest[] = [];
|
|
71
90
|
/**
|
|
72
91
|
* A list of each commit the thread processed
|
|
73
92
|
*/
|
|
@@ -78,12 +97,20 @@ export class WorkThread2 {
|
|
|
78
97
|
id = createId("ThreadID")
|
|
79
98
|
passes = 1;
|
|
80
99
|
|
|
100
|
+
/** Have we done any work yet? */
|
|
101
|
+
started = false;
|
|
102
|
+
/** Have we submitted our delta to the renderer? */
|
|
103
|
+
submitted = false;
|
|
104
|
+
|
|
81
105
|
constructor(tree: CommitTree2) {
|
|
82
106
|
this.tree = tree;
|
|
83
107
|
}
|
|
84
108
|
|
|
85
109
|
get done() {
|
|
86
|
-
return this.
|
|
110
|
+
return this.started && !this.hasWork;
|
|
111
|
+
}
|
|
112
|
+
get hasWork() {
|
|
113
|
+
return this.pendingTasks.length > 0 || this.missed.size > 0 || (this.started && !this.submitted);
|
|
87
114
|
}
|
|
88
115
|
|
|
89
116
|
/**
|
|
@@ -94,11 +121,16 @@ export class WorkThread2 {
|
|
|
94
121
|
* if the Thread has already rendered this element (you
|
|
95
122
|
* have to queue it in the next thread)
|
|
96
123
|
*/
|
|
97
|
-
queue(reason:
|
|
124
|
+
queue(reason: WorkRequest): QueueResult {
|
|
125
|
+
if (this.submitted) {
|
|
126
|
+
this.requestsMissed.push(reason);
|
|
127
|
+
return 'missed';
|
|
128
|
+
}
|
|
129
|
+
|
|
98
130
|
// We are very lazy in this function - we only
|
|
99
131
|
// want to create a new update at the worst possible
|
|
100
132
|
// case
|
|
101
|
-
this.
|
|
133
|
+
this.requests.push(reason);
|
|
102
134
|
|
|
103
135
|
// Mounts are really easy - they never have any history, so
|
|
104
136
|
// we don't need to check for conflicts.
|
|
@@ -177,7 +209,7 @@ export class WorkThread2 {
|
|
|
177
209
|
createCommit(element: Element, ref: CommitRef2) {
|
|
178
210
|
const output = this.tree.processElement(element, ref, null);
|
|
179
211
|
|
|
180
|
-
const commit =
|
|
212
|
+
const commit = new Commit2(ref, element, output.childRefs);
|
|
181
213
|
|
|
182
214
|
this.tree.commits.set(commit.ref.id, commit);
|
|
183
215
|
this.delta.add(commit);
|
|
@@ -187,7 +219,7 @@ export class WorkThread2 {
|
|
|
187
219
|
if (output.effects)
|
|
188
220
|
this.delta.addEffects(output.effects);
|
|
189
221
|
|
|
190
|
-
this.pendingTasks.push(...output.updates);
|
|
222
|
+
this.pendingTasks.push(...output.updates.toReversed());
|
|
191
223
|
}
|
|
192
224
|
updateCommit(commit: Commit2, element: Element, moved: boolean) {
|
|
193
225
|
const output = this.tree.processElement(element, commit.ref, commit);
|
|
@@ -196,7 +228,7 @@ export class WorkThread2 {
|
|
|
196
228
|
commit.update(element, output.childRefs);
|
|
197
229
|
this.delta.update(oldElement, commit, moved);
|
|
198
230
|
|
|
199
|
-
this.pendingTasks.push(...output.updates);
|
|
231
|
+
this.pendingTasks.push(...output.updates.toReversed());
|
|
200
232
|
if (output.effects)
|
|
201
233
|
this.delta.addEffects(output.effects);
|
|
202
234
|
}
|
|
@@ -208,16 +240,16 @@ export class WorkThread2 {
|
|
|
208
240
|
if (commit.ref.length === 1)
|
|
209
241
|
this.tree.roots.delete(commit.ref.id);
|
|
210
242
|
|
|
211
|
-
this.pendingTasks.push(...output.updates);
|
|
212
|
-
if (output.
|
|
213
|
-
this.delta.addEffects(output.
|
|
243
|
+
this.pendingTasks.push(...output.updates.toReversed());
|
|
244
|
+
if (output.effects)
|
|
245
|
+
this.delta.addEffects(output.effects);
|
|
214
246
|
}
|
|
215
247
|
skipCommit(commit: Commit2) {
|
|
216
248
|
const prevChildren = commit.children
|
|
217
249
|
.map(c => this.tree.commits.get(c.id) as Commit2);
|
|
218
250
|
|
|
219
251
|
const updates = prevChildren.map(prev => WorkTask.visit(prev));
|
|
220
|
-
this.pendingTasks.push(...updates);
|
|
252
|
+
this.pendingTasks.push(...updates.toReversed());
|
|
221
253
|
|
|
222
254
|
commit.update();
|
|
223
255
|
}
|
|
@@ -239,18 +271,23 @@ export class WorkThread2 {
|
|
|
239
271
|
processTask(task: WorkTask) {
|
|
240
272
|
const { next, prev, ref } = task;
|
|
241
273
|
|
|
242
|
-
|
|
274
|
+
if (next && prev) {
|
|
275
|
+
let identicalChange = (
|
|
276
|
+
(next.id === prev.element.id)
|
|
277
|
+
|| ((next.type === primitiveNodeTypes.string || next.type === primitiveNodeTypes.number) && next.props.value === prev.element.props.value)
|
|
278
|
+
);
|
|
243
279
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
280
|
+
if (identicalChange) {
|
|
281
|
+
const mustVisit = this.mustVisit.has(ref.id);
|
|
282
|
+
if (!mustVisit)
|
|
283
|
+
return;
|
|
248
284
|
|
|
249
|
-
|
|
285
|
+
const mustRender = this.mustRender.has(ref.id);
|
|
250
286
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
287
|
+
if (!mustRender) {
|
|
288
|
+
this.skipCommit(prev)
|
|
289
|
+
return
|
|
290
|
+
}
|
|
254
291
|
}
|
|
255
292
|
}
|
|
256
293
|
|
|
@@ -259,11 +296,16 @@ export class WorkThread2 {
|
|
|
259
296
|
|
|
260
297
|
work() {
|
|
261
298
|
const task = this.pendingTasks.pop();
|
|
299
|
+
|
|
262
300
|
if (task) {
|
|
301
|
+
this.started = true;
|
|
263
302
|
this.processTask(task);
|
|
264
|
-
|
|
265
|
-
} else if (!this.done) {
|
|
303
|
+
} else if (this.missed.size > 0) {
|
|
266
304
|
this.startNextPass();
|
|
305
|
+
} else if (this.started && !this.submitted) {
|
|
306
|
+
this.submit();
|
|
307
|
+
} else {
|
|
308
|
+
console.info(`Work on thread was requested, but no work was needed`)
|
|
267
309
|
}
|
|
268
310
|
}
|
|
269
311
|
|
|
@@ -272,6 +314,7 @@ export class WorkThread2 {
|
|
|
272
314
|
this.mustRender.clear();
|
|
273
315
|
this.mustVisit.clear();
|
|
274
316
|
this.visited.clear();
|
|
317
|
+
this.started = false
|
|
275
318
|
|
|
276
319
|
this.passes++;
|
|
277
320
|
|
|
@@ -298,4 +341,41 @@ export class WorkThread2 {
|
|
|
298
341
|
}
|
|
299
342
|
this.missed.clear();
|
|
300
343
|
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Clear the thread of all work,
|
|
347
|
+
* except for any missed requests
|
|
348
|
+
*/
|
|
349
|
+
reset() {
|
|
350
|
+
const missed = this.requestsMissed;
|
|
351
|
+
|
|
352
|
+
this.requests = [];
|
|
353
|
+
this.pendingTasks = [];
|
|
354
|
+
this.requestsMissed = [];
|
|
355
|
+
|
|
356
|
+
this.missed.clear();
|
|
357
|
+
this.mustRender.clear();
|
|
358
|
+
this.mustVisit.clear();
|
|
359
|
+
this.visited.clear();
|
|
360
|
+
this.delta = new Delta();
|
|
361
|
+
|
|
362
|
+
this.id = createId();
|
|
363
|
+
|
|
364
|
+
this.passes = 1;
|
|
365
|
+
|
|
366
|
+
this.submitted = false;
|
|
367
|
+
this.started = false;
|
|
368
|
+
|
|
369
|
+
for (const request of missed)
|
|
370
|
+
this.queue(request);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
bus = { render(delta: Delta): void {} }
|
|
374
|
+
|
|
375
|
+
submit() {
|
|
376
|
+
this.submitted = true;
|
|
377
|
+
|
|
378
|
+
this.bus.render(this.delta);
|
|
379
|
+
this.tree.runEffects(this.delta.effects);
|
|
380
|
+
}
|
|
301
381
|
}
|
package/tree.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { ContextID, Element, specialNodeTypes } from "@lukekaalim/act";
|
|
1
|
+
import { ContextID, EffectCleanup, EffectConstructor, Element, specialNodeTypes } from "@lukekaalim/act";
|
|
2
2
|
import { Commit2, CommitID, CommitRef2 } from "./commit.ts";
|
|
3
3
|
import { ElementOutput2 } from "./element.ts";
|
|
4
|
-
import { BoundaryState, ComponentState, ContextState, EffectID } from "./state.ts";
|
|
4
|
+
import { BoundaryState, ComponentState, ContextState, EffectID, EffectTask2 } from "./state.ts";
|
|
5
5
|
import { Reconciler2 } from "./reconciler.ts";
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
export type EffectCleanupState = {
|
|
8
|
+
id: EffectID,
|
|
9
|
+
ref: CommitRef2,
|
|
10
|
+
func: EffectCleanup
|
|
11
|
+
}
|
|
7
12
|
|
|
8
13
|
/**
|
|
9
14
|
* The CommitTree is responsible for keeping track
|
|
@@ -32,6 +37,8 @@ export class CommitTree2 {
|
|
|
32
37
|
contexts: Map<CommitID, ContextState<unknown>> = new Map();
|
|
33
38
|
boundaries: Map<CommitID, BoundaryState> = new Map();
|
|
34
39
|
|
|
40
|
+
cleanups: Map<EffectID, EffectCleanupState> = new Map();
|
|
41
|
+
|
|
35
42
|
commits: Map<CommitID, Commit2> = new Map();
|
|
36
43
|
roots: Set<CommitID> = new Set();
|
|
37
44
|
|
|
@@ -49,8 +56,6 @@ export class CommitTree2 {
|
|
|
49
56
|
rejection: null,
|
|
50
57
|
boundary: null,
|
|
51
58
|
hooks: null,
|
|
52
|
-
effectTasks: [],
|
|
53
|
-
cleanups: new Map(),
|
|
54
59
|
providers: new Map(),
|
|
55
60
|
values: new Map(),
|
|
56
61
|
deps: new Map(),
|
|
@@ -109,7 +114,6 @@ export class CommitTree2 {
|
|
|
109
114
|
}
|
|
110
115
|
}
|
|
111
116
|
|
|
112
|
-
|
|
113
117
|
unmountCommit(prev: Commit2) {
|
|
114
118
|
const output = new ElementOutput2(prev.ref);
|
|
115
119
|
output.prevChildren = prev.children.map(c => this.commits.get(c.id) as Commit2);
|
|
@@ -140,15 +144,12 @@ export class CommitTree2 {
|
|
|
140
144
|
if (componentState.boundary && componentState.rejection) {
|
|
141
145
|
componentState.boundary.clearThrow(prev.ref);
|
|
142
146
|
}
|
|
143
|
-
output.
|
|
144
|
-
for (const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
output.cleanups.push({
|
|
149
|
-
id,
|
|
147
|
+
output.effects = [];
|
|
148
|
+
for (const effectId of componentState.effects.values()) {
|
|
149
|
+
output.effects.push({
|
|
150
|
+
id: effectId,
|
|
151
|
+
effect: null,
|
|
150
152
|
ref: prev.ref,
|
|
151
|
-
func: cleanup
|
|
152
153
|
});
|
|
153
154
|
}
|
|
154
155
|
this.components.delete(prev.ref.id);
|
|
@@ -198,5 +199,34 @@ export class CommitTree2 {
|
|
|
198
199
|
|
|
199
200
|
return output;
|
|
200
201
|
}
|
|
201
|
-
}
|
|
202
202
|
|
|
203
|
+
runEffects(effects: ReadonlyMap<EffectID, EffectTask2>) {
|
|
204
|
+
// not every task actually has an effect - when
|
|
205
|
+
// a commit is unmounted with effects it generates
|
|
206
|
+
// a "teardown" task, which just runs the cleanup
|
|
207
|
+
const tasksWithEffects: EffectTask2[] = [];
|
|
208
|
+
|
|
209
|
+
// We will do this in two passes - the first
|
|
210
|
+
// to run all potential "cleanups", which
|
|
211
|
+
// we don't really know until the last moment
|
|
212
|
+
// if one exists.
|
|
213
|
+
for (const task of effects.values()) {
|
|
214
|
+
const cleanupState = this.cleanups.get(task.id);
|
|
215
|
+
if (cleanupState) {
|
|
216
|
+
cleanupState.func();
|
|
217
|
+
this.cleanups.delete(task.id);
|
|
218
|
+
}
|
|
219
|
+
if (task.effect)
|
|
220
|
+
tasksWithEffects.push(task);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// The second pass actually runs the effects
|
|
224
|
+
for (const task of tasksWithEffects) {
|
|
225
|
+
// (we already checked in the previous loop)
|
|
226
|
+
const cleanup = (task.effect as EffectConstructor)();
|
|
227
|
+
if (cleanup) {
|
|
228
|
+
this.cleanups.set(task.id, { id: task.id, ref: task.ref, func: cleanup });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
package/update.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ChangeEqualityTest
|
|
3
|
-
import { Commit2,
|
|
4
|
-
import { createObjectPool, ObjectPool } from "./pool.ts";
|
|
1
|
+
import { Element, specialNodeTypes } from "@lukekaalim/act";
|
|
2
|
+
import { ChangeEqualityTest } from "./algorithms.ts";
|
|
3
|
+
import { Commit2, CommitRef2 } from "./commit.ts";
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* A request to transform part of a tree specified by
|
|
@@ -9,16 +8,6 @@ import { createObjectPool, ObjectPool } from "./pool.ts";
|
|
|
9
8
|
* request
|
|
10
9
|
*/
|
|
11
10
|
export class WorkTask {
|
|
12
|
-
static pool = createObjectPool<WorkTask, Parameters<typeof this.new>>(
|
|
13
|
-
(ref, prev, next, moved) => new WorkTask(ref, prev, next, moved),
|
|
14
|
-
(task, ref, prev, next, moved = false) => {
|
|
15
|
-
task.ref = ref;
|
|
16
|
-
task.prev = prev;
|
|
17
|
-
task.next = next;
|
|
18
|
-
task.moved = moved;
|
|
19
|
-
}
|
|
20
|
-
)
|
|
21
|
-
|
|
22
11
|
/**
|
|
23
12
|
* The commit that should evaluate this
|
|
24
13
|
* update (if this commit does not exist,
|
|
@@ -27,7 +16,8 @@ export class WorkTask {
|
|
|
27
16
|
ref: CommitRef2;
|
|
28
17
|
|
|
29
18
|
/** If null, this update should cause
|
|
30
|
-
* this commit to be created
|
|
19
|
+
* this commit to be created
|
|
20
|
+
* */
|
|
31
21
|
prev: null | Commit2;
|
|
32
22
|
/** If null, this update should cause
|
|
33
23
|
* this commit to be removed
|
|
@@ -44,12 +34,8 @@ export class WorkTask {
|
|
|
44
34
|
this.moved = moved;
|
|
45
35
|
}
|
|
46
36
|
|
|
47
|
-
free() {
|
|
48
|
-
WorkTask.pool.release(this);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
37
|
private static new(ref: CommitRef2, prev: null | Commit2, next: null | Element, moved: boolean = false) {
|
|
52
|
-
return
|
|
38
|
+
return new WorkTask(ref, prev, next, moved)
|
|
53
39
|
}
|
|
54
40
|
|
|
55
41
|
static fresh(ref: CommitRef2, next: Element) {
|
package/pool.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* A short utility for re-using old objects
|
|
4
|
-
* to avoid doing too much GC thrashing, at the
|
|
5
|
-
* cost of increased memory.
|
|
6
|
-
*
|
|
7
|
-
* Don't know yet if the trade off it worth it.
|
|
8
|
-
*/
|
|
9
|
-
export type ObjectPool<T, TArgs extends unknown[]> = {
|
|
10
|
-
maxSize: number,
|
|
11
|
-
size: number,
|
|
12
|
-
|
|
13
|
-
all: T[],
|
|
14
|
-
available: T[],
|
|
15
|
-
|
|
16
|
-
acquire(...args: TArgs): T,
|
|
17
|
-
release(value: T): void,
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export const createObjectPool = <T, TArgs extends unknown[]>(
|
|
21
|
-
build: (...args: TArgs) => T,
|
|
22
|
-
assign: (value: T, ...args: TArgs) => void,
|
|
23
|
-
): ObjectPool<T, TArgs> => {
|
|
24
|
-
const pool = {
|
|
25
|
-
maxSize: 256,
|
|
26
|
-
available: [] as T[],
|
|
27
|
-
all: [] as T[],
|
|
28
|
-
get size() {
|
|
29
|
-
return pool.available.length;
|
|
30
|
-
},
|
|
31
|
-
acquire(...args: TArgs): T {
|
|
32
|
-
let object = pool.available.pop();
|
|
33
|
-
if (!object) {
|
|
34
|
-
object = build(...args);
|
|
35
|
-
pool.all.push(object);
|
|
36
|
-
return object;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
assign(object, ...args);
|
|
40
|
-
return object;
|
|
41
|
-
},
|
|
42
|
-
release(value: T) {
|
|
43
|
-
//if (pool.size < pool.maxSize)
|
|
44
|
-
pool.available.push(value);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return pool;
|
|
48
|
-
}
|