Skip to content

Instantly share code, notes, and snippets.

@gaearon
Last active January 30, 2024 05:08
Show Gist options
  • Save gaearon/c02f3eb38724b64ab812 to your computer and use it in GitHub Desktop.
Save gaearon/c02f3eb38724b64ab812 to your computer and use it in GitHub Desktop.

Revisions

  1. gaearon revised this gist May 6, 2015. 1 changed file with 1 addition and 11 deletions.
    12 changes: 1 addition & 11 deletions reduce-store-time-travel.js
    Original file line number Diff line number Diff line change
    @@ -72,17 +72,7 @@ function createCursor() {
    set: (nextAtom) => atom = nextAtom
    };
    }

    /**
    * Wraps a cursor into an array of middleware.
    */
    function useMiddleware(cursor, middlewares) {
    return middlewares.reduce(
    (cursor, middleware) => middleware(cursor).wrapped,
    cursor
    );
    }


    /**
    * A cursor middleware that lets consumer observe() mutations to individual stores.
    */
  2. gaearon created this gist May 6, 2015.
    217 changes: 217 additions & 0 deletions reduce-store-time-travel.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,217 @@
    /**
    * 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
    };
    }

    /**
    * Wraps a cursor into an array of middleware.
    */
    function useMiddleware(cursor, middlewares) {
    return middlewares.reduce(
    (cursor, middleware) => middleware(cursor).wrapped,
    cursor
    );
    }

    /**
    * 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.
    })