Skip to content

Instantly share code, notes, and snippets.

@airhorns
Last active July 5, 2020 23:55
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 airhorns/ccb161a4d6c67eeb0f1bc6854f835b59 to your computer and use it in GitHub Desktop.
Save airhorns/ccb161a4d6c67eeb0f1bc6854f835b59 to your computer and use it in GitHub Desktop.
mobx-state-tree Promise model
import { cast, flow, IAnyType, Instance, SnapshotOrInstance, types } from "mobx-state-tree";
import { assert } from "../lib/utils";
let counter = 0;
const RejectReasontype = types.maybe(types.frozen<Error>());
/**
* `mobx-state-tree` promise model factory that is observable and plays nice with the atomic middleware. Useful for embedding in the tree to fetch data on demand and reporting it's availability. It's kind of like an Apollo graphql query, but purely just state, more general, and observable.
* __Note__: This is different than normal `Promise`s because this is a factory: it produces one class of promise (a mst model) where instances of the model get configured with arguments to then execute that specific instancce
*/
export const PromiseModel = <T extends IAnyType, Args extends any[]>(
resolvedType: T,
resolve: (...args: Args) => Promise<SnapshotOrInstance<T>>,
resolvedTypeName?: string
) => {
const typename = `PromiseModel<${resolvedTypeName || resolvedType.name} - ${counter++}>`;
const type = types
.model(typename, {
_resolution: types.maybe(resolvedType),
_rejectReason: RejectReasontype,
args: types.frozen<Args>(),
})
.volatile(() => ({
loading: false,
resolved: false,
rejected: false,
}))
.extend((self) => {
// this PromiseModel instance should execute exactly once with all the same semantics as a normal, real promise; so it's easiest to implement by wrapping a closed over, real promise.
let mutexPromise: null | Promise<any> = null;
return {
actions: {
// required because you can't set volatile data from .create calls, so special constructors use this instead
__privateSetVolatile(data: any) {
Object.assign(self, data);
},
// this has to be modelled as an action that completes in order for it to commit
// if it raises, it gets rolled back by the atomic middleware, so we need to not raise, but return something to a view that does raise.
_runResolve: flow(function* () {
try {
const result = yield resolve(...self.args);
self._resolution = cast(result);
self.resolved = true;
} catch (error) {
self._rejectReason = error;
self.rejected = true;
}
self.loading = false;
return;
}),
// There's an inner wrapper and outer shell for this function so that the mutex is around the whole logic for the resolution, which is important to make sure that the rest of the action above runs before anything can react to the outer promise resolving or rejecting. Those outer reactions require the inner side effects in the above action to have run.
runResolve: flow(function* () {
if (!mutexPromise) {
mutexPromise = (self as any)._runResolve();
}
yield assert(mutexPromise);
return;
}),
},
};
})
.views((self) => ({
async resolve() {
if (self.rejected) {
throw self._rejectReason;
} else if (self.resolved) {
return self._resolution as Instance<T>;
} else {
await self.runResolve();
if (self.rejected) {
throw self._rejectReason;
} else if (self.resolved) {
return self._resolution as Instance<T>;
} else {
throw "internal PromiseModel error: wasn't resolved or rejected after running resolve";
}
}
},
}))
.views((self) => ({
get current() {
return self._resolution;
},
currentOrSuspend(): Instance<T> {
if (self.rejected) {
throw self._rejectReason;
} else if (self.resolved) {
return self._resolution as Instance<T>;
} else {
throw new Promise((resolve, reject) => {
console.debug(`${typename} promise suspending`, (self as any).$treenode.nodeId, (self as any).toJSON());
requestAnimationFrame(() => {
self.resolve().then(resolve).catch(reject);
});
});
}
},
}));
return Object.assign(type, {
// useful to create an already fulfilled version of this promise that won't ever run the resolver
resolved(value: SnapshotOrInstance<T>) {
const instance = type.create({ _resolution: value, args: [] } as any);
instance.__privateSetVolatile({ resolved: true });
return instance;
},
});
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment