mobx-keystone
Version:
A MobX powered state management solution based on data trees with first class support for TypeScript, snapshots, patches and much more
188 lines (157 loc) • 5.06 kB
text/typescript
import { ActionContextActionType } from "../action"
import {
ActionTrackingResult,
actionTrackingMiddleware,
SimpleActionContext,
} from "../actionMiddlewares"
import { fastGetRootPath, RootPath } from "../parent/path"
import { applySnapshot } from "../snapshot/applySnapshot"
import { getSnapshot } from "../snapshot/getSnapshot"
import { assertTweakedObject } from "../tweaker/core"
/**
* Connects a tree node to a redux dev tools instance.
*
* @param remotedevPackage The remotedev package (usually the result of `require("remoteDev")`) (https://www.npmjs.com/package/remotedev).
* @param remotedevConnection The result of a connect method from the remotedev package (usually the result of `remoteDev.connectViaExtension(...)`).
* @param target Object to use as root.
* @param [options] Optional options object. `logArgsNearName` if it should show the arguments near the action name (default is `true`).
*/
export function connectReduxDevTools(
remotedevPackage: any,
remotedevConnection: any,
target: object,
options?: {
logArgsNearName?: boolean
}
) {
assertTweakedObject(target, "target")
const opts = {
logArgsNearName: true,
...options,
}
let handlingMonitorAction = 0
// subscribe to change state (if need more than just logging)
remotedevConnection.subscribe((message: any) => {
if (message.type === "DISPATCH") {
handleMonitorActions(remotedevConnection, target, message)
}
})
const initialState = getSnapshot(target)
remotedevConnection.init(initialState)
let currentActionId = 0
const actionIdSymbol = Symbol("actionId")
actionTrackingMiddleware(target, {
onStart(ctx) {
ctx.data[actionIdSymbol] = currentActionId++
},
onResume(ctx) {
// give a chance to the parent to log its own changes before the child starts
if (ctx.parentContext) {
log(ctx.parentContext, undefined)
}
log(ctx, undefined)
},
onSuspend(ctx) {
log(ctx, undefined)
},
onFinish(ctx, ret) {
log(ctx, ret.result)
},
})
function handleMonitorActions(remotedev2: any, target2: any, message: any) {
try {
handlingMonitorAction++
switch (message.payload.type) {
case "RESET": {
applySnapshot(target2, initialState)
return remotedev2.init(initialState)
}
case "COMMIT":
return remotedev2.init(getSnapshot(target2))
case "ROLLBACK": {
const state = remotedevPackage.extractState(message)
applySnapshot(target2, state)
return remotedev2.init(state)
}
case "JUMP_TO_STATE":
case "JUMP_TO_ACTION": {
applySnapshot(target2, remotedevPackage.extractState(message))
return
}
case "IMPORT_STATE": {
const nextLiftedState = message.payload.nextLiftedState
const computedStates = nextLiftedState.computedStates
applySnapshot(target2, computedStates[computedStates.length - 1].state)
remotedev2.send(null, nextLiftedState)
return
}
default:
}
} finally {
handlingMonitorAction--
}
}
let lastLoggedSnapshot = initialState
function log(ctx: SimpleActionContext, result: ActionTrackingResult | undefined) {
if (handlingMonitorAction) {
return
}
const sn = getSnapshot(target)
// ignore actions that don't change anything (unless it is a throw)
if (sn === lastLoggedSnapshot && result !== ActionTrackingResult.Throw) {
return
}
lastLoggedSnapshot = sn
const rootPath = fastGetRootPath(ctx.target, false)
const name = getActionContextNameAndTypePath(ctx, rootPath, result)
const copy = {
type: name,
path: rootPath.path,
args: ctx.args,
}
remotedevConnection.send(copy, sn)
}
function getActionContextNameAndTypePath(
ctx: SimpleActionContext,
rootPath: RootPath<any>,
result: ActionTrackingResult | undefined
) {
const pathStr = "[/" + rootPath.path.join("/") + "] "
let name = pathStr + ctx.actionName
if (opts.logArgsNearName) {
let args = ctx.args
.map((a) => {
try {
return JSON.stringify(a)
} catch {
return "**unserializable**"
}
})
.join(", ")
if (args.length > 64) {
args = args.slice(0, 64) + "..."
}
name += `(${args})`
}
const actionId = ctx.data[actionIdSymbol]
name += ` (id ${actionId !== undefined ? actionId : "?"}`
if (ctx.type === ActionContextActionType.Async) {
name += ", async"
}
name += ")"
if (result === ActionTrackingResult.Throw) {
name += " -error thrown-"
}
if (ctx.parentContext) {
const parentName = getActionContextNameAndTypePath(
ctx.parentContext,
fastGetRootPath(ctx.parentContext.target, false),
undefined
)
if (parentName) {
name = `${parentName} >>> ${name}`
}
}
return name
}
}