-
-
Notifications
You must be signed in to change notification settings - Fork 141
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[MAJOR]: rewrite for v3 #379
Changes from 33 commits
a409d5b
5ab3b65
8e95c93
d01d6fa
9080648
51bbb89
ad682d2
9e2f4fa
3d78cc7
69ef57b
a1749d1
df3d415
f718828
c772951
7ec77ce
c85a33a
c213876
efab568
a8df21b
cd4490f
786f95a
0d93f8e
1d1f1d7
75a8cf8
79dfb28
88131ee
ca62792
40ff0fd
1ae403a
d30d7e5
2cfd7c1
82af80b
d9c6a62
004d77c
5c509f4
ae1afb7
42a22d2
e5ed01b
d18b6fb
ffc538a
9be116d
8bc34b6
ca58df3
75b2670
c36b81f
20b3438
e4750ed
e137dda
6dffd76
94f8c75
c5bf3cd
b4f58a8
afee3ff
b91aaff
d0f2bde
49785c1
a4499dc
a8feb0e
a5085d2
768026a
0f0d6cf
b4fdcd4
9c08ce2
4eac8fd
9215679
f373925
bc55557
afdebd5
6224202
4301bb4
2f8267d
f4ed5ac
9760ff1
8cf6f2d
54c6ba8
ab62972
472576f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import Notifier from './notifier'; | ||
|
||
export function notifierForEvent(object: any, eventName: string) { | ||
if (object._eventedNotifiers === undefined) { | ||
object._eventedNotifiers = {}; | ||
} | ||
|
||
let notifier = object._eventedNotifiers[eventName]; | ||
|
||
if (!notifier) { | ||
notifier = object._eventedNotifiers[eventName] = new Notifier(); | ||
} | ||
|
||
return notifier; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
export default function getDeep<T extends object>(root: T, path: string | string[]): any { | ||
let obj: any = root; | ||
|
||
if (path.indexOf('.') === -1) { | ||
return obj[path as string]; | ||
} | ||
let parts = typeof path === 'string' ? path.split('.') : path; | ||
|
||
for (let i = 0; i < parts.length; i++) { | ||
if (obj === undefined || obj === null) { | ||
return undefined; | ||
} | ||
|
||
// next iteration has next level | ||
obj = obj[parts[i]]; | ||
} | ||
|
||
return obj; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import isObject from '../utils/is-object'; | ||
|
||
// 'foo.bar.zaz' | ||
let keysUpToValue: any[] = []; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this is used as an accumulator for recursion, this should be an argument to the function. Having it persisted across function calls can be error prone. If you don't want to expose the accumulator outside this module, you can use this common pattern: const myRecursiveFunctionWithAccumulator = (a, acc) => {
/* do whatever recursive stuff you need to do */
};
export const myFunction = (a) => myRecursiveFunctionWithAccumulator(a, []); |
||
|
||
/** | ||
* traverse through target and return leaf nodes with `value` property | ||
* | ||
* @method keyValues | ||
* @param target | ||
*/ | ||
export default function keyValues<T extends { [key: string]: any}>(obj: T): object[] { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
let map = []; | ||
for (let key in obj) { | ||
keysUpToValue.push(key); | ||
|
||
if (obj[key] && isObject(obj[key])) { | ||
if (Object.prototype.hasOwnProperty.apply(obj[key], ['value'])) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you use |
||
map.push({ key: keysUpToValue.join('.'), value: obj[key].value }); | ||
// stop collecting keys | ||
keysUpToValue = []; | ||
} else if (key !== 'value') { | ||
map.push(...keyValues(obj[key])); | ||
} | ||
} | ||
} | ||
|
||
return map; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
interface Options { | ||
safeGet: any | ||
safeSet: any | ||
} | ||
|
||
function isMergeableObject(value: any): Boolean { | ||
return isNonNullObject(value) && !isSpecial(value); | ||
} | ||
|
||
function isNonNullObject(value: any): Boolean { | ||
return !!value && typeof value === 'object'; | ||
} | ||
|
||
function isSpecial(value: any): Boolean { | ||
let stringValue = Object.prototype.toString.call(value); | ||
|
||
return stringValue === '[object RegExp]' || stringValue === '[object Date]'; | ||
} | ||
|
||
function getEnumerableOwnPropertySymbols(target: any): any { | ||
return Object.getOwnPropertySymbols | ||
? Object.getOwnPropertySymbols(target).filter(symbol => { | ||
return target.propertyIsEnumerable(symbol) | ||
}) | ||
: []; | ||
} | ||
|
||
function getKeys(target: any) { | ||
return Object.keys(target).concat(getEnumerableOwnPropertySymbols(target)) | ||
} | ||
|
||
function propertyIsOnObject(object: any, property: any) { | ||
try { | ||
return property in object; | ||
} catch(_) { | ||
return false; | ||
} | ||
} | ||
|
||
// Protects from prototype poisoning and unexpected merging up the prototype chain. | ||
function propertyIsUnsafe(target: any, key: string): Boolean { | ||
return propertyIsOnObject(target, key) // Properties are safe to merge if they don't exist in the target yet, | ||
&& !(Object.hasOwnProperty.call(target, key) // unsafe if they exist up the prototype chain, | ||
&& Object.propertyIsEnumerable.call(target, key)); // and also unsafe if they're nonenumerable. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct indentation here. You might want to add prettier to keep consistency between tabs / spaces. |
||
} | ||
|
||
let kv: { [k: string]: any } = {}; | ||
let possibleKeys: string[] = []; | ||
|
||
/** | ||
* DFS - traverse depth first until find object with `value`. Then go back up tree and try on next key | ||
* need to exhaust all possible avenues. | ||
* | ||
* @method buildPathToValue | ||
*/ | ||
function buildPathToValue(source: any, options: Options): void { | ||
Object.keys(source).forEach((key: string): void => { | ||
let possible = source[key]; | ||
if (possible && possible.hasOwnProperty('value')) { | ||
possibleKeys.push(key); | ||
kv[possibleKeys.join('.')] = possible.value; | ||
possibleKeys = []; | ||
return; | ||
} | ||
|
||
if (typeof possible === 'object') { | ||
possibleKeys.push(key); | ||
buildPathToValue(possible, options); | ||
} else { | ||
possibleKeys = []; | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* `source` will always have a leaf key `value` with the proeprty we want to set | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. s/proeprty/property/ |
||
* @method mergeObject | ||
*/ | ||
function mergeObject(target: any, source: any, options: Options) { | ||
getKeys(source).forEach(key => { | ||
// proto poisoning. So can set by nested key path 'person.name' | ||
if (propertyIsUnsafe(target, key)) { | ||
if (options.safeSet) { | ||
buildPathToValue(source, options); | ||
if (Object.keys(kv).length > 0) { | ||
// we found some keys! | ||
for (key in kv) { | ||
const val = kv[key]; | ||
options.safeSet(target, key, val); | ||
} | ||
} | ||
|
||
kv = {}; | ||
} | ||
|
||
return; | ||
} | ||
|
||
// else safe key on object | ||
if (propertyIsOnObject(target, key) && isMergeableObject(source[key]) && !source[key].hasOwnProperty('value')) { | ||
target[key] = mergeDeep(options.safeGet(target, key), options.safeGet(source, key), options); | ||
} else { | ||
return target[key] = source[key].value; | ||
} | ||
}); | ||
|
||
return target; | ||
} | ||
|
||
/** | ||
* goal is to mutate target with source's properties, ensuring we dont encounter | ||
* pitfalls of { ..., ... } spread syntax overwriting keys on objects that we merged | ||
* | ||
* @method mergeDeep | ||
* @param target | ||
* @param source | ||
*/ | ||
export default function mergeDeep(target: any, source: any, options: Options = { safeGet: undefined, safeSet: undefined }): object | [any] { | ||
options['safeGet'] = options.safeGet || function(obj: any, key: string): any { return obj[key] }; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why the bracket? |
||
options['safeSet'] = options.safeSet; | ||
let sourceIsArray = Array.isArray(source); | ||
let targetIsArray = Array.isArray(target); | ||
let sourceAndTargetTypesMatch = sourceIsArray === targetIsArray; | ||
|
||
if (!sourceAndTargetTypesMatch) { | ||
return source; | ||
} else if (sourceIsArray) { | ||
return source; | ||
} else { | ||
return mergeObject(target, source, options); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import isObject from '../utils/is-object'; | ||
|
||
/** | ||
* traverse through target and unset `value` from leaf key | ||
* Shallow copy here is fine because we are swapping out the leaf nested object | ||
* rather than mutating a property in something with reference | ||
* | ||
* @method normalizeObject | ||
* @param target | ||
*/ | ||
export default function normalizeObject<T extends { [key: string]: any}>(target: T): T { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why the generic? |
||
let obj = { ...target }; | ||
|
||
for (let key in obj) { | ||
if (key === 'value') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we get this check to be the first thing to do? if ('value' in target) { // Or hasOwnProperty
return target.value;
} That way we prevent creating a new object and traversing any property. |
||
return obj[key]; | ||
} | ||
|
||
if (obj[key] && isObject(obj[key])) { | ||
if (Object.prototype.hasOwnProperty.apply(obj[key], ['value'])) { | ||
obj[key] = obj[key].value; | ||
} else { | ||
obj[key] = normalizeObject(obj[key]); | ||
} | ||
} | ||
} | ||
|
||
return obj; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
// this statefull class holds and notifies | ||
import { INotifier } from '../types/evented'; | ||
|
||
export default class Notifier implements INotifier { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a generic version of Notifier that check that callbacks admit the right arguments. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In general, types like |
||
listeners: Function[] | ||
|
||
constructor() { | ||
this.listeners = []; | ||
} | ||
|
||
addListener(callback: Function) { | ||
this.listeners.push(callback); | ||
return () => this.removeListener(callback); | ||
} | ||
|
||
removeListener(callback: Function) { | ||
this.listeners; | ||
|
||
for (let i = 0; i < this.listeners.length; i++) { | ||
if (this.listeners[i] === callback) { | ||
this.listeners.splice(i, 1); | ||
return; | ||
} | ||
} | ||
} | ||
|
||
trigger(...args: any[]) { | ||
this.listeners.slice(0).forEach(callback => callback(...args)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do you need the |
||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are you using a generic here?