Skip to content

Commit

Permalink
Merge b349802 into 717ad1f
Browse files Browse the repository at this point in the history
  • Loading branch information
FredyC committed Jun 13, 2019
2 parents 717ad1f + b349802 commit dd8239f
Show file tree
Hide file tree
Showing 22 changed files with 961 additions and 359 deletions.
11 changes: 11 additions & 0 deletions .editorconfig
@@ -0,0 +1,11 @@
root = true

[*]
end_of_line = lf
charset = utf-8
indent_style = space
tab_width = 4

[{package.json}]
indent_style = space
indent_size = 2
1 change: 1 addition & 0 deletions .prettierignore
@@ -0,0 +1 @@
/**/package.json
15 changes: 14 additions & 1 deletion package.json
Expand Up @@ -36,7 +36,9 @@
"coveralls": "^2.11.4",
"documentation": "^9.3.1",
"faucet": "*",
"husky": "^2.4.1",
"jest": "^24.7.1",
"lint-staged": "^8.2.0",
"lodash.clonedeep": "*",
"lodash.clonedeepwith": "*",
"lodash.intersection": "*",
Expand Down Expand Up @@ -81,6 +83,17 @@
],
"watchPathIgnorePatterns": [
"<rootDir>/node_modules/"
]
],
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"prettier --write",
"git add"
]
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
}
}
}
80 changes: 45 additions & 35 deletions src/computedFn.ts
@@ -1,5 +1,11 @@
import { DeepMap } from "./deepMap"
import { IComputedValue, computed, onBecomeUnobserved, _isComputingDerivation, isAction } from "mobx";
import {
IComputedValue,
computed,
onBecomeUnobserved,
_isComputingDerivation,
isAction
} from "mobx"

/**
* computedFn takes a function with an arbitrarily amount of arguments,
Expand Down Expand Up @@ -36,40 +42,44 @@ import { IComputedValue, computed, onBecomeUnobserved, _isComputingDerivation, i
* @param keepAlive
*/
export function computedFn<T extends (...args: any[]) => any>(fn: T, keepAlive = false) {
if (isAction(fn))
throw new Error("computedFn shouldn't be used on actions")
if (isAction(fn)) throw new Error("computedFn shouldn't be used on actions")

let memoWarned = false;
let i = 0;
const d = new DeepMap<IComputedValue<any>>()
let memoWarned = false
let i = 0
const d = new DeepMap<IComputedValue<any>>()

return function(...args: Parameters<T>): ReturnType<T> {
const self = this
const entry = d.entry(args)
// cache hit, return
if (entry.exists())
return entry.get().get()
// if function is invoked, and its a cache miss without reactive, there is no point in caching...
if (!keepAlive && !_isComputingDerivation()) {
if (!memoWarned) {
console.warn("invoking a computedFn from outside an reactive context won't be memoized, unless keepAlive is set")
memoWarned = true
}
return fn.apply(self, args)
return function(...args: Parameters<T>): ReturnType<T> {
const self = this
const entry = d.entry(args)
// cache hit, return
if (entry.exists()) return entry.get().get()
// if function is invoked, and its a cache miss without reactive, there is no point in caching...
if (!keepAlive && !_isComputingDerivation()) {
if (!memoWarned) {
console.warn(
"invoking a computedFn from outside an reactive context won't be memoized, unless keepAlive is set"
)
memoWarned = true
}
return fn.apply(self, args)
}
// create new entry
const c = computed(
() => {
return fn.apply(self, args)
},
{
name: `computedFn(${fn.name}#${++i})`,
keepAlive
}
)
entry.set(c)
// clean up if no longer observed
if (!keepAlive)
onBecomeUnobserved(c, () => {
d.entry(args).delete()
})
// return current val
return c.get()
}
// create new entry
const c = computed(() => {
return fn.apply(self, args)
}, {
name: `computedFn(${fn.name}#${(++i)})`,
keepAlive
})
entry.set(c)
// clean up if no longer observed
if (!keepAlive) onBecomeUnobserved(c, () => {
d.entry(args).delete()
})
// return current val
return c.get()
}
}
}
25 changes: 15 additions & 10 deletions src/create-transformer.ts
Expand Up @@ -4,8 +4,8 @@ import { invariant, addHiddenProp } from "./utils"
export type ITransformer<A, B> = (object: A) => B

