Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Time travelling concept with reducey stores and state atoms inspired by https://gist.github.com/threepointone/43f16389fd96561a8b0b#comment-1447275
/**
* Stores are just seed + reduce function.
* Notice they are plain objects and don't own the state.
*/
const countUpStore = {
seed: {
counter: 0
},
reduce(state, action) {
switch (action.type) {
case 'increment':
return { ...state, counter: state.counter + 1 };
case 'decrement':
return { ...state, counter: state.counter - 1 };
default:
return state;
}
}
};
const countDownStore = {
seed: {
counter: 10
},
reduce(state, action) {
// Never mind that I'm doing the opposite of what action says: I'm just
// showing that stores may handle actions differently.
switch (action.type) {
case 'increment':
return { ...state, counter: state.counter - 1 };
case 'decrement':
return { ...state, counter: state.counter + 1 };
default:
return state;
}
}
};
/**
* Dispatcher receives an array of stores and manages a global state atom,
* giving each store a slice of that atom using store index as an ID.
*
* It seeds the atom with the initial values and returns a dispatch function
* that, when called with an action, will gather the new reduced state and
* update the cursor with it.
*/
function createDispatcher(cursor, stores) {
// Create the seed atom
const seedAtom = stores.map(s => s.seed);
cursor.set(seedAtom);
return function dispatch(action) {
// Create an atom with the next state of stores
const prevAtom = cursor.get();
const nextAtom = stores.map((store, id) =>
store.reduce(prevAtom[id], action)
);
cursor.set(nextAtom);
}
}
/**
* Creates a cursor that holds the value for the state atom.
*/
function createCursor() {
let atom = null;
return {
get: () => atom,
set: (nextAtom) => atom = nextAtom
};
}
/**
* A cursor middleware that lets consumer observe() mutations to individual stores.
*/
function makeObservable(cursor) {
const observers = [];
/**
* Observes a store by its ID.
* Returns a real observable!
*/
function observe(id) {
if (!observers[id]) {
observers[id] = [];
}
function subscribe(observer) {
// Immediately fire the current value (Zalgo!)
const atom = cursor.get();
observer.onNext(atom[id]);
// Subscribe
const storeObservers = observers[id];
storeObservers.push(observer);
function dispose() {
// Unsubscribe
const index = storeObservers.indexOf(observer);
if (index > -1) {
storeObservers.splice(index, 1);
}
}
return { dispose };
}
return { subscribe };
}
const wrapper = {
get() {
return cursor.get();
},
set(nextAtom) {
const prevAtom = cursor.get();
cursor.set(nextAtom);
// Walk through each store's slice
for (let id = 0; id < nextAtom.length; id++) {
if (!observers[id] || !observers[id].length) {
continue;
}
// Notify the observers if state is referentially unequal
if (!prevAtom || prevAtom[id] !== nextAtom[id]) {
observers[id].forEach(o =>
o.onNext(nextAtom[id])
);
}
}
}
};
return { observe, cursor: wrapper };
}
it('whatever', () => {
let cursor = createCursor();
let observe;
// Wrap cursor into the observation middleware:
({ cursor, observe } = makeObservable(cursor));
// Pass stores to dispatcher
const dispatch = createDispatcher(cursor, [countDownStore, countUpStore]);
// We can now subscribe to store's individual updates without
// any involvement from the stores themselves:
const subscription = observe(1/* index in store array */).subscribe({
onNext(countUpState) {
console.log('countup store state', countUpState);
}
});
// Dispatch actions:
dispatch({ type: 'increment' });
dispatch({ type: 'increment' });
dispatch({ type: 'increment' });
dispatch({ type: 'decrement' });
// Unsubscription:
subscription.dispose();
dispatch({ type: 'decrement' }); // Silent
// The *really* interesting part is left as an exercise to the reader:
//
//
// let cursor = createCursor();
// let observe, peekAtPast, lock, unlock;
// ({ cursor, peekAtPast } = makePeekable(cursor)); // NEW! records values
// ({ cursor, lock, unlock } = makeLockable(cursor)); // NEW! ignores current atom and forces a constant
// ({ cursor, observe } = makeObservable(cursor)); // observe at the end of the chain
//
//
// Some boring stuff:
//
//
// const dispatch = createDispatcher(cursor, [countDownStore, countUpStore]);
// const subscription = observe(1/* index in store array */).subscribe({
// onNext(countUpState) {
// console.log('countup store state', countUpState);
// }
// });
// dispatch({ type: 'increment' });
// dispatch({ type: 'increment' });
// dispatch({ type: 'increment' });
// dispatch({ type: 'increment' });
//
//
// ... now comes the interesting part.
//
//
// const pastAtom = peekAtPast(2); // NEW! reaches back in time
// lock(pastAtom); // NEW! forces change handlers to always receive pastAtom instead of current atom
// ...
// unlock(); // NEW! switches to emit the current atom again
//
//
// Do you see? Because makeObservable() is last in chain, it will receive
// the values from makeLockable(). We can make a time travel interface on top of it,
// and components will receive past values as you drag a slider, but stores have
// *zero* knowledge of it and need no special time travelling logic.
})
@threepointone

This comment has been minimized.

Copy link

threepointone commented May 6, 2015

man, this is looking great!

ok now to be boring and nitpicky -

  • uncomfortable about indexes for dispatches. it'll get fairly hard to track those (especially if you're using them while the app's running - it's hard to know what the index is at a given point, unless you're tracking the indexes as well).
  • having to register all stores at one go; doable, but breaks being able to register/unregister stores individually. you might care for that. won't be needed if instead dispatch returned something (possibly that index itself).
  • I'm still seeing value in doing a full replay, because while debugging, I'd not just like to see the app at that given state, but also what led to it (from any M->N), again, doable with the above, but it'll be bypassing the stores which is what I'd be want to be debugging.

cheers man, will try an implementation soon.

@goatslacker

This comment has been minimized.

Copy link

goatslacker commented May 6, 2015

+1 to the points above. I rewrote the core bits of Alt to contain store reducers:

https://gist.github.com/goatslacker/da0377e1413a526aa5ce

This gives us the ability to record dispatches and do full replays, and we have references to all the stores.

I really like your cursors approach though and how easy time traveling is with this approach.

@gaearon

This comment has been minimized.

Copy link
Owner Author

gaearon commented May 6, 2015

uncomfortable about indexes for dispatches

Yeah, in real code that would be store string keys, but I figured indexes work well for a proof of concept.

having to register all stores at one go;

Agreed, I'm just doing something quick and dirty here.
In real code createDispatcher should probably return { dispatch, register }.

I'm still seeing value in doing a full replay

Yes. Both are valuable. For debugging stores, though, I'd rather have hot reload that replays actions, not replay during time travel. I haven't thought about how to fit not reload with true replay of actions into this model, but it shouldn't be hard.

@RnbWd

This comment has been minimized.

Copy link

RnbWd commented May 6, 2015

I really like the idea of stores being seed + reduce functions that don't own state. In my previous attempt at doing something like this, I ran into the quirkiest behavior with native array methods (map, reduce, push, splice), weirdness happening behind the scenes. Immutable.js didn't have the same issues because map/reduce is implemented differently and everything is guaranteed unique, but if I could do this without immutable - I would. I'm genuinely curious about the tradeoffs.

@threepointone

This comment has been minimized.

Copy link

threepointone commented May 7, 2015

what quirky behavior? haven't heard of this before.

@rpominov

This comment has been minimized.

Copy link

rpominov commented May 19, 2015

Thanks a lot for putting this out! I work on a Flux lib, that is hugely inspired by your prototype https://github.com/pozadi/fluce

@gaearon

This comment has been minimized.

Copy link
Owner Author

gaearon commented Jun 3, 2015

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.