Skip to content

Instantly share code, notes, and snippets.

@cowboyd
Last active January 18, 2019 16:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cowboyd/26b3ae4785f0b2ebdb3d0a95175f187c to your computer and use it in GitHub Desktop.
Save cowboyd/26b3ae4785f0b2ebdb3d0a95175f187c to your computer and use it in GitHub Desktop.
Show diffferent ways to model state changes in a store with persistent identities
import { valueOf } from 'microstates';
/**
* When modelling side-effects, you generally listen to the output of a
* `Store`, and then conditionally trigger state transitions based on
* some sequence of events. The references that the store generates
* are "smart" in the sense that they really represent a "path" into
* the central datastructure of the store. In that way, the same
* reference, can be used again and again and never become stale,
*
* For example:
* ```
* let bool = Store(create(Boolean, false));
* bool.toggle();
* bool.toggle();
* bool.toggle();
* ```
* Will actually work, because every transition knows to operate not
* on a value contained in the `bool` object, but rather on the value
* at the same path within the store.
*
* This is great, because whether `true` or `false`, the reference is
* of type `BooleanType` and so the toggle method will be present
* since it lives on the prototype.
*
* However, this doesn't work so great when you're using a Union type
* to represent a state machine. The reason is that the union type is
* always one of N discreet types. For example, let's take an `Either`'
* class and put it into a store instead of a Boolean.
* `Either` type:
*
* ```
* function Either(A, B) {
* return Union({
* Left: Either => class extends Either {
* value = A; // value of Left is of type A
* },
* Right: Either => class extends Either {
* value = B; // valuoe of Right is of type B
* }
* });
* }
*
* const { Left, Right } = Either(String, Number);
* let either = Store(Left.create('five'));
* either instanceof Left //=> true
* either.toRight(5) //=> internally the value is {type: 'Right', value: 5}
* // but the problem is that `either` is still of type `Left`
* either.value.increment() //> TypeError: "increment" is not a function.
* ```
*
* If we were to listen to the value that is emitted from the store,
* it would be of the proper type (Right). The problem is that we
* don't have the luxury of listening directly to the callback to get
* the freshest copy especially when we're drilled into the tree and
* we're handing an object over to some controll code. Kinda like we
* do in this upload controller.
*
*/
const URL = 'https://api.frontside.io/v1/dev/null';
export default function UploaderController(uploader) {
for (let upload of uploader.uploads) {
if (upload.isNew) {
makeRequest(upload);
}
}
}
function makeRequest(upload) {
let xhr = new XMLHttpRequest();
xhr.open('POST', URL);
xhr.onload = () => upload.finish(xhr) //=> `upload` ref is `New` instance, but `finish()` is on the `Started` state!!!
xhr.upload.onprogress = () => upload.progress(e); //`upload` ref is `New` instance, but `progress()` is on the `Started` state!!!
xhr.onabort = () => upload.abort(xhr);
xhr.send(upload.file);
upload.start()
}
/**
* What do we do? How do we keep the references fresh?
*/
/**
* We could have the `Id` proxy methods always return the same object,
* only refreshed from their new value. So whenever you want to use
* the result of a transition in a later callback, you save the return
* value.
*
* PROS:
* - similar to basic microstates way of always capturing return values.
* - simple-ish.
* CONS:
* - similary to basic microstates might be confusing because it always returns
* the same spot in the tree, not the root like normal microstates do.
* - could lead to problems if you're basically overriding a shared `let` variable
* that's getting written over all the time, could lead to race conditions.
*/
function makeRequest(upload) {
let xhr = new XMLHttpRequest();
xhr.open('POST', URL);
xhr.onload = () => upload = upload.finish(xhr)
xhr.upload.onprogress = () => upload = upload.progress(e); //`upload` ref is `New` instance, but `progress()` is on the `Started` state!!!
xhr.onabort = () => upload = upload.abort(xhr);
xhr.send(upload.file);
upload = upload.start()
}
/**
* We could have the `Id` proxy be super magic and base on ES Proxies and actually magically
* change its prototype based on what the runtime type is. This basically makes the original
* code "just work"
*
* PROS:
* - Simple to comprehend. Every reference in the tree is up-to-date always
* - Works for all references. In otherwords, if one reference affects another, both
* references will continue to work.
* CONS:
* - very magical. Proxies can be fiddly and hard to figure out.
* - super mutable feeling, probably want to enforce that there is no return value in this case.
*/
function makeRequest(upload) {
let xhr = new XMLHttpRequest();
xhr.open('POST', URL);
xhr.onload = () => upload.finish(xhr) //=> `upload` ref is `New` instance, but `finish()` is on the `Started` state!!!
xhr.upload.onprogress = () => upload.progress(e); //`upload` ref is `New` instance, but `progress()` is on the `Started` state!!!
xhr.onabort = () => upload.abort(xhr);
xhr.send(upload.file);
upload.start()
}
/**
* Have some sort of "reference" constructor that you can use to wrap an Id
* that will let you run code agains the "latest" version of it. Every time you
* need to use the id when time could have passed.
*
* PROS:
* - Simple to comprehend. Every reference in the tree is up-to-date always
* - Works for all references. In otherwords, if one reference affects another, both
* references will continue to work.
* CONS:
* - very magical. Proxies can be fiddly and hard to figure out.
* - super mutable feeling, probably want to enforce that there is no return value in this case.
*/
function makeRequest(upload) {
let xhr = new XMLHttpRequest();
xhr.open('POST', URL);
xhr.onload = () => upload.finish(xhr) //=> `upload` ref is now magically `Finished`
xhr.upload.onprogress = () => upload.progress(e);
xhr.onabort = () => upload.abort(xhr);
xhr.send(upload.file);
upload.start() //=> `upload` ref is now magically `Started` a totally new class!!
}
/**
* Use an async generator function on the store to explicitly yield changes
* back to the store.
* https://github.com/tc39/proposal-async-iteration
*
* PROS:
* - Requires less magic in the Store implementation
* - Makes very clear, where each transition point happens.
* CONS:
* - Uses new-ish syntax that requires transpilation for Safari.
* - potentially weird learning curve.
* - I don't really know how to make it work, but have a hunch that it could somehow?.
*/
function makeRequest(upload, store) {
store.effects(async function*() {
//???
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment