Skip to content

Commit

Permalink
Combine V4 & V5 together
Browse files Browse the repository at this point in the history
  • Loading branch information
danielkcz committed Dec 31, 2019
1 parent c0d2e3e commit 1bbe1ce
Show file tree
Hide file tree
Showing 94 changed files with 6,524 additions and 4 deletions.
9 changes: 5 additions & 4 deletions flow-typed/mobx.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,10 @@ export interface IObservableArray<T> extends Array<T> {
fireImmediately?: boolean
): Lambda;
intercept(handler: IInterceptor<IArrayWillChange<T> | IArrayWillSplice<T>>): Lambda;
intercept(handler: IInterceptor<IArrayChange<T> | IArraySplice<T>>): Lambda;
intercept<T>(handler: IInterceptor<IArrayChange<T> | IArraySplice<T>>): Lambda;
intercept(handler: IInterceptor<IArrayChange<T> | IArraySplice<T>>): Lambda; // TODO: remove in 4.0
intercept<T>(handler: IInterceptor<IArrayChange<T> | IArraySplice<T>>): Lambda; // TODO: remove in 4.0
clear(): T[];
peek(): T[];
replace(newItems: T[]): T[];
find(
predicate: (item: T, index: number, array: Array<T>) => mixed,
Expand Down Expand Up @@ -340,7 +341,7 @@ declare export function reaction<T>(
expression: (r: IReactionPublic) => T,
effect: (arg: T, r: IReactionPublic) => void,
opts?: IReactionOptions
): () => void
): () => mixed

export interface IWhenOptions {
name?: string;
Expand All @@ -356,7 +357,7 @@ declare export function when(
cond: () => boolean,
effect: Lambda,
options?: IWhenOptions
): () => void
): () => mixed
declare export function when(cond: () => boolean, options?: IWhenOptions): Promise<any>

