Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] redux extension integration #57

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 48 additions & 2 deletions src/utils/reactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
We explicitly update DOM only when it's needed and only if tags are changed.
*/
import { scheduleRevalidate } from '@/utils/runtime';
import { isFn, isTag, isTagLike, debugContext } from '@/utils/shared';
import { isFn, isTag, isTagLike, debugContext, ALIVE_CELLS } from '@/utils/shared';
import { supportChromeExtension } from './redux-devtools';

export const asyncOpcodes = new WeakSet<tagOp>();
// List of DOM operations for each tag
Expand Down Expand Up @@ -50,6 +51,39 @@ function keysFor(obj: object): Map<string | number | symbol, Cell<unknown>> {
return cellsMap.get(obj)!;
}

const result = supportChromeExtension({
get() {
const cells: Record<string, unknown> = {};
const allCells: Set<Cell> = new Set();
Array.from(ALIVE_CELLS).forEach((_cell) => {
const cell = _cell as MergedCell;
const nestedCells = Array.from(cell.relatedCells ?? []);
nestedCells.forEach((cell) => {
allCells.add(cell);
});
});
allCells.forEach((cell) => {
cells[cell._debugName!] = cell._value;
});
return cells;
},
skipDispatch: 0,
set() {
console.log('set', ...arguments);
},
on(timeLine: string, fn: () => any) {
console.log('on', timeLine, fn);
setTimeout(() => {
// debugger;
fn.call(this, 'updates', {})

}, 2000);
},
trigger() {
console.log('trigger', ...arguments);
}
});

