Last active
January 18, 2019 16:22
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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? | |
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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() | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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() | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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!! | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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