declare export function computed<T>(
Expand Down
78 changes: 78 additions & 0 deletions src/v4/api/action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {
IAction,
addHiddenProp,
boundActionDecorator,
createAction,
executeAction,
fail,
invariant,
namedActionDecorator
} from "../internal"

export interface IActionFactory {
// nameless actions
<T extends Function | null | undefined>(fn: T): T & IAction
// named actions
<T extends Function | null | undefined>(name: string, fn: T): T & IAction

// named decorator
(customName: string): (
target: Object,
key: string | symbol,
baseDescriptor?: PropertyDescriptor
) => void

// unnamed decorator
(target: Object, propertyKey: string | symbol, descriptor?: PropertyDescriptor): void

// @action.bound decorator
bound(target: Object, propertyKey: string | symbol, descriptor?: PropertyDescriptor): void
}

export const action: IActionFactory = function action(arg1, arg2?, arg3?, arg4?): any {
// action(fn() {})
if (arguments.length === 1 && typeof arg1 === "function")
return createAction(arg1.name || "<unnamed action>", arg1)
// action("name", fn() {})
if (arguments.length === 2 && typeof arg2 === "function") return createAction(arg1, arg2)

// @action("name") fn() {}
if (arguments.length === 1 && typeof arg1 === "string") return namedActionDecorator(arg1)

// @action fn() {}
if (arg4 === true) {
// apply to instance immediately
arg1[arg2] = createAction(arg1.name || arg2, arg3.value)
} else {
return namedActionDecorator(arg2).apply(null, arguments)
}
} as any

action.bound = boundActionDecorator as any

export function runInAction<T>(block: () => T): T
export function runInAction<T>(name: string, block: () => T): T
export function runInAction(arg1, arg2?) {
// TODO: deprecate?
const actionName = typeof arg1 === "string" ? arg1 : arg1.name || "<unnamed action>"
const fn = typeof arg1 === "function" ? arg1 : arg2

if (process.env.NODE_ENV !== "production") {
invariant(
typeof fn === "function" && fn.length === 0,
"`runInAction` expects a function without arguments"
)
if (typeof actionName !== "string" || !actionName)
fail(`actions should have valid names, got: '${actionName}'`)
}

return executeAction(actionName, fn, this, undefined)
}

export function isAction(thing: any) {
return typeof thing === "function" && thing.isMobxAction === true
}

export function defineBoundAction(target: any, propertyName: string, fn: Function) {
addHiddenProp(target, propertyName, createAction(propertyName, fn.bind(target)))
}
98 changes: 98 additions & 0 deletions src/v4/api/actiondecorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {
addHiddenProp,
createAction,
fail,
BabelDescriptor,
defineBoundAction,
action
} from "../internal"

function dontReassignFields() {
fail(process.env.NODE_ENV !== "production" && "@action fields are not reassignable")
}

export function namedActionDecorator(name: string) {
return function(target, prop, descriptor: BabelDescriptor) {
if (descriptor) {
if (process.env.NODE_ENV !== "production" && descriptor.get !== undefined) {
return fail("@action cannot be used with getters")
}
// babel / typescript
// @action method() { }
if (descriptor.value) {
// typescript
return {
value: createAction(name, descriptor.value),
enumerable: false,
configurable: true, // See #1477
writable: true // for typescript, this must be writable, otherwise it cannot inherit :/ (see inheritable actions test)
}
}
// babel only: @action method = () => {}
const { initializer } = descriptor
return {
enumerable: false,
configurable: true, // See #1477
writable: true, // See #1398
initializer() {
// N.B: we can't immediately invoke initializer; this would be wrong
return createAction(name, initializer!.call(this))
}
}
}
// bound instance methods
return actionFieldDecorator(name).apply(this, arguments)
}
}

export function actionFieldDecorator(name: string) {
// Simple property that writes on first invocation to the current instance
return function(target, prop, descriptor) {
Object.defineProperty(target, prop, {
configurable: true,
enumerable: false,
get() {
return undefined
},
set(value) {
addHiddenProp(this, prop, action(name, value))
}
})
}
}

export function boundActionDecorator(target, propertyName, descriptor, applyToInstance?: boolean) {
if (applyToInstance === true) {
defineBoundAction(target, propertyName, descriptor.value)
return null
}
if (descriptor) {
// if (descriptor.value)
// Typescript / Babel: @action.bound method() { }
// also: babel @action.bound method = () => {}
return {
configurable: true,
enumerable: false,
get() {
defineBoundAction(
this,
propertyName,
descriptor.value || descriptor.initializer.call(this)
)
return this[propertyName]
},
set: dontReassignFields
}
}
// field decorator Typescript @action.bound method = () => {}
return {
enumerable: false,
configurable: true,
set(v) {
defineBoundAction(this, propertyName, v)
},
get() {
return undefined
}
}
}
178 changes: 178 additions & 0 deletions src/v4/api/autorun.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import {
Lambda,
getNextId,
invariant,
EMPTY_OBJECT,
deprecated,
IReactionPublic,
IReactionDisposer,
isAction,
Reaction,
IEqualsComparer,
action,
comparer
} from "../internal"

export interface IAutorunOptions {
delay?: number
name?: string
/**
* Experimental.
* Warns if the view doesn't track observables
*/
requiresObservable?: boolean
scheduler?: (callback: () => void) => any
onError?: (error: any) => void
}

/**
* Creates a named reactive view and keeps it alive, so that the view is always
* updated if one of the dependencies changes, even when the view is not further used by something else.
* @param view The reactive view
* @returns disposer function, which can be used to stop the view from being updated in the future.
*/
export function autorun(
view: (r: IReactionPublic) => any,
opts: IAutorunOptions = EMPTY_OBJECT
): IReactionDisposer {
if (process.env.NODE_ENV !== "production") {
invariant(typeof view === "function", "Autorun expects a function as first argument")
invariant(
isAction(view) === false,
"Autorun does not accept actions since actions are untrackable"
)
}

const name: string = (opts && opts.name) || (view as any).name || "Autorun@" + getNextId()
const runSync = !opts.scheduler && !opts.delay
let reaction: Reaction

if (runSync) {
// normal autorun
reaction = new Reaction(
name,
function(this: Reaction) {
this.track(reactionRunner)
},
opts.onError,
opts.requiresObservable
)
} else {
const scheduler = createSchedulerFromOptions(opts)
// debounced autorun
let isScheduled = false

reaction = new Reaction(
name,
() => {
if (!isScheduled) {
isScheduled = true
scheduler(() => {
isScheduled = false
if (!reaction.isDisposed) reaction.track(reactionRunner)
})
}
},
opts.onError,
opts.requiresObservable
)
}

function reactionRunner() {
view(reaction)
}

reaction.schedule()
return reaction.getDisposer()
}

export type IReactionOptions = IAutorunOptions & {
fireImmediately?: boolean
equals?: IEqualsComparer<any>
}

const run = (f: Lambda) => f()

function createSchedulerFromOptions(opts: IReactionOptions) {
return opts.scheduler
? opts.scheduler
: opts.delay
? (f: Lambda) => setTimeout(f, opts.delay!)
: run
}

export function reaction<T>(
expression: (r: IReactionPublic) => T,
effect: (arg: T, r: IReactionPublic) => void,
opts: IReactionOptions = EMPTY_OBJECT
): IReactionDisposer {
if (typeof opts === "boolean") {
opts = { fireImmediately: opts }
deprecated(
`Using fireImmediately as argument is deprecated. Use '{ fireImmediately: true }' instead`
)
}
if (process.env.NODE_ENV !== "production") {
invariant(
typeof expression === "function",
"First argument to reaction should be a function"
)
invariant(typeof opts === "object", "Third argument of reactions should be an object")
}
const name = opts.name || "Reaction@" + getNextId()
const effectAction = action(
name,
opts.onError ? wrapErrorHandler(opts.onError, effect) : effect
)
const runSync = !opts.scheduler && !opts.delay
const scheduler = createSchedulerFromOptions(opts)

let firstTime = true
let isScheduled = false
let value: T

const equals = (opts as any).compareStructural
? comparer.structural
: opts.equals || comparer.default

const r = new Reaction(
name,
() => {
if (firstTime || runSync) {
reactionRunner()
} else if (!isScheduled) {
isScheduled = true
scheduler!(reactionRunner)
}
},
opts.onError,
opts.requiresObservable
)

function reactionRunner() {
isScheduled = false // Q: move into reaction runner?
if (r.isDisposed) return
let changed = false
r.track(() => {
const nextValue = expression(r)
changed = firstTime || !equals(value, nextValue)
value = nextValue
})
if (firstTime && opts.fireImmediately!) effectAction(value, r)
if (!firstTime && (changed as boolean) === true) effectAction(value, r)
if (firstTime) firstTime = false
}

r.schedule()
return r.getDisposer()
}

function wrapErrorHandler(errorHandler, baseFn) {
return function() {
try {
return baseFn.apply(this, arguments)
} catch (e) {
errorHandler.call(this, e)
}
}
}
Loading

0 comments on commit 1bbe1ce

Please sign in to comment.