Playing with transparent Proxy wrapper for s-js...
See original GitHub issueI 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:
- Created 6 years ago
- Comments:14 (2 by maintainers)
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 andobj.foo = val
set behavior, especially for people new to S. For reference, these were the open questions in my mind: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.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.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:
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
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…