@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 CHANGED
@@ -1,5 +1,16 @@
1
1
  # @lukekaalim/act-recon
2
2
 
3
+ ## 4.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - Improve debugger support
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies
12
+ - @lukekaalim/act@4.2.0
13
+
3
14
  ## 3.1.0
4
15
 
5
16
  ### Minor Changes
package/algorithms.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { MagicError } from "@lukekaalim/act";
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, EffectTask> = new Map();
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: EffectTask[]) {
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 | EffectTask[] = 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
- if (!state.hooks)
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 { last } from "./algorithms";
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
- ref: CommitRef2
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 (!state.effectTasks)
78
- state.effectTasks = [];
75
+ if (!output.effects)
76
+ output.effects = [];
79
77
 
80
- state.effectTasks.push({
78
+ output.effects.push({
81
79
  id: effectId,
82
- ref,
83
- func() {
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
@@ -10,5 +10,4 @@ export * from './state';
10
10
  export * from './thread';
11
11
  export * from './tree';
12
12
  export * from './update';
13
- export * from './pool';
14
13
  export * from './internal';
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@lukekaalim/act-recon",
3
3
  "type": "module",
4
- "version": "3.1.0",
4
+ "version": "4.0.0",
5
5
  "main": "mod.ts",
6
6
  "dependencies": {
7
- "@lukekaalim/act": "^4.1.0"
7
+ "@lukekaalim/act": "^4.2.0"
8
8
  }
9
9
  }
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
- pools = {
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 WorkThread2(this.tree);
42
+ this.thread = new WorkThread(this.tree);
46
43
 
47
- this.scheduler.setCallbackFunc(() => this.work());
48
- this.pools.commit.maxSize = 2048
49
- }
44
+ this.thread.bus = {
45
+ render: (delta) => this.bus.render(delta)
46
+ }
50
47
 
51
- startNewThread() {
52
- this.thread = new WorkThread2(this.tree);
48
+ this.scheduler.setCallbackFunc(() => this.work());
53
49
  }
54
50
 
55
- submitThread() {
56
- const currentThread = this.thread;
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
- for (const remove of currentThread.delta.removed.values())
70
- this.pools.commit.release(remove);
71
- }
54
+ if (this.thread.done)
55
+ this.thread.reset();
72
56
 
73
- work() {
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: () => void,
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 WorkReason =
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
- reasons: WorkReason[] = [];
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.pendingTasks.length === 0 && this.missed.size === 0;
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: WorkReason): QueueResult {
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.reasons.push(reason);
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 = this.tree.reconciler.pools.commit.acquire(ref, element, output.childRefs);
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.cleanups)
213
- this.delta.addEffects(output.cleanups);
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
- const identicalChange = next && prev && (next.id === prev.element.id);
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
- if (identicalChange) {
245
- const mustVisit = this.mustVisit.has(ref.id);
246
- if (!mustVisit)
247
- return;
280
+ if (identicalChange) {
281
+ const mustVisit = this.mustVisit.has(ref.id);
282
+ if (!mustVisit)
283
+ return;
248
284
 
249
- const mustRender = this.mustRender.has(ref.id);
285
+ const mustRender = this.mustRender.has(ref.id);
250
286
 
251
- if (!mustRender) {
252
- this.skipCommit(prev)
253
- return
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
- task.free();
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
- import { last } from "./algorithms.ts";
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.cleanups = [];
144
- for (const [index, cleanup] of componentState.cleanups) {
145
- if (!cleanup)
146
- continue;
147
- const id = componentState.effects.get(index) as EffectID;
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 { convertNodeToElements, createId, Element, Node, specialNodeTypes } from "@lukekaalim/act";
2
- import { ChangeEqualityTest, ChangeReport2 } from "./algorithms.ts";
3
- import { Commit2, CommitID, CommitRef2 } from "./commit.ts";
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 this.pool.acquire(ref, prev, next, moved)
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
- }