Skip to content

Commit 421f2c7

Browse files
committed
Working on timeJump & CreateComponent
1 parent 588316f commit 421f2c7

File tree

9 files changed

+298
-49
lines changed

9 files changed

+298
-49
lines changed
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// import typescript types
2+
import {
3+
// object with tree structure
4+
Fiber,
5+
} from '../../types/backendTypes';
6+
import {
7+
FunctionComponent,
8+
ClassComponent,
9+
IndeterminateComponent, // Before we know whether it is function or class
10+
HostRoot, // Root of a host tree. Could be nested inside another node.
11+
HostPortal, // A subtree. Could be an entry point to a different renderer.
12+
/**
13+
* Host Component: a type of component that represents a native DOM element in the browser environment, such as div, span, input, h1 etc.
14+
*/
15+
HostComponent, // has stateNode of html elements
16+
HostText,
17+
Fragment,
18+
Mode,
19+
ContextConsumer,
20+
ContextProvider,
21+
ForwardRef,
22+
Profiler,
23+
SuspenseComponent,
24+
MemoComponent,
25+
SimpleMemoComponent, // A higher order component where if the component renders the same result given the same props, react skips rendering the component and uses last rendered result. Has memoizedProps/memoizedState but no stateNode
26+
LazyComponent,
27+
IncompleteClassComponent,
28+
DehydratedFragment,
29+
SuspenseListComponent,
30+
FundamentalComponent,
31+
ScopeComponent,
32+
Block,
33+
OffscreenComponent,
34+
LegacyHiddenComponent,
35+
} from '../../types/backendTypes';
36+
// import function that creates a tree
37+
import Tree from '../../models/tree';
38+
// passes the data down to its components
39+
import componentActionsRecord from '../../models/masterState';
40+
import { getHooksStateAndUpdateMethod } from './statePropExtractors';
41+
42+
const nextJSDefaultComponent = new Set([
43+
'Root',
44+
'Head',
45+
'AppContainer',
46+
'Container',
47+
'ReactDevOverlay',
48+
'ErrorBoundary',
49+
'AppRouterContext',
50+
'SearchParamsContext',
51+
'PathnameContextProviderAdapter',
52+
'PathnameContext',
53+
'RouterContext',
54+
'HeadManagerContext',
55+
'ImageConfigContext',
56+
'RouteAnnouncer',
57+
'Portal',
58+
]);
59+
// ------------------------CREATE TREE TO SEND TO FRONT END-------------------
60+
/**
61+
* This is a recursive function that runs after every Fiber commit using the following logic:
62+
* 1. Traverse from FiberRootNode
63+
* 2. Create an instance of custom Tree class
64+
* 3. Build a new state snapshot
65+
* Every time a state change is made in the accompanying app, the extension creates a Tree “snapshot” of the current state, and adds it to the current “cache” of snapshots in the extension
66+
* @param currentFiberNode A Fiber object
67+
* @param tree A Tree object, default initialized to an instance given 'root' and 'root'
68+
* @param fromSibling A boolean, default initialized to false
69+
* @return An instance of a Tree object
70+
*/
71+
// TODO: Not sure why the ritd need to be outside of the createTree function. Want to put inside, but in case this need to be keep track for front end.
72+
export default function createComponentActionsRecord(
73+
currentFiberNode: Fiber,
74+
circularComponentTable: Set<Fiber> = new Set(),
75+
): Tree {
76+
// Base Case: if has visited, return
77+
if (circularComponentTable.has(currentFiberNode)) {
78+
return;
79+
} else {
80+
circularComponentTable.add(currentFiberNode);
81+
}
82+
const {
83+
sibling,
84+
stateNode,
85+
child,
86+
// with memoizedState we can grab the root type and construct an Abstract Syntax Tree from the hooks structure using Acorn in order to extract the hook getters and match them with their corresponding setters in an object
87+
memoizedState,
88+
elementType,
89+
tag,
90+
} = currentFiberNode;
91+
// Obtain component name:
92+
const componentName =
93+
elementType?._context?.displayName || //For ContextProvider
94+
elementType?._result?.name || //For lazy Component
95+
elementType?.render?.name ||
96+
elementType?.name ||
97+
'nameless';
98+
console.log('LinkFiber', {
99+
currentFiberNode,
100+
// tag,
101+
// elementType,
102+
componentName:
103+
elementType?._context?.displayName || //For ContextProvider
104+
elementType?._result?.name || //For lazy Component
105+
elementType?.render?.name ||
106+
elementType?.name ||
107+
elementType,
108+
// memoizedState,
109+
// stateNode,
110+
// _debugHookTypes,
111+
});
112+
113+
// ---------OBTAIN STATE & SET STATE METHODS FROM CLASS COMPONENT-----------
114+
// Check if node is a stateful class component when user use setState.
115+
// If user use setState to define/manage state, the state object will be stored in stateNode.state => grab the state object stored in the stateNode.state
116+
// Example: for tic-tac-toe demo-app: Board is a stateful component that use setState to store state data.
117+
if (
118+
!nextJSDefaultComponent.has(componentName) &&
119+
stateNode?.state &&
120+
(tag === ClassComponent || tag === IndeterminateComponent)
121+
) {
122+
// Save component's state and setState() function to our record for future
123+
// time-travel state changing. Add record index to snapshot so we can retrieve.
124+
componentActionsRecord.saveNew(stateNode);
125+
}
126+
127+
// --------OBTAIN STATE & DISPATCH METHODS FROM FUNCTIONAL COMPONENT--------
128+
// Check if node is a hooks useState function
129+
if (
130+
!nextJSDefaultComponent.has(componentName) &&
131+
memoizedState &&
132+
(tag === FunctionComponent ||
133+
// tag === ClassComponent || WE SHOULD NOT BE ABLE TO USE HOOK IN CLASS
134+
tag === IndeterminateComponent ||
135+
tag === ContextProvider) //TODOD: Need to figure out why we need context provider
136+
) {
137+
if (memoizedState.queue) {
138+
try {
139+
// Hooks states are stored as a linked list using memoizedState.next,
140+
// so we must traverse through the list and get the states.
141+
// We then store them along with the corresponding memoizedState.queue,
142+
// which includes the dispatch() function we use to change their state.
143+
const hooksStates = getHooksStateAndUpdateMethod(memoizedState);
144+
hooksStates.forEach(({ component }) => {
145+
// Save component's state and dispatch() function to our record for future time-travel state changing. Add record index to snapshot so we can retrieve.
146+
componentActionsRecord.saveNew(component);
147+
});
148+
} catch (err) {
149+
console.log('Failed Element', { component: elementType?.name });
150+
}
151+
}
152+
}
153+
154+
// ---------------------TRAVERSE TO NEXT FIBERNODE--------------------------
155+
// If currentFiberNode has children, recurse on children
156+
if (child) createComponentActionsRecord(child, circularComponentTable);
157+
158+
// If currentFiberNode has siblings, recurse on siblings
159+
if (sibling) createComponentActionsRecord(sibling, circularComponentTable);
160+
}