export function tracked(
klass: any,
key: string,
Expand Down Expand Up @@ -104,6 +138,8 @@ export function setIsRendering(value: boolean) {
function tracker() {
return new Set<Cell>();
}

let COUNTER = 0;
// "data" cell, it's value can be updated, and it's used to create derived cells
export class Cell<T extends unknown = unknown> {
_value!: T;
Expand All @@ -116,9 +152,12 @@ export class Cell<T extends unknown = unknown> {
constructor(value: T, debugName?: string) {
this._value = value;
if (IS_DEV_MODE) {
this._debugName = debugContext(debugName);
this._debugName = `${debugContext(debugName)}:${COUNTER++}`;
DEBUG_CELLS.add(this);
}
result.dispatch({
type: 'CELL_CREATED',
});
}
get value() {
if (currentTracker !== null) {
Expand All @@ -133,6 +172,9 @@ export class Cell<T extends unknown = unknown> {
this._value = value;
tagsToRevalidate.add(this);
scheduleRevalidate();
result.dispatch({
type: 'CELL_UPDATED',
});
}
}

Expand Down Expand Up @@ -340,3 +382,7 @@ export function getTracker() {
export function setTracker(tracker: Set<Cell> | null) {
currentTracker = tracker;
}



console.log('result', result);
200 changes: 200 additions & 0 deletions src/utils/redux-devtools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// inspired by https://www.npmjs.com/package/freezer-redux-devtools?activeTab=code
var ActionTypes = {
INIT: '@@INIT',
PERFORM_ACTION: 'PERFORM_ACTION',
TOGGLE_ACTION: 'TOGGLE_ACTION'
};

type Listener = () => void;

/**
* Redux middleware to make freezer and devtools
* talk to each other.
* @param {Freezer} State Freezer's app state.
*/
export function FreezerMiddleware( State ){
return function( next ){
return function StoreEnhancer( someReducer, someState ){
var commitedState = State.get(),
lastAction = 0,
/**
* Freezer reducer will trigger events on any
* devtool action to synchronize freezer's and
* devtool's states.
*
* @param {Object} state Current devtool state.
* @param {Object} action Action being dispatched.
* @return {Object} Freezer state after the action.
*/
reducer = function( state, action ){
if( action.type == ActionTypes.INIT ){
State.set( state || commitedState );
}
else if( lastAction != ActionTypes.PERFORM_ACTION ) {
// Flag that we are dispatching to not
// to dispatch the same action twice
State.skipDispatch = 1;
State.trigger.apply( State, [ action.type ].concat( action.arguments || [] ) );
}
// The only valid state is freezer's one.
return State.get();
},
store = next( reducer ),
liftedStore = store.liftedStore,
dtStore = store.devToolsStore || store.liftedStore,

toolsDispatcher = dtStore.dispatch
;

// Override devTools store's dispatch, to set commitedState
// on Commit action.
dtStore.dispatch = function( action ){
lastAction = action.type;

// If we are using redux-devtools we need to reset the state
// to the last valid one manually
if( liftedStore && lastAction == ActionTypes.TOGGLE_ACTION ){
var states = dtStore.getState().computedStates,
nextValue = states[ action.id - 1].state
;

State.set( nextValue );
}

toolsDispatcher.apply( dtStore, arguments );

return action;
};

// Dispatch any freezer "fluxy" event to let the devTools
// know about the update.
State.on('afterAll', function( reactionName ){
if( reactionName == 'update')
return;

// We don't dispatch if the flag is true
if( this.skipDispatch )
this.skipDispatch = 0;
else {
var args = [].slice.call( arguments, 1 );
store.dispatch({ type: reactionName, args: args });
}
});

return store;
};
};
}

/**
* Binds freezer store to the chrome's redux-devtools extension.
* @param {Freezer} State Freezer's app state
*/
export function supportChromeExtension( State ){
var devtools = window.__REDUX_DEVTOOLS_EXTENSION__
? window.__REDUX_DEVTOOLS_EXTENSION__()
: (f) => f;

return compose(
FreezerMiddleware( State ),
devtools
)(createStore)( function( state ){
return state;
});
}


/**
* Creates a valid redux store. Copied directly from redux.
* https://github.com/rackt/redux
*/
function createStore(reducer: any, initialState: any) {


if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.');
}

var currentReducer = reducer;
var currentState = initialState;
var listeners: Listener[] = [];
var isDispatching = false;
var ActionTypes = {
INIT: '@@redux/INIT'
};

function getState() {
return currentState;
}

function subscribe(listener: Listener) {
listeners.push(listener);
var isSubscribed = true;

return function unsubscribe() {
if (!isSubscribed) {
return;
}

isSubscribed = false;
var index = listeners.indexOf(listener);
listeners.splice(index, 1);
};
}

function dispatch(action: { type: string | undefined }) {
if (typeof action.type === 'undefined') {
throw new Error('Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant?');
}

if (isDispatching) {
throw new Error('Reducers may not dispatch actions.');
}

try {
isDispatching = true;
currentState = currentReducer(currentState, action);
} finally {
isDispatching = false;
}

listeners.slice().forEach(function (listener) {
return listener();
});
return action;
}

function replaceReducer(nextReducer: any) {
currentReducer = nextReducer;
dispatch({ type: ActionTypes.INIT });
}

// When a store is created, an "INIT" action is dispatched so that every
// reducer returns their initial state. This effectively populates
// the initial state tree.
dispatch({ type: ActionTypes.INIT });

return {
dispatch: dispatch,
subscribe: subscribe,
getState: getState,
replaceReducer: replaceReducer
};
}

/**
* Composes single-argument functions from right to left.
* Copied directly from redux.
* https://github.com/rackt/redux
*/
function compose() {
for (var _len = arguments.length, funcs = Array(_len), _key = 0; _key < _len; _key++) {
funcs[_key] = arguments[_key];
}

return function (arg: any) {
return funcs.reduceRight(function (composed, f) {
return f(composed);
}, arg);
};
}
1 change: 1 addition & 0 deletions src/utils/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const $args = 'args' as const;
export const $_debug_args = '_debug_args' as const;
export const $fwProp = '$fw' as const;
export const noop = () => {};
export const ALIVE_CELLS = new Set<AnyCell>();


export const IN_SSR_ENV =
Expand Down
4 changes: 3 additions & 1 deletion src/utils/vm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
opsFor,
inNewTrackingFrame,
} from './reactive';
import { isFn } from './shared';
import { ALIVE_CELLS, isFn } from './shared';

type maybeDestructor = undefined | (() => void);
type maybePromise = undefined | Promise<void>;
Expand Down Expand Up @@ -95,7 +95,9 @@ export function opcodeFor(tag: AnyCell, op: tagOp) {
evaluateOpcode(tag, op);
const ops = opsFor(tag)!;
ops.push(op);
ALIVE_CELLS.add(tag);
return () => {
ALIVE_CELLS.delete(tag);
// console.info(`Removing Updating Opcode for ${tag._debugName}`, tag);
const index = ops.indexOf(op);
if (index > -1) {
Expand Down
Loading