question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

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

See original GitHub issue

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.

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Comments:14 (2 by maintainers)

github_iconTop GitHub Comments

1reaction
adamhailecommented, Sep 18, 2017

Hi Andy – Sorry for the slow reply, it ended up being a busy weekend.

I played around with doing something like this but never got to the point I had something I thought was ready to share. There are obvious advantages to preserving a natural obj.foo get and obj.foo = val set behavior, especially for people new to S. For reference, these were the open questions in my mind:

  1. Design. At the time, MobX’s observablization was deep and infectious, and that made me a bit uneasy. I worried that the infectious behavior would lead to surprises like:
var todo = { title: "foo", complete: false };
store.todos[0] = todo;
store.todos[0] === todo; // FALSE!

As for the deep behavior, I also worked some years ago on an app that did deep observablization with knockout, and it led to increasing inertia as the app grew. Models that were just observablized JSON meant they were very weak (no methods, no computed fields, etc). Lots of functionality that should have been in the model got copied across controllers and view models. Midway in the app’s life we had to tear out the observablize utility and replace it with real constructors, each responsible for its own shallow observablization. So the utility I was exploring was something like MobX’s extendShallowObservable(this, { ... data ... }), but overall I wasn’t sure what the right way forward was. You’ve got much more experience with MobX than me, so I’d be curious to hear about how the MobX community thinks about these issues now.

  1. Performance. I was using defineProperty(), and at the time, I benched it as being about 3x slower than native S. This was a couple years ago, so the browsers may be better at optimizing defineProperty() now. I would expect a Proxy-based implementation to have a considerable performance loss, not just because the VMs might not optimize Proxy yet, but also because any time you use strings as identifiers, you lose most of the VMs’ optimizations around property access and fall back to hashtable access. If it’s useful, the current benchmarking script I use during S development is in the git repo. Pull and run npm run bench and you’ll see it.

  2. Less powerful. For my own code, I find it very powerful to have first-class signals. I can pass signals to functions, attach them to other objects, build higher-ordered signals, etc. Embedding the signals behind properties means you favor passing whole objects instead. One thing I’d explored in my POJO utility was an ability to access the backing signals.

Please don’t read this as saying what you’re doing is a bad idea – not at all! Like I say, I think POJO-like access could be really useful, just still playing with the right direction.

A couple more thoughts on the specific code:

  1. Have you looked at s-array? It proxies array mutators and does it in a way that preserves synchronous immutability. With your code, arrays are modified immediately:
const store = Store({ foos: [] });
S.freeze(() => {
    store.foos(); // returns []
    store.foos.push(1);
    store.foos(); // returns [1], signal mutated during freeze
});

const sa = SArray([]);
S.freeze(() => {
    sa(); // returns []
    sa.push(1);
    sa(); // still returns []
});
sa(); // *now* returns [1]
  1. Converting all functions to parentless computations means you lose S’s ability to manage their lifecycle for you and end up having to do manual resource management. If you miss calling object("dispose") somewhere in your code, you have both a memory and a performance leak. I would consider not wrapping passed-in functions with S(). Those properties will still be reactive – calling code will “see through” the getter to the referenced signals. They’ll re-evaluate every time they’re referenced, but that’s not necessarily a bad thing. In most cases, like here, computed properties are pretty short functions, and their runtime is often similar to the book-keeping cost of calling a computation. And if they do non-trivial work, I’d want greater visibility into that fact than an automatic conversion like this provides.

Best – Adam

0reactions
andyrjcommented, Sep 19, 2017

so I believe I have added snapshots with structural sharing and json-patch output to my little experiment…

https://github.com/andyrj/post-js/blob/snapshots/src/store.js#L325

gonna add tests for it so I keep my 100% coverage, but should be able to hook up redux-devtools extension to this soon…

Read more comments on GitHub >

github_iconTop Results From Across the Web

Configuring Web-Based Authentication - Cisco Content Hub
The web-based authentication feature, known as Web Authentication Proxy, enables you to authenticate end users on host systems that do not run the...
Read more >
ProxySG First Steps - Deploy a Transparent Proxy - YouTube
With a transparent proxy deployment, you can configure your ProxySG appliance to control and filter traffic for users, without having to do ...
Read more >
Fetch API - MDN Web Docs
The Fetch API provides an interface for fetching resources (including across the network). It will seem familiar to anyone who has used ...
Read more >
Compare revisions · nodejs/node-v0.x-archive Wiki · GitHub
... http-proxy-selective — Proxy server replace some remote static files with local ones ... barricane-db — a transparent object persistence mechanism ...
Read more >
Stevens-Johnson Syndrome and Toxic Epidermal Necrolysis ...
Granulysin is found in the lesions of patients with SJS/TEN and plays a significant pathogenic role in the condition, but the overall mechanisms...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found