@fluentui-react-native/framework-base 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.json +20 -0
- package/CHANGELOG.md +13 -0
- package/README.md +20 -0
- package/babel.config.js +1 -0
- package/eslint.config.js +3 -0
- package/lib/immutable-merge/Merge.d.ts +78 -0
- package/lib/immutable-merge/Merge.d.ts.map +1 -0
- package/lib/immutable-merge/Merge.js +179 -0
- package/lib/immutable-merge/Merge.js.map +1 -0
- package/lib/immutable-merge/Merge.test.d.ts +2 -0
- package/lib/immutable-merge/Merge.test.d.ts.map +1 -0
- package/lib/immutable-merge/Merge.test.js +262 -0
- package/lib/immutable-merge/Merge.test.js.map +1 -0
- package/lib/index.d.ts +9 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +7 -0
- package/lib/index.js.map +1 -0
- package/lib/memo-cache/getCacheEntry.d.ts +22 -0
- package/lib/memo-cache/getCacheEntry.d.ts.map +1 -0
- package/lib/memo-cache/getCacheEntry.js +49 -0
- package/lib/memo-cache/getCacheEntry.js.map +1 -0
- package/lib/memo-cache/getCacheEntry.test.d.ts +2 -0
- package/lib/memo-cache/getCacheEntry.test.d.ts.map +1 -0
- package/lib/memo-cache/getCacheEntry.test.js +95 -0
- package/lib/memo-cache/getCacheEntry.test.js.map +1 -0
- package/lib/memo-cache/getMemoCache.d.ts +11 -0
- package/lib/memo-cache/getMemoCache.d.ts.map +1 -0
- package/lib/memo-cache/getMemoCache.js +29 -0
- package/lib/memo-cache/getMemoCache.js.map +1 -0
- package/lib/memo-cache/getMemoCache.test.d.ts +2 -0
- package/lib/memo-cache/getMemoCache.test.d.ts.map +1 -0
- package/lib/memo-cache/getMemoCache.test.js +78 -0
- package/lib/memo-cache/getMemoCache.test.js.map +1 -0
- package/lib/memo-cache/index.d.ts +4 -0
- package/lib/memo-cache/index.d.ts.map +1 -0
- package/lib/memo-cache/index.js +3 -0
- package/lib/memo-cache/index.js.map +1 -0
- package/lib/memo-cache/memoize.d.ts +6 -0
- package/lib/memo-cache/memoize.d.ts.map +1 -0
- package/lib/memo-cache/memoize.js +17 -0
- package/lib/memo-cache/memoize.js.map +1 -0
- package/lib/memo-cache/memoize.test.d.ts +2 -0
- package/lib/memo-cache/memoize.test.d.ts.map +1 -0
- package/lib/memo-cache/memoize.test.js +45 -0
- package/lib/memo-cache/memoize.test.js.map +1 -0
- package/lib/merge-props/index.d.ts +4 -0
- package/lib/merge-props/index.d.ts.map +1 -0
- package/lib/merge-props/index.js +3 -0
- package/lib/merge-props/index.js.map +1 -0
- package/lib/merge-props/mergeProps.d.ts +6 -0
- package/lib/merge-props/mergeProps.d.ts.map +1 -0
- package/lib/merge-props/mergeProps.js +17 -0
- package/lib/merge-props/mergeProps.js.map +1 -0
- package/lib/merge-props/mergeStyles.d.ts +16 -0
- package/lib/merge-props/mergeStyles.d.ts.map +1 -0
- package/lib/merge-props/mergeStyles.js +32 -0
- package/lib/merge-props/mergeStyles.js.map +1 -0
- package/lib/merge-props/mergeStyles.test.d.ts +2 -0
- package/lib/merge-props/mergeStyles.test.d.ts.map +1 -0
- package/lib/merge-props/mergeStyles.test.js +94 -0
- package/lib/merge-props/mergeStyles.test.js.map +1 -0
- package/lib/merge-props/mergeStyles.types.d.ts +12 -0
- package/lib/merge-props/mergeStyles.types.d.ts.map +1 -0
- package/lib/merge-props/mergeStyles.types.js +2 -0
- package/lib/merge-props/mergeStyles.types.js.map +1 -0
- package/lib-commonjs/immutable-merge/Merge.d.ts +78 -0
- package/lib-commonjs/immutable-merge/Merge.d.ts.map +1 -0
- package/lib-commonjs/immutable-merge/Merge.js +186 -0
- package/lib-commonjs/immutable-merge/Merge.js.map +1 -0
- package/lib-commonjs/immutable-merge/Merge.test.d.ts +2 -0
- package/lib-commonjs/immutable-merge/Merge.test.d.ts.map +1 -0
- package/lib-commonjs/immutable-merge/Merge.test.js +264 -0
- package/lib-commonjs/immutable-merge/Merge.test.js.map +1 -0
- package/lib-commonjs/index.d.ts +9 -0
- package/lib-commonjs/index.d.ts.map +1 -0
- package/lib-commonjs/index.js +18 -0
- package/lib-commonjs/index.js.map +1 -0
- package/lib-commonjs/memo-cache/getCacheEntry.d.ts +22 -0
- package/lib-commonjs/memo-cache/getCacheEntry.d.ts.map +1 -0
- package/lib-commonjs/memo-cache/getCacheEntry.js +53 -0
- package/lib-commonjs/memo-cache/getCacheEntry.js.map +1 -0
- package/lib-commonjs/memo-cache/getCacheEntry.test.d.ts +2 -0
- package/lib-commonjs/memo-cache/getCacheEntry.test.d.ts.map +1 -0
- package/lib-commonjs/memo-cache/getCacheEntry.test.js +97 -0
- package/lib-commonjs/memo-cache/getCacheEntry.test.js.map +1 -0
- package/lib-commonjs/memo-cache/getMemoCache.d.ts +11 -0
- package/lib-commonjs/memo-cache/getMemoCache.d.ts.map +1 -0
- package/lib-commonjs/memo-cache/getMemoCache.js +33 -0
- package/lib-commonjs/memo-cache/getMemoCache.js.map +1 -0
- package/lib-commonjs/memo-cache/getMemoCache.test.d.ts +2 -0
- package/lib-commonjs/memo-cache/getMemoCache.test.d.ts.map +1 -0
- package/lib-commonjs/memo-cache/getMemoCache.test.js +80 -0
- package/lib-commonjs/memo-cache/getMemoCache.test.js.map +1 -0
- package/lib-commonjs/memo-cache/index.d.ts +4 -0
- package/lib-commonjs/memo-cache/index.d.ts.map +1 -0
- package/lib-commonjs/memo-cache/index.js +8 -0
- package/lib-commonjs/memo-cache/index.js.map +1 -0
- package/lib-commonjs/memo-cache/memoize.d.ts +6 -0
- package/lib-commonjs/memo-cache/memoize.d.ts.map +1 -0
- package/lib-commonjs/memo-cache/memoize.js +21 -0
- package/lib-commonjs/memo-cache/memoize.js.map +1 -0
- package/lib-commonjs/memo-cache/memoize.test.d.ts +2 -0
- package/lib-commonjs/memo-cache/memoize.test.d.ts.map +1 -0
- package/lib-commonjs/memo-cache/memoize.test.js +47 -0
- package/lib-commonjs/memo-cache/memoize.test.js.map +1 -0
- package/lib-commonjs/merge-props/index.d.ts +4 -0
- package/lib-commonjs/merge-props/index.d.ts.map +1 -0
- package/lib-commonjs/merge-props/index.js +8 -0
- package/lib-commonjs/merge-props/index.js.map +1 -0
- package/lib-commonjs/merge-props/mergeProps.d.ts +6 -0
- package/lib-commonjs/merge-props/mergeProps.d.ts.map +1 -0
- package/lib-commonjs/merge-props/mergeProps.js +21 -0
- package/lib-commonjs/merge-props/mergeProps.js.map +1 -0
- package/lib-commonjs/merge-props/mergeStyles.d.ts +16 -0
- package/lib-commonjs/merge-props/mergeStyles.d.ts.map +1 -0
- package/lib-commonjs/merge-props/mergeStyles.js +38 -0
- package/lib-commonjs/merge-props/mergeStyles.js.map +1 -0
- package/lib-commonjs/merge-props/mergeStyles.test.d.ts +2 -0
- package/lib-commonjs/merge-props/mergeStyles.test.d.ts.map +1 -0
- package/lib-commonjs/merge-props/mergeStyles.test.js +96 -0
- package/lib-commonjs/merge-props/mergeStyles.test.js.map +1 -0
- package/lib-commonjs/merge-props/mergeStyles.types.d.ts +12 -0
- package/lib-commonjs/merge-props/mergeStyles.types.d.ts.map +1 -0
- package/lib-commonjs/merge-props/mergeStyles.types.js +3 -0
- package/lib-commonjs/merge-props/mergeStyles.types.js.map +1 -0
- package/package.json +58 -0
- package/src/immutable-merge/Merge.test.ts +342 -0
- package/src/immutable-merge/Merge.ts +255 -0
- package/src/index.ts +20 -0
- package/src/memo-cache/getCacheEntry.test.ts +120 -0
- package/src/memo-cache/getCacheEntry.ts +67 -0
- package/src/memo-cache/getMemoCache.test.ts +91 -0
- package/src/memo-cache/getMemoCache.ts +37 -0
- package/src/memo-cache/index.ts +3 -0
- package/src/memo-cache/memoize.test.ts +48 -0
- package/src/memo-cache/memoize.ts +17 -0
- package/src/merge-props/index.ts +3 -0
- package/src/merge-props/mergeProps.ts +20 -0
- package/src/merge-props/mergeStyles.test.ts +124 -0
- package/src/merge-props/mergeStyles.ts +40 -0
- package/src/merge-props/mergeStyles.types.ts +9 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { CacheEntry } from './getCacheEntry';
|
|
2
|
+
import { getCacheEntry } from './getCacheEntry';
|
|
3
|
+
|
|
4
|
+
interface TestObj {
|
|
5
|
+
id: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type TestEntry = CacheEntry<TestObj>;
|
|
9
|
+
|
|
10
|
+
function compareResults(base: TestEntry, args: any[], argsNoMatch?: any[]): void {
|
|
11
|
+
const e1 = getCacheEntry(base, args);
|
|
12
|
+
expect(getCacheEntry(base, args)).toBe(e1);
|
|
13
|
+
|
|
14
|
+
if (argsNoMatch) {
|
|
15
|
+
expect(getCacheEntry(base, argsNoMatch)).not.toBe(e1);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('Memo cache unit tests', () => {
|
|
20
|
+
test('undefined args to return noargs', () => {
|
|
21
|
+
const base: TestEntry = {};
|
|
22
|
+
expect(getCacheEntry(base, undefined)).toBe(base.noargs);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('empty args array to return noargs entry', () => {
|
|
26
|
+
const base: TestEntry = {};
|
|
27
|
+
expect(getCacheEntry(base, [])).toBe(base.noargs);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('undefined goes to empty entry', () => {
|
|
31
|
+
const base: TestEntry = {};
|
|
32
|
+
expect(getCacheEntry(base, [undefined])).toBe(base.empty);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('null to go to empty entry', () => {
|
|
36
|
+
const base: TestEntry = {};
|
|
37
|
+
expect(getCacheEntry(base, [null])).toBe(base.empty);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('string gets keyed correctly', () => {
|
|
41
|
+
const base: TestEntry = {};
|
|
42
|
+
const key = 'foo';
|
|
43
|
+
expect(getCacheEntry(base, [key])).toBe(base.str[key]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('number gets keyed correctly', () => {
|
|
47
|
+
const base: TestEntry = {};
|
|
48
|
+
const val = 235;
|
|
49
|
+
const key = val + '';
|
|
50
|
+
expect(getCacheEntry(base, [val])).toBe(base.str[key]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('bool gets keyed correctly', () => {
|
|
54
|
+
const base: TestEntry = {};
|
|
55
|
+
const val = true;
|
|
56
|
+
const key = val + '';
|
|
57
|
+
expect(getCacheEntry(base, [val])).toBe(base.str[key]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('false bool gets keyed correctly', () => {
|
|
61
|
+
const base: TestEntry = {};
|
|
62
|
+
const val = false;
|
|
63
|
+
const key = val + '';
|
|
64
|
+
expect(getCacheEntry(base, [val])).toBe(base.str[key]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('object gets keyed correctly', () => {
|
|
68
|
+
const base: TestEntry = {};
|
|
69
|
+
const key = {};
|
|
70
|
+
expect(getCacheEntry(base, [key])).toBe(base.obj.get(key));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('function gets keyed correctly', () => {
|
|
74
|
+
const base: TestEntry = {};
|
|
75
|
+
const key = () => {
|
|
76
|
+
return 'hello world';
|
|
77
|
+
};
|
|
78
|
+
expect(getCacheEntry(base, [key])).toBe(base.obj.get(key));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('basic string retrieval', () => {
|
|
82
|
+
compareResults({}, ['hello', 'world'], ['hello world']);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('mixed keys with strings', () => {
|
|
86
|
+
compareResults({}, ['hello', 1, true, undefined, 'world'], ['hello', 1, true, '', 'world']);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('basic object matches', () => {
|
|
90
|
+
const obj = {};
|
|
91
|
+
compareResults({}, [obj]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('mixed object and string matches', () => {
|
|
95
|
+
const obj = {};
|
|
96
|
+
compareResults({}, [obj, 'hello', 1, 'world']);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('mixed obj and strings with obj at end', () => {
|
|
100
|
+
const obj = {};
|
|
101
|
+
const obj2 = {};
|
|
102
|
+
compareResults({}, ['hello', 1, 'world', obj], ['hello', 1, 'world', obj2]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('hybrid sets', () => {
|
|
106
|
+
const obj = {};
|
|
107
|
+
const obj2 = {};
|
|
108
|
+
compareResults({}, ['hello', obj, 'world', 1, false, obj2, undefined]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('sub cache on object', () => {
|
|
112
|
+
const obj = {};
|
|
113
|
+
const args1 = ['hello', obj];
|
|
114
|
+
const base = {};
|
|
115
|
+
const subRoot = getCacheEntry(base, args1);
|
|
116
|
+
const args2 = ['world', base];
|
|
117
|
+
const target = getCacheEntry(subRoot, args2);
|
|
118
|
+
expect(getCacheEntry(base, [...args1, ...args2])).toBe(target);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export type CacheEntry<T, TGet = any> = {
|
|
2
|
+
/** stores the cached value if any */
|
|
3
|
+
value?: T;
|
|
4
|
+
|
|
5
|
+
/** entry used for undefined and null values, these both collapse to the same type */
|
|
6
|
+
empty?: CacheEntry<TGet>;
|
|
7
|
+
|
|
8
|
+
/** entry used for the case where the array of args is null or length 0 */
|
|
9
|
+
noargs?: CacheEntry<TGet>;
|
|
10
|
+
|
|
11
|
+
/** all remaining non-object types are keyed as strings for lookups */
|
|
12
|
+
str?: { [key: string]: CacheEntry<TGet> };
|
|
13
|
+
|
|
14
|
+
/** object types are keyed in a weak map on object identity */
|
|
15
|
+
obj?: WeakMap<object, TGet>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* just wraps the common entry.foo = entry.foo || {} pattern
|
|
20
|
+
* @param entry - entry to ensure a key value for
|
|
21
|
+
* @param key - which key of that entry to ensure the value for
|
|
22
|
+
*/
|
|
23
|
+
function ensureAndReturn(entry: CacheEntry<any>, key: keyof CacheEntry<any>): CacheEntry<any> | { [key: string]: CacheEntry<any> } {
|
|
24
|
+
if ((key as string) === '__proto__' || (key as string) === 'constructor' || (key as string) === 'prototype') {
|
|
25
|
+
throw new Error('Invalid key');
|
|
26
|
+
}
|
|
27
|
+
return (entry[key] = entry[key] || {});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Step one level deeper in the cache, based on the key value from the current location
|
|
32
|
+
*
|
|
33
|
+
* @param entry - base entry to work from
|
|
34
|
+
* @param val - value to use as the key for progressing to the next level of the cache
|
|
35
|
+
*/
|
|
36
|
+
function jumpToCacheEntry(entry: CacheEntry<any>, val: any): CacheEntry<any> {
|
|
37
|
+
if (val === undefined || val === null) {
|
|
38
|
+
// undefined or null just routes directly to the empty object. This avoids the issues of string collisions with 'null' or 'undefined'
|
|
39
|
+
// when using the string key map, it also avoids creating the WeakMap (since null is technically typoef object), particularly in cases
|
|
40
|
+
// where null is just being set on non-object types.
|
|
41
|
+
return ensureAndReturn(entry, 'empty');
|
|
42
|
+
}
|
|
43
|
+
if (typeof val === 'object' || typeof val === 'function') {
|
|
44
|
+
// objects and functions will be treated as key values in a WeakMap
|
|
45
|
+
const byObj = (entry.obj = entry.obj || new WeakMap<object, CacheEntry<any>>());
|
|
46
|
+
return byObj.get(val) || byObj.set(val, {}).get(val);
|
|
47
|
+
}
|
|
48
|
+
// otherwise convert everything to a string and store it in the str object (using it as a map)
|
|
49
|
+
const key = val + '';
|
|
50
|
+
const byString = ensureAndReturn(entry, 'str');
|
|
51
|
+
return (byString[key] = byString[key] || {});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Given a base entry, either traverse or build the cache tree that matches the provided args
|
|
56
|
+
*
|
|
57
|
+
* @param entry - entry to use as the base of the cache walk
|
|
58
|
+
* @param args - array of arguments to use to progress deeper into the cache
|
|
59
|
+
*/
|
|
60
|
+
export function getCacheEntry<T, TGet = any>(entry: CacheEntry<T>, args: any[]): CacheEntry<TGet> {
|
|
61
|
+
// in the case where the args array exists and is > 0 length:
|
|
62
|
+
// - walk the cache from entry, like a linked list, jumping to the next entry by key, building it up as you go
|
|
63
|
+
// - otherwise if there are no args just use the noargs branch
|
|
64
|
+
return args && args.length > 0
|
|
65
|
+
? (args.reduce((previous: CacheEntry<any>, arg: any) => jumpToCacheEntry(previous, arg), entry) as CacheEntry<TGet>)
|
|
66
|
+
: ensureAndReturn(entry, 'noargs');
|
|
67
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { getMemoCache } from './getMemoCache';
|
|
2
|
+
|
|
3
|
+
interface TestObj {
|
|
4
|
+
id: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function getObjFactory() {
|
|
8
|
+
const obj: TestObj = { id: 0 };
|
|
9
|
+
return () => ({
|
|
10
|
+
id: obj.id++,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('getMemoCache unit tests', () => {
|
|
15
|
+
test('memoValue with null function', () => {
|
|
16
|
+
const memoValue = getMemoCache();
|
|
17
|
+
const [val] = memoValue(null, ['foo', 'bar']);
|
|
18
|
+
expect(val).toBeNull();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('memoValue with undefined', () => {
|
|
22
|
+
const memoValue = getMemoCache();
|
|
23
|
+
const [val] = memoValue(undefined, ['foo', 'bar']);
|
|
24
|
+
expect(val).toBeUndefined();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('memoValue with string', () => {
|
|
28
|
+
const memoValue = getMemoCache();
|
|
29
|
+
const [val] = memoValue('foo', ['bar', 'baz']);
|
|
30
|
+
expect(val).toEqual('foo');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('memoValue with object', () => {
|
|
34
|
+
const memoValue = getMemoCache();
|
|
35
|
+
const obj = { foo: 'hello', bar: 2, baz: 'you' };
|
|
36
|
+
const [val] = memoValue(obj, ['hello', obj]);
|
|
37
|
+
expect(val).toBe(obj);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('memoValue executes function', () => {
|
|
41
|
+
const memoValue = getMemoCache();
|
|
42
|
+
const fn = getObjFactory();
|
|
43
|
+
const v1 = fn();
|
|
44
|
+
const [v2] = memoValue(fn, ['bar', 'baz']);
|
|
45
|
+
expect(v1).not.toBe(v2);
|
|
46
|
+
expect(v1.id).not.toEqual(v2.id);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('memo calls function only once', () => {
|
|
50
|
+
const memoValue = getMemoCache();
|
|
51
|
+
const fn = getObjFactory();
|
|
52
|
+
const keys = ['hello', 'world'];
|
|
53
|
+
const [o1] = memoValue(fn, keys);
|
|
54
|
+
const [o2] = memoValue(fn, keys);
|
|
55
|
+
expect(o2).toBe(o1);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('memo calls function only once for empty inputs', () => {
|
|
59
|
+
const memoValue = getMemoCache();
|
|
60
|
+
const fn = getObjFactory();
|
|
61
|
+
const [o1] = memoValue(fn, undefined);
|
|
62
|
+
const [o2] = memoValue(fn, undefined);
|
|
63
|
+
expect(o2).toBe(o1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('sub caches are separate', () => {
|
|
67
|
+
const memoValue = getMemoCache();
|
|
68
|
+
const base1 = {};
|
|
69
|
+
const base2 = {};
|
|
70
|
+
const [, getCache1] = memoValue(null, [base1]);
|
|
71
|
+
const [, getCache2] = memoValue(null, [base2]);
|
|
72
|
+
const objKey = {};
|
|
73
|
+
const fn = getObjFactory();
|
|
74
|
+
const [o1] = getCache1(fn, [objKey]);
|
|
75
|
+
const [o2] = getCache2(fn, [objKey]);
|
|
76
|
+
const [o3] = getCache2(fn, [objKey]);
|
|
77
|
+
expect(o1).not.toBe(o2);
|
|
78
|
+
expect(o3).toBe(o2);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('sub caches work on branches', () => {
|
|
82
|
+
const memoValue = getMemoCache();
|
|
83
|
+
const keys1 = [{}, 2, 'hello'];
|
|
84
|
+
const keys2 = [{}, true];
|
|
85
|
+
const fn = getObjFactory();
|
|
86
|
+
const [, getMemoValue] = memoValue(null, keys1);
|
|
87
|
+
const [val1] = getMemoValue(fn, keys2);
|
|
88
|
+
const [val2] = memoValue(fn, [...keys1, ...keys2]);
|
|
89
|
+
expect(val2).toBe(val1);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { CacheEntry } from './getCacheEntry';
|
|
2
|
+
import { getCacheEntry } from './getCacheEntry';
|
|
3
|
+
|
|
4
|
+
export type ValueFactory<T> = () => T;
|
|
5
|
+
|
|
6
|
+
/** signature for the cache function */
|
|
7
|
+
export type GetMemoValue<T, TGet = any> = (factory: T | ValueFactory<T>, keys: any[]) => [T, GetMemoValue<TGet>];
|
|
8
|
+
|
|
9
|
+
/** base node used to remember references when a globalKey is set */
|
|
10
|
+
const _baseEntry: CacheEntry<any> = {};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Primary functional worker used to implement the caching pattern
|
|
14
|
+
*
|
|
15
|
+
* @param entry - entry to use as the base of the cache traversal
|
|
16
|
+
* @param factory - generally a function who's results will be cached, and returned via the set of keys
|
|
17
|
+
* @param keys - an ordered array of values of any type, used as keys to look up the entry
|
|
18
|
+
*/
|
|
19
|
+
function getMemoValueWorker<T, TGet = any>(entry: CacheEntry<any>, factory: T | ValueFactory<T>, keys: any[]): [T, GetMemoValue<TGet>] {
|
|
20
|
+
const foundEntry = getCacheEntry(entry, keys);
|
|
21
|
+
// check the key being set, not the value to disambiguate an undefined factory result/value from never having run the factory
|
|
22
|
+
if (!foundEntry.hasOwnProperty('value')) {
|
|
23
|
+
foundEntry.value = typeof factory === 'function' ? (factory as ValueFactory<T>)() : factory;
|
|
24
|
+
}
|
|
25
|
+
return [foundEntry.value, (fact: TGet | ValueFactory<TGet>, args: any[]) => getMemoValueWorker<TGet>(foundEntry, fact, args)];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get a memo cache instance, this can either be completely self-contained or associated with a global key
|
|
30
|
+
*
|
|
31
|
+
* @param globalKey - optional object reference to use as a key for this cache. If specified it can be used
|
|
32
|
+
* to retrieve the same cache from the global call. If not specified the returned cache will be completely isolated.
|
|
33
|
+
*/
|
|
34
|
+
export function getMemoCache<T = any>(globalKey?: object): GetMemoValue<T> {
|
|
35
|
+
const entry = globalKey ? getCacheEntry(_baseEntry, [globalKey]) : {};
|
|
36
|
+
return (fact: T | ValueFactory<T>, args: any[]) => getMemoValueWorker<T>(entry, fact, args);
|
|
37
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { memoize } from './memoize';
|
|
2
|
+
|
|
3
|
+
let _globalCalls = 0;
|
|
4
|
+
|
|
5
|
+
describe('Memoize unit tests', () => {
|
|
6
|
+
test('simple function no args', () => {
|
|
7
|
+
const noArgsFn = memoize(() => {
|
|
8
|
+
return ++_globalCalls;
|
|
9
|
+
});
|
|
10
|
+
const result1 = noArgsFn();
|
|
11
|
+
expect(noArgsFn()).toEqual(result1);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('simple function 1 arg', () => {
|
|
15
|
+
const incrementFn = memoize((bump: number) => {
|
|
16
|
+
++_globalCalls;
|
|
17
|
+
return bump + 1;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
expect(incrementFn(3)).toEqual(4);
|
|
21
|
+
const calls = _globalCalls;
|
|
22
|
+
incrementFn(3);
|
|
23
|
+
expect(_globalCalls).toEqual(calls);
|
|
24
|
+
incrementFn(4);
|
|
25
|
+
expect(_globalCalls).toEqual(calls + 1);
|
|
26
|
+
incrementFn(4);
|
|
27
|
+
expect(_globalCalls).toEqual(calls + 1);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('void function 2 args', () => {
|
|
31
|
+
const voidFn = memoize((a: number, b: string) => {
|
|
32
|
+
if (a) {
|
|
33
|
+
++_globalCalls;
|
|
34
|
+
} else if (b) {
|
|
35
|
+
++_globalCalls;
|
|
36
|
+
} else {
|
|
37
|
+
++_globalCalls;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
voidFn(3, 'hello');
|
|
42
|
+
const calls = _globalCalls;
|
|
43
|
+
voidFn(3, 'hello');
|
|
44
|
+
expect(_globalCalls).toEqual(calls);
|
|
45
|
+
voidFn(3, 'world');
|
|
46
|
+
expect(_globalCalls).toEqual(calls + 1);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getMemoCache } from './getMemoCache';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This wraps a function to memoize the results using the standard javascript memoization pattern
|
|
5
|
+
* @param fn - function to memoize
|
|
6
|
+
*/
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
8
|
+
export function memoize<T extends Function>(fn: T): T {
|
|
9
|
+
// create a unique cache that will be captured in the closure
|
|
10
|
+
const cache = getMemoCache<any>();
|
|
11
|
+
// create the closure which wraps the calling function
|
|
12
|
+
const closure = (...args: any[]) => {
|
|
13
|
+
return cache(() => fn(...(args || [])), args)[0];
|
|
14
|
+
};
|
|
15
|
+
// now return that closure strongly typed as the function.
|
|
16
|
+
return closure as unknown as T;
|
|
17
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { MergeOptions } from '../immutable-merge/Merge';
|
|
2
|
+
import { immutableMergeCore, filterToObjects } from '../immutable-merge/Merge';
|
|
3
|
+
|
|
4
|
+
import { mergeStyles } from './mergeStyles';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Props will not deeply merge with the exception of a style property. Also className needs to be handled specially.
|
|
8
|
+
*/
|
|
9
|
+
const mergePropsOptions: MergeOptions = {
|
|
10
|
+
className: (...names: any[]) => names.filter((v) => v && typeof v === 'string').join(' '),
|
|
11
|
+
style: mergeStyles,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Merge props together, flattening and merging styles as appropriate
|
|
16
|
+
* @param props - props to merge together
|
|
17
|
+
*/
|
|
18
|
+
export function mergeProps<TProps>(...props: (TProps | undefined)[]): TProps {
|
|
19
|
+
return immutableMergeCore(mergePropsOptions, ...filterToObjects<TProps>(props));
|
|
20
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { ColorValue } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { flattenStyle, mergeAndFlattenStyles, mergeStyles } from './mergeStyles';
|
|
4
|
+
import type { StyleProp } from './mergeStyles.types';
|
|
5
|
+
|
|
6
|
+
interface IFakeStyle {
|
|
7
|
+
backgroundColor?: ColorValue;
|
|
8
|
+
color?: ColorValue;
|
|
9
|
+
fontFamily?: string;
|
|
10
|
+
borderWidth?: number;
|
|
11
|
+
':hover'?: IFakeStyle;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type IFakeStyleProp = StyleProp<IFakeStyle>;
|
|
15
|
+
|
|
16
|
+
const s1: IFakeStyleProp = [
|
|
17
|
+
{ backgroundColor: 'blue' },
|
|
18
|
+
[{ color: 'red', borderWidth: 1 }, { fontFamily: 'segoe' }, [{ backgroundColor: 'bodyBackground' }]],
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const s1flatten: IFakeStyleProp = {
|
|
22
|
+
backgroundColor: 'bodyBackground',
|
|
23
|
+
color: 'red',
|
|
24
|
+
borderWidth: 1,
|
|
25
|
+
fontFamily: 'segoe',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const s2: IFakeStyleProp = {
|
|
29
|
+
borderWidth: 2,
|
|
30
|
+
fontFamily: 'primary',
|
|
31
|
+
color: 'bodyText',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const sMerged: IFakeStyleProp = {
|
|
35
|
+
backgroundColor: 'bodyBackground',
|
|
36
|
+
borderWidth: 2,
|
|
37
|
+
fontFamily: 'primary',
|
|
38
|
+
color: 'bodyText',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const sSelector: IFakeStyleProp = {
|
|
42
|
+
borderWidth: 1,
|
|
43
|
+
':hover': {
|
|
44
|
+
borderWidth: 2,
|
|
45
|
+
fontFamily: 'primary',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const sSelector2: IFakeStyleProp = {
|
|
50
|
+
backgroundColor: 'white',
|
|
51
|
+
':hover': {
|
|
52
|
+
backgroundColor: 'black',
|
|
53
|
+
borderWidth: 3,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const sArraySelector: IFakeStyleProp = [[sSelector]];
|
|
58
|
+
|
|
59
|
+
const sArraySelector2: IFakeStyleProp = [sSelector2];
|
|
60
|
+
|
|
61
|
+
const sMergedSelectors: IFakeStyleProp = {
|
|
62
|
+
borderWidth: 1,
|
|
63
|
+
backgroundColor: 'white',
|
|
64
|
+
':hover': {
|
|
65
|
+
borderWidth: 3,
|
|
66
|
+
fontFamily: 'primary',
|
|
67
|
+
backgroundColor: 'black',
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
describe('Style flatten and merge tests', () => {
|
|
72
|
+
test('flatten recursive arrays', () => {
|
|
73
|
+
const flattened = flattenStyle(s1);
|
|
74
|
+
expect(flattened).toEqual(s1flatten);
|
|
75
|
+
expect(flattened).not.toBe(s1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('flatten flat style returns style', () => {
|
|
79
|
+
const flattened = flattenStyle(s2);
|
|
80
|
+
expect(flattened).toBe(s2);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('merge also flattens', () => {
|
|
84
|
+
const merged = mergeAndFlattenStyles(undefined, undefined, s1, s2);
|
|
85
|
+
expect(merged).toEqual(sMerged);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('merge with sub objects', () => {
|
|
89
|
+
const merged = mergeAndFlattenStyles(undefined, undefined, sSelector, sSelector2);
|
|
90
|
+
expect(merged).toEqual(sMergedSelectors);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('merge sub objects in arrays', () => {
|
|
94
|
+
const merged = mergeAndFlattenStyles(undefined, undefined, sArraySelector, sArraySelector2);
|
|
95
|
+
expect(merged).toEqual(sMergedSelectors);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('memo recursive arrays', () => {
|
|
99
|
+
const flattened = mergeStyles(s1);
|
|
100
|
+
const flattened2 = mergeStyles(s1);
|
|
101
|
+
expect(flattened).toEqual(s1flatten);
|
|
102
|
+
expect(flattened2).toBe(flattened);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('memo flat style', () => {
|
|
106
|
+
const flattened = mergeStyles(s2);
|
|
107
|
+
const flattened2 = mergeStyles(s2);
|
|
108
|
+
expect(flattened).toBe(s2);
|
|
109
|
+
expect(flattened2).toBe(flattened);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('memo and flatten multiple', () => {
|
|
113
|
+
const flattened = mergeStyles(s1, s2);
|
|
114
|
+
const flattened2 = mergeStyles(s1, s2);
|
|
115
|
+
expect(flattened).toEqual(sMerged);
|
|
116
|
+
expect(flattened2).toBe(flattened);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('memo styles ignores undefined values', () => {
|
|
120
|
+
const result1 = mergeStyles(s1, s2, undefined, s1flatten);
|
|
121
|
+
const result2 = mergeStyles(s1, undefined, s2, s1flatten);
|
|
122
|
+
expect(result2).toBe(result1);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { immutableMerge } from '../immutable-merge/Merge';
|
|
2
|
+
import { getMemoCache } from '../memo-cache/getMemoCache';
|
|
3
|
+
|
|
4
|
+
import type { StyleProp } from './mergeStyles.types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Take a react-native style, which may be a recursive array, and return as a flattened
|
|
8
|
+
* style. This is analagous to the flatten routine that is part of the style sheet API
|
|
9
|
+
*
|
|
10
|
+
* @param style - StyleProp<TStyle> to flatten, this can be a TStyle or an array
|
|
11
|
+
*/
|
|
12
|
+
export function flattenStyle(style: StyleProp<object>): object {
|
|
13
|
+
return Array.isArray(style) ? immutableMerge(...style.map((v) => flattenStyle(v))) : style || {};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Merge styles together into a single flat object and optionally finalize them, can also be used to finalize a single style
|
|
18
|
+
*
|
|
19
|
+
* @param styles - array of styles to merge together. The styles will be flattened as part of the process
|
|
20
|
+
*/
|
|
21
|
+
export function mergeAndFlattenStyles(...styles: StyleProp<object>[]): object | undefined {
|
|
22
|
+
// baseline merge and flatten the objects
|
|
23
|
+
return immutableMerge(
|
|
24
|
+
...styles.map((styleProp: StyleProp<object>) => {
|
|
25
|
+
return flattenStyle(styleProp);
|
|
26
|
+
}),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const _styleCache = getMemoCache();
|
|
31
|
+
|
|
32
|
+
export function mergeStyles(...styles: StyleProp<object>[]): object | undefined {
|
|
33
|
+
// filter the style set to just objects (which might be arrays or plain style objects)
|
|
34
|
+
const inputs = styles.filter((s) => typeof s === 'object') as object[];
|
|
35
|
+
|
|
36
|
+
// now memo the results if there is more than one element or if the one element is an array
|
|
37
|
+
return inputs.length > 1 || (inputs.length === 1 && Array.isArray(inputs[0]))
|
|
38
|
+
? _styleCache(() => mergeAndFlattenStyles(undefined, ...inputs), inputs)[0]
|
|
39
|
+
: inputs[0] || {};
|
|
40
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This is a copy of the react-native style prop type, copied here to avoid RN dependencies for web clients
|
|
3
|
+
*/
|
|
4
|
+
type Falsy = undefined | null | false;
|
|
5
|
+
type RecursiveArray<T> = (T | RecursiveArray<T>)[];
|
|
6
|
+
/** Keep a brand of 'T' so that calls to `StyleSheet.flatten` can take `RegisteredStyle<T>` and return `T`. */
|
|
7
|
+
type RegisteredStyle<T> = number & { __registeredStyleBrand: T };
|
|
8
|
+
|
|
9
|
+
export type StyleProp<T> = T | RegisteredStyle<T> | RecursiveArray<T | RegisteredStyle<T> | Falsy> | Falsy;
|