src/backend/controllers/createTree/createTree.ts

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,23 @@ import {
4545
} from './statePropExtractors';
4646

4747
let rtidCounter = 0;
48-
const nextJSDefaultComponent = new Set(['ReactDevOverlay', 'Portal']);
48+
const nextJSDefaultComponent = new Set([
49+
'Root',
50+
'Head',
51+
'AppContainer',
52+
'Container',
53+
'ReactDevOverlay',
54+
'ErrorBoundary',
55+
'AppRouterContext',
56+
'SearchParamsContext',
57+
'PathnameContextProviderAdapter',
58+
'PathnameContext',
59+
'RouterContext',
60+
'HeadManagerContext',
61+
'ImageConfigContext',
62+
'RouteAnnouncer',
63+
'Portal',
64+
]);
4965
// -------------------------CREATE TREE TO SEND TO FRONT END--------------------
5066
/**
5167
* This is a recursive function that runs after every Fiber commit using the following logic:
@@ -92,6 +108,13 @@ export default function createTree(
92108
treeBaseDuration,
93109
_debugHookTypes,
94110
} = currentFiberNode;
111+
// Obtain component name:
112+
const componentName =
113+
elementType?._context?.displayName || //For ContextProvider
114+
elementType?._result?.name || //For lazy Component
115+
elementType?.render?.name ||
116+
elementType?.name ||
117+
'nameless';
95118
// console.log('LinkFiber', {
96119
// currentFiberNode,
97120
// // tag,
@@ -155,6 +178,22 @@ export default function createTree(
155178
context: {},
156179
};
157180
let isStatefulComponent = false;
181+
/**
182+
* The updated tree after adding the `componentData` obtained from `currentFiberNode`
183+
*/
184+
let newNode: Tree;
185+
186+
// console.log('LinkFiber', {
187+
// currentFiberNode,
188+
// // tag,
189+
// // elementType,
190+
// componentName,
191+
// // memoizedProps,
192+
// // memoizedState,
193+
// // stateNode,
194+
// // dependencies,
195+
// // _debugHookTypes,
196+
// });
158197