export interface ITransformerParams<A, B> {
onCleanup?: (resultObject: B | undefined, sourceObject?: A) => void,
debugNameGenerator?: (sourceObject?: A) => string,
onCleanup?: (resultObject: B | undefined, sourceObject?: A) => void
debugNameGenerator?: (sourceObject?: A) => string
}

let memoizationId = 0
Expand All @@ -19,12 +19,15 @@ let memoizationId = 0
export function createTransformer<A, B>(
transformer: ITransformer<A, B>,
onCleanup?: (resultObject: B | undefined, sourceObject?: A) => void
): ITransformer<A, B>;
): ITransformer<A, B>
export function createTransformer<A, B>(
transformer: ITransformer<A, B>,
arg2?: ITransformerParams<A, B>
): ITransformer<A, B>;
export function createTransformer<A, B>(transformer: ITransformer<A, B>, arg2?: any): ITransformer<A, B> {
): ITransformer<A, B>
export function createTransformer<A, B>(
transformer: ITransformer<A, B>,
arg2?: any
): ITransformer<A, B> {
invariant(
typeof transformer === "function" && transformer.length < 2,
"createTransformer expects a function that accepts one argument"
Expand All @@ -34,7 +37,7 @@ export function createTransformer<A, B>(transformer: ITransformer<A, B>, arg2?:
let views: { [id: number]: IComputedValue<B> } = {}
let onCleanup: Function = undefined
let debugNameGenerator: Function = undefined

function createView(sourceIdentifier: number, sourceObject: A) {
let latestValue: B
if (typeof arg2 === "object") {
Expand All @@ -46,9 +49,9 @@ export function createTransformer<A, B>(transformer: ITransformer<A, B>, arg2?:
onCleanup = undefined
debugNameGenerator = undefined
}
const prettifiedName = debugNameGenerator ?
debugNameGenerator(sourceObject) :
`Transformer-${(<any>transformer).name}-${sourceIdentifier}`
const prettifiedName = debugNameGenerator
? debugNameGenerator(sourceObject)
: `Transformer-${(<any>transformer).name}-${sourceIdentifier}`
const expr = computed(
() => {
return (latestValue = transformer(sourceObject))
Expand Down Expand Up @@ -81,7 +84,9 @@ function getMemoizationId(object: any) {
if (objectType === "number") return `number:${object}`
if (object === null || (objectType !== "object" && objectType !== "function"))
throw new Error(
`[mobx-utils] transform expected an object, function, string or number, got: ${String(object)}`
`[mobx-utils] transform expected an object, function, string or number, got: ${String(
object
)}`
)
let tid = object.$transformId
if (tid === undefined) {
Expand Down
4 changes: 2 additions & 2 deletions src/create-view-model.ts
Expand Up @@ -58,8 +58,8 @@ export class ViewModel<T> implements IViewModel<T> {
this.localComputedValues.set(key, computed(derivation.bind(this)))
}

const descriptor = Object.getOwnPropertyDescriptor(model, key);
const additionalDescriptor = descriptor ? { enumerable: descriptor.enumerable } : {};
const descriptor = Object.getOwnPropertyDescriptor(model, key)
const additionalDescriptor = descriptor ? { enumerable: descriptor.enumerable } : {}

Object.defineProperty(this, key, {
...additionalDescriptor,
Expand Down
142 changes: 69 additions & 73 deletions src/deepMap.ts
Expand Up @@ -2,93 +2,89 @@
* @private
*/
export class DeepMapEntry<T> {
private root: Map<any, any>
private closest: Map<any, any>
private closestIdx: number = 0
isDisposed = false
private root: Map<any, any>
private closest: Map<any, any>
private closestIdx: number = 0
isDisposed = false

constructor(private base: Map<any, any>, private args: any[]) {
let current: undefined | Map<any, any> = this.closest = this.root = base
let i = 0
for(; i < this.args.length -1; i++) {
current = current!.get(args[i])
if (current)
this.closest = current
else
break
constructor(private base: Map<any, any>, private args: any[]) {
let current: undefined | Map<any, any> = (this.closest = this.root = base)
let i = 0
for (; i < this.args.length - 1; i++) {
current = current!.get(args[i])
if (current) this.closest = current
else break
}
this.closestIdx = i
}
this.closestIdx = i
}

exists(): boolean {
this.assertNotDisposed()
const l = this.args.length
return this.closestIdx >= l - 1 && this.closest.has(this.args[l - 1])
}

get(): T {
this.assertNotDisposed()
if (!this.exists())
throw new Error("Entry doesn't exist")
return this.closest.get(this.args[this.args.length - 1])
}
exists(): boolean {
this.assertNotDisposed()
const l = this.args.length
return this.closestIdx >= l - 1 && this.closest.has(this.args[l - 1])
}

set(value: T) {
this.assertNotDisposed()
const l = this.args.length
let current: Map<any, any> = this.closest
// create remaining maps
for(let i = this.closestIdx; i < l - 1; i++) {
const m = new Map()
current.set(this.args[i], m)
current = m
get(): T {
this.assertNotDisposed()
if (!this.exists()) throw new Error("Entry doesn't exist")
return this.closest.get(this.args[this.args.length - 1])
}
this.closestIdx = l - 1
this.closest = current
current.set(this.args[l - 1], value)
}

delete() {
this.assertNotDisposed()
if (!this.exists())
throw new Error("Entry doesn't exist")
const l = this.args.length
this.closest.delete(this.args[l - 1])
// clean up remaining maps if needed (reconstruct stack first)
let c = this.root
const maps: Map<any, any>[] = [c]
for (let i = 0; i < l - 1; i++) {
c = c.get(this.args[i])!
maps.push(c)
set(value: T) {
this.assertNotDisposed()
const l = this.args.length
let current: Map<any, any> = this.closest
// create remaining maps
for (let i = this.closestIdx; i < l - 1; i++) {
const m = new Map()
current.set(this.args[i], m)
current = m
}
this.closestIdx = l - 1
this.closest = current
current.set(this.args[l - 1], value)
}
for (let i = maps.length - 1; i > 0; i--) {
if (maps[i].size === 0)
maps[i - 1].delete(this.args[i - 1])

delete() {
this.assertNotDisposed()
if (!this.exists()) throw new Error("Entry doesn't exist")
const l = this.args.length
this.closest.delete(this.args[l - 1])
// clean up remaining maps if needed (reconstruct stack first)
let c = this.root
const maps: Map<any, any>[] = [c]
for (let i = 0; i < l - 1; i++) {
c = c.get(this.args[i])!
maps.push(c)
}
for (let i = maps.length - 1; i > 0; i--) {
if (maps[i].size === 0) maps[i - 1].delete(this.args[i - 1])
}
this.isDisposed = true
}
this.isDisposed = true
}

private assertNotDisposed() {
// TODO: once this becomes annoying, we should introduce a reset method to re-run the constructor logic
if (this.isDisposed) throw new Error("Concurrent modification exception")
}
private assertNotDisposed() {
// TODO: once this becomes annoying, we should introduce a reset method to re-run the constructor logic
if (this.isDisposed) throw new Error("Concurrent modification exception")
}
}

/**
* @private
*/
export class DeepMap<T> {
private store = new Map<any, any>()
private argsLength = -1
private last: DeepMapEntry<T>
private store = new Map<any, any>()
private argsLength = -1
private last: DeepMapEntry<T>

entry(args: any[]): DeepMapEntry<T> {
if (this.argsLength === -1)
this.argsLength = args.length
else if (this.argsLength !== args.length)
throw new Error(`DeepMap should be used with functions with a consistent length, expected: ${this.argsLength}, got: ${args.length}`)
if (this.last) this.last.isDisposed = true

return this.last = new DeepMapEntry(this.store, args)
}
entry(args: any[]): DeepMapEntry<T> {
if (this.argsLength === -1) this.argsLength = args.length
else if (this.argsLength !== args.length)
throw new Error(
`DeepMap should be used with functions with a consistent length, expected: ${this.argsLength}, got: ${args.length}`
)
if (this.last) this.last.isDisposed = true

return (this.last = new DeepMapEntry(this.store, args))
}
}
6 changes: 3 additions & 3 deletions src/deepObserve.ts
Expand Up @@ -109,9 +109,9 @@ export function deepObserve<T = any>(
throw new Error(
`The same observable object cannot appear twice in the same tree, trying to assign it to '${buildPath(
parent
)}/${path}', but it already exists at '${buildPath(
entry.parent
)}/${entry.path}'`
)}/${path}', but it already exists at '${buildPath(entry.parent)}/${
entry.path
}'`
)
} else {
const entry = {
Expand Down

0 comments on commit dd8239f

Please sign in to comment.