Coder Social home page Coder Social logo

s's People

Contributors

adamhaile avatar jeffrmoore avatar shimaore avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

s's Issues

Better documentation (esp. regarding dependency management)

As of now, I do so believe that S is probably the most well thought through stream/reactivity library of all. Still, I find the documentation/readme somewhat lacking. I feel that because the way S works is so unique, it needs some more detailed explanation to let people understand how exactly S works, to see that S is not making horses*** claims. One of the points I feel might really need improvement is the part about:

Automatic Dependencies - No manual (un)subscription to change events. Dependencies in S are automatic and exact.

This really does sound too good/magical to be true, especially in this kind of library, if no further explanation is provided. I feel the documentation about:

When an S computation runs, S records what signals it references, thereby creating a live dependency graph of running code. When data changes, S uses that graph to figure out what needs to be updated and in what order.

is insufficient to convince a casual reader that this actually works, its not a crackpot claim. Another part of the reason why I'm asking here is that even I myself am somewhat confused, and want a confirmation from you that my understanding is correct:

  • Certainly, no Javascript program is capable of dissecting other functions dynamically to determine their dependencies from their symbols/source/reflection/etc.
  • Even when a S computation runs, not every expression in the computation is necessarily evaluated, so not all dependencies can be captured
  • However! given that the function in question is sufficiently pure, then all dependencies in the function will either be evaluated on the first run, or be hidden behind a fork (if/while etc.)
  • If the condition of the fork is also a S node, then any computations hidden behind the fork would be reevaluated whenever the fork condition is also updated (ie whenever the code in the fork might possibly be evaluated)
  • Thus, automatic dependency detection actually works (lazily)!!

If I am correct, I think it is important for the reader to understand the (reasonable) assumptions S has put on the code given to S computations, and how it actually works. If there was anything I could help with, I'd be glad to as well!

how to implement the rxjs .scan operator with S.js?

Hi @adamhaile

I am quite excited to investigate S as it has a clock system as do synchronous data flow languages (thinking about Lucid Synchrone for instance). I could not find an obvious way to replicate the behavior of the scan operator of Rxjs. The semantics can be found in the official documentation with marble diagrams. In short, a stream emits a value, a reducing function takes that value, and uses its accumulator to compute another value that is both emitted and put back in the accumulator. The resulting stream is thus a stream of accumulator values.

The issue here is that you need access to values of the accumulator at time n-1 (+ the triggering stream) to produce the values at time n. I tried using reduce but I got into running away clock messages.

To give you some background, I am trying to implement a chess game:

shortest chess game

The chess application behaviour follows stream equations (pseudo code follows), and have an implementation in Rxjs using the scan operator):

const behaviours$ = boardClicks.pipe(
  scan((acc, clickedSquare) => {
    const {
      gameOver,
      playerTurn,
      selectedPiece,
      whitePiecesPos: wPP,
      blackPiecesPos: bPP,
      squareStyles,
      position,
      whiteMove,
      blackMove,
      chessEngine
    } = acc;

    let nextGameOver, nextPlayerTurn, nextSelectedPiece, nextWhitePiecesPos;
    let nextBlackPiecesPos, nextSquareStyles, nextPosition;
    let nextWhiteMove, nextBlackMove;

    nextWhiteMove =
      !gameOver &&
      playerTurn === WHITE_TURN &&
      selectedPiece !== NO_PIECE_SELECTED &&
      wPP.indexOf(clickedSquare) === -1 &&
      isValidMove(selectedPiece, clickedSquare, chessEngine)
        ? { from: selectedPiece, to: clickedSquare }
        : NO_MOVE;

...

For information, the pseudo-code inspired from synchronous data-flow languages is as follows:

Move commands

---- Main types

-- one possible position on the check board
Square :: ["a1", "a2", ..., "a8", "b1", ..., "h8"]
-- type synonym for Square, with the added implication that there is a piece on that square
Square => PiecePos
- record describing a move with an origin square and target square
Move :: {from :: Square, to :: Square} | 

---- Application logic

-- a move is either a white piece move or a black piece move
moves = whiteMoves || blackMoves

-- Whites move iff: 
-- the game is not over, 
-- it is Whites turn to play, 
-- there is already a selected piece (origin square), 
-- user clicks on a square which does not contain a white piece (target square), 
-- the corresponding move (origin square to target square) is valid
whiteMoves =  `then` map boardClick(square)
   case !gameOver & 
        playerTurn == White & 
        selectedPiece & 
        !hasWhitePiece(square) & 
        isValidMove(selectedPiece, square): {from: selectedPiece, to: square}
   case _ : 
-- Black moves is the symmetric version of White moves
blackMoves =  `then` map boardClick(square)
  case ...

-- a piece is selected if the game is on, the click does not trigger a valid move and
-- + when Whites play, and a white piece is clicked on
-- + or when Blacks play, and a black piece is clicked on
-- 
-- a piece is deselected if the game is on, and the click triggers a valid move
selectedPiece :: Maybe PiecePos
selectedPiece =  `then` map boardClick(square)
  case gameOver: 
  case moves: 
  case playerTurn == White & square in whitePiecesPos: square
  case playerTurn == Black & square in blackPiecesPos: square
  case _: rec last selectedPiece 
  
-- the game is over if a valid move ends the game. 
gameOver = false `then` map moves isWinningMove

-- Whites initially have the turn, then that alternates with Blacks after every move
playerTurn = White `then`
  case whiteMoves: Black
  case blackMoves: White
  case _: rec last playerTurn

-- Board state
-- the position of white pieces is set initially per the chess game rules
-- then it changes with every move performed by the players
-- achtung! white pieces may be affected by a black piece move and vice versa
whitePiecesPos :: Array of PiecePos
whitePiecesPos = whitesStartPos `then`
  -- remove old white piece position and add new one
  case whiteMoves({from, to}): 
    rec last whitePiecesPos |> filter not(from) |> concat [to]
  -- remove old black piece position if any - case when a white piece gobbles a black one
  case blackMoves({from, to}):
    rec last whitePiecesPos |> filter not(from)

-- blackPiecesPos is deduced from whitePiecesPos by symmetry
blackPiecesPos :: Array of PiecePos
  ...

Render command

-- The render uses the ChessBoard React component. 
-- We specify some look and feel options for the component.
-- We essentially render a board position (`position`) 
-- and possibly one highlighted piece (`squareStyle`).
renderProps = {
  draggable: false,
  width: 320,
  boardStyle: {
    borderRadius: "5px",
    boxShadow: `0 5px 15px rgba(0, 0, 0, 0.5)`
  },
  onSquareClick: eventSourceFactory(boardClicks),
  -- only the following two properties vary
  squareStyles,
  position
}

-- the position *prop* is [as defined by the ChessBoard React component](https://chessboardjsx.com/props) we are using
position = "start" `then` map moves getPositionAfterMove

-- the squareStyle prop allows us to manage the style for a highlighted piece
squareStyles = selectedPiece(square)
  ? { [square]: { backgroundColor: "rgba(255, 255, 0, 0.4)" }}
  : {}

Karma / Jest testing

Hi, I'm having a hard time setting Karma or Jest to work with S.js.

Do you have any example projects to provide? It'd be awesome if they get included into this repository for future reference.

Todo sample, keeping count

S.js looks very promising. I've been playing around with the todo sample, and while it's easy to add a reactive computation that counts the number of completed tasks by calling reduce on the todos SArray, trying to incrementalize by avoiding iterating over all todo entries it yields surprising behaviour.

For instance, defining the completed count as todos.reduce((acc,e) => e.done() ? 1 : acc) works, but it's not incremental because in general there's no inverse for any function you may pass in. So instead I can make the completed count a variable that's updated upon completing a task:

completed = S.data(0),
...
toggleTask = (task) => {
    console.log(task.done());
    var delta = task.done() ? 1 : -1;
    completed(delta + completed());
},
...
        {todos.map(todo =>
            <div>
                <input type="checkbox" fn={data(todo.done)} onChange={() => toggleTask(todo)} />
                <input type="text" fn={data(todo.title)}/>
                <a onClick={() => todos.remove(todo)}>&times;</a>
            </div>)}

But todo.done() returns the opposite value, presumably because it's not yet updated from the checkbox value. You can see it in action here: https://codepen.io/anon/pen/bLaVwP

Is there an obvious or canonical way to solve this that I'm missing?

Recommended way to deal with "Runaway clock detected"?

Here's a small recursive example:

const word = S.data('John')
const number = S.data(123)

S.root(() => {
    S(() => {
        if (word() == 'mary') {
            number(number() + 1)
        }
    })  

    S(() => {
        console.log(number())
    })
})


word('blah')
word('mary')
word('foo')

It causes the first computation to run infinitely (until the error) because it reads and sets the number in the same computation.

What's you advice for these sorts of situations in general?

In this case, maybe the hypothetical user meant:

const word = S.data('John')
const number = S.data(123)

S.root(() => {
    S(() => {
        console.log(number())
    })
})

S.on(word, () => {
    if (word() === 'mary') {
        number(number() + 1)
    }
})

word('blah')
word('mary') // logs the new number
word('foo')

Curious to know in what sorts of cases we might run into the infinite recursion, and how to deal with it.

Make methods treeshake-able

I noticed all static methods are put on the S function.

I think it would pretty easy to make these separate exports instead and your consumers would get the benefit of being able to tree-shake some of the lesser used functions.

Why prevent signals from setting different pending values in a single clock tick?

I'm not going to lie. Most times I hit this I just bypass it anyway by not storing an undefined value in the signal and then reading the value from elsewhere on a trigger. In fact, my Proxy implementation does this for a different reason and is likely subject to whatever negative that comes with setting pending values more than once. If values aren't seen until the next clock that processes the update, I want to understand the case, where the last value wins, fails us.

I'm coming from a KnockoutJS background so I'm more comfortable with this inconsistency perhaps, but I'm gathering this has to do with the core differences between S and say MobX. Since in MobX significant execution reordering occurs to ensure the order of execution is predictable. I'm gathering this has to do with S being simpler (and more performant) and in so doesn't reorder execution just ensures that things are consistent at each tick.

cleanup and update phases are intermingled

Hi Adam, I know the project isn't active, but I'm tinkering with it while implementing a variation on the same idea and I think I found a bug in S. I'll leave it here if only for documentation:

const {data, freeze, cleanup, root} = S

root(()=>{
  const s1 = data(1)
  const s2 = data(2)

  const a = S(()=> {
    return "a:"+s1()
  })
  const b = S(()=> {
    console.log("b running", a())
    cleanup(()=>console.log("b cleanup", a()))
  })
  s1(10)
})

Expected:

b running a:1
b cleanup a:1
b running a:10

Actual output:

b running a:1
b cleanup a:10
b running a:10

Edit: This used to mention freeze and nested computations for accidental reasons, I had failed to reduce the test case. It looks like the behavior is by design, but I find it surprising, given the emphasis on atomic instants.

Here's the test case live

Question: Is there a "pending" state for S.data computations?

Hi there, me again! This time with a question, the one in the subject... 😉

Anyway, if I have something like:

const pending = S.data();
S.root(() => {
  S.on(pending, () => {
    console.log("should not have ran...");
  });
});

I would like the computation to not happen until a define something for pending!
Something like https://mithril.js.org/stream.html#stream-states

Reason: I'm trying to migrate a small ui from mithril so I can test/compare it with Surplus...

Thanks!
Grieb.

Handling Nested Computations under Branching Logic

I hit an interesting problem that I'm gathering I'm just attacking wrong but I wanted to see what you think. This goes back to issues around trying to do conditionals with boolean statements that can run too many times without changing their results. Obviously breaking synchronicity is another option. But as you may remember I'd been creating Roots to handle disposal. I came across an issue there recently that I hadn't noticed before, I'm gathering due to manual disposal getting queued up (ie it's frozen like the rest).

Here is the simplest reproduction I have. You can understand why you wouldn't expect data to be re-evaluated again after the ternary evaluates the other way. Thoughts?

https://codesandbox.io/s/objective-tdd-dif57

The issue is when the condition turned from true to false and value is set to undefined it still evaluates the nested child and fails to find a value with length.

EDIT
I was wrong this even happens when you break synchronicity:

import S from "s-js";

S.root(() => {
  const trigger = S.data(false),
    data = S.data(null),
    cache = S.value(S.sample(trigger)),
    child = data => {
      S(() => console.log("nested", data().length));
      return "Hi";
    };
  S(() => cache(trigger()));
  const memo = S(() => (cache() ? child(data) : undefined));
  S(() => console.log("view", memo()));
  S.freeze(() => {
    trigger(true);
    data("name");
  });
  S.freeze(() => {
    trigger(false);
    data(undefined);
  });
});

This produces the same error.

EDIT 2
https://codesandbox.io/s/vigorous-cherry-vo79c shows an even simpler re-production using only a single condition and no freezing.

import S from "s-js";

S.root(() => {
  const data = S.data(null),
    cache = S.value(S.sample(() => !!data())),
    child = data => {
      S(() => console.log("nested", data().length));
      return "Hi";
    };
  S(() => cache(!!data()));
  const memo = S(() => (cache() ? child(data) : undefined));
  S(() => console.log("view", memo()));
  console.log("ON");
  data("name");
  console.log("OFF");
  data(undefined);
});

Is this just by design and we should expect these states to exist at the same time?

Maps

Hi, absolutely love the speed, robustness, and elegance of S.js.

I am building something with S.js that requires me to keep a large data structure that looks something like a Map<string, DataSignal<any>>. I would like the insertions and deletions to the Map to be reactive because some of the computations in the map rebuild their computed values based on other values in the map being available or not available .

I don't need Mobx in this application, or else I would use it's reactive Map, because it's a very low level app, where the performance of S.js is desired. Any idea for a simple way to achieve observing some kind of keyed data structure with S.js? It doesn't have to a true ES6 Map, just some way to achieve the desired effect.

Playing with transparent Proxy wrapper for s-js...

I wanted to ask two questions, coming from a little mobx experience and having attempted to write my own observable state lib I was wondering if making a Proxy wrapper had been considered so that use of the s-js lib begins to look like POJO?

const S = require("s-js");
const SArray = require("s-array");

function isData(fn) {
  return fn.toString().startsWith("function data(value)");
}

function isComputation(fn) {
  return fn.toString().startsWith("function computation()");
}

// maybe this should be called s-pojo or something
function Store(state, actions) {
  const store = {};
  const proxy = new Proxy(store, {
    get: function(target, name) {
      if (name in target) {
        if (
          typeof target[name] === "function" &&
          (isData(target[name]) || isComputation(target[name]))
        ) {
          return target[name]();
        } else {
          return target[name];
        }
      } else {
        if (name in actions) {
          return actions[name];
        } else {
          return undefined;
        }
      }
    },
    set: function(target, name, value) {
      if (name in target) {
        if (typeof target[name] === "function") {
          if (isData(target[name])) {
            target[name](value);
          } else if (isComputation(target[name])) {
            return false;
          }
        } else {
          target[name] = value;
        }
        return true;
      } else {
        if (Array.isArray(value)) {
          target[name] = SArray(value);
        } else if (typeof value === "function") {
          let fn = value.bind(proxy);
          target[name] = S(fn);
        } else if (typeof value === "object") {
          // write logic here to recursively create new proxy for this obect?
          return false;
        } else {
          target[name] = S.data(value);
        }
        return true;
      }
    }
  });

  // attach data and computations to proxy...
  Object.keys(state).forEach(key => {
    proxy[key] = state[key];
  });

  return proxy;
}

// I prefer some kind of wrapper so that I can just pass POJO & functions...
const store = Store(
  {
    counter: 0,
    first: "Andy",
    last: "Johnson",
    /*nested: {
      foo: "BAR"
    },*/
    fullName: function() {
      return `${this.first} ${this.last}`;
    },
    fullCount: function() {
      return `${this.fullName}: ${this.counter}`;
    }
  },
  {
    updateData(firstName, lastName, count) {
      S.freeze(() => {
        this.counter = count;
        this.first = firstName;
        this.last = lastName;
      });
    }
  }
);

//  basically mobx autorun
S.root(() => {
  S(() => {
    console.log(store.fullName);
  });
  S(() => {
    console.log(store.fullCount);
  });
});

store.updateData("Jon", "Doe", 10);

I love the library I just dislike the set/get via function calls personally.

Is there anyway we could make the warning: "computations created without a root or parent will never be disposed" optional? I understand what it's complaining about since my computations are outside of the S.root thunk and thus won't be disposed, but I couldn't really find a better way to structure this to emulate what essentially is mobx lol (other than handling the nested object case...).

First day messing with the library so maybe I have overlooked how to avoid that error message for this kind of use case or maybe I'm completely off in left field.

Are subclocks generally available?

Is anybody using subclocks in production? Are they considered ok and bug free enough to use? If so, can anybody explain a use case in which they are using them?

Root + Async = Headaches

Is there a good way to gracefully handle disposals where I have to build up values or data signals from within async/await code? E.g. loading an image asynchronously and then attaching some styles to the element afterward that create and use signals.

It would be nice to have something akin to Objective-C's non-ARC autopool that acted similar to S.root() but as a bit more manual - for example, something like:

const sroot = S.root();
try {
    await myCode();
} finally {
    sroot.release();
}

Conditionally triggering downstream computations

Hey Adam!

I've been using your projects for a while now and found myself in a similar situation as the one @ryansolid described about conditionally rendering HTML elements (#21).

I just wanted to let you know that I've created a fork of S 1, and when I hit Ryans issue I added a new function to S, S.track.

What it does is that it adds the same capacity to a ComputationNode to behave like S.value, e.g. downstream computations are only reevaluated once the value of their source changes. I still need to add more tests to ensure that it actually works, and it would be great to hear your thoughts about this functionality (I'm still pretty new to reactive programming so if you have some edge cases where you think you can break it that would be really helpful, I'm struggling a bit to write proper specs).

Here is a simple example (I've changed some syntax in my own version, but I use yours here to illustrate how it works):

S.root(() => {
  let count = S.data(6);
  let onCountChange = S.track(() => {
    return count() > 5;
  })
  S(() => {
    console.log('Evaluating');
    if (onCountChange()) {
      S(() => {
        console.log('Tracked ' + count() + ' times.');
      });
    } else {
      console.log('Too low');
    }
  });
  count(7); // Tracked 7 times 
  count(4); // Evaluating. Too low.
});

Basically how it works is that it adds another state to ComputationNodes: PENDING, and then for tracking, instead of traversing through Log marking nodes STALE, we traverse nodes incrementing a pending counter, then after evaluating their sourced Stale node, either setting them as NotPending, or flagging them as Stale. This way, changes still propagate in correct order as every Computation is flagged for incoming changes, but not always evaluated.

I guess you could get this functionality wrapping computations in another DataSignal, but then they wouldn't respond until next tick.

I thought maybe this could be useful for Enumerable computations in S-array, as you could wrap computations in S.track, only triggering chained computations once the sequence changes.

1: I started out forking it to add type information for Closure Compiler, then found it so useful I've started extending it.

Push to array data signal

Beginner question, awesome library, thanks! If I have a DataSignal<T[]> and I want to cause it's dependent computations to trigger/change, I assume I have to do the following:

const arr = dataSignal()
arr.push(newValue)
dataSignal(arr)

Is there another or better way to do it?

Computed view not updating

I'm trying to marry s-js with lit-html, but I can't get the view to update. I've reduced the issue down to the smallest example I could.

This should output 123, but outputs 12.

import { html, render } from 'lit-html';
import S from 's-js';

const nums = S.data([1, 2]);
const view = S(() => html`<div>${nums()}</div>`);

const r = S.root(() => {
  render(view(), document.getElementById('app'));
});

nums(nums().push(3));

The render function is only getting called once. As far as I understand it, that function should also be called as a result of nums(nums().push(3)).

Can you enlighten me as to what I'm missing?

Edit: I also tried this with the same output result:

const nums = S.data([1, 2]);

const view = S.root(() => html`<div>${nums()}</div>`);

render(view, document.getElementById('app'));

nums(nums().push(3));

Can this nested computation behavior be explained?

Hey Adam.

const S = require('s-js');

const i = S.data(0);
const j = S.data(0);
const k = S.data(0);
const l = S.data(0);

function show() {
	console.error({i:i(), j:j(), k:k(), l:l()});
}

console.log('-- INIT --');
S.root(() => {
	S(() => {
		console.error('start i->j');
		j(i());
		console.error({i:S.sample(i), j:S.sample(j)});

		S(() => {
			console.error('start j->k');
			k(j());
			console.error({j:S.sample(j), k:S.sample(k)});

			S(() => {
				console.error('start k->l');
				l(k());
				console.error({k:S.sample(k), l:S.sample(l)});
				console.error('end k->l');
			});

			console.error('end j->k');
		});

		console.error('end i->j');
	});
});

console.log('-- i=10 --');
i(10);
show();
console.log('-- i=30 --');
i(30);
show();

Apologies for all of the debug output.

The above computations are just j = i, k = j, l = k, but nested within one another. I log when the function is entered and when it completes, and show the samples of each of the associated variables at each step.

However, the output really surprised me.

-- INIT --
start i->j
{ i: 0, j: 0 }
start j->k
{ j: 0, k: 0 }
start k->l
{ k: 0, l: 0 }
end k->l
end j->k
end i->j
start j->k
{ j: 0, k: 0 }
start k->l
{ k: 0, l: 0 }
end k->l
end j->k
start k->l
{ k: 0, l: 0 }
end k->l
-- i=10 --
start i->j
{ i: 10, j: 0 }
start j->k
{ j: 0, k: 0 }
start k->l
{ k: 0, l: 0 }
end k->l
end j->k
end i->j
start j->k
{ j: 10, k: 0 }
start k->l
{ k: 0, l: 0 }
end k->l
end j->k
start k->l
{ k: 10, l: 0 }
end k->l
{ i: 10, j: 10, k: 10, l: 10 }
-- i=30 --
start i->j
{ i: 30, j: 10 }
start j->k
{ j: 10, k: 10 }
start k->l
{ k: 10, l: 10 }
end k->l
end j->k
end i->j
start j->k
{ j: 30, k: 10 }
start k->l
{ k: 10, l: 10 }
end k->l
end j->k
start k->l
{ k: 30, l: 10 }
end k->l
{ i: 30, j: 30, k: 30, l: 30 }

It appears that the inner computations are run multiple times, even though they're declared once.

I see that this is required, as otherwise the order of scheduled writes vs outcome wouldn't make sense; for example, since the i(10) would immediately set i=10, then it makes sense that j(i()) would work fine, but on the next nested computation, j would still be the previous value (0) since it was only scheduled and not yet swapped. Therefore, without running the inner computations again, the result would be {i:10, j:10, k:0, l:0}.

This coincides with the test cases for the port I discussed with you:

│      ok: i == 0                                  ../test.cc:420
│      ok: j == 0                                  ../test.cc:421
│      ok: k == 0                                  ../test.cc:422
│      ok: l == 0                                  ../test.cc:423
│      ok: outer_calls == 1                        ../test.cc:424
│      ok: middle_calls == 1                       ../test.cc:425
│      ok: inner_calls == 1                        ../test.cc:426
│      ok: i == 10                                 ../test.cc:429
│      ok: j == 10                                 ../test.cc:430
│  not ok: k == 10                                 ../test.cc:431
│  not ok: l == 10                                 ../test.cc:432

However, what's the justification for running the inner computations again other than this? The re-run of the inner computation surprised me. Are cleanup functions run as well?

Small correction to localStorage example in documentation

Hi Adam! This is a small thing but it tripped me up for a while.

In the S readme file, the example code provided for integrating localStorage is as follows:

if (localStorage.todos) // load stored todos on start
    todos(JSON.parse(localStorage.todos).map(Todo));
S(() =>                 // store todos whenever they change
    localStorage.todos = JSON.stringify(todos()));

However, JSON.stringify(todos()) results in an array of empty objects.

The correct code taken from the functioning CodePen example is:

if (localStorage.todos) // load stored todos on start
    todos(JSON.parse(localStorage.todos).map(Todo));
S(() =>                 // store todos whenever they change
    localStorage.todos = JSON.stringify(todos().map(t => 
        ({ title: t.title(), done: t.done() }))
    ));

It would be super handy if the first syntax just worked automagically, but I don't know whether that's possible/feasible.

Dynamic Updating

So, does S.js automatically watches for changes on S objects inside DOM?
For example, I believe something like this should work:

var icon = 'fas fa-check'
var iconData = S.data(icon)

const loggedInLabel = S.root(() =>
  <label class={iconData()}/>
)
document.body.appendChild(loggedInLabel)
setTimeout(() => {icon = 'fas fa-cross'}, 1000)

But even if I assign the value of iconData again after changing the value of icon the DOM doesn't update. What am I doing wrong?

Creating computations when resolving Promises

When using the CKEditor5 API, creating a new editor returns a Promise which must be resolved to obtain the newly created editor object: ClassicEditor.create(textarea).then(editor => { ... }). I needed to bind a data signal to the editors change event, unfortunately by the time the promise is resolved, Owner information is no longer available so any computations created will never be disposed.

A simple solution would be to resolve the promise into a DataSignal and create a computation which depends thereby. Unfortunately, this means that the dependent computation will be run twice, and the DataSignal will remain in the graph until the parent is disposed.

I've implemented a solution S.resolve(promise, onfulfilled, onrejected) which behaves with the same semantics as Promise.then, but restores the Owner information when onfulfilled or onrejected is executed.

S.resolve = function <T, TResult1 = never, TResult2 = never>(promise: PromiseLike<T>, onfulfilled: ((value: T) => TResult1 | PromiseLike<TResult1>), onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): PromiseLike<TResult1 | TResult2> {
	const owner = Owner;

	const _onfulfilled = function (value: T): TResult1 | PromiseLike<TResult1> {
		Owner = owner;
		const result = onfulfilled(value);
		Owner = null;
		return result;
	}

	const _onrejected = onrejected ? function (reason: any): TResult2 | PromiseLike<TResult2> {
		Owner = owner;
		const result = onrejected(reason);
		Owner = null;
		return result;
	} : undefined;

	return promise.then(_onfulfilled, _onrejected);
}

This way any computations created when resolving the Promise will be owned by the correct ComputationNode.

I tried to implement a wrapper for Promises, so that async/await could be used while preserving Owner. But await has to resolve the Promise fully before continuing execution of the async function, so I'm fairly confident it's impossible to preserve the Owner inside async without exposing Owner as part of the API or through a proxy object.

How to initialize cyclical computations?

I'm looking to create a graph of computed data where there may exist dependencies. I had previously begun working on my own library, but came across this one and it seems to handle most of my requirements that other libraries don't, in particular the "no redundant computations" and its ability to handle cycles.

From my experimentation though, the only way to hook up a cycle is if the cycle loops by emitting onto a data signal. This has a few undesirable properties

  • A computation needs to know which data signals need to be updated
  • Cycles must involve data signals (i.e. can't have interdependent computations)
  • Replacing computations with data-signals loses the benefits of S doing dependency tracking (leading to redundant computations, inconsistent states, etc).

Boilerplate of what I'm trying to achieve: https://jsfiddle.net/uto837uf/

Example of a not-working cycle (by creating dependent computations): https://jsfiddle.net/uto837uf/3/

One solution that I can think of to this is to have a way to create a dummy node up front so that you have a reference to use for dependency tracking, and then "replace" the dummy node with the real thing later. I haven't found a way to do this from user-space though.

Computations creating signals

If a computation computes new signals, i.e. new state e.g. by computing a new S.data signal, are there any rules of which to be aware? I haven't been successful in exactly understanding the rules. In a lot of cases it works, but in some rare cases I have to create a new S.root and dispose that root manually in the cleanup method of the computation.

Most of the high level state that drives the rest of the computation of an app can be created in the main S.root but how to handle cases where you need to create new signals?

Compute value with prev != next check

Hi, how can I create a computed value (with prev != next checking similar to S.value)?

const computed = S(() => compute());

And I need to run all relative computations only if value that compute returns was changed, but code above run all relative computations even if compute returns the same value.

Possible solution:

let computed;

S(() => {
    if (!computed) {
        computed = S.value(compute());
    } else {
        computed(compute());
    }
});

It works as I expected, but it's too large and maybe S exists same functionality out of the box

Local state inside effect / hooks

I have a question. It's a bad idea for S to create something like a local state for effects (hooks?)?
For example

const $source = S.value(0);
const $target = S.value(0);

$.effect(() => {
  const $throttler = S.localValue(0); 
  const throttler = S.sample($throttler);
  const source = $source();

  $throttler(throttler + 1);
  
  if (throttler % 3 === 0) {
    $target(source);
  }
})

$source($source() + 1)
$source($source() + 1)
$source($source() + 1)

S.value multiple updates with no listeners fail

If you create an S.value and update it multiple times before attaching any listeners, the Value erroneously reports conflicting values.

var foo = S.value(100);
foo(200); // Succeeds
foo(300);
// Error: conflicting values: 300 is not the same as 200

I believe this happens, because on line 267 the DataNode does not cause to RootClock to tick if it has no listeners. So S.value's age is equal to the RootClock.time causing the fault.

A fix would be to preserve the age of the S.value when RunningClock == null and no listeners.

        if (arguments.length === 0) {
            return node.current();
        } 
        else if(RunningClock === null && node.log === null) {
            return current = node.next(update!);
        } else {
            var same = eq ? eq(current, update!) : current === update;
            if (!same) {
                var time = RootClock.time;
                if (age === time) 
                    throw new Error("conflicting values: " + update + " is not the same as " + current);
                age = time;
                current = update!;
                node.next(update!);
            }
            return update!;
        }

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.