@lukekaalim/act-recon 1.0.0 → 1.1.1
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 +18 -0
- package/algorithms.ts +21 -1
- package/commit.ts +5 -0
- package/context.ts +9 -50
- package/delta.ts +2 -89
- package/element.ts +155 -0
- package/errors.ts +20 -0
- package/hooks.ts +78 -0
- package/mod.ts +0 -1
- package/package.json +2 -2
- package/reconciler.ts +10 -10
- package/state.ts +14 -109
- package/thread.ts +150 -12
- package/tree.ts +26 -0
- package/update.ts +19 -4
- package/component.ts +0 -24
- package/delta.test.ts +0 -108
- package/effects.ts +0 -49
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# @lukekaalim/act-recon
|
|
2
|
+
|
|
3
|
+
## 1.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 0017c07: Fix packages emitting debugging console logs
|
|
8
|
+
|
|
9
|
+
## 1.1.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 4381035: Added error boundaries
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- Updated dependencies [4381035]
|
|
18
|
+
- @lukekaalim/act@3.1.0
|
package/algorithms.ts
CHANGED
|
@@ -43,4 +43,24 @@ export type SortedChangeReport = ChangeReport & {
|
|
|
43
43
|
};
|
|
44
44
|
export const calculateSortedChangedElements = (): SortedChangeReport => {
|
|
45
45
|
throw new MagicError();
|
|
46
|
-
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const first = <X, Y>(array: ReadonlyArray<X>, func: (value: X, index: number) => Y | null): Y | null => {
|
|
49
|
+
for (let i = 0; i < array.length; i++) {
|
|
50
|
+
const value = array[i];
|
|
51
|
+
const result = func(value, i);
|
|
52
|
+
if (result !== null)
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const last = <X, Y>(array: ReadonlyArray<X>, func: (value: X, index: number) => Y | null): Y | null => {
|
|
59
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
60
|
+
const value = array[i];
|
|
61
|
+
const result = func(value, i);
|
|
62
|
+
if (result !== null)
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
package/commit.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createId, Element, OpaqueID } from "@lukekaalim/act";
|
|
2
|
+
import { version } from "os";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* A single consistent id representing a commit in the act tree.
|
|
@@ -66,4 +67,8 @@ export const Commit = {
|
|
|
66
67
|
}
|
|
67
68
|
},
|
|
68
69
|
update: updateCommit,
|
|
70
|
+
version: (commit: Commit): Commit => ({
|
|
71
|
+
...commit,
|
|
72
|
+
version: createId(),
|
|
73
|
+
}),
|
|
69
74
|
}
|
package/context.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import { Context, ContextID
|
|
2
|
-
import { CommitID, CommitRef
|
|
3
|
-
import { WorkThread } from "./thread.ts";
|
|
4
|
-
|
|
5
|
-
export type ContextManager = ReturnType<typeof createContextManager>;
|
|
1
|
+
import { Context, ContextID } from "@lukekaalim/act";
|
|
2
|
+
import { CommitID, CommitRef } from "./commit.ts";
|
|
6
3
|
|
|
7
4
|
export type ContextState<T> = {
|
|
8
5
|
id: CommitID,
|
|
@@ -11,50 +8,12 @@ export type ContextState<T> = {
|
|
|
11
8
|
value: T,
|
|
12
9
|
}
|
|
13
10
|
|
|
14
|
-
export const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
* Take an element+commit, and if a change is detected,
|
|
20
|
-
* the context value is updated and the consumers are returned.
|
|
21
|
-
*/
|
|
22
|
-
processContextElement(element: Element, commitId: CommitID) {
|
|
23
|
-
if (element.type !== providerNodeType)
|
|
24
|
-
return;
|
|
25
|
-
const prevState: ContextState<unknown> = contextStates.get(commitId) || {
|
|
26
|
-
id: commitId,
|
|
27
|
-
contextId: element.props.id as ContextID,
|
|
28
|
-
consumers: new Map(),
|
|
29
|
-
value: element.props.value,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
if (prevState.value !== element.props.value || !contextStates.has(commitId)) {
|
|
33
|
-
contextStates.set(commitId, { ...prevState, value: element.props.value })
|
|
34
|
-
return [...prevState.consumers.values()];
|
|
35
|
-
}
|
|
36
|
-
return [];
|
|
37
|
-
},
|
|
38
|
-
subscribeContext<T>(ref: CommitRef, context: Context<T>): T {
|
|
39
|
-
const contextsInPath = ref.path.map(id => contextStates.get(id)).reverse();
|
|
40
|
-
|
|
41
|
-
const closestContextOfType = contextsInPath.find(cState => cState && cState.contextId === context.id);
|
|
42
|
-
if (!closestContextOfType)
|
|
43
|
-
return context.defaultValue;
|
|
44
|
-
closestContextOfType.consumers.set(ref.id, ref);
|
|
45
|
-
|
|
46
|
-
return closestContextOfType.value as T;
|
|
47
|
-
},
|
|
48
|
-
unsubscribeContext(ref: CommitRef, context: Context<unknown>) {
|
|
49
|
-
const contextsInPath = ref.path.map(id => contextStates.get(id)).reverse();
|
|
50
|
-
|
|
51
|
-
const closestContextOfType = contextsInPath.find(cState => cState && cState.contextId === context.id);
|
|
52
|
-
if (!closestContextOfType)
|
|
53
|
-
return;
|
|
54
|
-
closestContextOfType.consumers.delete(ref.id);
|
|
55
|
-
},
|
|
56
|
-
deleteContextValue(ref: CommitRef) {
|
|
57
|
-
contextStates.delete(ref.id);
|
|
11
|
+
export const findContext = <T>(contexts: Map<CommitID, ContextState<unknown>>, ref: CommitRef, context: Context<T>) => {
|
|
12
|
+
for (const id of [...ref.path].reverse()) {
|
|
13
|
+
const state = contexts.get(id);
|
|
14
|
+
if (state && state.contextId === context.id) {
|
|
15
|
+
return state;
|
|
58
16
|
}
|
|
59
|
-
}
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
60
19
|
};
|
package/delta.ts
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
import { Commit,
|
|
2
|
-
import { WorkThread } from "./thread.ts";
|
|
3
|
-
import { Update, calculateUpdates, isDescendant } from "./update.ts";
|
|
4
|
-
import { CommitTree } from "./tree.ts";
|
|
5
|
-
import { ComponentService } from "./component.ts";
|
|
1
|
+
import { Commit, CommitRef } from "./commit.ts";
|
|
6
2
|
|
|
7
3
|
export type CreateDelta = { ref: CommitRef, next: Commit };
|
|
8
4
|
export type UpdateDelta = { ref: CommitRef, next: Commit, prev: Commit };
|
|
9
5
|
export type RemoveDelta = { ref: CommitRef, prev: Commit };
|
|
10
|
-
export type SkipDelta = {
|
|
11
|
-
|
|
6
|
+
export type SkipDelta = { next: Commit };
|
|
12
7
|
|
|
13
8
|
export type DeltaSet = {
|
|
14
9
|
created: CreateDelta[],
|
|
@@ -16,85 +11,3 @@ export type DeltaSet = {
|
|
|
16
11
|
skipped: SkipDelta[],
|
|
17
12
|
removed: RemoveDelta[],
|
|
18
13
|
};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Given an Update, compute if there is any
|
|
23
|
-
* change to the tree (a "Delta"), and if more updates
|
|
24
|
-
* are needed to complete this work, appending them to
|
|
25
|
-
* the work thread
|
|
26
|
-
* */
|
|
27
|
-
export const applyUpdate = (
|
|
28
|
-
tree: CommitTree,
|
|
29
|
-
comp: ComponentService,
|
|
30
|
-
thread: WorkThread,
|
|
31
|
-
{ next, prev, ref, targets }: Update
|
|
32
|
-
) => {
|
|
33
|
-
/**
|
|
34
|
-
* A change is considered identical if the "next element"
|
|
35
|
-
* is the same as the "prev element" - its the same as there
|
|
36
|
-
* being no change at all.
|
|
37
|
-
* */
|
|
38
|
-
const identicalChange = (next && prev && next.id === prev.element.id);
|
|
39
|
-
/**
|
|
40
|
-
* If we're "on a target's path", then we have to continue rendering.
|
|
41
|
-
*/
|
|
42
|
-
const requiredChange = !!targets.find(target => target.path.includes(ref.id));
|
|
43
|
-
const requiresRerender = targets.some(target => target.id === ref.id);
|
|
44
|
-
|
|
45
|
-
if (identicalChange && !requiredChange)
|
|
46
|
-
return;
|
|
47
|
-
|
|
48
|
-
const prevChildren = prev && prev.children
|
|
49
|
-
.map(c => tree.commits.get(c.id) as Commit) || [];
|
|
50
|
-
|
|
51
|
-
// If we have a "Next", then this is a request to either
|
|
52
|
-
// Create or Update a commit.
|
|
53
|
-
if (next) {
|
|
54
|
-
|
|
55
|
-
// skip the change
|
|
56
|
-
if (identicalChange && !requiresRerender) {
|
|
57
|
-
const updates = prevChildren.map(prev => ({ ref: prev, prev, next: prev.element, targets }));
|
|
58
|
-
thread.pendingUpdates.push(...updates);
|
|
59
|
-
const commit = Commit.update(ref, prev.element, prev.children);
|
|
60
|
-
thread.deltas.skipped.push({ ref, next: commit });
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const contextTargets = comp.context.processContextElement(next, ref.id) || [];
|
|
65
|
-
const nextTargets = [...contextTargets, ...targets];
|
|
66
|
-
|
|
67
|
-
const childNode = comp.state.calculateCommitChildren(thread, next, ref);
|
|
68
|
-
|
|
69
|
-
const [childRefs, updates] = calculateUpdates(ref, prevChildren, childNode);
|
|
70
|
-
const finalUpdates = updates.map(update => ({
|
|
71
|
-
...update,
|
|
72
|
-
targets: nextTargets.filter(t => isDescendant(update.ref, t))
|
|
73
|
-
}))
|
|
74
|
-
|
|
75
|
-
const commit = Commit.update(ref, next, childRefs);
|
|
76
|
-
|
|
77
|
-
if (prev)
|
|
78
|
-
thread.deltas.updated.push({ ref, prev, next: commit });
|
|
79
|
-
else
|
|
80
|
-
thread.deltas.created.push({ ref, next: commit });
|
|
81
|
-
|
|
82
|
-
thread.pendingUpdates.push(...finalUpdates);
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
// If we have a prev, but no next, then this is a requets to
|
|
86
|
-
// delete this commit. We still have emit "delete" updates
|
|
87
|
-
// as well for all children of this node too.
|
|
88
|
-
else if (prev && !next) {
|
|
89
|
-
const [, updates] = calculateUpdates(ref, prevChildren, []);
|
|
90
|
-
comp.state.clearCommitState(thread, ref);
|
|
91
|
-
// No need to reclculate targets - no more re-rendering
|
|
92
|
-
// will happen on this set of updates.
|
|
93
|
-
|
|
94
|
-
thread.deltas.removed.push({ ref: prev, prev });
|
|
95
|
-
thread.pendingUpdates.push(...updates);
|
|
96
|
-
return;
|
|
97
|
-
} else {
|
|
98
|
-
throw new Error(`No prev, no next, did this commit ever exist?`)
|
|
99
|
-
}
|
|
100
|
-
};
|
package/element.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ContextID, Element, errorBoundaryType, Node,
|
|
3
|
+
providerNodeType
|
|
4
|
+
} from "@lukekaalim/act";
|
|
5
|
+
import { Commit, CommitID, CommitRef } from "./commit";
|
|
6
|
+
import { loadHooks } from "./hooks";
|
|
7
|
+
import { ContextState } from "./context";
|
|
8
|
+
import { ComponentState, EffectID, EffectTask } from "./state";
|
|
9
|
+
import { CommitTree } from "./tree";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* When processing an element, it may produce additional
|
|
13
|
+
* pieces of information: new targets, side effects, and boundary
|
|
14
|
+
* values
|
|
15
|
+
*/
|
|
16
|
+
export type ElementOutput = {
|
|
17
|
+
child: Node,
|
|
18
|
+
reject: null | unknown,
|
|
19
|
+
effects: EffectTask[],
|
|
20
|
+
targets: CommitRef[],
|
|
21
|
+
};
|
|
22
|
+
export const ElementOutput = {
|
|
23
|
+
new: (child: Node): ElementOutput => ({
|
|
24
|
+
child,
|
|
25
|
+
reject: null,
|
|
26
|
+
effects: [],
|
|
27
|
+
targets: [],
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ElementService = {
|
|
32
|
+
render(element: Element, ref: CommitRef): ElementOutput,
|
|
33
|
+
clear(ref: Commit): ElementOutput,
|
|
34
|
+
|
|
35
|
+
boundary: Map<CommitID, unknown>,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const createElementService = (
|
|
39
|
+
tree: CommitTree,
|
|
40
|
+
requestRender: (ref: CommitRef) => void
|
|
41
|
+
): ElementService => {
|
|
42
|
+
const contextStates = new Map<CommitID, ContextState<unknown>>();
|
|
43
|
+
const componentStates = new Map<CommitID, ComponentState>();
|
|
44
|
+
const boundaryValues = new Map<CommitID, unknown>();
|
|
45
|
+
|
|
46
|
+
const render = (
|
|
47
|
+
element: Element,
|
|
48
|
+
ref: CommitRef,
|
|
49
|
+
): ElementOutput => {
|
|
50
|
+
const output = ElementOutput.new(element.children);
|
|
51
|
+
|
|
52
|
+
switch (typeof element.type) {
|
|
53
|
+
case 'string':
|
|
54
|
+
break;
|
|
55
|
+
case 'symbol':
|
|
56
|
+
switch (element.type) {
|
|
57
|
+
case providerNodeType: {
|
|
58
|
+
let state = contextStates.get(ref.id);
|
|
59
|
+
if (!state) {
|
|
60
|
+
state = {
|
|
61
|
+
id: ref.id,
|
|
62
|
+
contextId: element.props.id as ContextID,
|
|
63
|
+
value: element.props.value,
|
|
64
|
+
consumers: new Map(),
|
|
65
|
+
}
|
|
66
|
+
contextStates.set(ref.id, state);
|
|
67
|
+
}
|
|
68
|
+
if (state.value !== element.props.value) {
|
|
69
|
+
state.value = element.props.value;
|
|
70
|
+
output.targets.push(...state.consumers.values());
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case errorBoundaryType: {
|
|
75
|
+
const error = CommitTree.getError(tree, ref.id);
|
|
76
|
+
if (error.state === 'error')
|
|
77
|
+
output.child = null;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
default:
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
case 'function': {
|
|
85
|
+
let state = componentStates.get(ref.id);
|
|
86
|
+
if (!state) {
|
|
87
|
+
state = {
|
|
88
|
+
unmounted: false,
|
|
89
|
+
ref,
|
|
90
|
+
cleanups: new Map(),
|
|
91
|
+
contexts: new Map(),
|
|
92
|
+
values: new Map(),
|
|
93
|
+
deps: new Map(),
|
|
94
|
+
effects: new Map(),
|
|
95
|
+
}
|
|
96
|
+
componentStates.set(ref.id, state);
|
|
97
|
+
}
|
|
98
|
+
loadHooks(contextStates, requestRender, state, ref, output);
|
|
99
|
+
const props = {
|
|
100
|
+
...element.props,
|
|
101
|
+
children: element.children,
|
|
102
|
+
} as Parameters<typeof element.type>[0];
|
|
103
|
+
try {
|
|
104
|
+
output.child = element.type(props);
|
|
105
|
+
} catch (thrownValue) {
|
|
106
|
+
output.child = null;
|
|
107
|
+
output.reject = thrownValue;
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
default:
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
return output;
|
|
115
|
+
}
|
|
116
|
+
const clear = (prev: Commit) => {
|
|
117
|
+
const output = ElementOutput.new(null);
|
|
118
|
+
|
|
119
|
+
switch (typeof prev.element.type) {
|
|
120
|
+
case 'symbol': {
|
|
121
|
+
switch (prev.element.type) {
|
|
122
|
+
case providerNodeType:
|
|
123
|
+
contextStates.delete(prev.id);
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
case 'function': {
|
|
128
|
+
const componentState = componentStates.get(prev.id) as ComponentState;
|
|
129
|
+
componentState.unmounted = true;
|
|
130
|
+
for (const [,context] of componentState.contexts) {
|
|
131
|
+
if (context.state)
|
|
132
|
+
context.state.consumers.delete(prev.id);
|
|
133
|
+
}
|
|
134
|
+
for (const [index, cleanup] of componentState.cleanups) {
|
|
135
|
+
if (!cleanup)
|
|
136
|
+
continue;
|
|
137
|
+
const id = componentState.effects.get(index) as EffectID;
|
|
138
|
+
output.effects.push({
|
|
139
|
+
id,
|
|
140
|
+
ref: prev,
|
|
141
|
+
func: () => {
|
|
142
|
+
cleanup();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
componentStates.delete(prev.id);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return output;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { render, clear, boundary: boundaryValues };
|
|
155
|
+
}
|
package/errors.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { CommitID } from "./commit"
|
|
2
|
+
|
|
3
|
+
export type ErrorBoundaryState = {
|
|
4
|
+
id: CommitID,
|
|
5
|
+
state: 'error' | 'normal',
|
|
6
|
+
value: unknown,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const ErrorBoundaryState = {
|
|
10
|
+
create(id: CommitID): ErrorBoundaryState {
|
|
11
|
+
return { id, value: null, state: 'normal' };
|
|
12
|
+
},
|
|
13
|
+
clear(state: ErrorBoundaryState) {
|
|
14
|
+
state.state = 'normal';
|
|
15
|
+
},
|
|
16
|
+
set(state: ErrorBoundaryState, value: unknown) {
|
|
17
|
+
state.state = 'error';
|
|
18
|
+
state.value = value;
|
|
19
|
+
}
|
|
20
|
+
}
|
package/hooks.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HookImplementation, hookImplementation, Context,
|
|
3
|
+
ValueOrCalculator, calculateValue, StateSetter,
|
|
4
|
+
runUpdater,
|
|
5
|
+
createId,
|
|
6
|
+
calculateDepsChange
|
|
7
|
+
} from "@lukekaalim/act";
|
|
8
|
+
import { ComponentState, EffectID } from "./state";
|
|
9
|
+
import { CommitID, CommitRef } from "./commit";
|
|
10
|
+
import { ElementOutput } from "./element";
|
|
11
|
+
import { ContextState, findContext } from "./context";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A fresh set of hook functions is created per component run.
|
|
15
|
+
*/
|
|
16
|
+
export const loadHooks = (
|
|
17
|
+
contexts: Map<CommitID, ContextState<unknown>>,
|
|
18
|
+
requestRender: (ref: CommitRef) => void,
|
|
19
|
+
|
|
20
|
+
state: ComponentState,
|
|
21
|
+
ref: CommitRef,
|
|
22
|
+
|
|
23
|
+
output: ElementOutput
|
|
24
|
+
) => {
|
|
25
|
+
let index = 0;
|
|
26
|
+
hookImplementation.useContext = <T>(context: Context<T>): T => {
|
|
27
|
+
let value = state.contexts.get(index);
|
|
28
|
+
if (!value) {
|
|
29
|
+
value = { state: findContext(contexts, ref, context) };
|
|
30
|
+
state.contexts.set(index, value);
|
|
31
|
+
if (value.state)
|
|
32
|
+
value.state.consumers.set(ref.id, ref);
|
|
33
|
+
}
|
|
34
|
+
if (value.state)
|
|
35
|
+
return value.state.value as T;
|
|
36
|
+
return context.defaultValue;
|
|
37
|
+
};
|
|
38
|
+
hookImplementation.useState = <T>(initialValue: ValueOrCalculator<T>) => {
|
|
39
|
+
const stateIndex = index++;
|
|
40
|
+
if (!state.values.has(stateIndex))
|
|
41
|
+
state.values.set(stateIndex, calculateValue(initialValue));
|
|
42
|
+
|
|
43
|
+
const value = state.values.get(stateIndex) as T;
|
|
44
|
+
const setValue: StateSetter<T> = (updater) => {
|
|
45
|
+
if (state.unmounted)
|
|
46
|
+
return;
|
|
47
|
+
const prevValue = state.values.get(stateIndex) as T;
|
|
48
|
+
const nextValue = runUpdater(prevValue, updater);
|
|
49
|
+
state.values.set(stateIndex, nextValue);
|
|
50
|
+
requestRender(ref);
|
|
51
|
+
};
|
|
52
|
+
return [value, setValue];
|
|
53
|
+
}
|
|
54
|
+
hookImplementation.useEffect = (effect, deps = null) => {
|
|
55
|
+
const effectIndex = index++;
|
|
56
|
+
if (!state.effects.has(effectIndex))
|
|
57
|
+
state.effects.set(effectIndex, createId());
|
|
58
|
+
|
|
59
|
+
const prevDeps = state.deps.get(effectIndex) || null;
|
|
60
|
+
const effectId = state.effects.get(effectIndex) as EffectID;
|
|
61
|
+
state.deps.set(effectIndex, deps);
|
|
62
|
+
const depsChanges = calculateDepsChange(prevDeps, deps)
|
|
63
|
+
if (depsChanges) {
|
|
64
|
+
output.effects.push({
|
|
65
|
+
id: effectId,
|
|
66
|
+
ref,
|
|
67
|
+
func() {
|
|
68
|
+
const prevCleanup = state.cleanups.get(effectId);
|
|
69
|
+
if (prevCleanup) {
|
|
70
|
+
state.cleanups.delete(effectId);
|
|
71
|
+
prevCleanup();
|
|
72
|
+
}
|
|
73
|
+
state.cleanups.set(effectId, effect());
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
};
|
package/mod.ts
CHANGED
package/package.json
CHANGED
package/reconciler.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
import { createThreadManager, WorkThread } from "./thread";
|
|
1
|
+
import { createThreadManager } from "./thread";
|
|
3
2
|
import { CommitTree } from "./tree";
|
|
4
|
-
import { ComponentService } from "./component";
|
|
5
3
|
import { DeltaSet } from "./delta";
|
|
4
|
+
import { createElementService } from "./element";
|
|
5
|
+
import { EffectTask } from "./state";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* A reconciler links all the relevant subsystems together.
|
|
@@ -16,21 +16,21 @@ export const createReconciler = (
|
|
|
16
16
|
requestWork: () => void,
|
|
17
17
|
) => {
|
|
18
18
|
const tree = CommitTree.new();
|
|
19
|
-
const
|
|
20
|
-
threads.request(ref);
|
|
21
|
-
});
|
|
19
|
+
const elements = createElementService(tree, ref => threads.request(ref));
|
|
22
20
|
|
|
23
|
-
const
|
|
21
|
+
const onThreadComplete = (deltas: DeltaSet, effects: EffectTask[]) => {
|
|
24
22
|
render(deltas);
|
|
25
23
|
|
|
26
24
|
// immedialty execute all side effects
|
|
27
25
|
for (const effect of effects)
|
|
28
|
-
effect.
|
|
29
|
-
}
|
|
26
|
+
effect.func();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const threads = createThreadManager(elements, tree, requestWork, onThreadComplete);
|
|
30
30
|
|
|
31
31
|
return {
|
|
32
32
|
threads,
|
|
33
|
-
|
|
33
|
+
elements,
|
|
34
34
|
tree,
|
|
35
35
|
}
|
|
36
36
|
}
|
package/state.ts
CHANGED
|
@@ -1,118 +1,23 @@
|
|
|
1
|
-
import { CommitRef
|
|
2
|
-
import {
|
|
1
|
+
import { CommitRef } from "./commit.ts";
|
|
2
|
+
import { ContextState } from "./context.ts";
|
|
3
3
|
import * as act from '@lukekaalim/act';
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export type EffectID = act.OpaqueID<"EffectID">;
|
|
7
|
+
export type EffectTask = {
|
|
8
|
+
ref: CommitRef,
|
|
9
|
+
id: EffectID,
|
|
10
|
+
func: () => void,
|
|
11
|
+
}
|
|
6
12
|
|
|
7
13
|
export type ComponentState = {
|
|
8
14
|
ref: CommitRef;
|
|
9
15
|
|
|
16
|
+
unmounted: boolean,
|
|
17
|
+
|
|
10
18
|
values: Map<number, unknown>;
|
|
11
19
|
deps: Map<number, act.Deps>;
|
|
12
20
|
effects: Map<number, EffectID>;
|
|
13
|
-
|
|
21
|
+
cleanups: Map<number, act.EffectCleanup>;
|
|
22
|
+
contexts: Map<number, { state: null | ContextState<unknown> }>;
|
|
14
23
|
};
|
|
15
|
-
|
|
16
|
-
export const createStateManager = (
|
|
17
|
-
effectManager: EffectManager,
|
|
18
|
-
rerender: (ref: CommitRef) => unknown,
|
|
19
|
-
contextManager: ContextManager | null = null
|
|
20
|
-
) => {
|
|
21
|
-
const states = new Map<CommitID, ComponentState>();
|
|
22
|
-
|
|
23
|
-
const createState = (ref: CommitRef): ComponentState => {
|
|
24
|
-
const state = {
|
|
25
|
-
ref,
|
|
26
|
-
values: new Map(),
|
|
27
|
-
effects: new Map(),
|
|
28
|
-
deps: new Map(),
|
|
29
|
-
contexts: new Map(),
|
|
30
|
-
};
|
|
31
|
-
states.set(ref.id, state);
|
|
32
|
-
return state;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const loadHookImplementation = (thread: WorkThread, ref: CommitRef) => {
|
|
36
|
-
const createHookImplementation = (): act.HookImplementation => {
|
|
37
|
-
const state = states.get(ref.id) || createState(ref);
|
|
38
|
-
let index = 0;
|
|
39
|
-
return {
|
|
40
|
-
useContext<T>(context: act.Context<T>): T {
|
|
41
|
-
if (!contextManager)
|
|
42
|
-
throw new act.MagicError();
|
|
43
|
-
|
|
44
|
-
state.contexts.set(index, context as act.Context<unknown>);
|
|
45
|
-
return contextManager.subscribeContext(ref, context);
|
|
46
|
-
},
|
|
47
|
-
useState<T>(initialValue: act.ValueOrCalculator<T>) {
|
|
48
|
-
const stateIndex = index++;
|
|
49
|
-
if (!state.values.has(stateIndex))
|
|
50
|
-
state.values.set(stateIndex, act.calculateValue(initialValue));
|
|
51
|
-
|
|
52
|
-
const value = state.values.get(stateIndex) as T;
|
|
53
|
-
const setValue: act.StateSetter<T> = (updater) => {
|
|
54
|
-
const prevValue = state.values.get(stateIndex) as T;
|
|
55
|
-
const nextValue = act.runUpdater(prevValue, updater);
|
|
56
|
-
state.values.set(stateIndex, nextValue);
|
|
57
|
-
rerender(ref);
|
|
58
|
-
};
|
|
59
|
-
return [value, setValue];
|
|
60
|
-
},
|
|
61
|
-
useEffect(effect, deps = null) {
|
|
62
|
-
const effectIndex = index++;
|
|
63
|
-
if (!state.effects.has(effectIndex))
|
|
64
|
-
state.effects.set(effectIndex, act.createId());
|
|
65
|
-
const prevDeps = state.deps.get(effectIndex) || null;
|
|
66
|
-
const effectId = state.effects.get(effectIndex) as EffectID;
|
|
67
|
-
state.deps.set(effectIndex, deps);
|
|
68
|
-
const depsChanges = act.calculateDepsChange(prevDeps, deps)
|
|
69
|
-
if (depsChanges)
|
|
70
|
-
effectManager.enqueueEffect(thread, effectId, effect);
|
|
71
|
-
return;
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
const hooks = createHookImplementation();
|
|
77
|
-
act.hookImplementation.useContext = hooks.useContext;
|
|
78
|
-
act.hookImplementation.useState = hooks.useState;
|
|
79
|
-
act.hookImplementation.useEffect = hooks.useEffect;
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const calculateCommitChildren = (
|
|
83
|
-
thread: WorkThread,
|
|
84
|
-
element: act.Element,
|
|
85
|
-
commit: CommitRef
|
|
86
|
-
) => {
|
|
87
|
-
const component = typeof element.type === "function" && element.type;
|
|
88
|
-
if (!component) return element.children;
|
|
89
|
-
|
|
90
|
-
const children = element.children;
|
|
91
|
-
|
|
92
|
-
const props = {
|
|
93
|
-
...element.props,
|
|
94
|
-
children,
|
|
95
|
-
} as Parameters<typeof component>[0];
|
|
96
|
-
|
|
97
|
-
loadHookImplementation(thread, commit);
|
|
98
|
-
const result = component(props);
|
|
99
|
-
|
|
100
|
-
return result;
|
|
101
|
-
};
|
|
102
|
-
const clearCommitState = (thread: WorkThread, ref: CommitRef) => {
|
|
103
|
-
const state = states.get(ref.id)
|
|
104
|
-
if (!state)
|
|
105
|
-
return;
|
|
106
|
-
const effects = [...state.effects.values()];
|
|
107
|
-
const contexts = [...state.contexts.values()];
|
|
108
|
-
for (const effect of effects)
|
|
109
|
-
effectManager.enqueueTeardown(thread, effect);
|
|
110
|
-
if (contextManager)
|
|
111
|
-
for (const context of contexts)
|
|
112
|
-
contextManager.unsubscribeContext(ref, context);
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
return { calculateCommitChildren, clearCommitState, states };
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
export type StateManager = ReturnType<typeof createStateManager>;
|
package/thread.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { convertNodeToElements, createId, Node } from "@lukekaalim/act";
|
|
1
|
+
import { convertNodeToElements, createId, ErrorBoundaryProps, errorBoundaryType, Node } from "@lukekaalim/act";
|
|
2
2
|
import { Commit, CommitID, CommitRef } from "./commit.ts";
|
|
3
|
-
import {
|
|
4
|
-
import { applyUpdate, DeltaSet } from "./delta.ts";
|
|
5
|
-
import { EffectTask } from "./effects.ts";
|
|
3
|
+
import { DeltaSet } from "./delta.ts";
|
|
6
4
|
import { CommitTree } from "./tree.ts";
|
|
7
|
-
import { Update } from "./update.ts";
|
|
5
|
+
import { calculateUpdates, isDescendant, Update } from "./update.ts";
|
|
6
|
+
import { ElementService } from "./element.ts";
|
|
7
|
+
import { EffectTask } from "./state.ts";
|
|
8
|
+
import { ErrorBoundaryState } from "./errors.ts";
|
|
9
|
+
import { first, last } from "./algorithms.ts";
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* A WorkThread is a mutable data struture that
|
|
@@ -19,6 +21,9 @@ export type WorkThread = {
|
|
|
19
21
|
pendingUpdates: Update[],
|
|
20
22
|
pendingEffects: EffectTask[],
|
|
21
23
|
|
|
24
|
+
errorNotifications: Set<CommitID>,
|
|
25
|
+
|
|
26
|
+
visited: Set<CommitID>,
|
|
22
27
|
deltas: DeltaSet,
|
|
23
28
|
};
|
|
24
29
|
export const WorkThread = {
|
|
@@ -27,6 +32,8 @@ export const WorkThread = {
|
|
|
27
32
|
started: false,
|
|
28
33
|
pendingEffects: [],
|
|
29
34
|
pendingUpdates: [],
|
|
35
|
+
errorNotifications: new Set(),
|
|
36
|
+
visited: new Set(),
|
|
30
37
|
deltas: {
|
|
31
38
|
created: [],
|
|
32
39
|
updated: [],
|
|
@@ -35,10 +42,43 @@ export const WorkThread = {
|
|
|
35
42
|
},
|
|
36
43
|
}
|
|
37
44
|
},
|
|
45
|
+
/**
|
|
46
|
+
* Remove all changes from a particular commit onward
|
|
47
|
+
*/
|
|
48
|
+
rollback(thread: WorkThread, from: CommitRef) {
|
|
49
|
+
thread.deltas.created = thread.deltas.created.filter(d => !isDescendant(from, d.ref));
|
|
50
|
+
thread.deltas.updated = thread.deltas.updated.filter(d => !isDescendant(from, d.ref));
|
|
51
|
+
thread.deltas.removed = thread.deltas.removed.filter(d => !isDescendant(from, d.ref));
|
|
52
|
+
thread.deltas.skipped = thread.deltas.skipped.filter(d => !isDescendant(from, d.next));
|
|
53
|
+
thread.pendingUpdates = thread.pendingUpdates.filter(update => !isDescendant(from, update.ref))
|
|
54
|
+
thread.pendingEffects = thread.pendingEffects.filter(effect => !isDescendant(from, effect.ref))
|
|
55
|
+
},
|
|
56
|
+
notifyError(thread: WorkThread, ref: CommitRef) {
|
|
57
|
+
thread.errorNotifications.add(ref.id);
|
|
58
|
+
},
|
|
59
|
+
/**
|
|
60
|
+
* Find the closest anscestor error boundary for a commit,
|
|
61
|
+
* either in the tree or one that was just created
|
|
62
|
+
* */
|
|
63
|
+
findClosestBoundary(thread: WorkThread, tree: CommitTree, ref: CommitRef): Commit | null {
|
|
64
|
+
return last(ref.path, id => {
|
|
65
|
+
if (tree.commits.has(id)) {
|
|
66
|
+
const commit = tree.commits.get(id) as Commit;
|
|
67
|
+
if (commit.element.type === errorBoundaryType)
|
|
68
|
+
return commit;
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
// We also might have just created the boundary
|
|
72
|
+
const freshBoundary = thread.deltas.created.find(c => c.ref.id === id && c.next.element.type === errorBoundaryType);
|
|
73
|
+
if (freshBoundary)
|
|
74
|
+
return freshBoundary.next;
|
|
75
|
+
return null;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
38
78
|
}
|
|
39
79
|
|
|
40
80
|
export const createThreadManager = (
|
|
41
|
-
|
|
81
|
+
elementService: ElementService,
|
|
42
82
|
tree: CommitTree,
|
|
43
83
|
requestWork: () => void,
|
|
44
84
|
onThreadComplete: (deltas: DeltaSet, effects: EffectTask[]) => unknown = _ => {},
|
|
@@ -50,7 +90,7 @@ export const createThreadManager = (
|
|
|
50
90
|
const run = () => {
|
|
51
91
|
const update = currentThread.pendingUpdates.pop();
|
|
52
92
|
if (update) {
|
|
53
|
-
applyUpdate(
|
|
93
|
+
applyUpdate(currentThread, update);
|
|
54
94
|
}
|
|
55
95
|
}
|
|
56
96
|
|
|
@@ -81,7 +121,7 @@ export const createThreadManager = (
|
|
|
81
121
|
tree.commits.set(delta.ref.id, delta.next);
|
|
82
122
|
|
|
83
123
|
for (const delta of currentThread.deltas.skipped)
|
|
84
|
-
tree.commits.set(delta.
|
|
124
|
+
tree.commits.set(delta.next.id, delta.next);
|
|
85
125
|
|
|
86
126
|
for (const delta of currentThread.deltas.updated)
|
|
87
127
|
tree.commits.set(delta.ref.id, delta.next);
|
|
@@ -89,6 +129,19 @@ export const createThreadManager = (
|
|
|
89
129
|
for (const delta of currentThread.deltas.removed)
|
|
90
130
|
tree.commits.delete(delta.ref.id);
|
|
91
131
|
|
|
132
|
+
for (const boundaryId of currentThread.errorNotifications) {
|
|
133
|
+
const commit = tree.commits.get(boundaryId) as Commit;
|
|
134
|
+
const { onError } = commit.element.props as ErrorBoundaryProps;
|
|
135
|
+
if (typeof onError === 'function') {
|
|
136
|
+
const state = CommitTree.getError(tree, commit.id);
|
|
137
|
+
const clear = () => {
|
|
138
|
+
ErrorBoundaryState.clear(state);
|
|
139
|
+
request(commit);
|
|
140
|
+
}
|
|
141
|
+
onError(state.value, clear);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
92
145
|
// Notify external
|
|
93
146
|
onThreadComplete(currentThread.deltas, currentThread.pendingEffects);
|
|
94
147
|
|
|
@@ -109,10 +162,16 @@ export const createThreadManager = (
|
|
|
109
162
|
|
|
110
163
|
const request = (target: CommitRef) => {
|
|
111
164
|
if (currentThread.started) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
165
|
+
const updateOnPath = currentThread.pendingUpdates.find(update => {
|
|
166
|
+
return isDescendant(update.ref, target)
|
|
167
|
+
})
|
|
168
|
+
if (updateOnPath) {
|
|
169
|
+
if (updateOnPath.targets.some(target => target.id === target.id)) {
|
|
170
|
+
updateOnPath.targets.push(target);
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
pendingUpdateTargets.set(target.id, target);
|
|
174
|
+
}
|
|
116
175
|
} else {
|
|
117
176
|
const roots = CommitTree.getRootCommits(tree);
|
|
118
177
|
for (const root of roots) {
|
|
@@ -136,6 +195,85 @@ export const createThreadManager = (
|
|
|
136
195
|
requestWork();
|
|
137
196
|
}
|
|
138
197
|
|
|
198
|
+
const applyUpdate = (thread: WorkThread, { next, prev, ref, targets, suspend }: Update) => {
|
|
199
|
+
thread.visited.add(ref.id);
|
|
200
|
+
|
|
201
|
+
const identicalChange = next && prev && (next.id === prev.element.id);
|
|
202
|
+
const prevChildren = prev && prev.children
|
|
203
|
+
.map(c => tree.commits.get(c.id) as Commit) || [];
|
|
204
|
+
|
|
205
|
+
if (identicalChange) {
|
|
206
|
+
const isOnTargetPath = targets.some(target => target.path.includes(ref.id));
|
|
207
|
+
if (!isOnTargetPath)
|
|
208
|
+
return;
|
|
209
|
+
|
|
210
|
+
const isSpecificallyTarget = targets.some(target => target.id === ref.id);
|
|
211
|
+
|
|
212
|
+
if (!isSpecificallyTarget) {
|
|
213
|
+
const updates = prevChildren.map(prev => Update.skip(prev, targets));
|
|
214
|
+
thread.pendingUpdates.push(...updates);
|
|
215
|
+
|
|
216
|
+
const commit = Commit.version(prev);
|
|
217
|
+
thread.deltas.skipped.push({ next: commit });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (next) {
|
|
222
|
+
const output = elementService.render(next, ref);
|
|
223
|
+
if (output.reject) {
|
|
224
|
+
const errorBoundary = WorkThread.findClosestBoundary(thread, tree, ref);
|
|
225
|
+
if (errorBoundary) {
|
|
226
|
+
const errorState = CommitTree.getError(tree, errorBoundary.id);
|
|
227
|
+
ErrorBoundaryState.set(errorState, output.reject);
|
|
228
|
+
WorkThread.rollback(thread, errorBoundary);
|
|
229
|
+
WorkThread.notifyError(thread, errorBoundary);
|
|
230
|
+
|
|
231
|
+
thread.pendingUpdates.push(Update.target(errorBoundary));
|
|
232
|
+
return;
|
|
233
|
+
} else {
|
|
234
|
+
console.error(output.reject);
|
|
235
|
+
console.error(`No boundary to catch error: Unmounting roots`);
|
|
236
|
+
for (const ref of tree.roots) {
|
|
237
|
+
WorkThread.rollback(thread, ref);
|
|
238
|
+
const prev = tree.commits.get(ref.id);
|
|
239
|
+
if (prev)
|
|
240
|
+
thread.pendingUpdates.push(Update.remove(prev));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const [childRefs, updates] = calculateUpdates(ref, prevChildren, output.child);
|
|
247
|
+
|
|
248
|
+
thread.pendingEffects.push(...output.effects);
|
|
249
|
+
thread.pendingUpdates.push(...updates.map(update => ({
|
|
250
|
+
...update,
|
|
251
|
+
targets: [...targets, ...output.targets.filter(t => isDescendant(update.ref, t))]
|
|
252
|
+
})));
|
|
253
|
+
|
|
254
|
+
const commit = Commit.update(ref, next, childRefs);
|
|
255
|
+
|
|
256
|
+
if (prev)
|
|
257
|
+
thread.deltas.updated.push({ ref, prev, next: commit });
|
|
258
|
+
else
|
|
259
|
+
thread.deltas.created.push({ ref, next: commit });
|
|
260
|
+
|
|
261
|
+
// Update tree
|
|
262
|
+
//tree.commits.set(ref.id, commit);
|
|
263
|
+
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
else if (prev && !next) {
|
|
267
|
+
const output = elementService.clear(prev);
|
|
268
|
+
thread.deltas.removed.push({ ref: prev, prev });
|
|
269
|
+
thread.pendingUpdates.push(...prevChildren.map(prev => Update.remove(prev)));
|
|
270
|
+
thread.pendingEffects.push(...output.effects);
|
|
271
|
+
return;
|
|
272
|
+
} else {
|
|
273
|
+
throw new Error(`No prev, no next, did this commit ever exist?`)
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
139
277
|
return { work, request, mount }
|
|
140
278
|
};
|
|
141
279
|
|
package/tree.ts
CHANGED
|
@@ -1,16 +1,42 @@
|
|
|
1
1
|
import { Commit, CommitID, CommitRef } from "./commit.ts";
|
|
2
|
+
import { ContextState } from "./context.ts";
|
|
3
|
+
import { ErrorBoundaryState } from "./errors.ts";
|
|
4
|
+
import { ComponentState } from "./state.ts";
|
|
2
5
|
|
|
3
6
|
export type CommitTree = {
|
|
7
|
+
components: Map<CommitID, ComponentState>,
|
|
8
|
+
contexts: Map<CommitID, ContextState<unknown>>,
|
|
9
|
+
errors: Map<CommitID, ErrorBoundaryState>,
|
|
10
|
+
|
|
4
11
|
commits: Map<CommitID, Commit>,
|
|
5
12
|
roots: Set<CommitRef>,
|
|
6
13
|
}
|
|
7
14
|
|
|
8
15
|
export const CommitTree = {
|
|
9
16
|
new: (): CommitTree => ({
|
|
17
|
+
errors: new Map(),
|
|
18
|
+
components: new Map(),
|
|
19
|
+
contexts: new Map(),
|
|
20
|
+
|
|
10
21
|
commits: new Map(),
|
|
11
22
|
roots: new Set(),
|
|
12
23
|
}),
|
|
13
24
|
getRootCommits: (tree: CommitTree) => {
|
|
14
25
|
return [...tree.roots].map(ref => tree.commits.get(ref.id) as Commit)
|
|
26
|
+
},
|
|
27
|
+
getError(tree: CommitTree, id: CommitID): ErrorBoundaryState {
|
|
28
|
+
if (tree.errors.has(id))
|
|
29
|
+
return tree.errors.get(id) as ErrorBoundaryState;
|
|
30
|
+
const state = ErrorBoundaryState.create(id);
|
|
31
|
+
tree.errors.set(id, state);
|
|
32
|
+
return state;
|
|
33
|
+
},
|
|
34
|
+
searchParents(tree: CommitTree, ref: CommitRef, func: (commit: Commit) => boolean): Commit | null {
|
|
35
|
+
for (const id of [...ref.path].reverse()) {
|
|
36
|
+
const commit = tree.commits.get(id) as Commit;
|
|
37
|
+
if (func(commit))
|
|
38
|
+
return commit
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
15
41
|
}
|
|
16
42
|
}
|
package/update.ts
CHANGED
|
@@ -2,6 +2,10 @@ import { convertNodeToElements, createId, Element, Node } from "@lukekaalim/act"
|
|
|
2
2
|
import { calculateChangedElements, ChangeEqualityTest } from "./algorithms.ts";
|
|
3
3
|
import { Commit, CommitID, CommitRef } from "./commit.ts";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* A request to transform part of a tree specified by
|
|
7
|
+
* the "ref"
|
|
8
|
+
*/
|
|
5
9
|
export type Update = {
|
|
6
10
|
/**
|
|
7
11
|
* The commit that should evaluate this
|
|
@@ -23,20 +27,31 @@ export type Update = {
|
|
|
23
27
|
* concequence of this update.
|
|
24
28
|
*/
|
|
25
29
|
targets: CommitRef[];
|
|
30
|
+
|
|
31
|
+
suspend: boolean,
|
|
26
32
|
};
|
|
27
33
|
|
|
28
34
|
export const Update = {
|
|
29
35
|
fresh: (ref: CommitRef, next: Element): Update => ({
|
|
30
|
-
ref, next, prev: null, targets: []
|
|
36
|
+
ref, next, prev: null, targets: [], suspend: false,
|
|
31
37
|
}),
|
|
32
38
|
existing: (prev: Commit, next: Element): Update => ({
|
|
33
|
-
ref: prev, next, prev, targets: [],
|
|
39
|
+
ref: prev, next, prev, targets: [], suspend: false,
|
|
34
40
|
}),
|
|
35
41
|
remove: (prev: Commit): Update => ({
|
|
36
|
-
ref: prev, next: null, prev, targets: [],
|
|
42
|
+
ref: prev, next: null, prev, targets: [], suspend: false,
|
|
37
43
|
}),
|
|
38
44
|
distant: (root: Commit, targets: CommitRef[]): Update => ({
|
|
39
|
-
ref: root, next: root.element, prev: root, targets,
|
|
45
|
+
ref: root, next: root.element, prev: root, targets, suspend: false,
|
|
46
|
+
}),
|
|
47
|
+
skip: (prev: Commit, targets: CommitRef[]): Update => ({
|
|
48
|
+
ref: prev, next: prev.element, prev, targets, suspend: false,
|
|
49
|
+
}),
|
|
50
|
+
target: (prev: Commit): Update => ({
|
|
51
|
+
ref: prev, next: prev.element, prev, targets: [prev], suspend: false,
|
|
52
|
+
}),
|
|
53
|
+
suspend: (prev: Commit): Update => ({
|
|
54
|
+
ref: prev, next: prev.element, prev, targets: [], suspend: true,
|
|
40
55
|
})
|
|
41
56
|
}
|
|
42
57
|
|
package/component.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { CommitRef } from "./commit";
|
|
2
|
-
import { ContextManager, createContextManager } from "./context"
|
|
3
|
-
import { createEffectManager, EffectManager } from "./effects"
|
|
4
|
-
import { createStateManager, StateManager } from "./state"
|
|
5
|
-
|
|
6
|
-
export type ComponentService = {
|
|
7
|
-
state: StateManager,
|
|
8
|
-
effect: EffectManager,
|
|
9
|
-
context: ContextManager,
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export const ComponentService = {
|
|
13
|
-
create: (requestRender: (ref: CommitRef) => unknown): ComponentService => {
|
|
14
|
-
const effect = createEffectManager();
|
|
15
|
-
const context = createContextManager();
|
|
16
|
-
const state = createStateManager(effect, requestRender, context);
|
|
17
|
-
|
|
18
|
-
return {
|
|
19
|
-
effect,
|
|
20
|
-
context,
|
|
21
|
-
state,
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
package/delta.test.ts
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { noop } from 'lodash-es';
|
|
2
|
-
|
|
3
|
-
import { describe, it } from "node:test";
|
|
4
|
-
import { CommitTree } from "./tree";
|
|
5
|
-
import { WorkThread } from "./thread";
|
|
6
|
-
import { ComponentService } from "./component";
|
|
7
|
-
import { applyUpdate } from "./delta";
|
|
8
|
-
import { Update } from "./update";
|
|
9
|
-
import { Commit, CommitRef } from "./commit";
|
|
10
|
-
import { h } from "@lukekaalim/act";
|
|
11
|
-
import { equal } from "node:assert/strict";
|
|
12
|
-
|
|
13
|
-
describe('delta library', () => {
|
|
14
|
-
|
|
15
|
-
describe('generated deltas', () => {
|
|
16
|
-
it('should make a create delta if there is no prev', () => {
|
|
17
|
-
const tree = CommitTree.new();
|
|
18
|
-
const comp = ComponentService.create(noop);
|
|
19
|
-
|
|
20
|
-
const thread = WorkThread.new();
|
|
21
|
-
const element = h('test-element');
|
|
22
|
-
const ref = CommitRef.new();
|
|
23
|
-
const update = Update.fresh(ref, element);
|
|
24
|
-
|
|
25
|
-
applyUpdate(tree, comp, thread, update);
|
|
26
|
-
|
|
27
|
-
equal(thread.deltas.created.length, 1);
|
|
28
|
-
const delta = thread.deltas.created[0];
|
|
29
|
-
equal(delta.next.element, element);
|
|
30
|
-
equal(delta.next.id, ref.id);
|
|
31
|
-
equal(delta.next.path, ref.path);
|
|
32
|
-
})
|
|
33
|
-
it('should not make any deltas if the prev and next are the same (and there is no targets)', () => {
|
|
34
|
-
const tree = CommitTree.new();
|
|
35
|
-
const comp = ComponentService.create(noop);
|
|
36
|
-
|
|
37
|
-
const thread = WorkThread.new();
|
|
38
|
-
const element = h('test-element');
|
|
39
|
-
const prev = Commit.new(element);
|
|
40
|
-
const update = Update.existing(prev, element);
|
|
41
|
-
tree.commits.set(prev.id, prev);
|
|
42
|
-
|
|
43
|
-
applyUpdate(tree, comp, thread, update);
|
|
44
|
-
|
|
45
|
-
equal(thread.deltas.created.length, 0);
|
|
46
|
-
equal(thread.deltas.updated.length, 0);
|
|
47
|
-
equal(thread.deltas.removed.length, 0);
|
|
48
|
-
equal(thread.deltas.skipped.length, 0);
|
|
49
|
-
})
|
|
50
|
-
it('should make a skip delta if the prev and the next are the same, but there is a target', () => {
|
|
51
|
-
const tree = CommitTree.new();
|
|
52
|
-
const comp = ComponentService.create(noop);
|
|
53
|
-
|
|
54
|
-
const thread = WorkThread.new();
|
|
55
|
-
const element = h('test-element');
|
|
56
|
-
const prev = Commit.new(element);
|
|
57
|
-
// an imaginary child
|
|
58
|
-
const target = CommitRef.new(prev.path);
|
|
59
|
-
|
|
60
|
-
const update = Update.existing(prev, element);
|
|
61
|
-
update.targets.push(target);
|
|
62
|
-
tree.commits.set(prev.id, prev);
|
|
63
|
-
|
|
64
|
-
applyUpdate(tree, comp, thread, update);
|
|
65
|
-
|
|
66
|
-
equal(thread.deltas.skipped.length, 1);
|
|
67
|
-
})
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('should create a delta for a new id, and queue its children for creation', () => {
|
|
71
|
-
const tree = CommitTree.new();
|
|
72
|
-
const comp = ComponentService.create(noop);
|
|
73
|
-
const thread = WorkThread.new();
|
|
74
|
-
const childA = h('child-element-a');
|
|
75
|
-
const childB = h('child-element-b');
|
|
76
|
-
const element = h('new-element', {}, [childA, childB]);
|
|
77
|
-
|
|
78
|
-
const update = Update.fresh(Commit.new(element), element);
|
|
79
|
-
applyUpdate(tree, comp, thread, update);
|
|
80
|
-
|
|
81
|
-
equal(thread.deltas.created.length, 1);
|
|
82
|
-
equal(thread.pendingUpdates.length, 2);
|
|
83
|
-
|
|
84
|
-
const delta = thread.deltas.created[0];
|
|
85
|
-
const updateA = thread.pendingUpdates[0];
|
|
86
|
-
const updateB = thread.pendingUpdates[1];
|
|
87
|
-
|
|
88
|
-
equal(delta.next.element, element);
|
|
89
|
-
equal(updateA.next, childA);
|
|
90
|
-
equal(updateB.next, childB);
|
|
91
|
-
})
|
|
92
|
-
it('should create a delete delta for a node, and queue its children to be deleted', () => {
|
|
93
|
-
const tree = CommitTree.new();
|
|
94
|
-
const comp = ComponentService.create(noop);
|
|
95
|
-
const thread = WorkThread.new();
|
|
96
|
-
|
|
97
|
-
const prev = Commit.new(h('element'))
|
|
98
|
-
|
|
99
|
-
tree.commits.set(prev.id, prev);
|
|
100
|
-
|
|
101
|
-
const update = Update.remove(prev);
|
|
102
|
-
|
|
103
|
-
applyUpdate(tree, comp, thread, update);
|
|
104
|
-
|
|
105
|
-
equal(thread.deltas.removed.length, 1);
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
})
|
package/effects.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { EffectCleanup, EffectConstructor, OpaqueID } from "@lukekaalim/act";
|
|
2
|
-
import { WorkThread } from "./thread.ts";
|
|
3
|
-
|
|
4
|
-
export type EffectID = OpaqueID<"EffectID">;
|
|
5
|
-
export type EffectTask = {
|
|
6
|
-
id: EffectID,
|
|
7
|
-
task: () => void,
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const createEffectManager = () => {
|
|
11
|
-
const cleanups = new Map<EffectID, EffectCleanup>();
|
|
12
|
-
|
|
13
|
-
const enqueueEffect = (
|
|
14
|
-
thread: WorkThread,
|
|
15
|
-
id: EffectID,
|
|
16
|
-
effect: EffectConstructor
|
|
17
|
-
) => {
|
|
18
|
-
thread.pendingEffects.push({
|
|
19
|
-
id,
|
|
20
|
-
task() {
|
|
21
|
-
const cleanup = cleanups.get(id);
|
|
22
|
-
if (cleanup) {
|
|
23
|
-
cleanups.delete(id);
|
|
24
|
-
cleanup();
|
|
25
|
-
}
|
|
26
|
-
cleanups.set(id, effect());
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
};
|
|
30
|
-
const enqueueTeardown = (
|
|
31
|
-
thread: WorkThread,
|
|
32
|
-
id: EffectID,
|
|
33
|
-
) => {
|
|
34
|
-
thread.pendingEffects.push({
|
|
35
|
-
id,
|
|
36
|
-
task() {
|
|
37
|
-
const cleanup = cleanups.get(id);
|
|
38
|
-
if (cleanup) {
|
|
39
|
-
cleanups.delete(id);
|
|
40
|
-
cleanup();
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
})
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
return { enqueueEffect, enqueueTeardown };
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export type EffectManager = ReturnType<typeof createEffectManager>;
|