159198
// ----------------APPEND PROP DATA FROM REACT DEV TOOL-----------------------
160199
// check to see if the parent component has any state/props
@@ -182,6 +221,7 @@ export default function createTree(
182221
// memoizedState
183222
// Note: if user use ReactHook, memoizedState.memoizedState can be a falsy value such as null, false, ... => need to specify this data is not undefined
184223
if (
224+
!nextJSDefaultComponent.has(componentName) &&
185225
(tag === FunctionComponent || tag === ClassComponent) &&
186226
memoizedState?.memoizedState !== undefined
187227
) {
@@ -203,7 +243,7 @@ export default function createTree(
203243
// if user uses useContext hook, context data will be stored in memoizedProps.value of the Context.Provider component => grab context object stored in memoizedprops
204244
// Different from other provider, such as Routes, BrowswerRouter, ReactRedux, ..., Context.Provider does not have a displayName
205245
// TODO: need to render this context provider when user use useContext hook.
206-
if (tag === ContextProvider && !elementType._context.displayName) {
246+
if (!nextJSDefaultComponent.has(componentName) &&tag === ContextProvider && !elementType._context.displayName) {
207247
let stateData = memoizedProps.value;
208248
if (stateData === null || typeof stateData !== 'object') {
209249
stateData = { CONTEXT: stateData };
@@ -222,7 +262,7 @@ export default function createTree(
222262
// Check if node is a stateful class component when user use setState.
223263
// If user use setState to define/manage state, the state object will be stored in stateNode.state => grab the state object stored in the stateNode.state
224264
// Example: for tic-tac-toe demo-app: Board is a stateful component that use setState to store state data.
225-
if (stateNode?.state && (tag === ClassComponent || tag === IndeterminateComponent)) {
265+
if (!nextJSDefaultComponent.has(componentName) && stateNode?.state && (tag === ClassComponent || tag === IndeterminateComponent)) {
226266
// Save component's state and setState() function to our record for future
227267
// time-travel state changing. Add record index to snapshot so we can retrieve.
228268
componentData.index = componentActionsRecord.saveNew(stateNode);
@@ -235,7 +275,7 @@ export default function createTree(
235275

236276
// ---------OBTAIN STATE & DISPATCH METHODS FROM FUNCTIONAL COMPONENT---------
237277
// Check if node is a hooks useState function
238-
if (
278+
if (!nextJSDefaultComponent.has(componentName) &&
239279
memoizedState &&
240280
(tag === FunctionComponent ||
241281
// tag === ClassComponent || WE SHOULD NOT BE ABLE TO USE HOOK IN CLASS
@@ -276,22 +316,25 @@ export default function createTree(
276316
// This grabs stateless components
277317
if (
278318
!isStatefulComponent &&
279-
(tag === FunctionComponent || tag === ClassComponent || tag === IndeterminateComponent)
319+
(tag === FunctionComponent ||
320+
tag === ClassComponent ||
321+
tag === IndeterminateComponent ||
322+
tag === ContextProvider)
280323
) {
281324
newState = 'stateless';
282325
}
283326

284327
// ------------------ADD COMPONENT DATA TO THE OUTPUT TREE--------------------
285-
/**
286-
* The updated tree after adding the `componentData` obtained from `currentFiberNode`
287-
*/
288-
let newNode: Tree;
328+
289329
/**
290330
* `rtid` - The `Root ID` is a unique identifier that is assigned to each React root instance in a React application.
291331
*/
292332
let rtid: string | null = null;
293333
// We want to add this fiber node to the snapshot
294-
if (isStatefulComponent || newState === 'stateless') {
334+
if (
335+
(isStatefulComponent || newState === 'stateless') &&
336+
!nextJSDefaultComponent.has(componentName)
337+
) {
295338
// Grab JSX Component & replace the 'fromLinkFiber' class value
296339
if (currentFiberNode.child?.stateNode?.setAttribute) {
297340
rtid = `fromLinkFiber${rtidCounter}`;
@@ -310,14 +353,6 @@ export default function createTree(
310353
currentFiberNode.child.stateNode.classList.add(rtid);
311354
}
312355
rtidCounter += 1; // I THINK THIS SHOULD BE UP IN THE IF STATEMENT. Still unsure the use of rtid
313-
314-
// Obtain component name:
315-
const componentName =
316-
elementType?._context?.displayName || //For ContextProvider
317-
elementType?._result?.name || //For lazy Component
318-
elementType?.render?.name ||
319-
elementType?.name ||
320-
'nameless';
321356
// checking if tree fromSibling is true
322357
if (fromSibling) {
323358
newNode = tree.addSibling(newState, componentName, componentData, rtid);

src/backend/controllers/timeJump.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const circularComponentTable = new Set();
1515
* The target snapshot portrays some past state we want to travel to `jump` recursively and setState for any stateful component.
1616
*
1717
* @param mode - The current mode (i.e. jumping, time-traveling, or paused)
18-
* @returns A function that takes a `target` snapshot and a boolean flag checking for `firstCall`, then invokes `jump` on that target snapshot
18+
* @returns A function that takes a `inputTarget` snapshot and a boolean flag checking for `firstCall`, then invokes `initiateJump` on that target snapshot
1919
*
2020
*/
2121
export default function timeJump(mode: Status) {
@@ -28,43 +28,49 @@ export default function timeJump(mode: Status) {
2828
*/
2929
// IMPORTANT: DO NOT move this function into return function. This function is out here so that it will not be redefined any time the return function is invoked. This is importatnt for removeEventListener for popstate to work.
3030
const popStateHandler = () => {
31+
console.log('POP STATE');
3132
initiateJump(target, mode);
3233
};
3334

3435
/**
36+
* This function that takes a `inputTarget` snapshot and a boolean flag checking for `firstCall`, then invokes `initiateJump` update browser to match states provided by the `inputTarget`
3537
* @param inputTarget - The target snapshot to re-render. The payload from index.ts is assigned to inputTarget
3638
* @param firstCall - A boolean flag checking for `firstCall`
3739
*/
38-
return (inputTarget: Tree, firstCall = false): void => {
39-
// Setting mode disables setState from posting messages to window
40-
mode.jumping = true;
40+
return async (inputTarget: Tree, firstCall = false): Promise<void> => {
41+
// Reset mode.navigating
42+
delete mode.navigating;
4143
// Set target for popStateHandler usage:
4244
target = inputTarget;
4345
// Clearn the circularComponentTable
4446
if (firstCall) circularComponentTable.clear();
4547
// Determine if user is navigating to another route
4648
// NOTE: Inside routes.navigate, if user is navigating, we will invoke history.go, which will go back/forth based on # of delta steps. This will trigger a popstate event. Since history.go is an async method, the event listener is the only way to invoke timeJump after we have arrived at the desirable route.
47-
const navigating: boolean = routes.navigate(inputTarget.route);
48-
if (navigating) {
49-
// Remove 'popstate' listener to avoid duplicate listeners
50-
removeEventListener('popstate', popStateHandler);
51-
// To invoke initateJump after history.go is complete
52-
addEventListener('popstate', popStateHandler);
53-
} else {
54-
// Intiate the jump immideately if not navigating
55-
initiateJump(inputTarget, mode);
56-
}
49+
// const navigating: boolean = routes.navigate(inputTarget.route);
50+
// if (navigating) {
51+
// // Remove 'popstate' listener to avoid duplicate listeners
52+
// removeEventListener('popstate', popStateHandler);
53+
// // To invoke initateJump after history.go is complete
54+
// addEventListener('popstate', popStateHandler);
55+
// } else {
56+
// // Intiate the jump immideately if not navigating
57+
// initiateJump(inputTarget, mode);
58+
// }
59+
await initiateJump(inputTarget, mode);
5760
};
5861
}
5962

6063
/**
61-
* This function initiate the request for jump and will pause the jump when user moves mouse over the body of the document.
64+
* This function initiates the request for jump and will pause the jump when user moves mouse over the body of the document.
6265
* @param target - The target snapshot to re-render
6366
* @param mode - The current mode (i.e. jumping, time-traveling, or paused)
6467
*/
6568
async function initiateJump(target, mode): Promise<void> {
69+
console.log('JUMP', { jumping: mode.jumping });
70+
console.log('JUMP', { target, componentAction: componentActionsRecord.getAllComponents() });
6671
updateTreeState(target).then(() => {
6772
document.body.onmouseover = () => {
73+
console.log('STOP JUMPING');
6874
mode.jumping = false;
6975
};
7076
});

0 commit comments

Comments
 (0)