@lukekaalim/act-recon 1.1.2 → 2.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 +17 -0
- package/commit.ts +7 -2
- package/delta.ts +35 -1
- package/element.ts +8 -5
- package/event.ts +34 -0
- package/mod.ts +10 -8
- package/package.json +2 -2
- package/reconciler.ts +105 -30
- package/scheduler.ts +10 -0
- package/thread.ts +249 -229
- package/tree.ts +19 -1
- package/update.ts +30 -21
- package/work.ts +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @lukekaalim/act-recon
|
|
2
2
|
|
|
3
|
+
## 2.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- Scheduling refactor
|
|
8
|
+
|
|
9
|
+
## 1.2.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- Added support for keys/reordering elements without unmounting them
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- Updated dependencies
|
|
18
|
+
- @lukekaalim/act@3.2.0
|
|
19
|
+
|
|
3
20
|
## 1.1.2
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/commit.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createId, Element, OpaqueID } from "@lukekaalim/act";
|
|
2
|
-
import { version } from "os";
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* A single consistent id representing a commit in the act tree.
|
|
@@ -28,13 +27,19 @@ export type CommitRef = {
|
|
|
28
27
|
path: CommitPath;
|
|
29
28
|
};
|
|
30
29
|
export const CommitRef = {
|
|
30
|
+
from(path: CommitPath) {
|
|
31
|
+
return {
|
|
32
|
+
path,
|
|
33
|
+
id: path[path.length - 1],
|
|
34
|
+
}
|
|
35
|
+
},
|
|
31
36
|
new(path: CommitPath = []) {
|
|
32
37
|
const id = createId<'CommitID'>();
|
|
33
38
|
return {
|
|
34
39
|
path: [...path, id],
|
|
35
40
|
id,
|
|
36
41
|
}
|
|
37
|
-
}
|
|
42
|
+
},
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
/**
|
package/delta.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Commit, CommitRef } from "./commit.ts";
|
|
2
|
+
import { CommitTree } from "./tree.ts";
|
|
2
3
|
|
|
3
4
|
export type CreateDelta = { ref: CommitRef, next: Commit };
|
|
4
|
-
export type UpdateDelta = { ref: CommitRef, next: Commit, prev: Commit };
|
|
5
|
+
export type UpdateDelta = { ref: CommitRef, next: Commit, prev: Commit, moved: boolean };
|
|
5
6
|
export type RemoveDelta = { ref: CommitRef, prev: Commit };
|
|
6
7
|
export type SkipDelta = { next: Commit };
|
|
7
8
|
|
|
@@ -11,3 +12,36 @@ export type DeltaSet = {
|
|
|
11
12
|
skipped: SkipDelta[],
|
|
12
13
|
removed: RemoveDelta[],
|
|
13
14
|
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Apply a deltaset to a tree, modifying it's commit list
|
|
18
|
+
* to match the changes produced by the thread.
|
|
19
|
+
*
|
|
20
|
+
* @param thread
|
|
21
|
+
* @param tree
|
|
22
|
+
*/
|
|
23
|
+
const applyDeltaSet = (deltas: DeltaSet, tree: CommitTree) => {
|
|
24
|
+
for (const delta of deltas.created)
|
|
25
|
+
tree.commits.set(delta.ref.id, delta.next);
|
|
26
|
+
|
|
27
|
+
for (const delta of deltas.skipped)
|
|
28
|
+
tree.commits.set(delta.next.id, delta.next);
|
|
29
|
+
|
|
30
|
+
for (const delta of deltas.updated)
|
|
31
|
+
tree.commits.set(delta.ref.id, delta.next);
|
|
32
|
+
|
|
33
|
+
for (const delta of deltas.removed)
|
|
34
|
+
tree.commits.delete(delta.ref.id);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const DeltaSet = {
|
|
38
|
+
create: (): DeltaSet => ({ created: [], updated: [], skipped: [], removed: [] }),
|
|
39
|
+
clone: (deltas: DeltaSet): DeltaSet => ({
|
|
40
|
+
created: [...deltas.created],
|
|
41
|
+
updated: [...deltas.updated],
|
|
42
|
+
skipped: [...deltas.skipped],
|
|
43
|
+
removed: [...deltas.removed],
|
|
44
|
+
}),
|
|
45
|
+
apply: applyDeltaSet,
|
|
46
|
+
}
|
|
47
|
+
|
package/element.ts
CHANGED
|
@@ -40,7 +40,6 @@ export const createElementService = (
|
|
|
40
40
|
requestRender: (ref: CommitRef) => void
|
|
41
41
|
): ElementService => {
|
|
42
42
|
const contextStates = new Map<CommitID, ContextState<unknown>>();
|
|
43
|
-
const componentStates = new Map<CommitID, ComponentState>();
|
|
44
43
|
const boundaryValues = new Map<CommitID, unknown>();
|
|
45
44
|
|
|
46
45
|
const render = (
|
|
@@ -82,7 +81,7 @@ export const createElementService = (
|
|
|
82
81
|
}
|
|
83
82
|
break;
|
|
84
83
|
case 'function': {
|
|
85
|
-
let state =
|
|
84
|
+
let state = tree.components.get(ref.id);
|
|
86
85
|
if (!state) {
|
|
87
86
|
state = {
|
|
88
87
|
unmounted: false,
|
|
@@ -93,7 +92,7 @@ export const createElementService = (
|
|
|
93
92
|
deps: new Map(),
|
|
94
93
|
effects: new Map(),
|
|
95
94
|
}
|
|
96
|
-
|
|
95
|
+
tree.components.set(ref.id, state);
|
|
97
96
|
}
|
|
98
97
|
loadHooks(contextStates, requestRender, state, ref, output);
|
|
99
98
|
const props = {
|
|
@@ -125,7 +124,7 @@ export const createElementService = (
|
|
|
125
124
|
break;
|
|
126
125
|
}
|
|
127
126
|
case 'function': {
|
|
128
|
-
const componentState =
|
|
127
|
+
const componentState = tree.components.get(prev.id) as ComponentState;
|
|
129
128
|
componentState.unmounted = true;
|
|
130
129
|
for (const [,context] of componentState.contexts) {
|
|
131
130
|
if (context.state)
|
|
@@ -143,7 +142,7 @@ export const createElementService = (
|
|
|
143
142
|
}
|
|
144
143
|
});
|
|
145
144
|
}
|
|
146
|
-
|
|
145
|
+
tree.components.delete(prev.id);
|
|
147
146
|
break;
|
|
148
147
|
}
|
|
149
148
|
}
|
|
@@ -153,3 +152,7 @@ export const createElementService = (
|
|
|
153
152
|
|
|
154
153
|
return { render, clear, boundary: boundaryValues };
|
|
155
154
|
}
|
|
155
|
+
|
|
156
|
+
export const ElementService = {
|
|
157
|
+
create: createElementService
|
|
158
|
+
}
|
package/event.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type Subscription = { cancel: () => void };
|
|
2
|
+
export type EventHandler<T> = (event: T) => unknown;
|
|
3
|
+
export type EventMap = Record<string, unknown>;
|
|
4
|
+
|
|
5
|
+
export type EventEmitter<T extends EventMap> = {
|
|
6
|
+
on<K extends keyof T>(type: K, handler: EventHandler<T[K]>): Subscription,
|
|
7
|
+
call<K extends keyof T>(type: K, event: T[K]): void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const createEventEmitter = <T extends EventMap>(): EventEmitter<T> => {
|
|
11
|
+
type AnyEvent = T[keyof T];
|
|
12
|
+
type AnyHandler = EventHandler<AnyEvent>
|
|
13
|
+
const handlers = new Map<keyof T, Set<AnyHandler>>();
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
on(type, handler) {
|
|
17
|
+
const set = handlers.get(type) || new Set<AnyHandler>();
|
|
18
|
+
handlers.set(type, set);
|
|
19
|
+
set.add(handler as AnyHandler);
|
|
20
|
+
return {
|
|
21
|
+
cancel() {
|
|
22
|
+
set.delete(handler as AnyHandler);
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
call(type, event) {
|
|
27
|
+
const set = handlers.get(type);
|
|
28
|
+
if (!set)
|
|
29
|
+
return;
|
|
30
|
+
for (const handler of set)
|
|
31
|
+
handler(event);
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
};
|
package/mod.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
export * from './commit
|
|
2
|
-
export * from './delta
|
|
3
|
-
export * from './state
|
|
4
|
-
export * from './algorithms
|
|
5
|
-
export * from './
|
|
6
|
-
export * from './
|
|
7
|
-
export * from './
|
|
1
|
+
export * from './commit'
|
|
2
|
+
export * from './delta';
|
|
3
|
+
export * from './state';
|
|
4
|
+
export * from './algorithms';
|
|
5
|
+
export * from './scheduler';
|
|
6
|
+
export * from './update';
|
|
7
|
+
export * from './thread';
|
|
8
|
+
export * from './tree';
|
|
9
|
+
export * from './event';
|
|
8
10
|
|
|
9
|
-
export * from './reconciler
|
|
11
|
+
export * from './reconciler';
|
package/package.json
CHANGED
package/reconciler.ts
CHANGED
|
@@ -1,36 +1,111 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { convertNodeToElements, Node, OpaqueID } from "@lukekaalim/act";
|
|
2
|
+
import { CommitID, CommitRef } from "./commit";
|
|
3
|
+
import { WorkThread } from "./thread"
|
|
2
4
|
import { CommitTree } from "./tree";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
5
|
+
import { ElementService } from "./element";
|
|
6
|
+
import { Scheduler, WorkID } from "./scheduler";
|
|
7
|
+
import { createEventEmitter, EventEmitter } from "./event";
|
|
8
|
+
|
|
9
|
+
export type Reconciler = {
|
|
10
|
+
mount(node: Node): void,
|
|
11
|
+
render(ref: CommitRef): void,
|
|
12
|
+
|
|
13
|
+
state: ReconcilerState,
|
|
14
|
+
tree: CommitTree,
|
|
15
|
+
elements: ElementService,
|
|
16
|
+
on: EventEmitter<ReconcilerEvents>["on"],
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ReconcilerState = {
|
|
20
|
+
thread: WorkThread | null,
|
|
21
|
+
work: WorkID | null,
|
|
22
|
+
/**
|
|
23
|
+
* These are targets that can't be fulfilled with the current thread
|
|
24
|
+
* */
|
|
25
|
+
pendingTargets: Map<CommitID, CommitRef>,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type ReconcilerEvents = {
|
|
29
|
+
'on-thread-start': WorkThread,
|
|
30
|
+
'on-thread-update': WorkThread,
|
|
31
|
+
'on-thread-complete': WorkThread,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const createReconciler = (scheduler: Scheduler): Reconciler => {
|
|
35
|
+
const events = createEventEmitter<ReconcilerEvents>();
|
|
36
|
+
const state: ReconcilerState = {
|
|
37
|
+
thread: null,
|
|
38
|
+
work: null,
|
|
39
|
+
pendingTargets: new Map(),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const work = () => {
|
|
43
|
+
state.work = null;
|
|
44
|
+
if (!state.thread)
|
|
45
|
+
return;
|
|
46
|
+
|
|
47
|
+
const update = state.thread.pendingUpdates.pop();
|
|
48
|
+
if (update) {
|
|
49
|
+
WorkThread.update(state.thread, update, tree, elements);
|
|
50
|
+
state.work = scheduler.requestWork(work);
|
|
51
|
+
} else {
|
|
52
|
+
const completedThread = state.thread;
|
|
53
|
+
state.thread = null;
|
|
54
|
+
|
|
55
|
+
const pendingTargets = [...state.pendingTargets]
|
|
56
|
+
state.pendingTargets.clear();
|
|
57
|
+
|
|
58
|
+
for (const [,target] of pendingTargets)
|
|
59
|
+
render(target);
|
|
60
|
+
|
|
61
|
+
WorkThread.apply(completedThread, tree);
|
|
62
|
+
events.call('on-thread-complete', completedThread);
|
|
63
|
+
|
|
64
|
+
// Run side effects
|
|
65
|
+
for (const effect of completedThread.pendingEffects) {
|
|
66
|
+
try {
|
|
67
|
+
effect.func();
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error(error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const start = () => {
|
|
76
|
+
if (!state.thread) {
|
|
77
|
+
state.thread = WorkThread.new();
|
|
78
|
+
events.call('on-thread-start', state.thread);
|
|
79
|
+
}
|
|
80
|
+
if (!state.work) {
|
|
81
|
+
state.work = scheduler.requestWork(work);
|
|
82
|
+
}
|
|
83
|
+
return state.thread;
|
|
27
84
|
}
|
|
28
85
|
|
|
29
|
-
const
|
|
86
|
+
const mount = (node: Node) => {
|
|
87
|
+
const thread = start();
|
|
88
|
+
const elements = convertNodeToElements(node)
|
|
30
89
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
90
|
+
for (const element of elements) {
|
|
91
|
+
const ref = CommitRef.new()
|
|
92
|
+
tree.roots.add(ref);
|
|
93
|
+
WorkThread.queueMount(thread, ref, element);
|
|
94
|
+
}
|
|
95
|
+
events.call('on-thread-update', thread);
|
|
96
|
+
};
|
|
97
|
+
const render = (ref: CommitRef) => {
|
|
98
|
+
const thread = start();
|
|
99
|
+
|
|
100
|
+
if (WorkThread.queueTarget(thread, ref, tree)) {
|
|
101
|
+
events.call('on-thread-update', thread);
|
|
102
|
+
} else {
|
|
103
|
+
state.pendingTargets.set(ref.id, ref);
|
|
104
|
+
}
|
|
35
105
|
}
|
|
106
|
+
|
|
107
|
+
const tree = CommitTree.new();
|
|
108
|
+
const elements = ElementService.create(tree, render);
|
|
109
|
+
|
|
110
|
+
return { mount, render, state, tree, elements, on: events.on };
|
|
36
111
|
}
|
package/scheduler.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { OpaqueID } from "@lukekaalim/act";
|
|
2
|
+
|
|
3
|
+
export type WorkID = OpaqueID<"WorkID">;
|
|
4
|
+
/**
|
|
5
|
+
* Work provider is platform agnostic timer/callback interface
|
|
6
|
+
*/
|
|
7
|
+
export type Scheduler = {
|
|
8
|
+
requestWork(callback: () => void): WorkID,
|
|
9
|
+
cancelWork(workId: WorkID): void,
|
|
10
|
+
};
|
package/thread.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { convertNodeToElements, createId, ErrorBoundaryProps, errorBoundaryType, Node } from "@lukekaalim/act";
|
|
1
|
+
import { convertNodeToElements, createId, Element, ErrorBoundaryProps, errorBoundaryType, Node } from "@lukekaalim/act";
|
|
2
2
|
import { Commit, CommitID, CommitRef } from "./commit.ts";
|
|
3
3
|
import { DeltaSet } from "./delta.ts";
|
|
4
4
|
import { CommitTree } from "./tree.ts";
|
|
@@ -8,273 +8,293 @@ import { EffectTask } from "./state.ts";
|
|
|
8
8
|
import { ErrorBoundaryState } from "./errors.ts";
|
|
9
9
|
import { first, last } from "./algorithms.ts";
|
|
10
10
|
|
|
11
|
+
export type WorkReason =
|
|
12
|
+
| { type: 'mount', element: Element, ref: CommitRef }
|
|
13
|
+
| { type: 'target', ref: CommitRef }
|
|
14
|
+
|
|
11
15
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
16
|
+
* Apply a thread to a tree, modifying it's commit list
|
|
17
|
+
* to match the changes produced by the thread.
|
|
14
18
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
19
|
+
* @param thread
|
|
20
|
+
* @param tree
|
|
17
21
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
pendingEffects: EffectTask[],
|
|
23
|
-
|
|
24
|
-
errorNotifications: Set<CommitID>,
|
|
22
|
+
const applyWorkThread = (thread: WorkThread, tree: CommitTree) => {
|
|
23
|
+
DeltaSet.apply(thread.deltas, tree);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
pendingEffects: [],
|
|
34
|
-
pendingUpdates: [],
|
|
35
|
-
errorNotifications: new Set(),
|
|
36
|
-
visited: new Set(),
|
|
37
|
-
deltas: {
|
|
38
|
-
created: [],
|
|
39
|
-
updated: [],
|
|
40
|
-
removed: [],
|
|
41
|
-
skipped: [],
|
|
42
|
-
},
|
|
27
|
+
const notifyErrorBoundaries = (thread: WorkThread, tree: CommitTree) => {
|
|
28
|
+
for (const [boundaryId] of thread.errorNotifications) {
|
|
29
|
+
const commit = tree.commits.get(boundaryId) as Commit;
|
|
30
|
+
const { onError, ref } = commit.element.props as ErrorBoundaryProps;
|
|
31
|
+
if (typeof onError === 'function') {
|
|
32
|
+
const state = CommitTree.getError(tree, commit.id);
|
|
33
|
+
onError(state.value);
|
|
43
34
|
}
|
|
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
35
|
}
|
|
78
36
|
}
|
|
79
37
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Remove all changes from a particular commit and all it's children
|
|
40
|
+
* - essentially making it as if it had never rendered at all
|
|
41
|
+
* @param thread
|
|
42
|
+
* @param from
|
|
43
|
+
*/
|
|
44
|
+
const rollbackWorkThread = (thread: WorkThread, from: CommitRef) => {
|
|
45
|
+
// TODO: rollbacks are hard: need rollback compatible with new
|
|
46
|
+
// thread model
|
|
47
|
+
throw new Error();
|
|
48
|
+
/*
|
|
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));
|
|
87
53
|
|
|
88
|
-
|
|
54
|
+
thread.visited = new Set([...thread.visited].filter(v => !isDescendant(from, v)))
|
|
89
55
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
}
|
|
56
|
+
thread.pendingUpdates = thread.pendingUpdates.filter(update => !isDescendant(from, update.ref))
|
|
57
|
+
thread.pendingEffects = thread.pendingEffects.filter(effect => !isDescendant(from, effect.ref))
|
|
58
|
+
*/
|
|
59
|
+
}
|
|
96
60
|
|
|
97
|
-
|
|
98
|
-
|
|
61
|
+
const updateWorkThread = (thread: WorkThread, update: Update, tree: CommitTree, element: ElementService) => {
|
|
62
|
+
const { next, prev, ref, moved } = update;
|
|
99
63
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
64
|
+
const identicalChange = next && prev && (next.id === prev.element.id);
|
|
65
|
+
const prevChildren = prev && prev.children
|
|
66
|
+
.map(c => tree.commits.get(c.id) as Commit) || [];
|
|
103
67
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
68
|
+
if (identicalChange) {
|
|
69
|
+
const mustVisit = thread.mustVisit.has(ref.id);
|
|
70
|
+
if (!mustVisit)
|
|
71
|
+
return;
|
|
107
72
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
73
|
+
const mustRender = thread.mustRender.has(ref.id);
|
|
74
|
+
|
|
75
|
+
if (!mustRender) {
|
|
76
|
+
const updates = prevChildren.map(prev => Update.skip(prev));
|
|
77
|
+
thread.pendingUpdates.push(...updates);
|
|
78
|
+
const commit = Commit.version(prev);
|
|
79
|
+
thread.deltas.skipped.push({ next: commit });
|
|
80
|
+
return;
|
|
113
81
|
}
|
|
114
|
-
// completed all work
|
|
115
|
-
return true;
|
|
116
82
|
}
|
|
83
|
+
thread.visited.set(ref.id, ref);
|
|
84
|
+
|
|
85
|
+
if (next) {
|
|
86
|
+
const output = element.render(next, ref);
|
|
87
|
+
if (output.reject) {
|
|
88
|
+
const errorBoundary = WorkThread.findClosestBoundary(thread, tree, ref);
|
|
89
|
+
if (errorBoundary) {
|
|
90
|
+
const errorState = CommitTree.getError(tree, errorBoundary.id);
|
|
91
|
+
ErrorBoundaryState.set(errorState, output.reject);
|
|
92
|
+
WorkThread.rollback(thread, errorBoundary);
|
|
93
|
+
WorkThread.notifyError(thread, errorBoundary);
|
|
117
94
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
for (const delta of currentThread.deltas.removed)
|
|
130
|
-
tree.commits.delete(delta.ref.id);
|
|
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);
|
|
95
|
+
thread.pendingUpdates.push(Update.target(errorBoundary));
|
|
96
|
+
return;
|
|
97
|
+
} else {
|
|
98
|
+
console.error(output.reject);
|
|
99
|
+
console.error(`No boundary to catch error: Unmounting roots`);
|
|
100
|
+
for (const ref of tree.roots) {
|
|
101
|
+
WorkThread.rollback(thread, ref);
|
|
102
|
+
const prev = tree.commits.get(ref.id);
|
|
103
|
+
if (prev)
|
|
104
|
+
thread.pendingUpdates.push(Update.remove(prev));
|
|
140
105
|
}
|
|
141
|
-
onError(state.value, clear);
|
|
142
106
|
}
|
|
143
107
|
}
|
|
144
108
|
|
|
145
|
-
|
|
146
|
-
onThreadComplete(currentThread.deltas, currentThread.pendingEffects);
|
|
109
|
+
const [childRefs, updates] = calculateUpdates(ref, prevChildren, output.child);
|
|
147
110
|
|
|
148
|
-
|
|
149
|
-
|
|
111
|
+
thread.pendingEffects.push(...output.effects);
|
|
112
|
+
thread.pendingUpdates.push(...updates);
|
|
150
113
|
|
|
151
|
-
|
|
152
|
-
if (pendingUpdateTargets.size > 0) {
|
|
153
|
-
const roots = CommitTree.getRootCommits(tree);
|
|
154
|
-
const targets = [...pendingUpdateTargets.values()];
|
|
155
|
-
for (const root of roots) {
|
|
156
|
-
currentThread.pendingUpdates.push(Update.distant(root, targets));
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
pendingUpdateTargets.clear();
|
|
160
|
-
}
|
|
161
|
-
}
|
|
114
|
+
const commit = Commit.update(ref, next, childRefs);
|
|
162
115
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
}
|
|
175
|
-
} else {
|
|
176
|
-
const roots = CommitTree.getRootCommits(tree);
|
|
177
|
-
for (const root of roots) {
|
|
178
|
-
currentThread.started = true;
|
|
179
|
-
currentThread.pendingUpdates.push(Update.distant(root, [target]));
|
|
180
|
-
}
|
|
181
|
-
}
|
|
116
|
+
if (prev)
|
|
117
|
+
thread.deltas.updated.push({ ref, prev, next: commit, moved });
|
|
118
|
+
else
|
|
119
|
+
thread.deltas.created.push({ ref, next: commit });
|
|
182
120
|
|
|
183
|
-
|
|
184
|
-
|
|
121
|
+
// Update tree
|
|
122
|
+
//tree.commits.set(ref.id, commit);
|
|
185
123
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
tree.roots.add(ref);
|
|
192
|
-
currentThread.pendingUpdates.push(Update.fresh(ref, element));
|
|
193
|
-
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
else if (prev && !next) {
|
|
127
|
+
// We should delay this?
|
|
128
|
+
const output = element.clear(prev);
|
|
194
129
|
|
|
195
|
-
|
|
130
|
+
thread.deltas.removed.push({ ref: prev, prev });
|
|
131
|
+
thread.pendingUpdates.push(...prevChildren.map(prev => Update.remove(prev)));
|
|
132
|
+
thread.pendingEffects.push(...output.effects);
|
|
133
|
+
return;
|
|
134
|
+
} else {
|
|
135
|
+
throw new Error(`No prev, no next, did this commit ever exist?`)
|
|
196
136
|
}
|
|
137
|
+
};
|
|
197
138
|
|
|
198
|
-
|
|
199
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Start a new Update in the current Thread.
|
|
141
|
+
* @param thread
|
|
142
|
+
* @param ref
|
|
143
|
+
* @param tree
|
|
144
|
+
*/
|
|
145
|
+
const startWorkThreadUpdate = (thread: WorkThread, ref: CommitRef, prev: Commit | null, next: Element | null) => {
|
|
146
|
+
// Once a update starts,
|
|
147
|
+
// all "parents" are considered to have been visited,
|
|
148
|
+
// and cannot be rendered in this pass.
|
|
149
|
+
for (const id of [...ref.path].reverse().slice(1))
|
|
150
|
+
thread.visited.set(id, ref);
|
|
200
151
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
152
|
+
thread.pendingUpdates.push({
|
|
153
|
+
ref,
|
|
154
|
+
prev,
|
|
155
|
+
next,
|
|
156
|
+
moved: false,
|
|
157
|
+
})
|
|
158
|
+
}
|
|
204
159
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
160
|
+
/**
|
|
161
|
+
* Request that a commit be re-rendered
|
|
162
|
+
*
|
|
163
|
+
* If returns false, the update cannot be queued in the current
|
|
164
|
+
* thread (maybe it already re-rendered?).
|
|
165
|
+
* @param thread
|
|
166
|
+
* @param ref
|
|
167
|
+
* @returns
|
|
168
|
+
*/
|
|
169
|
+
const queueWorkThreadTarget = (thread: WorkThread, ref: CommitRef, tree: CommitTree): boolean => {
|
|
170
|
+
// If the thread _already_ has this ref as a target,
|
|
171
|
+
// do nothing
|
|
172
|
+
if (thread.mustRender.has(ref.id))
|
|
173
|
+
return true;
|
|
209
174
|
|
|
210
|
-
|
|
175
|
+
// We cant do work on a commit that has
|
|
176
|
+
// already been visited
|
|
177
|
+
if (thread.visited.has(ref.id))
|
|
178
|
+
return false;
|
|
211
179
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
180
|
+
thread.reasons.push({ type: 'target', ref });
|
|
181
|
+
thread.mustRender.set(ref.id, ref);
|
|
182
|
+
|
|
183
|
+
// Search through all the parents, looking to see if
|
|
184
|
+
// there are any pendingUpdates that might
|
|
185
|
+
// lead to this commit. If so, make sure ancestor commit
|
|
186
|
+
// is on the MustVisit so they should make their way down
|
|
187
|
+
// eventually
|
|
188
|
+
for (const id of [...ref.path].reverse().slice(1)) {
|
|
189
|
+
thread.mustVisit.set(ref.id, ref);
|
|
190
|
+
|
|
191
|
+
for (const update of thread.pendingUpdates) {
|
|
192
|
+
// Found an ancestor pending update - it should
|
|
193
|
+
// handle our target eventually
|
|
194
|
+
if (update.ref.id === id)
|
|
195
|
+
return true;
|
|
220
196
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
}
|
|
197
|
+
}
|
|
198
|
+
// otherwise, start a new update from the root
|
|
199
|
+
const prev = tree.commits.get(ref.id) as Commit;
|
|
200
|
+
startWorkThreadUpdate(thread, ref, prev, prev.element);
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
const queueWorkThreadMount = (thread: WorkThread, ref: CommitRef, element: Element) => {
|
|
204
|
+
thread.reasons.push({ type: 'mount', element, ref });
|
|
205
|
+
startWorkThreadUpdate(thread, ref, null, element);
|
|
206
|
+
};
|
|
242
207
|
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const [childRefs, updates] = calculateUpdates(ref, prevChildren, output.child);
|
|
247
208
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
209
|
+
/**
|
|
210
|
+
* A WorkThread is a mutable data structure that
|
|
211
|
+
* represents a particular "Tree Traversal Task".
|
|
212
|
+
*
|
|
213
|
+
* Its expected when you start rendering, you
|
|
214
|
+
* may start rendering more nodes due to updates.
|
|
215
|
+
*
|
|
216
|
+
* A thread can be "worked" to remove an update off the
|
|
217
|
+
* "pending updates" list, which may optionally produce more
|
|
218
|
+
* updates, effects, or error notification.
|
|
219
|
+
*/
|
|
220
|
+
export type WorkThread = {
|
|
221
|
+
reasons: WorkReason[],
|
|
253
222
|
|
|
254
|
-
|
|
223
|
+
mustRender: Map<CommitID, CommitRef>,
|
|
224
|
+
mustVisit: Map<CommitID, CommitRef>,
|
|
255
225
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
else
|
|
259
|
-
thread.deltas.created.push({ ref, next: commit });
|
|
226
|
+
pendingUpdates: Update[],
|
|
227
|
+
pendingEffects: EffectTask[],
|
|
260
228
|
|
|
261
|
-
|
|
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
|
-
};
|
|
229
|
+
errorNotifications: Map<CommitID, CommitRef>,
|
|
276
230
|
|
|
277
|
-
|
|
231
|
+
/**
|
|
232
|
+
* A list of each commit the thread processed
|
|
233
|
+
*/
|
|
234
|
+
visited: Map<CommitID, CommitRef>,
|
|
235
|
+
deltas: DeltaSet,
|
|
278
236
|
};
|
|
279
237
|
|
|
280
|
-
export
|
|
238
|
+
export const cloneWorkThread = (thread: WorkThread): WorkThread => {
|
|
239
|
+
return {
|
|
240
|
+
reasons: [...thread.reasons],
|
|
241
|
+
pendingEffects: [...thread.pendingEffects],
|
|
242
|
+
pendingUpdates: [...thread.pendingUpdates],
|
|
243
|
+
errorNotifications: new Map(thread.errorNotifications),
|
|
244
|
+
|
|
245
|
+
mustVisit: new Map(thread.mustVisit),
|
|
246
|
+
mustRender: new Map(thread.mustRender),
|
|
247
|
+
|
|
248
|
+
visited: new Map(thread.visited),
|
|
249
|
+
|
|
250
|
+
deltas: DeltaSet.clone(thread.deltas),
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export const WorkThread = {
|
|
255
|
+
new(): WorkThread {
|
|
256
|
+
return {
|
|
257
|
+
reasons: [],
|
|
258
|
+
pendingEffects: [],
|
|
259
|
+
pendingUpdates: [],
|
|
260
|
+
errorNotifications: new Map(),
|
|
261
|
+
|
|
262
|
+
mustVisit: new Map(),
|
|
263
|
+
mustRender: new Map(),
|
|
264
|
+
|
|
265
|
+
visited: new Map(),
|
|
266
|
+
|
|
267
|
+
deltas: DeltaSet.create(),
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
rollback: rollbackWorkThread,
|
|
271
|
+
apply: applyWorkThread,
|
|
272
|
+
update: updateWorkThread,
|
|
273
|
+
queueTarget: queueWorkThreadTarget,
|
|
274
|
+
queueMount: queueWorkThreadMount,
|
|
275
|
+
|
|
276
|
+
clone: cloneWorkThread,
|
|
277
|
+
|
|
278
|
+
notifyError(thread: WorkThread, ref: CommitRef) {
|
|
279
|
+
thread.errorNotifications.set(ref.id, ref);
|
|
280
|
+
},
|
|
281
|
+
/**
|
|
282
|
+
* Find the closest ancestor error boundary for a commit,
|
|
283
|
+
* either in the tree or one that was just created
|
|
284
|
+
* */
|
|
285
|
+
findClosestBoundary(thread: WorkThread, tree: CommitTree, ref: CommitRef): Commit | null {
|
|
286
|
+
return last(ref.path, id => {
|
|
287
|
+
if (tree.commits.has(id)) {
|
|
288
|
+
const commit = tree.commits.get(id) as Commit;
|
|
289
|
+
if (commit.element.type === errorBoundaryType)
|
|
290
|
+
return commit;
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
// We also might have just created the boundary
|
|
294
|
+
const freshBoundary = thread.deltas.created.find(c => c.ref.id === id && c.next.element.type === errorBoundaryType);
|
|
295
|
+
if (freshBoundary)
|
|
296
|
+
return freshBoundary.next;
|
|
297
|
+
return null;
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
package/tree.ts
CHANGED
|
@@ -21,9 +21,27 @@ export const CommitTree = {
|
|
|
21
21
|
commits: new Map(),
|
|
22
22
|
roots: new Set(),
|
|
23
23
|
}),
|
|
24
|
+
clone(tree: CommitTree): CommitTree {
|
|
25
|
+
return {
|
|
26
|
+
errors: new Map(tree.errors),
|
|
27
|
+
components: new Map(tree.components),
|
|
28
|
+
contexts: new Map(tree.contexts),
|
|
29
|
+
|
|
30
|
+
commits: new Map(tree.commits),
|
|
31
|
+
roots: new Set(tree.roots),
|
|
32
|
+
}
|
|
33
|
+
},
|
|
24
34
|
getRootCommits: (tree: CommitTree) => {
|
|
25
|
-
return [...tree.roots]
|
|
35
|
+
return [...tree.roots]
|
|
36
|
+
.map(ref => tree.commits.get(ref.id))
|
|
37
|
+
.filter(c => !!c)
|
|
26
38
|
},
|
|
39
|
+
/**
|
|
40
|
+
* Each commit ID _may_ have an ErrorBoundaryState associated with it
|
|
41
|
+
* @param tree
|
|
42
|
+
* @param id
|
|
43
|
+
* @returns
|
|
44
|
+
*/
|
|
27
45
|
getError(tree: CommitTree, id: CommitID): ErrorBoundaryState {
|
|
28
46
|
if (tree.errors.has(id))
|
|
29
47
|
return tree.errors.get(id) as ErrorBoundaryState;
|
package/update.ts
CHANGED
|
@@ -22,37 +22,32 @@ export type Update = {
|
|
|
22
22
|
*/
|
|
23
23
|
next: null | Element;
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
* concequence of this update.
|
|
28
|
-
*/
|
|
29
|
-
targets: CommitRef[];
|
|
30
|
-
|
|
31
|
-
suspend: boolean,
|
|
25
|
+
// TODO: maybe expose prev/next index information?
|
|
26
|
+
moved: boolean,
|
|
32
27
|
};
|
|
33
28
|
|
|
34
29
|
export const Update = {
|
|
35
30
|
fresh: (ref: CommitRef, next: Element): Update => ({
|
|
36
|
-
ref, next, prev: null,
|
|
31
|
+
ref, next, prev: null, moved: false,
|
|
37
32
|
}),
|
|
38
|
-
existing: (prev: Commit, next: Element): Update => ({
|
|
39
|
-
ref: prev, next, prev,
|
|
33
|
+
existing: (prev: Commit, next: Element, moved: boolean = false): Update => ({
|
|
34
|
+
ref: prev, next, prev, moved,
|
|
40
35
|
}),
|
|
41
36
|
remove: (prev: Commit): Update => ({
|
|
42
|
-
ref: prev, next: null, prev,
|
|
37
|
+
ref: prev, next: null, prev, moved: false,
|
|
43
38
|
}),
|
|
44
|
-
distant: (root: Commit
|
|
45
|
-
ref: root, next: root.element, prev: root,
|
|
39
|
+
distant: (root: Commit): Update => ({
|
|
40
|
+
ref: root, next: root.element, prev: root, moved: false,
|
|
46
41
|
}),
|
|
47
|
-
skip: (prev: Commit
|
|
48
|
-
ref: prev, next: prev.element, prev,
|
|
42
|
+
skip: (prev: Commit): Update => ({
|
|
43
|
+
ref: prev, next: prev.element, prev, moved: false,
|
|
49
44
|
}),
|
|
50
45
|
target: (prev: Commit): Update => ({
|
|
51
|
-
ref: prev, next: prev.element, prev,
|
|
46
|
+
ref: prev, next: prev.element, prev, moved: false,
|
|
52
47
|
}),
|
|
53
48
|
suspend: (prev: Commit): Update => ({
|
|
54
|
-
ref: prev, next: prev.element, prev,
|
|
55
|
-
})
|
|
49
|
+
ref: prev, next: prev.element, prev, moved: false,
|
|
50
|
+
}),
|
|
56
51
|
}
|
|
57
52
|
|
|
58
53
|
/**
|
|
@@ -89,6 +84,18 @@ export const calculateFastUpdate = (
|
|
|
89
84
|
const simpleElementEqualityTest: ChangeEqualityTest<Commit, Element> = (prev, next, prev_index, next_index) =>
|
|
90
85
|
prev.element.type === next.type && prev_index === next_index;
|
|
91
86
|
|
|
87
|
+
const keyedElementEqualityTest: ChangeEqualityTest<Commit, Element> = (prev, next, prev_index, next_index) => {
|
|
88
|
+
const compatible = prev.element.type === next.type;
|
|
89
|
+
if (!compatible)
|
|
90
|
+
return false;
|
|
91
|
+
const prevKey = prev.element.props.key;
|
|
92
|
+
const nextKey = next.props.key;
|
|
93
|
+
if (prevKey || nextKey)
|
|
94
|
+
return prevKey === nextKey;
|
|
95
|
+
|
|
96
|
+
return prev_index === next_index;
|
|
97
|
+
}
|
|
98
|
+
|
|
92
99
|
/**
|
|
93
100
|
* Returns a list of all updates that should
|
|
94
101
|
* occur -- given a set of commits and a
|
|
@@ -110,7 +117,7 @@ export const calculateUpdates = (
|
|
|
110
117
|
if (commits.length <= 1 && elements.length == 1)
|
|
111
118
|
return calculateFastUpdate(parentRef, commits[0], elements[0])
|
|
112
119
|
|
|
113
|
-
const change_report = calculateChangedElements(commits, elements,
|
|
120
|
+
const change_report = calculateChangedElements(commits, elements, keyedElementEqualityTest);
|
|
114
121
|
|
|
115
122
|
const newOrPersisted = elements.map((next, index) => {
|
|
116
123
|
const prevIndex = change_report.nextToPrev[index];
|
|
@@ -118,8 +125,10 @@ export const calculateUpdates = (
|
|
|
118
125
|
|
|
119
126
|
if (!prev)
|
|
120
127
|
return Update.fresh(CommitRef.new(parentRef.path), next);
|
|
121
|
-
|
|
122
|
-
|
|
128
|
+
|
|
129
|
+
const moved = index === prevIndex;
|
|
130
|
+
|
|
131
|
+
return Update.existing(prev, next, moved);
|
|
123
132
|
});
|
|
124
133
|
const removed = change_report.removed.map((index) => {
|
|
125
134
|
const prev = commits[index];
|
package/work.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type WorkRequestFunc<ID> = (callback: () => void) => ID;
|
|
2
|
+
export type WorkCancelFunc<ID> = (id: ID) => void;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A work manger is a service
|
|
6
|
+
*/
|
|
7
|
+
export type WorkManager<ID> = {
|
|
8
|
+
request: WorkRequestFunc<ID>,
|
|
9
|
+
cancel: WorkCancelFunc<ID>,
|
|
10
|
+
}
|